9、内存泄漏排查:Valgrind与AddressSanitizer实战

内存泄漏,说白了就是程序申请了内存,用完后没还给系统。

你想想看,一个长期运行的服务,每天漏一点,一周后可能就崩了。我在项目中遇到过好几次线上事故,最后定位都是内存泄漏。今天我们就来聊聊怎么用 Valgrind 和 AddressSanitizer 这两个利器,把泄漏揪出来。

9.1 内存泄漏的本质

先看一个最简单的泄漏例子:

void leak_example() {
    int *p = (int *)malloc(10 * sizeof(int));
    // 忘记 free(p)
    return;
}

每次调用这个函数,就泄漏 40 字节。如果每秒调用一次,一天下来就是 3.3MB。嗯,看起来不多?但如果是嵌入式设备,这点内存可能就要命了。

核心要点:内存泄漏 = 申请 + 丢失指针 + 无法释放

9.2 Valgrind 实战

Valgrind 是我最早接触的内存检测工具。它通过模拟 CPU 来检测内存操作,精度很高,但代价是程序会跑得慢很多。

9.2.1 基本用法

// test_leak.c
#include <stdlib.h>

void func1() {
    char *buf = (char *)malloc(100);
    // 泄漏 100 字节
}

void func2() {
    int *arr = (int *)malloc(200 * sizeof(int));
    // 泄漏 800 字节
}

int main() {
    func1();
    func2();
    return 0;
}

编译时记得加 -g 选项,保留调试信息:

gcc -g -o test_leak test_leak.c
valgrind --leak-check=full ./test_leak

输出结果会告诉你:

  • 哪里申请的内存
  • 泄漏了多少字节
  • 调用栈信息

个人习惯:我一般会加上 --show-leak-kinds=all 参数,这样能显示所有类型的泄漏,包括"仍可访问"的泄漏。

9.2.2 常见泄漏类型

类型 含义 严重程度
definitely lost 确定泄漏,指针丢失
indirectly lost 间接泄漏,结构体内部泄漏
possibly lost 可能泄漏,指针被覆盖
still reachable 指针还在,但未释放

我曾经在一个网络协议栈里遇到 indirectly lost 的问题。链表节点释放了,但节点里的数据缓冲区没释放。Valgrind 直接帮我定位到了具体行号。

9.2.3 Valgrind 的局限性

说实话,Valgrind 有两个痛点:

  1. 慢。 程序运行速度会下降 10-20 倍。我有个项目跑一次要 3 小时,用 Valgrind 得等一天。
  2. 内存占用高。 它本身就需要大量内存来记录信息。

所以,对于大型项目,我更喜欢用 AddressSanitizer。

9.3 AddressSanitizer 实战

AddressSanitizer(简称 ASan)是 Google 开发的工具,编译时插入检测代码,运行时检查。速度比 Valgrind 快很多,大概只慢 2-3 倍。

9.3.1 基本用法

gcc -g -fsanitize=address -o test_asan test_leak.c
./test_asan

就这么简单。运行时如果发生泄漏,程序会直接报错退出,并打印详细信息。

注意:ASan 默认会在程序退出时报告泄漏。如果想在运行过程中就检测,可以设置环境变量 ASAN_OPTIONS=detect_leaks=1

9.3.2 实战案例:循环泄漏

void loop_leak() {
    for (int i = 0; i < 100; i++) {
        char *p = (char *)malloc(1024);
        // 每次循环泄漏 1KB
    }
}

int main() {
    loop_leak();
    return 0;
}

用 ASan 跑一下,输出会告诉你:

  • 泄漏总大小:100KB
  • 泄漏发生的位置:loop_leak 函数
  • 调用栈:main -> loop_leak -> malloc

嗯,这里要注意:ASan 的泄漏报告默认只显示一次。如果你想看每次泄漏的详细信息,可以加 ASAN_OPTIONS=leak_check_at_exit=false

9.3.3 与 Valgrind 的对比

特性 Valgrind AddressSanitizer
速度 慢 10-20 倍 慢 2-3 倍
内存占用 中等
检测精度 极高
使用方式 直接运行 重新编译
适用场景 小规模、精确分析 大规模、持续集成

我的建议:开发阶段用 ASan,因为它快,能快速反馈。发布前再用 Valgrind 做一次全面检查,确保万无一失。

9.4 避坑指南

我曾经踩过的坑:

  • 在信号处理函数里 malloc,但没 free。信号处理函数执行频率低,泄漏不明显,但积累久了就出问题。
  • 第三方库的内存泄漏。用 Valgrind 时记得加 --suppressions 参数过滤掉库的泄漏,只看自己的代码。
  • 多线程环境下的泄漏。ASan 默认支持多线程,但 Valgrind 需要加 --tool=helgrind--tool=drd

9.5 知识体系图

下面这张图总结了内存泄漏排查的核心流程:

内存泄漏排查知识体系 内存泄漏排查 Valgrind AddressSanitizer --leak-check=full --show-leak-kinds -fsanitize=address ASAN_OPTIONS 开发用 ASan,发布前用 Valgrind 双剑合璧,内存泄漏无处遁形

9.6 实战总结

内存泄漏排查,说白了就是两件事:

  1. 找到泄漏点。 Valgrind 和 ASan 都能帮你做到。
  2. 修复泄漏。 这需要你理解代码逻辑,确保每个 malloc 都有对应的 free。

我个人习惯在项目的 Makefile 里加一个 asan 编译目标,平时开发就用它。发布前再用 Valgrind 跑一遍回归测试。这样既能保证效率,又能确保质量。

记住:内存泄漏不是 bug,是债务。今天不还,明天连本带利。


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