AddressSanitizer (ASan) 是一款基于编译器的快速工具,用于检测原生代码中的内存错误。
ASan 检测
- 堆栈和堆缓冲区溢出/下溢
- 堆释放后使用
- 堆栈超出作用域使用
- 重复释放/非法释放
ASan 可在 32 位和 64 位 ARM 以及 x86 和 x86-64 上运行。ASan 的 CPU 开销约为 2 倍,代码大小开销介于 50% 到 2 倍之间,并且内存开销很大(取决于您的分配模式,但约为 2 倍)。
Android 10 和 AArch64 上的 AOSP 主分支支持硬件辅助 AddressSanitizer (HWASan),这是一款类似的工具,具有更低的 RAM 开销和更大的检测错误范围。除了 ASan 检测到的错误之外,HWASan 还能检测堆栈返回后使用。
HWASan 具有类似的 CPU 和代码大小开销,但 RAM 开销要小得多 (15%)。HWASan 是非确定性的。只有 256 个可能的标记值,因此遗漏任何错误的概率固定为 0.4%。HWASan 没有 ASan 用于检测溢出的有限大小的红色区域和用于检测释放后使用的有限容量隔离区,因此溢出有多大或内存被释放多久对 HWASan 而言无关紧要。这使得 HWASan 比 ASan 更好。您可以详细了解 HWASan 的设计或关于 在 Android 上使用 HWASan 的信息。
ASan 除了堆溢出外,还可以检测堆栈/全局溢出,并且速度快,内存开销极小。
本文档介绍了如何使用 ASan 构建和运行部分/全部 Android。如果您要使用 ASan 构建 SDK/NDK 应用,请参阅Address Sanitizer。
使用 ASan 清理单个可执行文件
将 LOCAL_SANITIZE:=address
或 sanitize: { address: true }
添加到可执行文件的构建规则。您可以搜索代码以查找现有示例或查找其他可用的清理器。
当检测到错误时,ASan 会将详细报告同时输出到标准输出和 logcat
,然后使进程崩溃。
使用 ASan 清理共享库
由于 ASan 的工作方式,使用 ASan 构建的库只能供使用 ASan 构建的可执行文件使用。
要清理在多个可执行文件中使用的共享库(并非所有可执行文件都使用 ASan 构建),您需要该库的两个副本。建议的方法是将以下内容添加到相关模块的 Android.mk
中
LOCAL_SANITIZE:=address LOCAL_MODULE_RELATIVE_PATH := asan
这会将库放在 /system/lib/asan
而不是 /system/lib
中。然后,使用以下命令运行您的可执行文件:
LD_LIBRARY_PATH=/system/lib/asan
对于系统守护进程,请将以下内容添加到 /init.rc
或 /init.$device$.rc
的相应部分。
setenv LD_LIBRARY_PATH /system/lib/asan
通过读取 /proc/$PID/maps
验证进程是否在使用 /system/lib/asan
中的库(如果存在)。如果不是,您可能需要停用 SELinux
adb root
adb shell setenforce 0
# restart the process with adb shell kill $PID # if it is a system service, or may be adb shell stop; adb shell start.
更好的堆栈轨迹
ASan 使用基于帧指针的快速展开器来记录程序中每个内存分配和释放事件的堆栈轨迹。大多数 Android 都是在没有帧指针的情况下构建的。因此,您通常只能获得一个或两个有意义的帧。要解决此问题,请使用 ASan 重新构建库(推荐!),或者使用
LOCAL_CFLAGS:=-fno-omit-frame-pointer LOCAL_ARM_MODE:=arm
或者在进程环境中设置 ASAN_OPTIONS=fast_unwind_on_malloc=0
。后者可能非常占用 CPU 资源,具体取决于负载。
符号化
最初,ASan 报告包含对二进制文件和共享库中偏移量的引用。有两种方法可以获取源文件和行信息:
- 确保
llvm-symbolizer
二进制文件存在于/system/bin
中。llvm-symbolizer
是从third_party/llvm/tools/llvm-symbolizer
中的源代码构建的。 - 通过
external/compiler-rt/lib/asan/scripts/symbolize.py
脚本过滤报告。
第二种方法可以提供更多数据(即 file:line
位置),因为主机上提供了符号化的库。
应用中的 ASan
ASan 无法查看 Java 代码,但它可以检测 JNI 库中的错误。为此,您需要使用 ASan 构建可执行文件,在本例中为 /system/bin/app_process(32|64)
。这将同时在设备上的所有应用中启用 ASan,这是一个很大的负载,但具有 2 GB RAM 的设备应该能够处理这种情况。
将 LOCAL_SANITIZE:=address
添加到 frameworks/base/cmds/app_process
中的 app_process
构建规则。暂时忽略同一文件中的 app_process__asan
目标(如果在您阅读本文档时它仍然存在)。
编辑相应的 system/core/rootdir/init.zygote(32|64).rc
文件的 service zygote
部分,将以下行添加到包含 class main
的缩进行块中,并且也缩进相同的量
setenv LD_LIBRARY_PATH /system/lib/asan:/system/lib setenv ASAN_OPTIONS allow_user_segv_handler=true
构建、adb sync、fastboot flash boot,然后重启。
使用 wrap 属性
上一节中的方法将 ASan 放入系统中的每个应用(实际上是 Zygote 进程的每个后代)。可以仅使用 ASan 运行一个(或多个)应用,以一些内存开销换取较慢的应用启动速度。
这可以通过使用 wrap.
属性启动您的应用来完成。以下示例在 ASan 下运行 Gmail 应用:
adb root
adb shell setenforce 0 # disable SELinux
adb shell setprop wrap.com.google.android.gm "asanwrapper"
在此上下文中,asanwrapper
将 /system/bin/app_process
重写为 /system/bin/asan/app_process
,后者是使用 ASan 构建的。它还在动态库搜索路径的开头添加了 /system/lib/asan
。这样,当使用 asanwrapper
运行时,来自 /system/lib/asan
的 ASan 检测库优先于 /system/lib
中的普通库。
如果发现错误,应用会崩溃,并且报告会打印到日志中。
SANITIZE_TARGET
Android 7.0 及更高版本包括一次性使用 ASan 构建整个 Android 平台的支持。(如果您构建的版本高于 Android 9,HWASan 是更好的选择。)
在同一构建树中运行以下命令。
make -j42
SANITIZE_TARGET=address make -j42
在此模式下,userdata.img
包含额外的库,也必须刷写到设备。使用以下命令行:
fastboot flash userdata && fastboot flashall
这将构建两组共享库:/system/lib
中的普通库(第一次 make 调用)和 /data/asan/lib
中的 ASan 检测库(第二次 make 调用)。第二次构建中的可执行文件会覆盖第一次构建中的可执行文件。ASan 检测的可执行文件会获得不同的库搜索路径,该路径通过 PT_INTERP
中的 /system/bin/linker_asan
在 /system/lib
之前包含 /data/asan/lib
。
当 $SANITIZE_TARGET
值发生更改时,构建系统会清理中间对象目录。这会在保留 /system/lib
下的已安装二进制文件的同时强制重建所有目标。
某些目标无法使用 ASan 构建
- 静态链接的可执行文件
LOCAL_CLANG:=false
目标- 对于
SANITIZE_TARGET=address
,LOCAL_SANITIZE:=false
未进行 ASan 处理
在 SANITIZE_TARGET
构建中会跳过此类可执行文件,并且第一次 make 调用中的版本会保留在 /system/bin
中。
此类库在不使用 ASan 的情况下构建。它们可能包含来自其依赖的静态库的一些 ASan 代码。