AddressSanitizer 实践:几个常见的错误
前言
笔者是一个经典的C/C++程序员,我相信玩这两个的,都是说明咱们是有潜在的直接操作内存的机会,但是我们知道,咱们都是人,很容易在内存操作上犯错。比如说笔者就出现过在一个比较复杂的逻辑中返回了栈上变量地址的逆天操作。排查了好久才发现的。
所以,有没有工具去检测呢?还真有,甚至不需要你额外的下载什么东西。我们下面就来继续延展我们的讨论。
为什么要学习内存检测?
C/C++ 语言的最大优势在于直接操作内存,但这也是它最危险的地方。一次小小的指针越界、一个未释放的堆对象、一个返回局部变量地址的错误,都可能让程序崩溃甚至带来安全隐患。小型几百行的程序,一个合格的程序员应该三下五除二就能看出来问题。但是几千行几万行呢?说不好了对不对?所以,智能化自动化的检测工具,我们有必要学习和使用。
下面,笔者就自己工程经验出现的三大问题
- Heap Buffer Overflow:申请的堆内存写使用越界
- Heap Use-After-Free:释放后再写入(或者说是非法内存读写)
- Stack Use After Return:栈内存越界(经典案例就是返回了栈上变量地址且使用)
💥Example 1: Heap Buffer Overflow
这个爆炸的emoji挺好玩的,留在这了
C语言中,咱们的堆内存申请走的是C标准接口malloc和free(当然你会问C++的new和delete呢,啊,差不多,是构造/析构函数和malloc/free的C++封装)。为了简单期间,咱们上一个小例子,聚焦问题本身。走你:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main() {
char* buffer = (char*)malloc(5 * sizeof(char));
strcpy(buffer, "Hello, World!");
printf("Buffer: %s\n", buffer);
free(buffer);
return 0;
}
大伙一看就乐了,这小伙子往一个大小为5个字节的内存里送了14个字节的"Hello, World!"。这就是大名鼎鼎的Heap Buffer Overflow,越界写入。
我们下一步就是利用内存检测工具AddressSanitize来搞,别担心很简单:
gcc -g -fsanitize=address -fno-omit-frame-pointer demo.c -o demo
./demo
输出很多,笔者先贴出来,您先自己看看:
=================================================================
==734==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x7baeeade0015 at pc 0x7f8eec51e1a7 bp 0x7fff8ed4bb70 sp 0x7fff8ed4b318
WRITE of size 14 at 0x7baeeade0015 thread T0
#0 0x7f8eec51e1a6 in memcpy /usr/src/debug/gcc/gcc/libsanitizer/sanitizer_common/sanitizer_common_interceptors_memintrinsics.inc:115
#1 0x564e041df1e9 in main /home/Charliechen/demo.c:9
#2 0x7f8eec027674 (/usr/lib/libc.so.6+0x27674) (BuildId: 4fe011c94a88e8aeb6f2201b9eb369f42b4a1e9e)
#3 0x7f8eec027728 in __libc_start_main (/usr/lib/libc.so.6+0x27728) (BuildId: 4fe011c94a88e8aeb6f2201b9eb369f42b4a1e9e)
#4 0x564e041df0e4 in _start (/home/Charliechen/demo+0x10e4) (BuildId: 51fb8592c538277c33230ef0791a8732a55a4add)
0x7baeeade0015 is located 0 bytes after 5-byte region [0x7baeeade0010,0x7baeeade0015)
allocated by thread T0 here:
#0 0x7f8eec520cb5 in malloc /usr/src/debug/gcc/gcc/libsanitizer/asan/asan_malloc_linux.cpp:67
#1 0x564e041df1ca in main /home/Charliechen/demo.c:8
#2 0x7f8eec027674 (/usr/lib/libc.so.6+0x27674) (BuildId: 4fe011c94a88e8aeb6f2201b9eb369f42b4a1e9e)
#3 0x7f8eec027728 in __libc_start_main (/usr/lib/libc.so.6+0x27728) (BuildId: 4fe011c94a88e8aeb6f2201b9eb369f42b4a1e9e)
#4 0x564e041df0e4 in _start (/home/Charliechen/demo+0x10e4) (BuildId: 51fb8592c538277c33230ef0791a8732a55a4add)
SUMMARY: AddressSanitizer: heap-buffer-overflow /home/Charliechen/demo.c:9 in main
Shadow bytes around the buggy address:
0x7baeeaddfd80: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x7baeeaddfe00: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x7baeeaddfe80: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x7baeeaddff00: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x7baeeaddff80: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
=>0x7baeeade0000: fa fa[05]fa fa fa fa fa fa fa fa fa fa fa fa fa
0x7baeeade0080: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x7baeeade0100: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x7baeeade0180: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x7baeeade0200: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x7baeeade0280: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
Shadow byte legend (one shadow byte represents 8 application bytes):
Addressable: 00
Partially addressable: 01 02 03 04 05 06 07
Heap left redzone: fa
Freed heap region: fd
Stack left redzone: f1
Stack mid redzone: f2
Stack right redzone: f3
Stack after return: f5
Stack use after scope: f8
Global redzone: f9
Global init order: f6
Poisoned by user: f7
Container overflow: fc
Array cookie: ac
Intra object redzone: bb
ASan internal: fe
Left alloca redzone: ca
Right alloca redzone: cb
==734==ABORTING
OK不要着急,一个一个看。基本上这类报告分为三个部分:
- 你在代码的哪里踩坑了?
- 你在内存的哪里踩坑了?
- 检测器的状态
我们整理一下:
| 报告项 | 含义 |
|---|---|
heap-buffer-overflow | 对堆分配内存越界写入 |
WRITE of size 14 at 0x... | 实际写入了 14 字节数据 |
[0x7baeeade0010,0x7baeeade0015) | 有效的堆内存范围,仅 5 字节 |
allocated by thread T0 here | 分配调用栈(malloc)位置 |
Shadow Bytes 与 “红区” 机制
你一定注意到了这段:
Shadow bytes around the buggy address:
=>0x7baeeade0000: fa fa[05]fa fa fa fa fa fa fa fa fa fa fa fa fa
这是 ASan 的“红区”显示。每个字节都不是实际的内存,而是 Shadow Memory —— 它标记了主内存的访问状态。
| Shadow 值 | 含义 |
|---|---|
00 | 可访问 |
fa | 红区(保护区域) |
fd | 已释放的堆 |
f5 | 函数返回后的栈 |
f9 | 全局变量红区 |
红区就像一圈“围栏”,ASan 在每次内存访问时都会检查是否踩到了红区。如果越过,就立刻报错。AddressSanitizer 在堆上分配内存时,会自动“包一层红区”:
[左红区] [用户数据] [右红区]
假设我们 malloc(5),实际分配的会更多:
<--- 16 bytes left redzone ---> [5 bytes user memory] <--- 16 bytes right redzone --->
当程序访问 buffer[5] 时,就进入了右红区,ASan 即刻检测到并报告。
💥 Example 2: Heap Use-After-Free
堆上越界访问让程序崩溃是显而易见的,而 释放后继续使用内存 更隐蔽,堪称程序员心头的大坑。
AddressSanitizer 能帮助我们立即发现这个问题。
Use After Free是这样的,一个子系统可能将托管的内存释放掉之后,其他成员也跑过来,他根据他可能的标记来决定是不是要访问,显然,如果我们标记和实际内存状态不一致,就会出现Heap Use-After-Free
#include <stdio.h>
#include <stdlib.h>
int main() {
char* buffer = (char*)malloc(5 * sizeof(char));
strcpy(buffer, "Hi!");
free(buffer); // 堆内存释放
printf("buffer: %s\n", buffer); // Use-after-free, 可能会在其他地方了
return 0;
}
注意最后一行:buffer 已经被释放,但仍然被访问。这个行为就是 use-after-free。检测这类错误我们还是按照相同的方式进行编译。
gcc -g -fsanitize=address -fno-omit-frame-pointer demo.c -o demo
./demo
我们能得到:
=================================================================
==783==ERROR: AddressSanitizer: heap-use-after-free on address 0x7c147f7e0010 at pc 0x55d335653245 bp 0x7ffcf9e73c90 sp 0x7ffcf9e73c80
READ of size 1 at 0x7c147f7e0010 thread T0
#0 0x55d335653244 in main /home/Charliechen/demo.c:14
#1 0x7ff480a27674 (/usr/lib/libc.so.6+0x27674) (BuildId: 4fe011c94a88e8aeb6f2201b9eb369f42b4a1e9e)
#2 0x7ff480a27728 in __libc_start_main (/usr/lib/libc.so.6+0x27728) (BuildId: 4fe011c94a88e8aeb6f2201b9eb369f42b4a1e9e)
#3 0x55d3356530e4 in _start (/home/Charliechen/demo+0x10e4) (BuildId: 8f16545c3797a5bc3231b7dda7fb011f9af1bac8)
0x7c147f7e0010 is located 0 bytes inside of 5-byte region [0x7c147f7e0010,0x7c147f7e0015)
freed by thread T0 here:
#0 0x7ff480f1f79d in free /usr/src/debug/gcc/gcc/libsanitizer/asan/asan_malloc_linux.cpp:51
#1 0x55d335653210 in main /home/Charliechen/demo.c:13
#2 0x7ff480a27674 (/usr/lib/libc.so.6+0x27674) (BuildId: 4fe011c94a88e8aeb6f2201b9eb369f42b4a1e9e)
#3 0x7ff480a27728 in __libc_start_main (/usr/lib/libc.so.6+0x27728) (BuildId: 4fe011c94a88e8aeb6f2201b9eb369f42b4a1e9e)
#4 0x55d3356530e4 in _start (/home/Charliechen/demo+0x10e4) (BuildId: 8f16545c3797a5bc3231b7dda7fb011f9af1bac8)
previously allocated by thread T0 here:
#0 0x7ff480f20cb5 in malloc /usr/src/debug/gcc/gcc/libsanitizer/asan/asan_malloc_linux.cpp:67
#1 0x55d3356531ca in main /home/Charliechen/demo.c:8
#2 0x7ff480a27674 (/usr/lib/libc.so.6+0x27674) (BuildId: 4fe011c94a88e8aeb6f2201b9eb369f42b4a1e9e)
#3 0x7ff480a27728 in __libc_start_main (/usr/lib/libc.so.6+0x27728) (BuildId: 4fe011c94a88e8aeb6f2201b9eb369f42b4a1e9e)
#4 0x55d3356530e4 in _start (/home/Charliechen/demo+0x10e4) (BuildId: 8f16545c3797a5bc3231b7dda7fb011f9af1bac8)
SUMMARY: AddressSanitizer: heap-use-after-free /home/Charliechen/demo.c:14 in main
Shadow bytes around the buggy address:
0x7c147f7dfd80: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x7c147f7dfe00: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x7c147f7dfe80: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x7c147f7dff00: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x7c147f7dff80: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
=>0x7c147f7e0000: fa fa[fd]fa fa fa fa fa fa fa fa fa fa fa fa fa
0x7c147f7e0080: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x7c147f7e0100: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x7c147f7e0180: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x7c147f7e0200: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x7c147f7e0280: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
Shadow byte legend (one shadow byte represents 8 application bytes):
Addressable: 00
Partially addressable: 01 02 03 04 05 06 07
Heap left redzone: fa
Freed heap region: fd
Stack left redzone: f1
Stack mid redzone: f2
Stack right redzone: f3
Stack after return: f5
Stack use after scope: f8
Global redzone: f9
Global init order: f6
Poisoned by user: f7
Container overflow: fc
Array cookie: ac
Intra object redzone: bb
ASan internal: fe
Left alloca redzone: ca
Right alloca redzone: cb
==783==ABORTING
💥 Example 3:栈内存越界(Stack Use After Return)
#include <stdio.h>
int* get_printed_int() {
int i = 5;
printf("Inner address: %p\n", &i);
return &i;
}
int main() {
int* buffer = get_printed_int();
printf("Outer address: %p\n", buffer);
printf("Get the Buffer:> %d\n", *buffer);
return 0;
}
编译命令:
gcc -fsanitize=address -O1 -g stack_use_after_scope.c -o stack_use_after_scope
运行结果:
Inner address: 0x7b55a6300060
Outer address: 0x7b55a6300060
=================================================================
==1057==ERROR: AddressSanitizer: stack-use-after-return on address 0x7b55a6300060
READ of size 4 at 0x7b55a6300060 thread T0
#0 0x55b2385413c6 in main (...)
#1 0x7f55a8a27674 (/usr/lib/libc.so.6+0x27674)
Address 0x7b55a6300060 is located in stack of thread T0 at offset 32 in frame
#0 ... get_printed_int
[32,36) 'i' (line 4) <== Memory access at offset 32 is inside this variable
很显然,我们触发了:
stack-use-after-return:函数返回后仍然访问它的局部变量。- ASan 把
i的栈帧标记为“已返回区域”(Shadow Byte =f5),任何访问都会立即报错。 - 报告明确指出问题行、函数名、变量名。
AddressSanitizer(ASan)还能做什么?
| 检测类型 | 说明 | 示例错误 |
|---|---|---|
| Stack use-after-return | 栈帧访问错误 | 返回局部变量地址 |
| Heap use-after-free | 堆释放后访问 | free 后仍使用指针 |
| Heap buffer overflow | 堆越界 | 数组下标越界 |
| Global buffer overflow | 全局变量越界 | 静态数组访问错误 |
| Double free | 重复释放 | free 两次 |
| Memory leaks | 内存泄漏(需 LSan) | 未 free |
更多检测组合
# Address + Undefined Behavior
gcc -fsanitize=address,undefined -O1 -g main.c -o main
# 检测内存泄漏(LeakSanitizer)
gcc -fsanitize=leak -O1 -g main.c -o leak_test
# 多线程检测(ThreadSanitizer)
gcc -fsanitize=thread -O1 -g thread_test.c -o thread_test
2万+

被折叠的 条评论
为什么被折叠?



