はじめに
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
では、-enableAddressSanitizer
をYES
にすればよい)
- "Link Binary With Libraries"に、
libclang_rt.asan_ios_dynamic.dylib
を追加 -
ENABLE_BITCODE
を"NO"にする - 後述の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で、コピーさせる必要があります。
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