17、调试内存问题:使用Valgrind检测内存泄漏、使用AddressSanitizer

内存泄漏,说白了就是程序申请了内存,用完后忘了还。一次两次没事,跑上几天几夜,内存就被吃光了。我见过不少线上事故,都是这么来的。

今天聊两个工具:ValgrindAddressSanitizer。一个老牌经典,一个新贵高效。我个人习惯两个都掌握,因为不同场景各有优势。

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 本章知识体系

下面这张图总结了内存调试的核心流程和工具选择:

内存调试工具选择流程 发现内存问题 是否可重新编译? AddressSanitizer Valgrind Valgrind 检测 • 内存泄漏 • 未初始化读取 ASan 检测 • 越界访问 • Use-After-Free 交叉验证,确保万无一失

说白了,内存调试没有银弹。Valgrind 和 ASan 各有千秋,我建议你两个都装好,根据场景灵活切换。记住一点:内存问题越早发现,修复成本越低。别等到线上崩了再查,那时候就晚了。


公众号:蓝海资料掘金营,微信deep3321