AddressSanitizer

Android NDK は、API レベル 27(Android O MR 1)以降で AddressSanitizer(ASan とも呼ばれます)をサポートしています。

ASan は、ネイティブ コードのメモリバグを検出するための高速なコンパイラ ベースのツールです。 ASan の検出対象は次のとおりです。

  • スタックとヒープのバッファ オーバーフロー / アンダーフロー
  • 解放後のヒープ使用
  • スコープ外のスタック使用
  • 二重解放 / ワイルド解放

ASan の CPU オーバーヘッドは約 2 倍、コードサイズ オーバーヘッドは 50% から 2 倍です。メモリ オーバーヘッドは大きく、割り当てパターンにもよりますが、およそ 2 倍になります。

サンプルアプリ

サンプルアプリは、Asan 用にビルド バリアントを構成する方法を示しています。

ビルド

AddressSanitizer を使用してアプリのネイティブ(JNI)コードをビルドする方法は次のとおりです。

ndk-build

Application.mk 内で以下のように指定します。

APP_STL := c++_shared # Or system, or none.
APP_CFLAGS := -fsanitize=address -fno-omit-frame-pointer
APP_LDFLAGS := -fsanitize=address

Android.mk 内の各モジュールごとに、以下のように指定します。

LOCAL_ARM_MODE := arm

CMake

モジュールの build.gradle 内で以下のように指定します。

android {
    defaultConfig {
        externalNativeBuild {
            cmake {
                // Can also use system or none as ANDROID_STL.
                arguments "-DANDROID_ARM_MODE=arm", "-DANDROID_STL=c++_shared"
            }
        }
    }
}

CMakeLists.txt 内のターゲットごとに、以下のように指定します。

target_compile_options(${TARGET} PUBLIC -fsanitize=address -fno-omit-frame-pointer)
set_target_properties(${TARGET} PROPERTIES LINK_FLAGS -fsanitize=address)

実行

Android O MR1(API レベル 27)以降では、アプリが、アプリプロセスをラップまたは置換することのできる wrap シェル スクリプトを提供できます。これにより、デバッグ可能なアプリがアプリの起動をカスタマイズすることができ、それによって、本番デバイス上で ASan を使用できるようになります。

  1. アプリ マニフェストに android:debuggable を追加します。
  2. アプリの build.gradle ファイルで useLegacyPackagingtrue に設定します。詳細については、wrap シェル スクリプト ガイドをご覧ください。
  3. アプリ モジュールの jniLibs に ASan ランタイム ライブラリを追加します。
  4. 以下の内容を含んだ wrap.sh ファイルを、src/main/resources/lib ディレクトリの各ディレクトリに追加します。

    #!/system/bin/sh
    HERE="$(cd "$(dirname "$0")" && pwd)"
    export ASAN_OPTIONS=log_to_syslog=false,allow_user_segv_handler=1
    ASAN_LIB=$(ls $HERE/libclang_rt.asan-*-android.so)
    if [ -f "$HERE/libc++_shared.so" ]; then
        # Workaround for https://github.com/android-ndk/ndk/issues/988.
        export LD_PRELOAD="$ASAN_LIB $HERE/libc++_shared.so"
    else
        export LD_PRELOAD="$ASAN_LIB"
    fi
    "$@"
    

プロジェクトのアプリ モジュールの名前が app の場合、最終的なディレクトリ構造には以下が含まれます。

<project root>
└── app
    └── src
        └── main
            ├── jniLibs
            │   ├── arm64-v8a
            │   │   └── libclang_rt.asan-aarch64-android.so
            │   ├── armeabi-v7a
            │   │   └── libclang_rt.asan-arm-android.so
            │   ├── x86
            │   │   └── libclang_rt.asan-i686-android.so
            │   └── x86_64
            │       └── libclang_rt.asan-x86_64-android.so
            └── resources
                └── lib
                    ├── arm64-v8a
                    │   └── wrap.sh
                    ├── armeabi-v7a
                    │   └── wrap.sh
                    ├── x86
                    │   └── wrap.sh
                    └── x86_64
                        └── wrap.sh

スタック トレース

AddressSanitizer は、malloc / realloc / free が呼び出されるたびにスタックを巻き戻す必要があります。これには次の 2 つの方法があります。

  1. フレーム ポインタに基づく「高速」の unwinder。これは、ビルドのセクションの手順に沿って使用されるものです。

  2. 「低速」の CFI unwinder。このモードでは、ASan は _Unwind_Backtrace を使用します。必要なのは -funwind-tables のみで、通常はデフォルトで有効になっています。

高速の unwinder は、malloc、realloc、free のデフォルトです。低速の unwinder は、致命的なスタック トレースのデフォルトです。wrap.sh で ASAN_OPTIONS 変数に fast_unwind_on_malloc=0 を追加すると、すべてのスタック トレースに対して低速の unwinder を有効にできます。