此文章是基于官方文档 Address Sanitizer的基础上做了一些扩展说明。
从 API 级别 27 (Android O MR 1) 开始,Android NDK 可支持 Address Sanitizer(也称为 ASan)。为啥从27开始呢?因为wrap.sh
仅适用于 API 级别 27 及更高级别。
ASan 是一种基于编译器的快速检测工具,用于检测原生代码中的内存错误。ASan 可以检测以下问题:
- 堆栈和堆缓冲区上溢/下溢
- 释放之后的堆使用情况
- 超出范围的堆栈使用情况
- 重复释放/错误释放
ASan 可在 32 位和 64 位 ARM 以及 x86 和 x86-64 上运行。ASan 的 CPU 开销约为 2 倍,代码大小开销在 50% 到 2 倍之间,并且内存开销很大(具体取决于您的分配模式,但约为 2 倍)。
对于 64 位 ARM,强烈建议使用HWAddress Sanitizer。
构建
如需使用 Address Sanitizer 构建应用的原生 (JNI) 代码,请执行以下操作:
# 在 Application.mk 中:
APP_PLATFORM := android-17
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
如果APP_PLATFORM版本小于17,那么编译的时候就会出现如下错误:
/Users/stone/Library/Android/sdk/ndk-bundle/toolchains/llvm/prebuilt/darwin-x86_64/lib64/clang/5.0.300080/lib/linux/libclang_rt.asan-arm-android.so: error: undefined reference to '__vsnprintf_chk', version 'LIBC'
clang++: error: linker command failed with exit code 1 (use -v to see invocation)
在链接选项中添加-fsanitize=address
(属于llvm的特性),那么它在链接的时候会到llvm的库目录寻找这个库,库的位置在如下位置:
./toolchains/llvm/prebuilt/darwin-x86_64/lib64/clang/5.0/lib/linux/libclang_rt.asan-arm-android.so
./toolchains/llvm/prebuilt/darwin-x86_64/lib64/clang/5.0/lib/linux/libclang_rt.asan-mips64-android.so
./toolchains/llvm/prebuilt/darwin-x86_64/lib64/clang/5.0/lib/linux/libclang_rt.asan-i686-android.so
./toolchains/llvm/prebuilt/darwin-x86_64/lib64/clang/5.0/lib/linux/libclang_rt.asan-x86_64-android.so
./toolchains/llvm/prebuilt/darwin-x86_64/lib64/clang/5.0/lib/linux/libclang_rt.asan-aarch64-android.so
./toolchains/llvm/prebuilt/darwin-x86_64/lib64/clang/5.0/lib/linux/libclang_rt.asan-mips-android.so
查看编译出来的库依赖关系:readelf -d libs/armeabi-v7a/santest
,可以看到它确实依赖了libclang_rt.asan-arm-android.so
Dynamic section at offset 0xe98 contains 34 entries:
Tag Type Name/Value
0x00000003 (PLTGOT) 0x1fe4
0x00000002 (PLTRELSZ) 32 (bytes)
0x00000017 (JMPREL) 0x4a4
0x00000014 (PLTREL) REL
0x00000011 (REL) 0x474
0x00000012 (RELSZ) 48 (bytes)
0x00000013 (RELENT) 8 (bytes)
0x6ffffffa (RELCOUNT) 6
0x00000015 (DEBUG) 0x0
0x00000006 (SYMTAB) 0x224
0x0000000b (SYMENT) 16 (bytes)
0x00000005 (STRTAB) 0x2b4
0x0000000a (STRSZ) 340 (bytes)
0x00000004 (HASH) 0x408
0x00000001 (NEEDED) Shared library: [libclang_rt.asan-arm-android.so]
0x00000001 (NEEDED) Shared library: [libc++_shared.so]
0x00000001 (NEEDED) Shared library: [liblog.so]
0x00000001 (NEEDED) Shared library: [libc.so]
0x00000001 (NEEDED) Shared library: [libm.so]
0x00000001 (NEEDED) Shared library: [libstdc++.so]
0x00000001 (NEEDED) Shared library: [libdl.so]
0x0000001a (FINI_ARRAY) 0x1e74
0x0000001c (FINI_ARRAYSZ) 8 (bytes)
0x00000019 (INIT_ARRAY) 0x1e7c
0x0000001b (INIT_ARRAYSZ) 20 (bytes)
0x00000020 (PREINIT_ARRAY) 0x1e90
0x00000021 (PREINIT_ARRAYSZ) 0x8
0x0000000f (RPATH) Library rpath: [/Users/stone/Library/Android/sdk/ndk-bundle/toolchains/llvm/prebuilt/darwin-x86_64/lib64/clang/5.0.300080/lib/linux/arm]
0x0000001e (FLAGS) BIND_NOW
0x6ffffffb (FLAGS_1) Flags: NOW
0x6ffffff0 (VERSYM) 0x440
0x6ffffffe (VERNEED) 0x454
0x6fffffff (VERNEEDNUM) 1
0x00000000 (NULL) 0x0
注意:在使用 libc++_static 时,ASan 目前不兼容 C++ 异常处理。使用 libc++_shared 或不使用异常处理的应用或者不受影响,或者有相应解决方法。如需了解详情,请参阅问题 988。
运行
从 Android O MR1(API 级别 27)开始,应用可以提供可封装或替换应用进程的封装 Shell 脚本。这样一来,可调试的应用就可对其应用启动过程进行自定义,以便在生产设备上使用 ASan。
注意:以下说明将介绍如何将 ASan 与 Android Studio 项目结合使用。对于非 Android Studio 项目,请参阅封装 Shell 脚本文档。
- 将
android:debuggable
和android:extractNativeLibs=true
添加到应用清单。请注意,后者是某些配置的默认设置。如需了解详情,请参阅封装 Shell 脚本。 - 将 ASan 运行时库添加到应用模块的
jniLibs
中。 - 将包含以下内容的
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 时都展开堆栈。这里介绍两个选项:
- 基于帧指针的“快速”展开程序。请按照构建部分中的说明使用此展开程序。
- “慢速”CFI 展开程序。在此模式下,ASan 会使用 _Unwind_Backtrace。它只需要使用 -funwind-tables(通常默认处于启用状态)。
注意:“慢速”展开程序速度缓慢(速度差距达 10 倍或更多,具体取决于您调用 malloc/free 的频率)。
快速展开程序是 malloc/realloc/free 的默认选项。慢速展开程序是严重异常所对应堆栈轨迹的默认选项。通过将 fast_unwind_on_malloc=0
添加到 wrap.sh 的 ASAN_OPTIONS
变量中,即可为所有堆栈轨迹启用慢速展开程序。
二进制测试
由于是二进制程序测试,所以API版本只要高于17就可以了。
- 测试代码
#include <iostream>
int main()
{
std::cout << "Android NDK Address Sanitizer." << std::endl;
char *p = new char[5];
p[5] = 5;
delete (p+1);
return 0;
}
- wrap.sh
#!/system/bin/sh
HERE="$(cd "$(dirname "$0")" && pwd)"
export ASAN_OPTIONS="log_to_syslog=true,allow_user_segv_handler=1,fast_unwind_on_malloc=0"
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
"$@"
- 推送wrap.sh和编译的可执行程序santest到手机
$ adb push libs/armeabi-v7a/santest /data/local/tmp
$ adb push wrap.sh /data/local/tmp
- 执行
$ adb shell
$ cd /data/local/tmp
$ ./wrap.sh ./santest
- 奔溃信息
WARNING: linker: /data/local/tmp/santest: unused DT entry: type 0xf arg 0x331
ASAN:DEADLYSIGNAL
=================================================================
==8874==ERROR: AddressSanitizer: SEGV on unknown address 0x0000001f (pc 0xe98ffa54 bp 0xffadf268 sp 0xffadeb00 T0)
==8874==The signal is caused by a READ memory access.
==8874==Hint: address points to the zero page.
<empty stack>
AddressSanitizer can not provide additional info.
SUMMARY: AddressSanitizer: SEGV
==8874==ABORTING
Aborted