19、内存调试工具:Valgrind、AddressSanitizer、GDB

写C语言程序,最头疼的是什么?

我个人觉得,不是算法难写,也不是逻辑复杂,而是——内存问题。

野指针、内存泄漏、越界访问……这些家伙就像幽灵一样,平时不出现,一出现就让你加班到凌晨。我刚开始工作的头两年,没少被它们折磨。后来我学乖了,开始用工具。今天聊的三把刀——Valgrind、AddressSanitizer、GDB,就是我这些年最顺手的家伙。

19.1 Valgrind:内存泄漏的照妖镜

Valgrind 是什么?说白了,它是一个动态分析工具。它会在你的程序运行时,悄悄地在背后盯着每一块内存的申请和释放。

我记得第一次用 Valgrind 时,是在一个服务器项目里。程序跑了两天,内存占用从 200MB 涨到了 2GB。领导问我怎么回事,我一脸懵。后来用 Valgrind 一查,好家伙,一个循环里每次 new 了对象,但只有一半被 delete。

19.1.1 基本用法

// 示例:一个典型的内存泄漏
#include <stdlib.h>

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

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

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

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

输出会告诉你:哪一行申请的內存没释放,有多少字节泄漏了。清清楚楚,明明白白。

核心参数速查:

  • --leak-check=full:显示每个泄漏的详细信息
  • --show-leak-kinds=all:显示所有类型的泄漏(definitely lost, indirectly lost 等)
  • --track-origins=yes:追踪未初始化值的来源(会慢一些,但信息更全)

19.1.2 常见问题与避坑

Valgrind 跑起来会慢很多,通常是正常速度的 5-10 倍。所以别在生产环境上直接跑,我一般是在测试环境或者本地调试时用。

注意:Valgrind 只能检测动态分配的内存问题(malloc/calloc/realloc)。栈上的越界访问,它管不了。这时候需要 AddressSanitizer 出马。

19.2 AddressSanitizer:编译时的守护神

AddressSanitizer(简称 ASan)是 Google 开发的工具。它跟 Valgrind 不一样,是在编译阶段插入检查代码的。说白了,它让你的程序自己给自己做体检。

我为什么喜欢 ASan?因为它快。Valgrind 跑一个大型项目可能要等半小时,ASan 编译时多花点时间,运行时只慢 2-3 倍。对于日常开发来说,这个代价完全可以接受。

19.2.1 基本用法

// 示例:数组越界
#include <stdio.h>

int main() {
    int arr[5] = {1, 2, 3, 4, 5};
    printf("%d\n", arr[10]);  // 越界访问!
    return 0;
}

编译时加上 -fsanitize=address

gcc -g -fsanitize=address -o asan_test asan_test.c
./asan_test

程序一跑,ASan 会直接报错,告诉你哪一行越界了,访问了哪个地址,甚至画出内存布局图。

我的习惯:在开发阶段的 CMakeLists.txt 里,我会默认加上 -fsanitize=address -fno-omit-frame-pointer。等要发布 Release 版本时再去掉。这样既能保证开发质量,又不影响线上性能。

19.2.2 ASan 能检测什么

问题类型 说明
堆越界 malloc 出来的内存,访问超出申请范围
栈越界 局部数组、局部变量访问越界
全局变量越界 全局数组或静态数组访问越界
释放后使用 free 之后继续访问那块内存
双重释放 对同一块内存 free 两次

你看,ASan 覆盖的场景比 Valgrind 更广。而且它还能检测栈上的问题,这是 Valgrind 做不到的。

19.3 GDB:最后的防线

Valgrind 和 ASan 都搞不定的时候,就该 GDB 上场了。

GDB 是 GNU 的调试器。它不像前两个工具那样自动帮你找问题,它更像一个显微镜,让你自己去看程序内部发生了什么。

我曾经遇到过一个极其诡异的 bug:程序在 Release 模式下跑得好好的,Debug 模式就崩溃。用 Valgrind 和 ASan 都查不出问题。最后是 GDB 一步步单步跟踪,发现是一个未初始化的局部变量在 Release 模式下被编译器优化掉了,Debug 模式下却保留了垃圾值。

19.3.1 常用命令速记

gcc -g -o my_program my_program.c
gdb ./my_program

进入 GDB 后,常用的命令:

  • break main:在 main 函数入口设断点
  • break file.c:42:在 file.c 的第 42 行设断点
  • run:开始运行程序
  • next:单步执行(不进入函数内部)
  • step:单步执行(进入函数内部)
  • print var:打印变量 var 的值
  • backtrace:查看函数调用栈
  • info locals:查看当前函数的所有局部变量

调试内存问题的 GDB 技巧:

  • print ptr 查看指针的值,确认它是不是 NULL
  • x/10x ptr 查看指针指向的内存内容(16进制显示10个字节)
  • watch *ptr 监控某个内存地址的变化,一有改动就停下来

19.3.2 实战:用 GDB 定位段错误

// segment_fault.c
#include <stdio.h>

void crash_me() {
    int *p = NULL;
    *p = 42;  // 对空指针解引用,必崩
}

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

编译运行,程序直接段错误。用 GDB 跑一下:

gdb ./segment_fault
(gdb) run
// 程序崩溃,GDB 自动停在崩溃位置
(gdb) backtrace
// 显示调用栈:main -> crash_me
(gdb) print p
// 输出:$1 = (int *) 0x0
// 看到没?p 是 NULL,问题找到了

嗯,就是这么简单。但实际项目中,调用栈可能很深,变量名也可能很复杂。这时候就需要耐心,一层层往上查。

19.4 三把刀怎么选

我个人的经验是这样的:

  • 日常开发:编译时开 ASan,跑单元测试。大部分问题都能在这步解决。
  • 集成测试:用 Valgrind 跑一遍,重点查内存泄漏。尤其是长时间运行的服务,泄漏一点积累起来就是大问题。
  • 疑难杂症:ASan 和 Valgrind 都查不出来,或者报错信息看不懂,上 GDB 手动调试。

这三把刀配合使用,基本上能覆盖 95% 以上的内存问题。剩下的 5%,嗯……那可能是硬件问题或者编译器 bug,遇到了算你运气好。

内存调试工具知识体系 内存调试工具 Valgrind AddressSanitizer GDB 检测内存泄漏 运行时动态分析 速度慢,信息全 编译时插入检查 覆盖堆/栈/全局越界 速度快,适合日常开发 手动单步调试 查看调用栈/变量 解决疑难杂症

一句话总结:Valgrind 查泄漏,ASan 查越界,GDB 查疑难。三个工具配合使用,内存问题无处遁形。

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