乐于分享
好东西不私藏

你还在为 C++ 内存崩溃发愁?这个工具能救你

你还在为 C++ 内存崩溃发愁?这个工具能救你

如果你是一名c++ coder, 我相信你一定大概率遇到过和我类似的遭遇在产品稳定性测试接近正式发布尾声时,不幸遇到程序在批量环境运行时,零星出现因内存崩溃重启问题(开发环境难以构建)。具体的完整场景时,加速卡整机发布时,在120个容器实例长稳运行极限压力测试时,平均运行5天出现一起实例崩溃而重启,面对完整的监控无处可逃,通过core文件分析,出现位置随机,比如一个变量声明,函数结束,或者一个stl容器尺寸超大,或者malloc size超大,总之是不应该出现的位置或者说程序死亡时早已不是第一现场。我们知道内存问题尤其是写越界不一定会表现或者很快表现出来,当然也不是我们想复现就能复现出来。关于内存泄漏类问题很多成熟手段是可以轻松定位,但是写越界问题一直是个挑战,尤其在笔者的环境,涉及多个团队多个so之间的内存互相访问对问题定性定界都尤为困难。我在2个月内断断续续更新了50个版本到测试环境进行验证,有幸的是最终通过asan工具彻底解决了内存崩溃问题,甚至挖掘出上游团队隐藏数年的历史bug

01

内存崩溃噩梦
1.问题表现
通过3-4次复现,并通过core文件分析,表现出堆栈随机不固定,且出现不应该出现问题,比如结构体生命,vector尺寸异常,函数退出时。通过gdb查看个内存变量值,出现某一块内存数据被破坏。同时对相关业务代码进行了详细走读,并无明显收获。刚开始我确实尝试有小范围通过asan版本诊断,但是由于其他组件库出现读溢出导致无法继续,由于我的工程和运行环境是否复杂,对ASAN是否能解决我的问题仍然比较忐忑,在其他手段无效条件下,推动多个团队(芯片库,算法库)同时提供ASAN版本助力挖掘这个问题。
2.ASAN诊断结果
ASAN的报告如下,指向非常明确,说明谁正在读取已经被谁释放的内存。经非常详细的业务流程的逻辑分析,发现当数据压力处于极限状态时,确实可能出现这种概率较小的时机导致同一个数据结构被并发读写,通过模拟极限条件触发出崩溃现象(非ASAN版本)。而同时对ASAN报告中一些读溢出的警告进行深度排查,确实定位到一些历史bug有读内存问题,虽然不至于崩溃,但仍然是非常有价值的发现。最终将ASAN报告体现的错误全部清扫干净之后,程序变得非常稳定。

02

什么是 ASAN?
1.1 定义
AddressSanitizer (ASAN) 是编译器级别的快速内存错误检测器,专门用于发现 C/C++ 程序中的内存问题。
简单来说: 一个编译选项,让你的程序自己报告内存错误
1.2 ASAN 能检测什么?
错误类型
说明
检测能力
堆溢出
malloc/new 分配的内存越界
✓精确
栈溢出
栈变量越界访问
✓ 精确
全局变量溢出
全局/静态变量越界
✓ 精确
Use After Free
释放后继续使用
✓精确
Double Free
重复释放
✓精确
Use After Return
使用已返回的栈变量
✓精确
内存泄漏
未释放的内存
✓完整
部分字节越界
只越界 1-2 字节
✓精确(独有优势)

03

ASAN vs 其他内存工具
2.1 工具对比总表
维度
ASAN
Valgrind
MALLOC_CHECK_
ElectricFence
性能开销**
2x
20-100x
1.1x
2-3x
堆溢出检测
✓✓✓
✓✓
栈溢出检测
✓✓✓
部分字节越界
✓✓✓
跨 SO 精确定位
✓✓✓
易用性
✓✓✓
✓✓✓
生产环境
2.2 性能实测数据
测试场景 :100,000 次内存分配和访问操作
无 ASAN(基线): 0.010s
有 ASAN:  0.212s  (约 21x)
Valgrind:  ~10s (约 1000x)
结论 :ASAN 性能开销仅 2 倍,远低于 Valgrind,适合日常开发使用。
2.3 核心优势对比
优势 1:栈溢出检测
栈溢出通常非常隐藏,极容易产生次生灾害,因为不同业务逻辑触发栈的深度可能不一致,栈溢出不一定表现为崩溃,取决于读到的脏数据是否会产生致命影响。
测试代码 :
voidfunction () {    int arr [ 10 ];    arr [ 20 ] = 999 ; // 栈越界}
工具对比 :
  • ASAN : 立即报告 `stack-buffer-overflow in function:3`
  • Valgrind : 无法检测
  • MALLOCCHECK : 无法检测
优势 2:部分字节越界检测
部分字节越界在实际场景中也是极其隐晦,通常条件下它的危害可能一直存在但不一定表现出来,同样取决于被越界数据区的内容。
测试代码 :
char * buf = malloc ( 10 );memset ( buf + 8 , 'X' , 3 ); // 只越界 1 字节
工具对比 :
  • ASAN : 精确报告”0 bytes to the right”
  • Valgrind : 可能漏检
  • MALLOCCHECK : 无法检测
优势 3:跨 SO 精确定位(最核心优势)
测试场景 :主程序分配内存 → 传递给共享库 → 库内部越界
这个场景在实际项目中极为常见,涉及跨团队,甚至跨公司(比如SDK形式对接);如果是release版本发布,通常core堆栈无法快速有效分析。
ASAN 报告 :
ERROR: AddressSanitizer: heap-buffer-overflow
#0 in writetobuffer(char, int, char const) mylib.cpp:26
#1 in main main.cpp:28
ASAN : 精确显示 `mylib.cpp` 第 26 行
Valgrind : 只能显示 `???` ,无法定位 SO 内部
工程价值 :在有多达几十个 SO 库的大型项目中,能节省数天的排查时间!
即使是strip版本,笔者实践验证看ASAN仍然可以有效定位。

04

ASAN 检测能力详解
3.1 堆缓冲区溢出
一句话说就是数据长度大于缓冲区长度,这个比如容易出现在预分配内存空间的场景,预分配的长度极端情况小于实际数据长度,且在数据拷贝过程中未作加固检查,一般这种情况下需要动态伸缩(如果合理条件下)。
测试代码 :
#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 ;}
ASAN 实际输出 :
=================================================================
==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`
3.2 Use After Free
简单来说就是野指针,释放后的内存地址仍然被访问,这个在跨库或者异步逻辑中极易出现。
测试代码 :
intmain(){    int* p = (int*) malloc (sizeof (int));    *p = 42 ;    printf ( "分配并写入: *p = 42 \n " );    free ( p );    printf ( "已释放内存 \n " );    printf ( "尝试读取:p = %d (释放后使用!)\n ", p );    return 0 ;}
ASAN 实际输出 :
=================================================================
==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)
freed by thread T0 here:
#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
三段式报告 :
  1. 当前错误位置(读取)
  2. 内存释放位置
  3. 内存分配位置
3.3 跨共享库问题定位
测试代码 :
// main.cppintmain(){    char * buf = create_buffer ( 10 ); // 调用共享库    writetobuffer (buf , 10 , "This string is way too long!" );    free_buffer ( buf );    return 0 ;}// mylib.cpp(共享库)voidwritetobufferchar* buffer , int size , constchar data ){    memcpy ( buffer , data , strlen ( data )); // 越界!}
ASAN 实际输出 :
=================================================================
==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 排查!

05

快速上手指南
4.1 基本编译选项
最简单的编译命令 :
g++ -fsanitize=address -g -O1 your_program.cpp -o program
编译选项说明 :
  • `-fsanitize=address` :启用 ASAN
  • `-g` :保留调试信息(必须)
  • `-O1` :优化级别(推荐)
4.2 编译选项注意事项 
错误示范
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
4.3 运行程序
./program
如果检测到内存错误,程序会立即终止并打印详细报告。
4.4 常用运行选项
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
 ./program
4.5 多模块项目编译
关键要求:所有模块必须使用相同的编译选项
CMake 配置(推荐) :
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 ..
make
注意 :如果主程序用 ASAN 编译,但某个 SO 没有,会导致:
  • 链接失败
  • 或无法检测该 SO 内部的内存问题

06

资源要求
5.1 性能开销
实际测试数据 (100,000 次内存操作):
无 ASAN:  0.010s (1.0x)
有 ASAN:  0.212s (21x)  ← 约 2 倍性能
影响 :
  • 开发阶段:完全可以接受
  • 测试环境:推荐使用
  • 性能测试:建议禁用
  • 生产环境:不推荐
5.2 内存开销
典型内存占用 :
小程序(无 ASAN):    50 MB
小程序(有 ASAN):    150 MB (3x)
中型项目(无 ASAN):  500 MB
中型项目(有 ASAN):  1.5 GB (3x)
大型项目(无 ASAN):  2 GB
大型项目(有 ASAN):  6 GB+ (3x)
影响 :
  • ✓ PC 环境:通常无影响
  • ⚠️ 嵌入式设备:可能内存不足
  • ⚠️ Docker 容器:需要调整内存限制
5.3 平台支持
平台
支持状态
说明
Linux x86_64
✓✓✓ 完全支持
推荐
Linux ARM64
✓✓ 良好支持
可用
macOS x86_64
✓✓ 良好支持
可用
macOS ARM64
✓✓ 良好支持
可用
Windows
✓ 有限支持
需要特殊配置

07

ASAN 工作原理简介
6.1 影子内存(Shadow Memory)
ASAN 为应用程序的每段内存分配一个”影子字节”,记录该内存是否可访问。
应用内存
 影子内存
 0x7f..
0x10
8 字节 
1 字节
影子字节值具体含义:
0  → 完全可访问(8 字节都有效)
1-7 → 部分可访问(前 k 字节有效)
负数 → 不可访问(已释放、未分配、红区)
6.2 红区(Redzone)
ASAN 在内存区域周围插入填充区(红区),用于检测越界访问。
堆内存布局:
Red  
Usable  
Red
Zone 
Memory  
Zone 
 32B
 N B 
32B
任何访问红区都会触发报告
作用 :即使只越界 1 字节也会访问红区,触发报告。
6.3 编译时插桩
原始代码 :
*p = 42 ;
ASAN 插桩后(概念) :
if (! IsAccessible ( p )) {    ReportError ( p , "write" );}*p = 42 ;
开销 :每次内存访问多几条汇编指令,因此性能开销约 2x。

08

快速验证 
7.1 验证方法 1:检查链接库
$ ldd ./program | grep asan
libasan.so.5 => /lib/x86_64-linux-gnu/libasan.so.5
如果看到 `libasan.so` ,说明 ASAN 已正确链接。
7.2 验证方法 2:运行测试程序
创建测试代码 :
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
$ ./test
=================================================================
==12345==ERROR: AddressSanitizer: heap-use-after-free on address 0x…
#0 0x… in main test.cpp:7
如果看到类似的错误报告,说明 ASAN 工作正常。

09

常见问题 
Q1: ASAN 会漏检吗?
A : 会。ASAN 主要检测:
  • ✓ 内存越界
  • ✓ Use After Free
  • ✗ 未初始化内存(需要 MSAN)
  • ✗ 线程竞争(需要 TSAN)
Q2: 能在 C 语言中使用吗?
A : 可以!编译命令只需将 `g++` 改为 `gcc` :
gcc -fsanitize=address -g -O1 program.c -o program
Q3: 能和其他 sanitizers 一起用吗?
A : 部分可以:
g++ -fsanitize=address,undefined -g -O1 program.cpp
g++ -fsanitize=address,thread -g -O1 program.cpp # 错误!
Q4: 误报怎么办?
A : 常见原因和解决方法:
  1. 优化级别太高 → 使用 `-O1`
  2. 某些模块没用 ASAN 编译 → 确保所有模块统一
  3. 自定义内存分配器 → 使用 `no_sanitize` 属性

10

总结
核心优势
  1. 检测能力强 :覆盖几乎所有内存错误类型
  2. 性能开销小 :仅 2x,远低于 Valgrind
  3. 定位精确 :精确到文件和行号
  4. 跨 SO 支持 :精确定位共享库内部问题(核心优势)
适用场景
  • ✓ 开发阶段调试
  • ✓ 单元测试
  • ✓ CI/CD 集成
  • ✓ 崩溃问题排查
  • ✗ 生产环境(开销太大)
下一步
后面将介绍:
  • 工程实践案例
  • 最佳实践
  • CI/CD 集成
  • 踩坑经验
本站文章均为手工撰写未经允许谢绝转载:夜雨聆风 » 你还在为 C++ 内存崩溃发愁?这个工具能救你

猜你喜欢

  • 暂无文章