86、Valgrind 内存检测:如何使用 Valgrind 检测内存泄漏?

内存泄漏,说白了就是程序申请了内存,用完之后忘了还回去。你想想看,嵌入式设备内存本来就金贵,泄漏一点可能没事,但日积月累,系统迟早会崩。我早年做机顶盒项目时,就吃过这个亏——设备运行三天后必死机,查了整整一周,最后发现是一个回调函数里每次调用都 malloc 了 128 字节,但从来没 free。

嗯,今天我们就来聊聊 Valgrind,这个 Linux 下最经典的内存检测工具。它就像给你的程序装了个 X 光机,能清清楚楚看到每一块内存的来龙去脉。

Valgrind 是什么?

Valgrind 是一个开源的内存调试和性能分析工具集。它的核心是一个虚拟的 CPU,程序跑在它上面,所有内存操作都会被监控。说白了,它就是个「内存警察」,谁申请了没释放、谁越界访问了、谁用了野指针,它全给你记下来。

我个人习惯在开发阶段一直开着 Valgrind 跑测试用例,尤其是那些边界条件。你想想看,很多 bug 在正常路径上根本不会暴露,但 Valgrind 能帮你提前揪出来。

核心能力:

  • 检测内存泄漏(Memory Leak)
  • 检测非法内存访问(越界、野指针)
  • 检测使用未初始化内存
  • 检测 double free 或 free 错误地址

安装 Valgrind

大多数 Linux 发行版都自带 Valgrind,如果没有,一行命令搞定:

# Ubuntu / Debian
sudo apt-get install valgrind

# CentOS / RHEL
sudo yum install valgrind

# 验证安装
valgrind --version

我建议你装最新版,老版本对某些 ARM 平台的支持不太好。嵌入式交叉编译时,记得在目标板上也装一份。

基本用法

使用 Valgrind 非常简单,只需要在程序执行命令前加上 valgrind 即可:

valgrind ./my_program

默认情况下,它会检测内存泄漏并输出摘要。但更常用的做法是加上 --leak-check=full 参数,这样能显示泄漏的具体位置:

valgrind --leak-check=full ./my_program

来看一个经典的泄漏例子:

#include <stdlib.h>

void leak_memory(void) {
    int *p = (int *)malloc(100 * sizeof(int));
    // 忘记 free(p)
}

int main(void) {
    leak_memory();
    return 0;
}

编译并运行 Valgrind:

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

输出结果中,你会看到类似这样的信息:

==12345== HEAP SUMMARY:
==12345==     in use at exit: 400 bytes in 1 blocks
==12345==   total heap usage: 1 allocs, 0 frees, 400 bytes allocated
==12345== 
==12345== 400 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_memory (leak_demo.c:4)
==12345==    by 0x40053E: main (leak_demo.c:8)

看到了吗?它明确告诉你:leak_demo.c 第 4 行申请的 400 字节「definitely lost」了。这个「definitely lost」是 Valgrind 最严重的泄漏等级,意味着程序确实丢失了这块内存的指针,再也无法释放。

理解 Valgrind 的输出

Valgrind 把内存泄漏分为几个等级,我整理了一张表:

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

我曾经在项目中遇到过「still reachable」的情况,很多新手会忽略它。但我的建议是:只要程序退出前没有主动释放,都应该视为问题。尤其是在嵌入式系统中,长期运行的进程会不断累积「still reachable」的内存,最终导致 OOM。

常用参数详解

Valgrind 的参数非常多,但日常开发中记住这几个就够了:

  • --leak-check=full:显示每个泄漏的详细信息
  • --show-leak-kinds=all:显示所有类型的泄漏
  • --track-origins=yes:追踪未初始化内存的来源
  • --log-file=valgrind.log:将输出重定向到文件
  • --tool=memcheck:默认工具,也可以显式指定

一个比较完整的用法:

valgrind --tool=memcheck --leak-check=full \
         --show-leak-kinds=all --track-origins=yes \
         --log-file=valgrind_report.log ./my_program

嗯,这里要注意:--track-origins=yes 会显著降低运行速度,但能帮你定位到未初始化变量的源头。调试阶段可以开着,性能测试时关掉。

检测非法内存访问

除了泄漏,Valgrind 还能检测越界访问。来看这个例子:

#include <stdlib.h>

int main(void) {
    int *arr = (int *)malloc(5 * sizeof(int));
    arr[5] = 100;  // 越界!
    free(arr);
    return 0;
}

运行 Valgrind 后,你会看到:

==12345== Invalid write of size 4
==12345==    at 0x40052E: main (invalid_access.c:5)
==12345==  Address 0x5204044 is 0 bytes after a block of size 20 alloc'd
==12345==    at 0x4C2B800: malloc (vg_replace_malloc.c:299)
==12345==    by 0x40051E: main (invalid_access.c:4)

它精确地告诉你:第 5 行写了一个大小为 4 字节的非法地址,这个地址位于 malloc 分配的 20 字节块之后。这种 bug 在运行时可能不会立刻崩溃,但 Valgrind 能帮你提前发现。

SVG 流程图:Valgrind 检测流程

下面这张图展示了 Valgrind 检测内存泄漏的完整流程:

Valgrind 内存泄漏检测流程 1. 编译程序(加 -g) 2. valgrind --leak-check=full 3. 监控所有 malloc/free 操作 有泄漏? 输出泄漏位置 修复代码 无泄漏 程序通过 注:-g 选项保留调试符号,Valgrind 才能显示源码行号

实战技巧与避坑指南

我在实际项目中总结了一些经验,分享给你:

技巧 1:使用 suppressions 文件

有些第三方库或系统库本身就有内存泄漏,你不想每次都看到它们。可以生成一个 suppressions 文件来过滤:

valgrind --gen-suppressions=all --leak-check=full ./my_program 2> supp.txt

然后提取出需要的规则,下次运行时加上 --suppressions=supp.txt 即可。

警告:Valgrind 的局限性

  • 程序运行速度会降低 10-20 倍,不适合性能测试
  • 不支持所有嵌入式平台(如某些 RTOS)
  • 无法检测静态分配的内存泄漏
  • 对多线程程序的检测可能不够精确

我曾经在调试一个网络协议栈时,Valgrind 报告了上百个「possibly lost」。一开始我以为是代码问题,后来发现是 libc 的某些内部实现导致的。这时候就需要用 suppressions 文件来过滤掉这些误报。

在嵌入式环境中的替代方案

如果你的目标平台不支持 Valgrind(比如 ARM Cortex-M 系列 MCU),可以考虑以下替代方案:

  • dmalloc:轻量级内存调试库,可移植性好
  • mtrace:GNU 工具,通过设置环境变量启用
  • 自定义 malloc 钩子:重写 malloc/free,记录分配信息
  • 静态分析工具:如 Coverity、PC-lint

嗯,这里要提醒一句:Valgrind 虽然强大,但它不是万能的。养成良好的编程习惯才是根本——每次 malloc 后立即写对应的 free,使用智能指针(C++)或 RAII 思想,这些才是治本之策。

核心要点回顾:

  1. 编译时加 -g 选项,Valgrind 才能显示源码行号
  2. 使用 --leak-check=full 获取详细泄漏信息
  3. 重点关注「definitely lost」和「indirectly lost」
  4. 「still reachable」在长期运行的程序中也要处理
  5. 学会使用 suppressions 文件过滤第三方库的误报

说白了,Valgrind 就是你写 C 程序时的「安全带」。每次提交代码前跑一遍,能省下后面调试的无数时间。我现在的开发流程里,Valgrind 已经和编译一样,成了 CI 流水线的必备环节。

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