Help us understand the problem. What is going on with this article?

Address Sanitizerで独自Frameworkのメモリエラーを検出する

More than 1 year has passed since last update.

はじめに

Xcode7より、AddressSanitizer(aka ASan)という、強力なメモリエラー検知ツールが追加されたことは、ご存知の方も多いかもしれません[0]

お仕事で、ASanで、独自Framework内のメモリエラーを調査する機会があったので、その際に、学んだ、使い方やTipsについて書き留めます。

もし何か不備等ありましたら、気軽にコメントください :)

Address Sanitizerとは?

AddressSanitizer(aka ASan)は、メモリエラー検出ツールです。

ASanは、不正なメモリ操作を検出するコードをコンパイル時に挿入します

ここがポイントですね。

ASanのコードは、メモリエラーを検知するとアプリをクラッシュさせるとともに、詳細なログをコンソールに出力します。

このあたりは、WWDC15のSession413[0]が詳しいです。

そのため、Valgrindなどとは違い、よりオーバーヘッドが少なく、2x程度に抑えられるそうです。

ASanは、Clang/LLVMの機能です。なので、iOSアプリに限らず、C/C++言語であれば、利用できます[1]。また、Android NDKでも利用できるそうです[2]。

検出項目としては以下が上げられています。

  • Use-after-free
  • Heap buffer overflow
  • Stack buffer overflow
  • Global variable overflow
  • Overflows in C++ containers
  • Use after return

例えば、以下のようなStack/Heap-buffer-overflowを検出してくれます。

  char buff[10];
  buff[10] = 'a';
  char *ptr  = (char*) malloc(10);
  ptr[10] = 'c';

How To Use

Run an app with Xcode

Xcodeからアプリを実行し、アプリコードのメモリエラーを検出するだけであれば、Schemeに設定するだけ。

Edit Scheme > Run > Diagnostics > Address Sanitizer

にチェック✔を入れます。

Build an IPA with xcodebuild

App only

xcodebuildでipaを作成したい場合は、Build Settingsに、-fsanitize=addressを追加します。

  • OTHER_CFLAGS
  • OTHER_CXXFLAGS
    • $(OTHER_CFLAGS)となっていれば必要ありません

⚠️ Xcode 9では、xcodebuild archive時に以下が必要なようです。
(xcodebuild buildでは、-enableAddressSanitizerYESにすればよい)

  1. "Link Binary With Libraries"に、libclang_rt.asan_ios_dynamic.dylibを追加
  2. ENABLE_BITCODEを"NO"にする
  3. 後述のTips 2にあるように、Appバンドルにlibclang_rt.asan_ios_dynamic.dylibをコピーする

App + Frameworks

さらに、アプリにリンクさせる独自Frameworkを調査するときは、別の設定が必要です。
※今回私が検証したFrameworkは、Static libraryから生成したFrameworkバンドルです。

Framework project: Build Settingsに、-fsanitize=addressを追加します。

  • OTHER_CFLAGS
  • OTHER_CXXFLAGS
    • $(OTHER_CFLAGS)となっていれば必要ありません

App project: 加えてOTHER_LDFLAGSにも、-fsanitize=addressを追加します。

  • OTHER_CFLAGS
  • OTHER_CXXFLAGS
  • OTHER_LDFLAGS

これで、Address Sanitizerが有効化されて、ソースコードがコンパイルされます[3]。

また、-fno-omit-frame-pointerを追加すると、StacktraceがよりわかりやすくなるとGithubのWikiには書いてありますが、Xcode 8では、付けなくてもよさそうです[4]。

設定は以上です。簡単ですね。

しかし、実際利用する際は、以下の3点に注意が必要です。

Tips 1: Xcodeのバージョンを一致させる

Frameworkとアプリは、同じXcodeバージョンで、ビルドしなければなりません。

でないと、Undefined symbols for architectureというエラーが発生します。

これは、Clang/LLVMによって、コンパイル時に挿入されるASanの関数は、Xcodeのバージョンによって、シンボル名が異なる(Clang/LLVMのバージョンが異なる)ためです。

Tips 2: IPAには、Run Scriptで、ASanのdylibをバンドルする

Run with Xcodeと違い、xcodebuildでは、ASanのdylibが、AppBundleにコピーされません。よって、単にIPAを生成するだけだとrpathエラーでクラッシュします。

dyld: Library not loaded: @rpath...

これを防ぐためには、Xcode.appから、libclang_rt.asan_ios_dynamic.dylibを取り出し、Projectに追加後、Run ScriptのCopy Filesで、コピーさせる必要があります。

Screen Shot 2016-10-14 at 5.29.20 PM.png

Xcode 8では以下の場所にあります。

Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/clang/8.0.0/lib/darwin/libclang_rt.asan_ios_dynamic.dylib

※これよりもrsync?などを使ったスマートな方法があったら教えてください!

Tips 3: コンソールログを取得する

ipaファイルをインストールして、アプリを起動した場合、コンソールログを必ず記録しましょう。

メモリエラーが発生するとASanにより、アプリはEXC_CORPSE_NOTIFYでクラッシュし、クラッシュログが残りますが、なんと、その内容に、ASanログは記録されません。

コンソールログに出力されます。

そのため、テスト時は、クラッシュ後、必ず、Xcodeでコンソールログの取得を行ってください。そうしないと、ASanの真骨頂である、有用な情報が得られません。

最後に

ASanは、簡単な設定で、メモリエラー検出し、エラーの状況を詳細にレポートしてくれる、バグ修正には非常有用なツールです。

正直、無料のツール[5]で、ここまで詳細な調査が出来ることに驚きました。

EXC_CORPSE_NOTIFYクラッシュに遭遇したら、一度試してみるをオススメします。

なお、今回、CocoaPodsを利用しなかったので、試していません。おそらく、以下のように、設定をすればいいのではないかなと思いますがどうでしょうか。

post_install do |installer|
  installer.pods_project.targets.each do |target|
    target.build_configurations.each do |config|
      config.build_settings['OTHER_LDFLAGS'] += '-fsanitize=address'
      config.build_settings['OTHER_CFLAGS'] += '-fsanitize=address'
    end
  end
end

References

[0] Advanced Debugging and the Address Sanitizer
https://developer.apple.com/videos/play/wwdc2015/413/

[1] 以下のサイトが参考になります。

Clang: Address Sanitizer
http://clang.llvm.org/docs/AddressSanitizer.html

GitHub: google/sanitizers
https://github.com/google/sanitizers
https://github.com/google/sanitizers/wiki/AddressSanitizer

実際のコードはここかな?
https://github.com/llvm-mirror/llvm/blob/master/lib/Transforms/Instrumentation/AddressSanitizer.cpp

[2] https://source.android.com/devices/tech/debug/asan.html

[3] 逆に言えば、たとえ、Schemeで、Address Sanitizerが有効化し、アプリをビルドしても、Framework/Static Libraryなどの既にコンパイル済みのバイナリには適用できない。

[4] 上記に示した簡単なメモリエラーを起こしたところ、差分は確認できませんでした。オプションを設定せずとも十分に記録は残っていたので、設定しなくてもよいかと思います。

確認したclangのバージョンはこちら。
Apple LLVM version 8.0.0 (clang-800.0.38)
Target: x86_64-apple-darwin15.6.0

なお、-fno-omit-frame-pointer以外にも、ここを読むと、-O1-fno-optimize-sibling-callsもあるとより詳しく表示されると書いてあります。

[5] 有料では、Coverityなどがありますね。
http://www.coverity.com/html_ja/products/code-advisor/index.html

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away