第4章:Valgrind实战:安装与配置、Memcheck工具使用、检测泄漏点、生成调用栈、抑制文件的使用
说到内存泄漏排查,Valgrind 绝对是我工具箱里最趁手的家伙。干机顶盒这行十年,我至少有三分之一的内存问题都是靠它揪出来的。今天咱们就把它掰开揉碎,看看怎么在嵌入式环境里用好它。
4.1 安装与配置:别让环境卡住你
Valgrind 的安装其实不复杂,但嵌入式环境里坑不少。我个人习惯先在开发主机上装好,再交叉编译到目标板。
主机端安装(Ubuntu/Debian):
sudo apt-get install valgrind
嗯,就这么简单。但如果你用的是老旧的内核,比如 2.6.x 系列,我建议你从源码编译。为什么?因为老内核的 ptrace 接口有差异,直接装包可能跑不起来。
交叉编译到机顶盒:
./configure --host=arm-linux-gnueabihf --prefix=/opt/valgrind-arm
make
make install
这里要注意一点:目标板的 libc 版本必须和编译链匹配。我曾经在一个项目里吃过亏,编译链是 glibc 2.19,目标板却是 2.17,结果 Valgrind 一跑就段错误。折腾了两天才发现是版本不匹配。
--host 参数和你目标板的架构完全一致。armv7 和 armv8 的指令集不同,选错了跑都跑不起来。
4.2 Memcheck工具使用:最常用的那个
Memcheck 是 Valgrind 的默认工具,也是我排查内存泄漏的首选。说白了,它就是在你的程序外面包了一层虚拟机,每条内存操作都会被它盯着。
基本用法:
valgrind --tool=memcheck ./your_stb_app
跑起来之后,你会看到类似这样的输出:
==12345== Memcheck, a memory error detector
==12345== Copyright (C) 2002-2017, and GNU GPL'd, by Julian Seward et al.
==12345== Using Valgrind-3.15.0 and LibVEX; rerun with -h for copyright info
==12345== Command: ./your_stb_app
==12345==
...(程序运行输出)...
==12345== HEAP SUMMARY:
==12345== in use at exit: 1,024 bytes in 1 blocks
==12345== total heap usage: 100 allocs, 99 frees, 50,000 bytes allocated
==12345==
==12345== 1,024 bytes in 1 blocks are definitely lost in loss record 1 of 1
==12345== at 0x4847568: malloc (vg_replace_malloc.c:381)
==12345== by 0x4005A7: main (test.c:10)
看到「definitely lost」了吗?这就是泄漏点。我一般先看这个,因为它最确定。至于「possibly lost」和「still reachable」,嗯,那些可以稍后处理,但「definitely lost」必须立刻解决。
--leak-check=full 参数。这样能显示更详细的泄漏信息,包括每个泄漏块的大小和分配位置。
4.3 检测泄漏点:从海量日志里捞针
机顶盒程序通常很大,几十万行代码是常事。第一次跑 Memcheck,输出日志可能有好几万行。怎么快速定位?我有个习惯:先看摘要,再看细节。
常用参数组合:
valgrind --tool=memcheck --leak-check=full --show-leak-kinds=all --log-file=leak.log ./your_stb_app
日志文件里,我会先搜索「definitely lost」和「indirectly lost」。这两个是硬伤,必须修。然后看「possibly lost」,这个可能是误报,也可能是野指针导致的。最后看「still reachable」,这个通常可以忽略——除非你明确知道程序退出前应该释放所有内存。
我记得有一次排查一个视频解码模块的泄漏,日志里显示「still reachable」有 2MB。团队里有人觉得不用管,但我坚持查了一下。结果发现是一个全局缓存没有在模块卸载时清理,虽然程序退出时会被系统回收,但运行过程中这 2MB 就白白占着。修完之后,机顶盒连续运行 7 天不重启也没问题了。
4.4 生成调用栈:找到谁调了谁
光知道内存泄漏在哪个函数还不够,你得知道是谁调了这个函数。调用栈就是干这个的。
默认情况下,Valgrind 会显示调用栈:
==12345== 1,024 bytes in 1 blocks are definitely lost in loss record 1 of 1
==12345== at 0x4847568: malloc (vg_replace_malloc.c:381)
==12345== by 0x4005A7: create_buffer (buffer.c:25)
==12345== by 0x4005D3: init_video_pipeline (video.c:120)
==12345== by 0x4005F0: main (main.c:45)
你看,从 main 到 init_video_pipeline 再到 create_buffer,最后在 malloc 里分配了内存但没释放。这条链就是你的排查路径。
但有时候调用栈不够深,或者被优化掉了。这时候我会加上:
--num-callers=30
这个参数能显示最多 30 层调用栈。我一般设成 20 就够用了,太多反而眼花。
-g 选项保留调试符号。否则你看到的调用栈全是地址,没有函数名和行号,那排查起来就痛苦了。
4.5 抑制文件的使用:过滤掉已知的噪音
机顶盒里经常用到第三方库,比如芯片厂商提供的闭源驱动。这些库可能有内存泄漏,但你改不了源码。每次跑 Valgrind 都看到一堆它们的错误,很烦人。
抑制文件就是用来干这个的——告诉 Valgrind:「这些错误我已知,别报了」。
生成抑制文件:
valgrind --tool=memcheck --gen-suppressions=all --log-file=supp.log ./your_stb_app
然后从 supp.log 里提取出你想要的抑制规则,保存到 my_suppressions.supp 文件里。格式大概是这样的:
{
<厂商驱动泄漏抑制>
Memcheck:Leak
match-leak-kinds: definite
fun:malloc
...
fun:vendor_driver_init
}
使用抑制文件:
valgrind --tool=memcheck --suppressions=my_suppressions.supp ./your_stb_app
我个人习惯维护一个项目专用的抑制文件,里面放三类内容:
- 第三方闭源库的已知泄漏
- 内核态驱动通过 mmap 映射的内存(Valgrind 管不到)
- 某些编译器生成的临时对象(比如 STL 容器的内部缓存)
好了,Valgrind 的基本用法就这些。下一章咱们聊聊怎么在机顶盒这种资源受限的环境里,让 Valgrind 跑得更顺畅。毕竟,有些机顶盒只有 128MB 内存,Valgrind 本身就要吃掉不少资源。