部署 AddressSanitizer(ASan)定位内存泄漏、内存越界
AddressSanitizer(ASan)
ASan(AddressSanitizer,地址消毒器)是一个内存错误检测工具,主要用于帮程序员:
检测内存越界访问(比如访问数组边界外的数据)
检测使用已释放的内存(Use-after-free)
检测堆、栈、全局变量的内存错误
发现内存泄漏(部分版本支持)
它是编译器提供的一种运行时检测机制,通过插桩程序,在程序运行时监控内存访问行为,及时发现和定位内存安全问题,帮助开发者快速修复难以排查的 bug。
但是NDK r21e 不支持内存泄漏检查:
(base) wyl9738@arc-PowerEdge-R740xd:/data/standard_sdk/toolchains/NDK21E/android-ndk-r21e/toolchains/llvm/prebuilt/linux-x86_64/lib64/clang/9.0.9/lib/linux$ strings ./libclang_rt.asan-aarch64-android.so | grep detect_leaks
detect_leaks
%s: detect_leaks is not supported on this platform.
Invoke leak checking in an atexit handler. Has no effect if detect_leaks=false, or if __lsan_do_leak_check() is called before the handler has a chance to run.
✅ 升级方案建议
- 下载 NDK r25c。
- 修改你的 CMake 工程,改成使用新的 toolchain(路径替换原来的 r21e)。
- 确保编译命令中保留:
部署 AddressSanitizer(ASan)流程(以Android 平台为例)
1、部署Asan
cmake文件中添加:
project(***)
set(CMAKE_CXX_STANDARD 11)
# 原本的 warning 设置也要保留原有值
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -g -std=c++11 -Wpedantic -Wextra -Wshadow -Wundef -Wmaybe-uninitialized \
-Wno-unused-parameter -Wno-missing-field-initializers -Wno-format-nonliteral -Wno-cast-qual -Wunreachable-code -Wno-switch-default \
-Wreturn-type -Wmultichar -Wformat -Wformat-security -Wdouble-promotion -Wclobbered -Wdeprecated -Wempty-body -Wstack-usage=2048 \
-Wtype-limits -Wsizeof-pointer-memaccess -Wpointer-arith -Werror=return-type")
set(CMAKE_SYSROOT "/data/standard_sdk/toolchains/NDK21E/android-ndk-r21e/toolchains/llvm/prebuilt/linux-x86_64/sysroot")
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -Wl,-rpath-link=${CMAKE_SYSROOT}/usr/lib/aarch64-linux-android/29")
# 启用 AddressSanitizer 添加 ASan 相关 flags(必须放在最后)
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -fsanitize=address,leak -fno-omit-frame-pointer")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fsanitize=address,leak -fno-omit-frame-pointer")
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -fsanitize=address")
# 添加库搜索路径
link_directories("${CMAKE_SYSROOT}/usr/lib/aarch64-linux-android/29")
如果找不到动态库,可以在交叉编译工具路径下搜索:
安卓:find ./ -name "libclang_rt.asan-aarch64-android.so"
linux:find ./ -name "libasan.so*"
standard_sdk/toolchains/arm-ca53-linux-gnueabihf-8.4$ find ./ -name "libasan.so*"
./arm-ca53-linux-gnueabihf/lib/libasan.so.5
./arm-ca53-linux-gnueabihf/lib/libasan.so
./arm-ca53-linux-gnueabihf/lib/libasan.so.5.0.0
2、compile.sh中添加 -DANDROID_TOOLCHAIN=clang \
#!/bin/sh
ANDROID_NDK=/data/standard_sdk/toolchains/NDK21E/android-ndk-r21e
cd ../../src_code/
rm -rf build
mkdir -p build
cd build
cmake .. -DCMAKE_TOOLCHAIN_FILE=$ANDROID_NDK/build/cmake/android.toolchain.cmake \
-DANDROID_ABI="arm64-v8a" \
-DANDROID_NDK=$ANDROID_NDK \
-DANDROID_PLATFORM=android-29 \
-DCMAKE_BUILD_TYPE=Debug \
-DANDROID_TOOLCHAIN=clang \
-DMODULE="$MODULE" \
-DLEVEL="$LEVEL"
make -j$(nproc)
3、编译通过后检查是否生效
方法1:查看编译时的链接信息
如果你能拿到编译好的可执行文件(比如 Api_Test_Tool),可以在编译环境用 readelf 或 nm 等工具检查:
readelf -d Api_Test_Tool | grep asan
方法2:用 strings 查找符号
在编译好的二进制中用 strings 搜索 __asan_ 符号:
strings Api_Test_Tool | grep __asan_
4、asan动态库拷贝到设备上, export ASAN_OPTIONS=detect_leaks=1
命令查找asan库:find /data/standard_sdk/ -name "libclang_rt.asan-aarch64-android.so"
输出:
/data/standard_sdk/toolchains/NDK21E/android-ndk-r21e/toolchains/llvm/prebuilt/linux-x86_64/lib64/clang/9.0.9/lib/linux/libclang_rt.asan-aarch64-android.so
把这个动态库放到设备上和别的so一起
(查找所有依赖的动态库 readelf -d Api_Test_Tool 或 ldd (如果支持) 查看依赖库)
linux平台:
standard_sdk/toolchains/arm-ca53-linux-gnueabihf-8.4$ find ./ -name "libasan.so*"
./arm-ca53-linux-gnueabihf/lib/libasan.so.5
./arm-ca53-linux-gnueabihf/lib/libasan.so
./arm-ca53-linux-gnueabihf/lib/libasan.so.5.0.0
5、在设备上执行程序,看到发生问题的偏移地址
6、用addr2line 查看是哪一行代码
命令:addr2line -e Api_Test_Tool(可执行程序) 0x89354(偏移地址)
7、Asan输出解析
🚨 第一部分:检测到的错误类型和位置
==11188==ERROR: AddressSanitizer: heap-buffer-overflow on address 0xfec86eb0705a
- ==11188==:当前进程 ID 是 11188,ASan 对这个进程做了监控。
- ERROR: AddressSanitizer::说明是由 ASan 发现的问题。
- heap-buffer-overflow:表示在堆上分配的内存发生了越界访问,比如 malloc(10) 之后访问 buf[10]。
- on address 0xfec86eb0705a:问题发生的内存地址。
🧭 第二部分:发生错误时的调用栈(Backtrace)
at pc 0xc73027ab9358 bp 0xffffe6f33750 sp 0xffffe6f33748
- pc:程序计数器(Program Counter)当前在哪条指令。
- bp:栈基址(Base Pointer)。
- sp:栈顶指针(Stack Pointer)。
- 都是当前线程在执行越界访问指令时的寄存器值。
🛠 第三部分:哪一行代码越界写了内存?
WRITE of size 1 at 0xfec86eb0705a thread T0
- 说明写入了 1 字节的数据,地址是 0xfec86eb0705a
- thread T0:在主线程 T0 中发生。
🧱 第四部分:调用堆栈(stack trace)
你看到的是每一帧函数调用,最底层是 main,往上是调用链:
#0 0xc73027ab9354 (/data/local/tmp/DMS/API_TEST/Api_Test_Tool_0805+0x89354)
#1 0xc73027ac9850 ...
...
#10 0xfee776275798 (/apex/com.android.runtime/lib64/bionic/libc.so+0x7d798)
#0 0xc73027ab9354 (/data/local/tmp/DMS/API_TEST/Api_Test_Tool_0805+0x89354)
0xc73027ab9354 是程序运行时的 虚拟地址
0x89354 是可执行文件中的 偏移地址,也就是函数或语句的位置
🔍 第五部分:越界写的区域在哪里?
0xfec86eb0705a is located 0 bytes to the right of 10-byte region [0xfec86eb07050,0xfec86eb0705a)
- 说明你 malloc(10) 了一个缓冲区,地址从 0x7050 到 0x705a(不含)。
- 而你写入了 0x705a,也就是第 11 个字节,正好越界 1 字节。
💡 第六部分:ASan 追踪到了谁分配的这个内存?
allocated by thread T0 here:
#0 0xfee773af4e24 (libclang_rt.asan-aarch64-android.so+0x9fe24)
#1 0xc73027ab92f0 (/data/local/tmp/DMS/API_TEST/Api_Test_Tool_0805+0x892f0)
...
- ASan 自动记录了这块内存的分配栈。
- 第一帧是 libclang_rt.asan 内部的 malloc。
- 第二帧 onward 是你程序分配内存的代码位置。你可以用 addr2line 查出具体源代码位置
🧪 第七部分:Shadow Bytes(阴影字节)
ruby
复制编辑
Shadow bytes around the buggy address:
0x1fe90dd60db0: fa fa 00 fa fa fa 04 fa ...
=>0x1fe90dd60e00: fa fa 00 fa fa fa 00 00 fa fa 00[02]fa fa fa fa
- ASan 在内存两侧加了 红区(Redzone),保护内存边界。
- 如果你写入了红区,它会立即报警。
- 00 代表合法使用,fa 是红区,02 代表只允许访问前 2 字节。
- 你的访问命中了非法的 redzone,触发错误。
mtrace和ASan 核心对比
特性 | ASan | mtrace |
✅ 能检测内存泄漏 | 是(部分版本支持 LeakSanitizer) | 是 |
✅ 检测越界访问(heap/stack) | 是 | 否 |
✅ 检测 use-after-free | 是 | 否 |
✅ 检测未初始化内存访问 | 是(部分支持) | 否 |
⚙️ 实现方式 | 编译期插桩 + 运行时监控 | glibc 提供的 malloc/free hook |
⚠️ 使用成本 | 需要重新编译(加 -fsanitize=address) | 只需 glibc 和设置环境变量 |
📊 性能开销 | 中等偏高(约 2~3 倍内存+CPU) | 很低 |
🛠️ 运行平台 | 多平台(x86、arm、Android) | 仅限使用 GNU glibc 的 Linux |
📋 报告输出 | 实时终端输出,或日志 | 生成 log 文件,要用 mtrace 命令查看 |
📦 嵌入式适配性 | ✅ 可交叉编译,适合嵌入式调试 | ❌ glibc 限制,不适用于 musl/uClibc/Android |
ASan原理
通过 编译器插桩(compiler instrumentation)+ runtime 库
实现以下检测:
功能 | 原理说明 |
越界访问 | 为每个对象分配前后 redzone 并填充特殊值,运行时访问触发检查 |
use-after-free | free 后填充为不可访问 shadow 值,一旦访问即报错 |
内存泄漏 (via LSan) | 程序退出前做 GC-like 遍历,发现无法访问的 heap block 就认为是泄漏 |
双重释放 | 内部记录每次释放,第二次访问将触发错误 |
Shadow Memory 机制:
ASan 使用一段shadow memory区域,按 8:1 比例映射应用内存,并标记每个内存块的状态(有效、redzone、已释放等)。
ASAN_OPTIONS 环境变量参数
参数名 | 说明 | 示例设置 |
abort_on_error | 出错时是否调用 abort 终止程序 | abort_on_error=0(不终止) |
log_path | 将 ASan 日志写入指定文件 | log_path=asan.log |
detect_stack_use_after_return | 检测函数返回后栈空间使用 | detect_stack_use_after_return=1 |
detect_leaks | 是否开启 LeakSanitizer 检测 | detect_leaks=1 |
allocator_may_return_null | 分配失败时是否返回 nullptr 而非崩溃 | allocator_may_return_null=1 |
fast_unwind_on_malloc | malloc/free 调用栈回溯是否使用快速模式 | fast_unwind_on_malloc=0 |
suppressions | 指定抑制文件,忽略特定错误 | suppressions=suppress.txt |
verbosity | 日志详细程度(0-3) | verbosity=2 |
max_malloc_fill_size | 分配内存时填充最大字节数 | max_malloc_fill_size=0 |
设置环境变量
不?终止,日志写文件,开启额外检测
export ASAN_OPTIONS="abort_on_error=1:log_path=./asan.log:detect_stack_use_after_return=1:disable_coredump=0"
ASAN_OPTIONS="abort_on_error=0:log_path=./asan.log:detect_stack_use_after_return=1" ./your_program
确认环境变量生效
env | grep ASAN_OPTIONS
关于 AddressSanitizer (ASan) 对内存泄漏检测(LeakSanitizer,简称 LSan)的支持情况:
- LLVM/Clang 支持情况
- LeakSanitizer 是 ASan 的一个子集,主要负责内存泄漏检测。
- LeakSanitizer 在 Clang 3.8 及以后版本中开始得到较好的支持。
- Android NDK 从 r15 版本开始集成了 ASan 和 LeakSanitizer 支持,但不同 NDK 版本的支持情况可能会有所不同。
- GCC 支持情况
- GCC 在较新的版本(9.0 及以后)也支持 ASan 和 LeakSanitizer,但 Android NDK 默认使用的 GCC 较旧,可能不支持 LeakSanitizer。
- Android NDK 版本支持
- 从 Android NDK r16 及以后的版本,ASan 与 LeakSanitizer 支持更完善。
- Android NDK r21 及以后版本对 LeakSanitizer 支持较好,能检测内存泄漏。
- 注意
- LeakSanitizer 并不是所有 ASan 版本默认启用,有些版本需要手动开启。
- 需要确保链接时带上 -fsanitize=leak 或 -fsanitize=address(新版 ASan 会自动包含 LeakSanitizer)。
- 在某些交叉编译环境下,LeakSanitizer 可能无法正常工作,特别是早期的 Android NDK。