第二课:内存泄漏的“侦探游戏”:Valgrind与AddressSanitizer实战

各位同学,欢迎来到第二课。

上一节课我们聊了内存布局,知道了程序的内存是怎么划分的。但光知道理论没用,实际写代码时,内存泄漏就像个隐形杀手。你程序跑着跑着,内存越吃越多,最后系统卡死、重启——这种问题,我当年在智能音箱项目里可没少遇到。

今天我们就来玩一场“侦探游戏”。工具就是两把利器:ValgrindAddressSanitizer。它们能帮你揪出内存泄漏、越界访问这些“罪犯”。

为什么需要内存检测工具?

你想想看,C/C++ 程序员最头疼的是什么?

  • malloc 了,忘记 free
  • new 了,忘记 delete
  • 数组越界,写到了别人的地盘
  • 释放后还在用(use-after-free)

这些问题,编译器不会报错。程序可能跑几个小时才崩溃。我有个同事,调试一个内存泄漏问题花了整整三天,最后发现是少写了一个 free。嗯,这种痛,经历过的人都懂。

所以,我们需要专门的工具来帮我们“破案”。

第一把利器:Valgrind

Valgrind 是个老牌工具了。它通过模拟 CPU 来检测内存操作。说白了,你的程序跑在 Valgrind 构建的“虚拟机”里,每一步内存读写它都盯着。

基本用法

假设你有这样一个程序 leak.c

#include <stdlib.h>

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

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

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

gcc -g -o leak leak.c

然后用 Valgrind 运行:

valgrind --leak-check=full ./leak

输出会告诉你:

==12345== 40 bytes in 1 blocks are definitely lost in loss record 1 of 1
==12345==    at 0x4C2B0E0: malloc (vg_replace_malloc.c:299)
==12345==    by 0x40052E: func (leak.c:4)
==12345==    by 0x40053E: main (leak.c:8)

看到了吗?它直接告诉你:40 字节泄漏了,在 func 函数的第 4 行。这就是证据。

关键参数说明:

  • --leak-check=full:显示每个泄漏的详细信息
  • --show-leak-kinds=all:显示所有类型的泄漏(definite, indirect, possible, reachable)
  • --track-origins=yes:追踪未初始化变量的来源

我在项目中遇到的坑

我个人习惯,在智能音箱的音频处理模块开发时,每次提交代码前都会跑一遍 Valgrind。有一次,我发现一个“still reachable”的泄漏——程序退出时还有内存没释放。虽然操作系统会回收,但长期运行的服务程序,这种泄漏会慢慢吃掉内存。

排查后发现,是一个全局链表在程序退出时没有清理。嗯,这种问题,Valgrind 一抓一个准。

Valgrind 的局限性

说实话,Valgrind 有个大问题:。你的程序跑起来会慢 10-20 倍。在嵌入式设备上,有时候根本跑不动。我试过在树莓派上跑 Valgrind,一个简单的音频解码程序,等了十分钟还没出结果。

所以,我们需要更轻量的方案。

第二把利器:AddressSanitizer (ASan)

AddressSanitizer 是 Google 开发的工具,集成在 GCC 和 Clang 中。它不需要模拟 CPU,而是在编译时插入检测代码。速度比 Valgrind 快得多,通常只慢 2-3 倍。

基本用法

还是刚才那个泄漏程序,用 ASan 编译:

gcc -fsanitize=address -g -o leak_asan leak.c

直接运行:

./leak_asan

输出:

=================================================================
==12345==ERROR: LeakSanitizer: detected memory leaks

Direct leak of 40 byte(s) in 1 object(s) allocated from:
    #0 0x4c2b0e0 in malloc (/usr/lib/.../libasan.so.4)
    #1 0x40052e in func /home/user/leak.c:4
    #2 0x40053e in main /home/user/leak.c:8

SUMMARY: AddressSanitizer: 40 byte(s) leaked in 1 allocation(s).

效果和 Valgrind 类似,但速度快多了。

ASan 还能检测什么?

除了内存泄漏,ASan 还能抓出很多“坏蛋”:

问题类型 代码示例 ASan 检测结果
堆缓冲区溢出 int *p = malloc(4); p[4] = 0; heap-buffer-overflow
栈缓冲区溢出 int arr[4]; arr[4] = 0; stack-buffer-overflow
释放后使用 free(p); *p = 0; heap-use-after-free
双重释放 free(p); free(p); double-free

注意:ASan 会增加内存占用(约 2-3 倍),并且生成的二进制文件会变大。在资源受限的嵌入式设备上,建议只在调试阶段使用,发布版本一定要关闭。

实战:智能音箱音频缓冲区泄漏排查

我来分享一个真实案例。当时我们做智能音箱的语音唤醒功能,程序跑着跑着,内存占用从 50MB 涨到 200MB,然后系统 OOM(内存耗尽)被杀死。

我用 ASan 编译后运行,很快就定位到了问题:

==5678==ERROR: LeakSanitizer: detected memory leaks

Direct leak of 4096 byte(s) in 1 object(s) allocated from:
    #0 0x4c2b0e0 in malloc
    #1 0x401234 in audio_buffer_create audio_buffer.c:42
    #2 0x401abc in wakeup_process wakeup.c:78
    #3 0x402345 in main main.c:120

看到 audio_buffer.c:42 了吗?我打开一看,原来是在音频处理循环中,每次都会创建一个新的缓冲区,但只有在特定条件下才会释放。条件不满足时,缓冲区就泄漏了。

修复很简单:把释放逻辑移到循环末尾,确保每次创建后都会释放。改完后再跑 ASan,干干净净,零泄漏。

我的建议:

  • 开发阶段:用 ASan,速度快,反馈及时
  • 集成测试阶段:用 Valgrind,检测更全面
  • 嵌入式设备:如果 ASan 跑不动,可以用 Valgrind 的 --tool=memcheck 配合 --vgdb=yes 远程调试

避坑指南

我曾经犯过一个低级错误:在 ASan 检测通过后,直接发布了程序。结果用户反馈还是内存泄漏。排查了半天,发现是 ASan 的 -fsanitize=address 在发布版本中被去掉了,但代码里有个条件编译的路径,在发布版本中才会执行,而那条路径上有泄漏。

所以,一定要在发布前的最终版本上,用 Valgrind 再做一次全面检测。ASan 和 Valgrind 不是替代关系,而是互补的。

总结

内存泄漏检测,说白了就是一场“侦探游戏”。Valgrind 是那个慢条斯理但细致入微的老侦探,ASan 是那个雷厉风行但偶尔会漏掉细节的年轻探员。两者结合,才能把内存问题一网打尽。

下一节课,我们会深入智能音箱的音频处理流水线,看看如何用工具分析 CPU 热点,找到性能瓶颈。到时候,我会分享一个让音频延迟从 200ms 降到 30ms 的真实案例。

嗯,今天就到这里。记得动手试试,光看不练是学不会的。