接下来是,heap的第五大板块——ASan(Address Sanitizer)和HWASan(Hardware Address Sanitizer)。可以将其称为:地址清理器
与其说是Heap板块,不如说是debug板块。
ASan是一个集成在编译器中的工具,因此只需要在编译的时候设置好Flag即可。而HWASan则可以认为是ASan的plus版本。HWASan比ASan有如下的优点:
注意:自2023年起,ASan不再支持,建议使用HWASan。
本文章先简单介绍一下原理,然后详细介绍ASan的使用,以及对输出结果的解析。在下一篇中介绍HWASan的使用
ASan在编译和链接阶段,将一些特殊的检查代码和内存管理代码插入到程序中。当程序运行时,这些插入的代码将负责管理内存的分配和释放。
比如:当应用程序调用malloc时,实际上调用的是Asan提供的malloc版本。ASan版本的malloc除了基本的内存分配以外,还会做额外的动作如:在分配的内存周围加上一个特殊区域(Red zones)
用于检测内存越界问题;同时对分配的内存每8个字节一组,分配一个影子内存,记录该组的使用情况,这样可以精确的记录每个内存的使用情况。
除了上面的介绍的Red Zone和影子内存外,ASan还会使用,如下的技术:
注意:ASan似乎和malloc debug功能一致。事实上,因为ASan可以在编译和链接阶段插入代码,它比malloc debug的动态检测更加丰富和齐全。当然ASan会有更大的开销。
现在只需要修改相应的Flag即可。如下:
## 在编译的时候,启用ASan,并且不要省略栈帧(-fno-omit-frame-pointer),这对于打印可读的栈帧非常友好
APP_CFLAGS := -fsanitize=address -fno-omit-frame-pointer
## 在链接的时候,启用ASan
APP_LDFLAGS := -fsanitize=address
ASan对内存布局和寻址方式上面有一定的要求,如果是arm架构,需要明确指定,以arm模式编译,而不是thumb模式编译如下:
## 在每一个Android.mk中都需要添加如下的代码
LOCAL_ARM_MODE := arm
除了makefie以外,还可以使用Cmakefile如下:
## 设置编译选项,启用ASan功能,并且不省略栈帧(-fno-omit-frame-pointer)
target_compile_options(${TARGET} PUBLIC -fsanitize=address -fno-omit-frame-pointer)
## 设置链接选项,启用ASan功能
set_target_properties(${TARGET} PROPERTIES LINK_FLAGS -fsanitize=address)
同样也需要设置arm模式编译,如下:
defaultConfig {
externalNativeBuild {
cmake {
abiFilters "arm64-v8a"
cmake {
arguments "-DANDROID_ARM_MODE=arm"
}
}
}
}
因为ASan需要使用各种动态库,而这些动态库在Android设备默认是没有的。因此需要将这些动态库,放入Android设备中。
有两种方法将相应的动态库放入设备中:
方法一:使用wrap.sh
wrap.sh的详细介绍,参见:android 如何分析应用的内存(七)下面只做使用说明。
#!/system/bin/sh
## 获取当前脚本的路径,并将其赋值给HERE
HERE="$(cd "$(dirname "$0")" && pwd)"
## 定义几个环境变量,其中allow_user_segv_handler表示当程序运行出现问题时,内核发出的
## SIGSEGV信号,可以被处理,而不是简单的结束程序
export ASAN_OPTIONS=log_to_syslog=false,allow_user_segv_handler=1
## ASAN_LIB为运行时库
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
## 执行程序
"$@"
上面的例子我只放入了arm64-v8a的运行时库。同时其他的so库,为前面文章需要的so库。
注意:使用wrap.sh只能是大于等于 Android 8.1的设备
方法二:使用NDK中的脚本
使用如下的脚本:NDK目录/toolchains/llvm/prebuilt/host平台/lib64/clang/版本/bin/asan_device_setup.
将运行时库,push到设备中,该设备必须能够取得root权限。功能如同wrap.sh中一样。asan_device_setup的内部细节不在赘述,因为这里更加推荐wrap.sh的用法。事实上,asan_device_setup会修改相应的app_process,让它能够方便的加载一些预定义库。
注意:这种方法,并未得到官方的大力支持,因此部分设备可能存在错误
如果想要撤销对应的运行时库,可如下运行:
asan_device_setup --revert
做如下代码验证:
auto *p1 = new int;
*(p1+4) = 2345678;
选取libtest_malloc.so加号后面的地址,传递给llvm-symbolizer或者addr2line.举例如下:
~/Library/Android/sdk/ndk/25.2.9519653/toolchains/llvm/prebuilt/darwin-x86_64/bin/llvm-symbolizer -e ./app/build/intermediates/merged_native_libs/debug/out/lib/arm64-v8a/libtest_malloc.so 0x289a0
Java_com_example_test_1malloc_MainActivity_stringFromJNI
/Users/biaowan/AndroidStudioProjects/Test_Malloc.old/app/src/main/cpp/native-lib.cpp:212:13
可以解析问题点在具体的文件和行数。
__attribute__((no_sanitize_address))
extern "C" JNIEXPORT jstring JNICALL
Java_com_example_test_1malloc_MainActivity_stringFromJNI(JNIEnv* env,jobject /* this */){}
可以看见如下选项
其中可以打开,new和delete不匹配的检测,malloc和demalloc不匹配的检查等
添加ASAN_OPTIONS=log_path=/sdcard/asan
会将信息输出到/sdcard/asan.pid文件中
ASAN_OPTIONS=detect_leaks=true
注意:实验未通过,没有很好的检查出内存泄漏
ASAN_OPTIONS=disable_coredump=0
注意:如果无法生成coredump,则需要检查,Android是否打开了coredump。因为每个平台的打开coredump 的步骤不同,因此具体平台,请参阅相应文档
ASan还可以和gdb和lldb联合使用。gdb和lldb的使用,见前面的章节。
如果想要gdb或lldb停留在ASan报告错误之前,可以在如下函数设置断点:
__asan::ReportGenericError
如果想要gdb或lldb停留在ASan报告错误之后,可以在如下函数设置断点:
__sanitizer::Die
如果想要gdb或lldb打印Asan描述的内存信息,可以调用下面的函数
__asan_describe_address(地址)
如下:(gdb例子)
(gdb) set overload-resolution off
(gdb) p __asan_describe_address(0x7ffff73c3f80)
0x7ffff73c3f80 is located 0 bytes inside of 10-byte region [0x7ffff73c3f80,0x7ffff73c3f8a)
freed by thread T0 here:
...
第一句话是关闭重载函数的解析。第二句话是,调用__asan_describe_address函数,解析0x7ffff73c3f80地址。输出则为对应地址的描述信息
至此,Android的ASan介绍完毕,下一篇会介绍HWASan的使用,因为HWAsan需要编译AOSP,篇幅较长,敬请期待