17、调试内存问题:使用Valgrind检测内存泄漏、使用AddressSanitizer
内存泄漏,说白了就是程序申请了内存,用完后忘了还。一次两次没事,跑上几天几夜,内存就被吃光了。我见过不少线上事故,都是这么来的。
今天聊两个工具:Valgrind 和 AddressSanitizer。一个老牌经典,一个新贵高效。我个人习惯两个都掌握,因为不同场景各有优势。
17.1 内存泄漏到底长什么样?
先看一个最典型的例子:
#include <stdlib.h>
void leak() {
int *p = (int*)malloc(10 * sizeof(int));
// 忘记 free(p)
}
int main() {
leak();
return 0;
}
这段代码跑完,10个int的内存就丢了。程序退出时操作系统会回收,但如果是长期运行的服务,每次调用leak()都丢40字节,一天下来就是几MB,一个月呢?
核心观点:内存泄漏的可怕之处不在于单次泄漏量,而在于累积效应。服务器跑一个月后突然OOM,你查日志会发现——嗯,就是这种小泄漏堆出来的。
17.2 Valgrind:老牌内存检测工具
Valgrind 是一个工具集,其中最常用的是 Memcheck。它能检测:
- 未初始化的内存读取
- 越界访问(数组下标超了)
- 使用已释放的内存(野指针)
- 内存泄漏(malloc了没free)
17.2.1 基本用法
// 编译时加 -g 保留调试信息
gcc -g -o test test.c
// 用 Valgrind 运行
valgrind --leak-check=full ./test
输出大概长这样:
==12345== HEAP SUMMARY:
==12345== in use at exit: 40 bytes in 1 blocks
==12345== total heap usage: 1 allocs, 0 frees, 40 bytes allocated
==12345==
==12345== 40 bytes in 1 blocks are definitely lost in loss record 1 of 1
==12345== at 0x4C2B800: malloc (vg_replace_malloc.c:299)
==12345== by 0x40052E: leak (test.c:4)
==12345== by 0x40053E: main (test.c:9)
看到 "definitely lost" 了吗?这就是明确的内存泄漏。Valgrind 还会告诉你泄漏发生在哪个文件的哪一行。
个人经验:我习惯在开发阶段就用 Valgrind 跑一遍单元测试。尤其是那些涉及链表、树结构的代码,很容易漏掉某个节点的释放。Valgrind 一跑,漏一个报一个,省心。
17.2.2 常见泄漏类型
| Valgrind 报告类型 | 含义 | 常见原因 |
|---|---|---|
| definitely lost | 明确泄漏,指针丢失 | malloc后没free,且指针被覆盖 |
| indirectly lost | 间接泄漏,结构体内部指针丢失 | 链表头节点释放了,但子节点没释放 |
| possibly lost | 可能泄漏,指针被部分覆盖 | 指针运算后指向了中间位置 |
| still reachable | 程序退出时仍有未释放内存,但指针还在 | 全局变量指向的堆内存,程序退出前没释放 |
注意:"still reachable" 不一定是错误。比如全局配置结构体,程序退出时操作系统会回收。但如果你写的是长期运行的服务,最好还是显式释放,养成好习惯。
17.2.3 Valgrind 的缺点
Valgrind 最大的问题是——慢。程序跑起来会慢 10-20 倍。我曾经在一个大型项目上跑 Valgrind,原本 1 秒的测试跑了 20 秒。所以它适合开发阶段,不适合线上环境。
17.3 AddressSanitizer:现代编译器的利器
AddressSanitizer(简称 ASan)是 LLVM/GCC 内置的检测工具。它比 Valgrind 快得多,而且能检测更多类型的内存错误。
17.3.1 基本用法
// 编译时加 -fsanitize=address
gcc -g -fsanitize=address -o test test.c
// 直接运行
./test
ASan 会在程序崩溃时输出详细的错误信息:
==12345==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x6020000000f4
WRITE of size 4 at 0x6020000000f4 thread T0
#0 0x4006b4 in main test.c:5
#1 0x7f8c9b2c0b96 in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x21b96)
0x6020000000f4 is located 0 bytes to the right of 20-byte region [0x6020000000e0,0x6020000000f4)
看到 "heap-buffer-overflow" 了吗?这就是越界访问。ASan 会精确告诉你:你在第5行写了一个int,这个int刚好在合法区域的右边。
17.3.2 ASan 能检测什么?
- 堆越界:malloc 了 20 字节,你写了第 21 个字节
- 栈越界:数组下标超了
- 全局变量越界:全局数组访问越界
- Use-After-Free:释放后继续使用指针
- Double Free:重复释放同一块内存
- 内存泄漏:配合
ASAN_OPTIONS=detect_leaks=1
避坑指南:我曾经在一个项目中遇到 Use-After-Free 的 bug,程序跑 3 小时才崩溃一次。用 Valgrind 跑太慢,后来换成 ASan,第一次运行就抓到了。ASan 的检测能力确实强,而且只慢 2-3 倍,适合日常开发。
17.3.3 ASan 的配置选项
// 通过环境变量控制
export ASAN_OPTIONS="detect_leaks=1:log_path=./asan.log"
// 或者直接运行
ASAN_OPTIONS=detect_leaks=1 ./test
常用选项:
| 选项 | 作用 | 默认值 |
|---|---|---|
| detect_leaks | 启用内存泄漏检测 | 0(关闭) |
| log_path | 错误日志输出路径 | stderr |
| allocator_may_return_null | 内存不足时返回NULL而非崩溃 | 0 |
| max_redzone | 越界检测的冗余区域大小 | 128 |
17.4 两者对比:什么时候用哪个?
| 对比维度 | Valgrind | AddressSanitizer |
|---|---|---|
| 速度 | 慢 10-20 倍 | 慢 2-3 倍 |
| 内存占用 | 高(模拟CPU) | 中等(约2倍) |
| 检测范围 | 内存泄漏、未初始化、越界 | 越界、Use-After-Free、Double Free、泄漏 |
| 是否需要重新编译 | 不需要(直接运行) | 需要(加编译选项) |
| 适用场景 | 开发阶段、小规模测试 | 开发阶段、CI/CD、甚至线上(谨慎) |
我的建议:日常开发用 ASan,因为它快,能集成到 CI 流程中。遇到疑难杂症再用 Valgrind 做交叉验证。两个工具配合使用,基本能覆盖 99% 的内存问题。
17.5 实战:一个完整的检测流程
假设你有这样一个函数:
#include <stdlib.h>
#include <string.h>
char* create_string(const char* src) {
char* p = (char*)malloc(strlen(src) + 1);
strcpy(p, src);
return p;
}
int main() {
char* s = create_string("hello");
// 忘记 free(s)
s[10] = 'x'; // 越界写入
return 0;
}
用 ASan 编译运行:
gcc -g -fsanitize=address -o test test.c
./test
你会看到:
==12345==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x60200000001a
WRITE of size 1 at 0x60200000001a thread T0
#0 0x4006c8 in main test.c:10
同时,如果开启了泄漏检测:
ASAN_OPTIONS=detect_leaks=1 ./test
还会报告:
==12345==ERROR: LeakSanitizer: detected memory leaks
Direct leak of 6 byte(s) in 1 object(s) allocated from:
#0 0x4c2b800 in malloc
#1 0x4006a0 in create_string test.c:4
一次运行,两个问题全抓到。这就是 ASan 的威力。
注意:ASan 会占用更多内存(大约 2 倍),而且会改变程序的内存布局。有些 bug 在 ASan 下可能不出现,关了反而出现。所以最终测试还是要关掉 ASan 跑一遍。
17.6 本章知识体系
下面这张图总结了内存调试的核心流程和工具选择:
说白了,内存调试没有银弹。Valgrind 和 ASan 各有千秋,我建议你两个都装好,根据场景灵活切换。记住一点:内存问题越早发现,修复成本越低。别等到线上崩了再查,那时候就晚了。
公众号:蓝海资料掘金营,微信deep3321