Address Sanitizer

Android NDK는 API 수준 27(Android O MR 1)부터 Address Sanitizer(ASan이라고도 함)를 지원합니다.

ASan은 네이티브 코드의 메모리 버그 감지를 위한 빠른 컴파일러 기반 도구입니다. ASan은 다음을 감지합니다.

  • 스택 및 힙 버퍼 오버플로우/언더플로우
  • 프리 후 힙 사용
  • 범위를 벗어난 스택 사용
  • 더블 프리/와일드 프리

ASan의 CPU 오버헤드는 약 2x이고 코드 크기 오버헤드는 50%와 2x 사이이며 메모리 오버헤드는 대규모(할당 패턴에 따라 다르지만 대략 2x 정도)입니다.

샘플 앱

샘플 앱에서 Asan용 빌드 변형을 구성하는 방법을 확인할 수 있습니다.

빌드

Address Sanitizer를 사용하여 앱의 네이티브(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)부터 애플리케이션은 애플리케이션 프로세스를 래핑하거나 대체할 수 있는 래핑 셸 스크립트를 제공할 수 있습니다. 이 스크립트를 통해 디버그 가능한 애플리케이션에서 애플리케이션 시작을 맞춤설정하여 프로덕션 기기에서 ASan을 사용할 수 있습니다.

  1. 애플리케이션 매니페스트에 android:debuggable을 추가합니다.
  2. 앱의 build.gradle 파일에서 useLegacyPackagingtrue로 설정합니다. 자세한 내용은 래핑 셸 스크립트를 참고하세요.
  3. ASan 런타임 라이브러리를 앱 모듈의 jniLibs에 추가합니다.
  4. src/main/resources/lib 디렉터리의 각 디렉터리에 다음 콘텐츠가 포함된 wrap.sh 파일을 추가합니다.

    #!/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

스택 트레이스

Address Sanitizer는 모든 malloc/realloc/free 호출에서 스택을 해제해야 합니다. 다음과 같은 두 가지 옵션이 있습니다.

  1. '고속' 프레임 포인터 기반 언와인더: 빌드 섹션의 지침에 따라 사용되는 옵션입니다.

  2. '저속' CFI 언와인더: 이 모드에서 ASan은 _Unwind_Backtrace를 사용합니다. -funwind-tables만 필요하며 보통은 기본적으로 사용 설정되어 있습니다.

고속 언와인더는 malloc/realloc/free의 기본값입니다. 저속 언와인더는 치명적인 스택 트레이스의 기본값입니다. wrap.sh의 ASAN_OPTIONS 변수에 fast_unwind_on_malloc=0을 추가하여 모든 스택 트레이스에 저속 언와인더를 사용 설정할 수 있습니다.