内存问题排查工具ASan初探
在复杂软件开发中,代码量大,往往由多人进行协同开发,当遇到内存使用不当导致程序崩溃时,通过代码走读、加打印等人工排查的方法,可能很难定位问题,如果能借助一些工具来排查,也许能有事半功倍的效果。
本篇就来介绍ASan这款内存错误检测工具,来帮助分析内存使用的相关问题。
1 ASan简介
ASan(AddressSanitizer),是 Google 开发的一种内存错误检测工具,广泛用于 C、C++ 等语言的代码中,主要用于检测和调试内存相关问题,如:使用未分配的内存、使用已释放的内存、堆内存溢出等。
ASan在编译时将额外的代码插入到目标程序中,对内存的读写操作进行检测和记录。
在程序运行时,ASan会监测内存访问,一旦发现内存访问错误,会立即输出错误信息并中断程序执行,并提供详细报告帮助开发者定位问题。
LeakSanitizer(内存泄漏检测器)是集成在 AddressSanitizer中的内存泄漏检测工具。
2 使用
2.1 基本使用(-fsanitize=address)
使用支持 ASan 的编译器,如GCC或Clang,并开启ASan相关编译选项。
gcc -fsanitize=address your_code.c -g -o your_program
编译后,运行代码,当ASan检测到代码有相应的内存问题,就会结束程序,并给出类似如下的错误信息
=================================================================
==8950==ERROR: LeakSanitizer: detected memory leaksDirect leak of 20 byte(s) in 1 object(s) allocated from:#0 0x7f4fed3a8acb in __interceptor_malloc (/usr/lib/x86_64-linux-gnu/liblsan.so.0+0xeacb)#1 0x55ed2291e7d2 in main /home/xxpcb/myTest/linux/ASan/MemLeak/test1.cpp:9#2 0x7f4fecfcac86 in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x21c86)Direct leak of 4 byte(s) in 1 object(s) allocated from:#0 0x7f4fed3a9c3b in operator new(unsigned long) (/usr/lib/x86_64-linux-gnu/liblsan.so.0+0xfc3b)#1 0x55ed2291e7e0 in main /home/xxpcb/myTest/linux/ASan/MemLeak/test1.cpp:11#2 0x7f4fecfcac86 in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x21c86)SUMMARY: LeakSanitizer: 24 byte(s) leaked in 2 allocation(s).
2.2 -fsanitize的其它参数
除了-fsanitize=address
,GCC 中还有许多类似的参数,用于检测不同类型的程序错误:
-
-fsanitize=leak
:用于检测内存泄漏。该功能其实已集成到 AddressSanitizer 中,使用
-fsanitize=address
也能检测内存泄漏,不过使用此参数可单独关闭 ASAN 的其他内存错误检测,只专注于内存泄漏检查。 -
-fsanitize=thread
:开启 ThreadSanitizer,用于检测多线程程序中的数据竞争和死锁等问题。需要注意的是,它不能与
-fsanitize=address
和-fsanitize=leak
共用。 -
-fsanitize=undefined
:开启 UndefinedBehaviorSanitizer,可检测程序中的未定义行为。如除零错误、访问越界数组、未初始化变量的使用等。
-
-fsanitize=memory
:开启 MemorySanitizer,主要用于检测未初始化内存问题。能帮助开发者找出程序中读取未初始化内存的地方。
关于-fno-omit-frame-pointer参数:
-fno-omit-frame-pointer
是 GCC 编译器的一个选项,用于禁用帧指针优化,加上该参数,可以提升 AddressSanitizer 等内存检测工具的准确性,在初步了解阶段,本篇实例代码先不加此参数
3 实例
3.1 内存泄漏(memory leaks)
ASan对应内存泄漏的打印为:LeakSanitizer: detected memory leaks
写一个测试代码,通过malloc或new进行申请内存但未释放,导致内存泄漏:
//g++ -fsanitize=address -g test1.cpp -o test
#include <stdio.h>
#include <stdlib.h>int main()
{printf("hello\n");int *p1 = (int *) malloc(sizeof(int) * 5);int *p2 = new int(10);return 0;
}
编译运行后结果如下:
可以看到ASan工具检测到了内存泄漏:
- 检测到20 byte的内存泄漏,在test1.cpp的第9行(int类型是4byte,x5=20byte,p1只申请了内存,未赋值)
- 检测到4 byte的内存泄漏,在test1.cpp第11行(int类型是4byte,,p2申请的内存赋值为数值10)
- 总结:在2处地方一共检测出24字节的内存泄漏
使用-g参数是为了将具体的代码行号能打印出来,如果不带-g参数,效果如下:
3.2 堆缓冲区溢出(heap-buffer-overflow)
堆缓冲区溢出,是指程序试图向堆内存的非法区域写入数据,导致越界访问。
ASan对应堆缓冲区溢出的打印为:AddressSanitizer: heap-buffer-overflow
写一个测试代码,通过malloc申请内存(堆缓冲区),然后memcpy的数据长度超过malloc申请的长度,导致堆缓冲区溢出:
//g++ -fsanitize=address -g test1.cpp -o test
#include <stdio.h>
#include <stdlib.h>
#include <string.h>void test_heap_buffer_overflow()
{printf("[%s] in\n", __func__);char *p1 = (char *) malloc(sizeof(char) * 5);memcpy(p1, "hello world", sizeof("hello world"));free(p1);printf("[%s] out\n", __func__);
}int main()
{printf("[%s] in\n", __func__);test_heap_buffer_overflow();printf("[%s] out\n", __func__);return 0;
}
运行结果如下:
主要看这里:
WRITE of size 12 at 0x602000000015 thread T0#0 0x7f5470e6975c (/usr/lib/x86_64-linux-gnu/libasan.so.5+0x3f75c)#1 0x5600b4f1bb7a in test_heap_buffer_overflow() /home/xxpcb/myTest/linux/ASan/HeapBuffOverflow/test1.cpp:11#2 0x5600b4f1bbc2 in main /home/xxpcb/myTest/linux/ASan/HeapBuffOverflow/test1.cpp:21#3 0x7f5470a5ac86 in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x21c86)#4 0x5600b4f1ba79 in _start (/home/xxpcb/myTest/linux/ASan/HeapBuffOverflow/test+0xa79)0x602000000015 is located 0 bytes to the right of 5-byte region [0x602000000010,0x602000000015)
错误位置
- 写入地址:
0x602000000015
(堆内存) - 写入大小:
12字节
- 出错代码:
/home/xxpcb/myTest/linux/ASan/HeapBuffOverflow/test1.cpp:11
内存分配信息
- 出错地址位于一个5 字节内存块的右边界之外(
0x602000000010
到0x602000000015
,不包含结束地址) - 该内存块通过
malloc()
分配于:test1.cpp:10
3.3 栈缓冲区溢出(stack-buffer-overflow)
栈缓冲区溢出,是指程序试图向堆内存的非法区域写入数据,导致越界访问。
ASan对应堆缓冲区溢出的打印为:AddressSanitizer: stack-buffer-overflow
写一个测试代码,通过声明一个固定长度的buf(栈缓冲区),然后memcpy的数据长度超过其固有长度,导致栈缓冲区溢出:
//g++ -fsanitize=address -g test1.cpp -o test
#include <stdio.h>
#include <stdlib.h>
#include <string.h>void test_stack_buffer_overflow()
{printf("[%s] in\n", __func__);char buf[5] = {0};memcpy(buf, "hello world", sizeof("hello world"));printf("[%s] out\n", __func__);
}int main()
{printf("[%s] in\n", __func__);test_stack_buffer_overflow();printf("[%s] out\n", __func__);return 0;
}
运行结果如下:
主要看这里:
=================================================================
==7451==ERROR: AddressSanitizer: stack-buffer-overflow on address 0x7ffee5037555 at pc 0x7f0b3f96975d bp 0x7ffee5037520 sp 0x7ffee5036cc8
WRITE of size 12 at 0x7ffee5037555 thread T0#0 0x7f0b3f96975c (/usr/lib/x86_64-linux-gnu/libasan.so.5+0x3f75c)#1 0x55abda7d9d48 in test_stack_buffer_overflow() /home/xxpcb/myTest/linux/ASan/StackBufferOverflow/test1.cpp:11#2 0x55abda7d9dcf in main /home/xxpcb/myTest/linux/ASan/StackBufferOverflow/test1.cpp:20#3 0x7f0b3f55ac86 in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x21c86)#4 0x55abda7d9b69 in _start (/home/xxpcb/myTest/linux/ASan/StackBufferOverflow/test+0xb69)Address 0x7ffee5037555 is located in stack of thread T0 at offset 37 in frame#0 0x55abda7d9c34 in test_stack_buffer_overflow() /home/xxpcb/myTest/linux/ASan/StackBufferOverflow/test1.cpp:7This frame has 1 object(s):[32, 37) 'buf' <== Memory access at offset 37 overflows this variable
错误类型
stack-buffer-overflow
(栈缓冲区溢出)
错误位置
- 写入地址:
0x7ffee5037555
(栈内存) - 写入大小:
12字节
- 出错代码:
/home/xxpcb/myTest/linux/ASan/StackBufferOverflow/test1.cpp:11
栈帧信息
- 溢出发生在变量
buf
的偏移量37
处 buf
的有效范围:[32, 37)
(即 5 字节空间)- 变量声明于:
test1.cpp:7
3.4 双重释放(double-Free)
双重释放,是指对已释放的内存块再次调用free
,属于未定义行为
ASan对应双重释放的打印为:AddressSanitizer: attempting double-free
对同一指针调用free
两次会导致严重的内存错误,属于未定义行为,可能引发以下后果:
- 程序崩溃:第二次释放时,内存分配器可能发现该内存块已被释放,触发断言失败或崩溃(如 glibc 的
malloc
会触发double free or corruption
错误)。 - 内存泄漏与数据损坏:两次释放可能破坏内存分配器的元数据(如空闲链表结构),导致后续内存分配出现异常,甚至覆盖其他数据。
注意,free两次与free空指针是不同的,调用free(NULL)
是合法操作,因为free
函数在内部会先检查指针是否为NULL
,若为NULL
则直接返回,不执行释放内存的操作
另外,free后,指针是不为空,可以在下面测试代码中加打印验证。
测试代码:
//g++ -fsanitize=address -g test1.cpp -o test
#include <stdio.h>
#include <stdlib.h>
#include <string.h>void test_free_null()
{char *p1 = (char *) malloc(sizeof(char) * 64);memcpy(p1, "hello world", sizeof("hello world"));free(p1);if (nullptr == p1){printf("[%s] now p1 is nullptr\n", __func__);}free(p1);
}int main()
{test_free_null();return 0;
}
运行结果如下:
错误类型
double-free
(双重释放)
内存地址与操作
- 重复释放的地址:
0x606000000020
- 首次释放位置:
test1.cpp:10
- 第二次释放位置:
test1.cpp:15
- 内存块大小:64 字节(分配于
test1.cpp:8
)
3.5 非法释放(free on address which was not malloc)
非法释放,释放的指针不是内存分配函数(malloc
/calloc
/realloc
)返回的原始地址,分配器无法找到对应的元数据,导致校验失败。
ASan对应非法释放的打印为:AddressSanitizer: attempting free on address which was not malloc()-ed
测试代码
//g++ -fsanitize=address -g test2.cpp -o test
#include <stdio.h>
#include <stdlib.h>
#include <string.h>void test_free_null()
{char *p1 = (char *) malloc(sizeof(char) * 64);memcpy(p1, "hello world", sizeof("hello world"));char *p2 = p1 + 5;free(p2);free(p1);
}int main()
{test_free_null();return 0;
}
运行结果如下:
错误类型
bad-free
(非法释放)
地址与操作细节
- 尝试释放的地址:
0x606000000025
- 该地址位于一个 64 字节内存块的偏移 5 字节处(块范围:
0x606000000020 ~ 0x606000000060
) - 内存块分配于:
test2.cpp:8
(使用malloc
) - 释放操作位于:
test2.cpp:11
3.6 使用栈上返回的变量(stack-use-after-return)
当函数返回后,栈帧被销毁,内存变为无效,使用栈上返回的变量,例如打印返回的字符串hello world,可能有下面这些情况:
- 垃圾值(如
hello world
后面跟随乱码) - 空值(导致段错误)
- 仍然显示
hello world
(取决于栈内存是否被覆盖)
ASan对应的打印为:**AddressSanitizer: stack-use-after-return **
测试代码
//g++ -fsanitize=address -g test1.cpp -o test
//ASAN_OPTIONS=detect_stack_use_after_return=1 ./test
#include <stdio.h>
#include <stdlib.h>
#include <string.h>char *test_return_stack_p()
{char buf[64] = {0};memcpy(buf, "hello world", sizeof("hello world"));char *ret = &buf[0];printf("[%s] str:%s\n", __func__, ret);return ret;
}int main()
{char *str = test_return_stack_p();printf("[%s] str:%s\n", __func__, str);return 0;
}
运行结果如下:
直接运行是检测不到问题的,需要再加上ASAN_OPTIONS=detect_stack_use_after_return=1参数,加上参数的运行结果如下:
错误类型
stack-use-after-return
(栈内存使用后返回)
关键位置
- 无效读取发生在:
test1.cpp:19
(main
函数中的printf
) - 栈变量声明在:
test1.cpp:7
(char buf[64]
) - 内存地址
0x7f8fe8f00020
属于buf
数组的起始位置。
栈帧状态
- ASan 标记栈帧为
f5
(Stack after return
),表示函数已返回,栈内存不再有效。 - 读取操作(
READ of size 12
)访问了已释放的栈区域,触发错误。
3.7 使用退出作用域的变量(stack-use-after-scope)
ASan对应的打印为:AddressSanitizer: stack-use-after-scope
stack-use-after-scope与上面的stack-use-after-return比较类似,这里对比看下:
类型 | stack-use-after-return | stack-use-after-scope |
---|---|---|
触发时机 | 函数返回后访问其栈帧内的变量 | 变量作用域结束后访问该变量 |
内存状态 | 函数栈帧已被销毁(通常被系统回收或覆盖) | 变量作用域结束,但栈帧可能尚未完全销毁 |
典型场景 | 返回栈上变量的指针并在函数外使用 | 在代码块结束后使用块内定义的变量 |
ASan 错误信息特征 | 包含 stack-use-after-return 关键词 | 包含 stack-use-after-scope 关键词 |
本质风险 | 访问已被释放的栈内存(可能已被覆盖) | 访问作用域已结束的变量(值可能无效) |
测试代码:
//g++ -fsanitize=address -g test1.cpp -o test
#include <stdio.h>
#include <stdlib.h>
#include <string.h>void test_stack_use_after_scope()
{char *str;{char buf[64] = {0};memcpy(buf, "hello world", sizeof("hello world"));str = &buf[0];printf("[%s] str:%s\n", __func__, str);}printf("[%s] str:%s\n", __func__, str);
}int main()
{test_stack_use_after_scope();return 0;
}
运行结果如下:
错误类型
stack-use-after-scope
(栈作用域后使用)
关键位置
- 无效读取发生在:
test2.cpp:16
(test_stack_use_after_scope
函数内) - 变量声明在:
test2.cpp:7
(char buf[64]
) - 内存地址
0x7ffd6e426590
属于buf
数组的起始位置。
栈内存状态
- ASan 标记该内存区域为
f8
(Stack use after scope
),表示变量作用域已结束,内存逻辑上不再有效。 - 读取操作(
READ of size 12
)触发了 ASan 的安全检查。
3.8 使用释放后的堆内存(heap-use-after-free)
ASan对应的打印为:AddressSanitizer: heap-use-after-free
测试代码:
//g++ -fsanitize=address -g test1.cpp -o test
#include <stdio.h>
#include <stdlib.h>
#include <string.h>void test_heap_use_after_free()
{char *p1 = (char *) malloc(sizeof(char) * 64);memcpy(p1, "hello world", sizeof("hello world"));char a = p1[0];printf("[%s] a:%c\n", __func__, a);free(p1);char b = p1[0];printf("[%s] b:%c\n", __func__, b);
}int main()
{test_heap_use_after_free();return 0;
}
运行结果:
错误类型
heap-use-after-free
(堆内存使用后释放)
关键位置
- 无效读取发生在:
test1.cpp:14
(test_heap_use_after_free
函数内) - 内存释放发生在:
test1.cpp:12
- 内存分配发生在:
test1.cpp:8
- 内存地址
0x606000000020
属于 64 字节堆块的起始位置。
内存状态
- ASan 标记该内存区域为
fd
(Freed heap region
),表示内存已释放,不可访问。 - 读取操作(
READ of size 1
)触发了 ASan 的安全检查,中断程序执行。
4 ASan报告解读归纳
上面的实例,演示了ASan检测出的多种内存问题,并生成了报告,对于这种报告的解读,再来归纳一下。
4.1 主要信息部分
- 找到
ERROR
开头的行:确认错误类型(如detected memory leaks
、heap-buffer-overflow
、stack-use-after-scope
)。 - 查看
READ/WRITE of size X
:明确是读还是写越界,以及访问的字节数。 - 检查调用栈(
#0 #1 ...
):定位到代码中触发错误的具体行(文件名 + 行号)。
4.2 影子内存信息部分
ASan用影子内存(Shadow Memory) 标记程序内存的状态,每个影子字节对应 8 个应用程序字节,用于快速检测非法内存访问。
- 地址范围:
0x100030347680
到0x0c047fff8050
是错误地址附近的影子内存区域。 - 颜色与标记
影子字节 | 含义 | 作用 / 检测场景 | 典型示例 |
---|---|---|---|
00 | 可访问内存(Addressable) | 标记正常可读写的内存区域 | 全局变量、栈变量、合法堆内存(malloc/new 分配) |
fa | 堆左边界红区(Heap left redzone) | 检测堆缓冲区溢出(malloc 前的保护) | char* p = new char[10]; → p 前的 fa 区域 |
fd | 已释放堆内存(Freed heap region) | 检测 use-after-free | free(p); 后,p 指向的内存被标记为 fd |
f1 | 栈左边界红区(Stack left redzone) | 检测栈缓冲区溢出(函数栈帧前保护) | 函数局部数组 char buf[10]; 前的 f1 区域 |
f2 | 栈中间红区(Stack mid redzone) | 检测栈变量越界(复杂栈布局保护) | 编译器插入的栈变量间保护区域 |
f3 | 栈右边界红区(Stack right redzone) | 检测栈缓冲区溢出(函数栈帧后保护) | 函数局部数组 char buf[10]; 后的 f3 区域 |
f5 | 函数返回后栈内存(Stack after return) | 检测 use-after-return | 函数返回后,栈帧被标记为 f5 |
f8 | 作用域结束后栈内存(Stack use after scope) | 检测 use-after-scope | 代码块(if /for )内变量作用域结束后标记为 f8 |
f9 | 全局红区(Global redzone) | 检测全局变量溢出 | 全局数组 char g_buf[10]; 前后的 f9 区域 |
f6 | 全局初始化顺序(Global init order) | 检测全局变量初始化依赖问题 | 全局变量初始化时的特殊标记(ASan 内部使用) |
f7 | 用户毒化内存(Poisoned by user) | 检测手动毒化内存的访问 | __asan_poison_memory_region 标记的区域 |
fc | 容器溢出(Container overflow) | 检测 STL 容器越界(C++) | vector<int> v(5); v[5] = 0; → 触发 fc |
ac | 数组 Cookie(Array cookie) | 检测数组越界(堆 / 栈数组保护) | 堆数组 new char[10]; 前后的 ac 标记 |
bb | 对象内部红区(Intra object redzone) | 检测对象成员越界 | 类成员变量间的保护区域(ASan 内部使用) |
fe | ASan 内部(ASan internal) | ASan 自身使用的内存标记 | 无需关注,仅用于工具内部 |
ca | 左变长数组红区(Left alloca redzone) | 检测变长数组(alloca )溢出 | alloca(10); 前的保护区域 |
cb | 右变长数组红区(Right alloca redzone) | 检测变长数组(alloca )溢出 | alloca(10); 后的保护区域 |
5 总结
本篇介绍了内存错误检测工具ASan的基础使用,并通过一些C/C++实例,来演示多种内存使用错误的场景,并分析ASan对应生成的错误信息报告,从而实现对程序中内存问题的定位。