如果你是一名c++ coder, 我相信你一定大概率遇到过和我类似的遭遇: 在产品稳定性测试接近正式发布尾声时,不幸遇到程序在批量环境运行时,零星出现因内存崩溃重启问题(开发环境难以构建)。具体的完整场景时,加速卡整机发布时,在120个容器实例长稳运行极限压力测试时,平均运行5天出现一起实例崩溃而重启,面对完整的监控无处可逃,通过core文件分析,出现位置随机,比如一个变量声明,函数结束,或者一个stl容器尺寸超大,或者malloc size超大,总之是不应该出现的位置或者说程序死亡时早已不是第一现场。我们知道内存问题尤其是写越界不一定会表现或者很快表现出来,当然也不是我们想复现就能复现出来。关于内存泄漏类问题很多成熟手段是可以轻松定位,但是写越界问题一直是个挑战,尤其在笔者的环境,涉及多个团队多个so之间的内存互相访问对问题定性定界都尤为困难。我在2个月内断断续续更新了50个版本到测试环境进行验证,有幸的是最终通过asan工具彻底解决了内存崩溃问题,甚至挖掘出上游团队隐藏数年的历史bug。
通过3-4次复现,并通过core文件分析,表现出堆栈随机不固定,且出现不应该出现问题,比如结构体生命,vector尺寸异常,函数退出时。通过gdb查看个内存变量值,出现某一块内存数据被破坏。同时对相关业务代码进行了详细走读,并无明显收获。刚开始我确实尝试有小范围通过asan版本诊断,但是由于其他组件库出现读溢出导致无法继续,由于我的工程和运行环境是否复杂,对ASAN是否能解决我的问题仍然比较忐忑,在其他手段无效条件下,推动多个团队(芯片库,算法库)同时提供ASAN版本助力挖掘这个问题。
ASAN的报告如下,指向非常明确,说明谁正在读取已经被谁释放的内存。经非常详细的业务流程的逻辑分析,发现当数据压力处于极限状态时,确实可能出现这种概率较小的时机导致同一个数据结构被并发读写,通过模拟极限条件触发出崩溃现象(非ASAN版本)。而同时对ASAN报告中一些读溢出的警告进行深度排查,确实定位到一些历史bug有读内存问题,虽然不至于崩溃,但仍然是非常有价值的发现。最终将ASAN报告体现的错误全部清扫干净之后,程序变得非常稳定。
AddressSanitizer (ASAN) 是编译器级别的快速内存错误检测器,专门用于发现 C/C++ 程序中的内存问题。
简单来说: 一个编译选项,让你的程序自己报告内存错误
结论 :ASAN 性能开销仅 2 倍,远低于 Valgrind,适合日常开发使用。
栈溢出通常非常隐藏,极容易产生次生灾害,因为不同业务逻辑触发栈的深度可能不一致,栈溢出不一定表现为崩溃,取决于读到的脏数据是否会产生致命影响。
voidfunction () { int arr [ 10 ]; arr [ 20 ] = 999 ; // 栈越界}
-
ASAN : 立即报告 `stack-buffer-overflow in function:3`
部分字节越界在实际场景中也是极其隐晦,通常条件下它的危害可能一直存在但不一定表现出来,同样取决于被越界数据区的内容。
char * buf = malloc ( 10 );memset ( buf + 8 , 'X' , 3 ); // 只越界 1 字节
-
ASAN : 精确报告”0 bytes to the right”
测试场景 :主程序分配内存 → 传递给共享库 → 库内部越界
这个场景在实际项目中极为常见,涉及跨团队,甚至跨公司(比如SDK形式对接);如果是release版本发布,通常core堆栈无法快速有效分析。
ERROR: AddressSanitizer: heap-buffer-overflow
#0 in writetobuffer(char, int, char const) mylib.cpp:26
ASAN : 精确显示 `mylib.cpp` 第 26 行
Valgrind : 只能显示 `???` ,无法定位 SO 内部
工程价值 :在有多达几十个 SO 库的大型项目中,能节省数天的排查时间!
即使是strip版本,笔者实践验证看ASAN仍然可以有效定位。
一句话说就是数据长度大于缓冲区长度,这个比如容易出现在预分配内存空间的场景,预分配的长度极端情况小于实际数据长度,且在数据拷贝过程中未作加固检查,一般这种情况下需要动态伸缩(如果合理条件下)。
#include<stdio.h>#include<stdlib.h>#include<string.h>intmain(){ char* buf = (char*) malloc (10); printf ( "分配了 10 字节 \n " ); memset ( buf , 'A' , 20 ); // 越界 10 字节 printf ( "写入 20 字节(越界!) \n " ); free ( buf ); return 0 ;}
=================================================================
==2805==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x602000000010
WRITE of size 20 at 0x602000000010 thread T0
#0 0x55844ebce2f9 in main demoheapoverflow.cpp:10
#1 0x7feb2d58b082 in libcstartmain ../csu/libc-start.c:308
0x60200000001a is located 0 bytes to the right of 10-byte region [0x602000000010,0x60200000001a)
allocated by thread T0 here:
#0 0x7feb2dbb2808 in interceptormalloc asanmalloc_linux.cc:144
#1 0x55844ebce263 in main demoheapoverflow.cpp:7
SUMMARY: AddressSanitizer: heap-buffer-overflow
-
✓ 错误类型: `heap-buffer-overflow`
-
✓ 错误位置: `demoheapoverflow.cpp:10`
-
✓ 越界详情: `0 bytes to the right` (刚越界)
-
✓ 内存分配位置: `demoheapoverflow.cpp:7`
简单来说就是野指针,释放后的内存地址仍然被访问,这个在跨库或者异步逻辑中极易出现。
intmain(){ int* p = (int*) malloc (sizeof (int)); *p = 42 ; printf ( "分配并写入: *p = 42 \n " ); free ( p ); printf ( "已释放内存 \n " ); printf ( "尝试读取:p = %d (释放后使用!)\n ", p ); return 0 ;}
=================================================================
==2810==ERROR: AddressSanitizer: heap-use-after-free on address 0x602000000010
READ of size 4 at 0x602000000010 thread T0
#0 0x56042a43a2c3 in main demouseafter_free.cpp:11
0x602000000010 is located 0 bytes inside of 4-byte region [0x602000000010,0x602000000014)
#0 0x7fb63971040f in interceptorfree asanmalloc_linux.cc:122
#1 0x56042a43a27a in main demouseafter_free.cpp:9
previously allocated by thread T0 here:
#0 0x7fb639710808 in interceptormalloc asanmalloc_linux.cc:144
#1 0x56042a43a263 in main demouseafter_free.cpp:6
-
-
-
// main.cppintmain(){ char * buf = create_buffer ( 10 ); // 调用共享库 writetobuffer (buf , 10 , "This string is way too long!" ); free_buffer ( buf ); return 0 ;}// mylib.cpp(共享库)voidwritetobuffer( char* buffer , int size , constchar data ){ memcpy ( buffer , data , strlen ( data )); // 越界!}
=================================================================
==2818==ERROR: AddressSanitizer: heap-buffer-overflow
WRITE of size 43 at 0x60200000001a thread T0
#0 in interceptormemcpy sanitizercommon_interceptors.inc:790
#1 in memcpy /usr/include/x8664-linux-gnu/bits/stringfortified.h:34
#2 in writetobuffer(char, int, char const) mylib.cpp:26 ← 精确到 SO 行号!
#3 in main 11crossso_overflow.cpp:28
0x60200000001a is located 0 bytes to the right of 10-byte region [0x602000000010,0x60200000001a)
allocated by thread T0 here:
#0 in interceptormalloc asanmalloc_linux.cc:144
#1 in create_buffer(int) mylib.cpp:13
核心价值 :直接显示 `mylib.cpp:26` ,无需逐个 SO 排查!
g++ -fsanitize=address -g -O1 your_program.cpp -o program
-
`-fsanitize=address` :启用 ASAN
g++ -fsanitize=address -g -O0 program.cpp
g++ -fsanitize=address -g -O2 program.cpp
g++ -fsanitize=address -O1 program.cpp
g++ -fsanitize=address -g -O1 program.cpp -o program
g++ -fsanitize=address -g3 -O1 program.cpp -o program
如果检测到内存错误,程序会立即终止并打印详细报告。
ASANOPTIONS = halton_error = 0 ./program
ASANOPTIONS = detectleaks = 0 ./program
ASANOPTIONS = malloccontext_size = 30 ./program
ASANOPTIONS = logpath = asan.log ./program
ASANOPTIONS = haltonerror = 0: detectleaks = 1: malloccontextsize = 3
option(ENABLE_ASAN "Enable AddressSanitizer" OFF)if(ENABLE_ASAN)addcompileoptions(-fsanitize=address -g -O1)addlinkoptions(-fsanitize=address)message(STATUS "AddressSanitizer enabled")endif()add_executable(myapp main.cpp)add_library(mylib SHARED lib.cpp)add_library(testlib SHARED test.cpp)
cmake -DENABLE_ASAN=ON ..
注意 :如果主程序用 ASAN 编译,但某个 SO 没有,会导致:
有 ASAN: 0.212s (21x) ← 约 2 倍性能
中型项目(有 ASAN): 1.5 GB (3x)
ASAN 为应用程序的每段内存分配一个”影子字节”,记录该内存是否可访问。
ASAN 在内存区域周围插入填充区(红区),用于检测越界访问。
作用 :即使只越界 1 字节也会访问红区,触发报告。
if (! IsAccessible ( p )) { ReportError ( p , "write" );}*p = 42 ;
开销 :每次内存访问多几条汇编指令,因此性能开销约 2x。
$ ldd ./program | grep asan
libasan.so.5 => /lib/x86_64-linux-gnu/libasan.so.5
如果看到 `libasan.so` ,说明 ASAN 已正确链接。
intmain () { int* p = (int*) malloc ( sizeof ( int )); free ( p ); int val = * p ; // Use After Free return 0 ;}
$ g++ -fsanitize=address -g -O1 test.cpp -o test
=================================================================
==12345==ERROR: AddressSanitizer: heap-use-after-free on address 0x…
#0 0x… in main test.cpp:7
如果看到类似的错误报告,说明 ASAN 工作正常。
A : 可以!编译命令只需将 `g++` 改为 `gcc` :
gcc -fsanitize=address -g -O1 program.c -o program
Q3: 能和其他 sanitizers 一起用吗?
g++ -fsanitize=address,undefined -g -O1 program.cpp
g++ -fsanitize=address,thread -g -O1 program.cpp # 错误!
-
-
某些模块没用 ASAN 编译 → 确保所有模块统一
-
自定义内存分配器 → 使用 `no_sanitize` 属性
-
-
-
-
跨 SO 支持 :精确定位共享库内部问题(核心优势)