简介:这份资源面向在高通8155p平台上进行QNX+Android9双系统开发的工程师,聚焦于开启ASAN(AddressSanitizer)以定位内存越界、泄漏等疑难问题的完整配置方法。包内同时提供Android.mk与Android.bp两套构建脚本示例,覆盖libAsanDebug、asan_debugd及libclang_rt.asan等模块,帮助开发者解决ASAN无法使能、编译报错等常见障碍。资源共18个文件,包含4个mk、4个bp、4个cpp源码、2个so动态库、2个头文件及2个mk备份,压缩包约1.21MB,结构清晰,可直接对照参考。已有1995人学习下载,适合不熟悉Android.mk与Android.bp写法、或反复遭遇编译问题的中高级开发者,能快速掌握ASAN在QNX+Android9环境下的集成思路与排错方法。
1. 高通8155p 上开 ASAN 就翻车:为什么你的 Android9 编译一开就挂
在高通8155p 这类车机平台上做 Android9 开发,内存问题是最难缠的一类故障。堆越界、use-after-free、双重释放,这些 bug 在正常编译下往往表现得很“玄学”——今天跑得好好的,明天换个场景就 native crash,tombstone 里只有一串地址,根本看不出是谁踩了谁。这时候大家都会想到 ASAN(AddressSanitizer),但真正动手开的时候,十有八九第一步就卡住:要么编译直接报错,要么链接缺符号,要么刷进去开机就起不来。更麻烦的是,8155p 的工程里 Android.mk 和 Android.bp 两套构建文件同时存在,ASAN 的开关在两边写法完全不同,改错一个地方就白忙活。这篇笔记就是把这套流程里我踩过的坑一个个拆开,从报错现象到根因,再到能直接抄的配置,帮你把 ASAN 在 QNX+Android9 双系统环境里真正跑起来。
2. 编译期就报错:Android.mk 与 Android.bp 的 ASAN 开关到底写在哪
很多人第一次开 ASAN,是在BoardConfig.mk里加了一句SANITIZE_TARGET := address,然后make一跑,编译到一半就炸了。报错五花八门,但根因基本都指向同一个:ASAN 的开启粒度是模块级的,不是全局一句话就能搞定。Android9 的构建系统里,SANITIZE_TARGET只对 Soong(Android.bp)体系生效,而大量高通私有模块、HAL、vendor 库还是走 Android.mk,两边必须分别处理。
2.1 Android.bp 模块:sanitize 属性怎么写才不报错
先看 Android.bp 这边。Android9 的 Soong 支持在模块里直接声明 sanitize,写法是:
cc_binary { name: "vendor.demo.memtest", srcs: ["memtest.cpp"], vendor: true, // 关键:开启 address sanitizer,仅对本模块生效 sanitize: { address: true, }, shared_libs: [ "liblog", "libbase", ], }这段配置的逻辑是:sanitize.address: true告诉 Soong 在编译和链接这个模块时插入 ASAN 的运行时插桩,并在链接阶段自动带上libclang_rt.asan相关库。参数上要注意两点:一是vendor: true的模块如果开了 ASAN,链接器会去找 vendor 分区里的 ASAN 运行时,而 Android9 默认的 vendor 镜像里不一定带这个库,所以后面还要处理运行时依赖;二是sanitize块里不要同时写address和hwaddress,两者互斥,同时写会直接报conflicting sanitizers。
如果你在 Android.bp 里写完之后编译报unknown property "sanitize",那说明你的 Soong 版本对这个属性的支持被裁剪过,常见做法是改用cflags手动加-fsanitize=address,但这样链接阶段还得自己补-fsanitize=address的 ldflags,否则会出现“编译过了、链接找不到符号”的经典翻车。
2.2 Android.mk 模块:LOCAL_SANITIZE 的正确姿势
Android.mk 这边走的是另一套变量。Android9 的 make 体系里,ASAN 开关是LOCAL_SANITIZE:
include $(CLEAR_VARS) LOCAL_MODULE := demo_memtest LOCAL_SRC_FILES := memtest.cpp LOCAL_VENDOR_MODULE := true # 关键:声明本模块使用 address sanitizer LOCAL_SANITIZE := address # 如果模块同时依赖静态库,静态库也要带 ASAN 插桩 LOCAL_STATIC_LIBRARIES := libdemo_utils LOCAL_SHARED_LIBRARIES := liblog libbase include $(BUILD_EXECUTABLE)这里最容易踩的坑是:LOCAL_SANITIZE := address只对当前模块生效,它依赖的静态库如果没开 ASAN,链接时会出现undefined reference to __asan_report_load4这类符号缺失。解决办法是把被依赖的静态库模块也加上LOCAL_SANITIZE := address,或者干脆把工具代码直接编进同一个模块。另一个坑是LOCAL_SANITIZE和LOCAL_SANITIZE_RECOVER的区别:前者遇到错误直接 abort,后者会尝试恢复继续跑,定位内存泄漏时用 recover 模式更合适,但定位越界写建议用默认的 abort 模式,能第一时间拿到完整调用栈。
2.3 全局开关 SANITIZE_TARGET 的边界在哪
SANITIZE_TARGET := address写在BoardConfig.mk里,作用是让整个 Soong 构建体系默认带 ASAN。但它的边界很明确:只影响 Android.bp 模块,不影响 Android.mk 模块。而且一旦全局打开,system 分区体积会暴涨,8155p 的 system 分区本来就紧张,很容易刷机时提示partition size exceeded。我一般只在定位阶段对特定模块开,定位完立刻关掉,避免影响正常出包。如果你确实需要全局开,记得同步在BoardConfig.mk里加SANITIZE_TARGET_DIAG := address,否则有些诊断路径不会带插桩,栈信息会缺帧。
3. 链接期符号缺失:undefined reference to __asan 系列报错怎么解
编译过了,链接挂了,报错长这样:undefined reference to '__asan_init'、undefined reference to '__asan_report_load8'。这类报错在 8155p 的 Android9 工程里特别常见,因为高通把很多库拆得很碎,ASAN 运行时库的传递链容易断。
3.1 根因:ASAN 运行时库没有正确传递到 vendor 分区
ASAN 插桩后的模块在链接时需要libclang_rt.asan系列库。在 Android9 里,system 分区的模块能自动找到libclang_rt.asan-aarch64-android.so,但vendor 分区的模块默认找不到,因为 vendor 和 system 的库搜索路径是隔离的。报错里出现__asan符号缺失,九成是这个原因。
解决办法是在模块的链接参数里显式指定 ASAN 运行时,或者把运行时库复制到 vendor 分区。常见做法是在BoardConfig.mk里加:
# 让 vendor 模块也能链接到 ASAN 运行时 BOARD_VENDOR_ASAN_LIBS := libclang_rt.asan-aarch64-android然后在模块的Android.mk里:
LOCAL_SHARED_LIBRARIES += libclang_rt.asan-aarch64-android注意这个库名要和你实际的架构匹配,8155p 是 aarch64,所以是-aarch64-android,写错架构会报library not found。
3.2 静态库依赖链断裂的排查顺序
如果加了运行时库还是报符号缺失,那问题多半在静态库依赖链上。排查顺序我一般是这样:
第一步,用nm -u看报错的.o文件里到底缺哪些符号:
# 在 out 目录下找到报错的中间目标文件 nm -u out/target/product/xxx/obj/STATIC_LIBRARIES/libdemo_utils_intermediates/demo_utils.o | grep asan如果输出里有一堆__asan_开头的未定义符号,说明这个静态库本身没被 ASAN 插桩,但调用它的模块被插桩了,链接时对不上。
第二步,确认静态库模块的Android.mk里有没有LOCAL_SANITIZE := address。没有就补上,然后make clean重新编这个库,因为增量编译不会重新插桩已经编好的.o。
第三步,如果静态库是预编译的.a文件(高通经常这么干),那基本无解——预编译库没法重新插桩。这时候只能把预编译库换成源码编译,或者在链接时用--allow-multiple-definition绕过,但后者只是掩盖问题,运行时照样会崩。
3.3 用 readelf 验证插桩是否真的生效
链接过了不代表插桩生效了。我习惯用readelf确认一下:
# 检查最终可执行文件是否依赖 ASAN 运行时 readelf -d out/target/product/xxx/vendor/bin/demo_memtest | grep -i asan如果输出里有libclang_rt.asan-aarch64-android.so,说明链接阶段带上了。再看符号表:
readelf -s out/target/product/xxx/vendor/bin/demo_memtest | grep __asan_init有__asan_init符号,才说明插桩真正生效。这两个命令能帮你快速区分“编译链接过了但没插桩”和“真正开了 ASAN”两种情况,避免刷进去发现没效果又回头重查。
4. 运行时崩溃:ASAN 报错日志看不懂、进程起不来怎么办
编译链接都过了,刷进 8155p 开机,结果进程直接 abort,logcat 里一堆 ASAN 输出,但关键信息被截断或者看不懂。这一章专门解决运行时的问题。
4.1 ASAN 日志被截断:logcat 缓冲区与 abort 时机
ASAN 报错时默认往 stderr 输出,Android 上 stderr 会重定向到 logcat。但 logcat 的缓冲区有限,ASAN 的完整报告动辄几百行,很容易被冲掉。解决办法有两个:
一是设置 ASAN 选项,把报告写到文件:
# 在 init.rc 或启动脚本里设置环境变量 export ASAN_OPTIONS=log_path=/data/local/tmp/asan_log:abort_on_error=1:detect_leaks=0log_path会让 ASAN 把报告写到/data/local/tmp/asan_log.<pid>,不会被 logcat 冲掉。abort_on_error=1保证出错立即 abort,方便抓 tombstone。detect_leaks=0是因为 Android9 上 LeakSanitizer 经常误报,定位越界问题时先关掉。
二是调大 logcat 缓冲区:
# 临时调大 logcat 缓冲区,重启失效 adb logcat -G 16M但这个方法治标不治本,ASAN 报告太长还是可能丢,所以优先用log_path。
4.2 进程起不来:ASAN 运行时库没进 vendor 镜像
刷机后进程直接起不来,logcat里报CANNOT LINK EXECUTABLE ... library "libclang_rt.asan-aarch64-android.so" not found。这是最典型的运行时依赖缺失。根因是 ASAN 运行时库只编进了 system 分区,但你的模块在 vendor 分区,vendor 的 linker 搜索路径里没有 system 的库。
解决办法是在BoardConfig.mk里把 ASAN 运行时库加到 vendor 镜像:
# 把 ASAN 运行时库复制到 vendor/lib64 BOARD_VENDOR_ASAN_RUNTIME := true如果这个变量在你的工程里不生效,那就手动在device.mk里加:
PRODUCT_COPY_FILES += \ prebuilts/clang/host/linux-x86/clang-4691093/lib64/clang/6.0.2/lib/linux/libclang_rt.asan-aarch64-android.so:$(TARGET_COPY_OUT_VENDOR)/lib64/libclang_rt.asan-aarch64-android.so路径里的 clang 版本号要和你实际用的编译器版本一致,写错会报file not found。复制完之后make vendorimage重新刷 vendor 分区,进程就能起来了。
4.3 QNX+Android9 双系统下 ASAN 的额外注意点
8155p 是 QNX 和 Android9 双系统,ASAN 只对 Android 侧的进程生效,QNX 侧的进程不能用这套方法。另外,双系统共享内存区域如果被 Android 侧 ASAN 插桩的进程访问,ASAN 会把共享内存的合法访问误判为越界,因为 ASAN 的 shadow memory 不覆盖 QNX 管理的那段物理内存。常见做法是在 ASAN 选项里加:
export ASAN_OPTIONS=log_path=/data/local/tmp/asan_log:abort_on_error=1:detect_leaks=0:intercept_tls_get_addr=0intercept_tls_get_addr=0能减少跨系统 TLS 访问的误报。如果还是误报,那就只能对访问共享内存的代码段单独关掉插桩,用__attribute__((no_sanitize_address))标注相关函数。
5. 避坑清单:开 ASAN 时最容易翻车的五个操作
这一章是我自己踩过的坑,每条都按“现象 → 原因 → 解决”写,你对照着排查能省不少时间。
现象一:make报SANITIZE_TARGET未知变量。原因是你把它写在了Android.mk或device.mk里,这个变量只认BoardConfig.mk。解决:挪到BoardConfig.mk,并且确认你的 Soong 版本支持这个变量,不支持就改用模块级sanitize。
现象二:编译过了,刷机后进程一启动就 abort,但 ASAN 报告里全是unknown module。原因是符号表被 strip 了,ASAN 拿不到函数名。解决:在模块的Android.mk里加LOCAL_STRIP_MODULE := false,或者在BoardConfig.mk里加TARGET_STRIP_MODULE := false,保留符号表再刷。
现象三:ASAN 报告里调用栈只有地址没有行号。原因是编译时没加-g。解决:在模块的cflags里加-g,Android.bp 里加cflags: ["-g"],Android.mk 里加LOCAL_CFLAGS += -g。注意-g和-O2可以共存,不会影响性能太多。
现象四:开了 ASAN 之后系统卡顿严重,开机时间翻倍。原因是全局SANITIZE_TARGET把所有模块都插桩了,CPU 和内存开销巨大。解决:只对目标模块开 ASAN,全局开关定位完立刻关掉。如果必须全局开,把detect_leaks关掉,能省不少开销。
现象五:Android.mk 和 Android.bp 同时存在时,改了 bp 没生效。原因是同名模块在 make 体系里优先级更高,Soong 的配置被覆盖了。解决:确认模块到底走哪套构建,用make的-d参数看模块解析路径,或者直接看out目录下模块的Android.mk和Android.bp哪个被实际使用。
6. 快速定位 ASAN 问题的排查路径与验证习惯
定位 ASAN 问题,我总结了一条从快到慢的路径,按这个顺序走,基本不会绕远路。
第一步,确认插桩是否生效。用readelf -s看目标文件里有没有__asan_init,没有就回头查编译配置,别急着刷机。这一步能过滤掉一半的“假 ASAN”问题。
第二步,确认运行时库是否到位。用adb shell进设备,ls /vendor/lib64/libclang_rt.asan*看库在不在,不在就查BoardConfig.mk和device.mk的复制规则。
第三步,抓完整 ASAN 报告。设置ASAN_OPTIONS=log_path=/data/local/tmp/asan_log:abort_on_error=1,复现问题后从/data/local/tmp/拉报告,别依赖 logcat。
第四步,解析调用栈。报告里的地址用addr2line转成行号:
# 用带符号的库解析地址 aarch64-linux-android-addr2line -e out/target/product/xxx/vendor/bin/demo_memtest -f -C 0x123456-f输出函数名,-C做符号 demangle,两个参数一起用才能看到可读的 C++ 函数名。
第五步,如果栈信息还是不全,检查是不是跨模块调用。ASAN 只能看到插桩模块内部的调用栈,跨到没插桩的模块就断了。这时候要么把相关模块都插桩,要么用no_sanitize_address标注边界函数,缩小排查范围。
下面这张表是我常用的 ASAN 选项速查,按场景选:
| 场景 | ASAN_OPTIONS 配置 | 说明 |
|---|---|---|
| 定位越界写 | abort_on_error=1:detect_leaks=0 | 出错立即 abort,抓 tombstone |
| 定位内存泄漏 | detect_leaks=1:abort_on_error=0 | 不 abort,跑完输出泄漏报告 |
| 报告太长被截断 | log_path=/data/local/tmp/asan_log | 写文件,不依赖 logcat |
| 跨系统共享内存误报 | intercept_tls_get_addr=0 | 减少 QNX 共享内存误报 |
| 性能开销太大 | detect_leaks=0:quarantine_size_mb=16 | 减小隔离区,降开销 |
从那以后我每次在 8155p 上开 ASAN,都强制走一遍“readelf 验证插桩 → 确认运行时库 → 设 log_path → addr2line 解析”这四步,不再凭感觉刷机。这套流程帮我省下了大量反复刷机的时间,也希望帮到你。
本文还有配套的精品资源,点击获取