2. 内存泄漏检测工具链:Valgrind、AddressSanitizer、MTK自研MallocDebug工具介绍

做嵌入式开发,尤其是座舱这种7x24小时跑的系统,内存泄漏就是慢性毒药。我见过太多项目,前期功能跑得飞起,一跑耐久测试就崩。说白了,内存泄漏检测不是「要不要做」的问题,而是「用什么工具做」的问题。

今天我就把三把常用的刀——Valgrind、AddressSanitizer、MTK自研的MallocDebug——掰开揉碎了讲清楚。每把刀都有它的脾气,用对了事半功倍,用错了...嗯,你会怀疑人生的。

2.1 Valgrind:Linux世界的瑞士军刀

Valgrind这玩意儿,做Linux开发的应该都不陌生。它是个重量级选手,原理是模拟一个CPU,在程序运行时做动态分析。说白了,你的程序跑在Valgrind上,就像演员在绿幕前演戏——一举一动都被盯着。

核心能力:检测未释放的内存、越界访问、使用未初始化变量等。

我在项目中遇到过最典型的一个场景:一个后台服务跑了三天,内存从50MB涨到了800MB。用Valgrind一跑,发现是一个定时器回调里每次new了对象但没delete。你想想看,这种问题靠肉眼review代码,得看到猴年马月去。

2.1.1 基本用法

# 编译时加 -g 保留调试信息
g++ -g -O0 my_app.cpp -o my_app

# 运行Valgrind
valgrind --tool=memcheck --leak-check=full ./my_app

我个人习惯加几个参数:

valgrind --tool=memcheck --leak-check=full \
         --show-leak-kinds=all \
         --track-origins=yes \
         --verbose ./my_app 2> valgrind_report.log

--track-origins=yes 这个参数特别有用。它能告诉你未初始化变量的源头在哪里。我曾经靠这个参数追到一个底层驱动库里的bug——一个结构体成员没赋值就被拿去用了。

2.1.2 输出解读

Valgrind的输出分几类:

类型 含义 严重程度
definitely lost 确定泄漏,没人持有指针 🔴 高
indirectly lost 间接泄漏,比如丢失了链表头 🔴 高
possibly lost 可能泄漏,指针被覆盖了 🟡 中
still reachable 程序退出时还有指针指向,但没释放 🟢 低

我的经验:「still reachable」在嵌入式里不一定安全。座舱系统退出时如果不做清理,下次启动可能就出问题。我一般把这类也当泄漏处理。

2.1.3 嵌入式上的局限

Valgrind有个硬伤——慢。程序跑起来会慢10-20倍。在MTK8676这种座舱芯片上,如果你跑的是图形界面或者多媒体应用,Valgrind基本跑不动。我试过一次,一个视频播放器启动花了三分钟...直接放弃。

所以Valgrind更适合:

  • 后台服务、协议栈这类计算密集度不高的模块
  • 单元测试阶段,跑小规模的用例
  • 本地开发环境,不是目标板

2.2 AddressSanitizer (ASan):编译时的守护神

ASan是Google搞出来的,现在已经是GCC和Clang的内置功能了。它的思路和Valgrind完全不同——不是在运行时模拟,而是在编译时插桩。

说白了,编译器帮你往代码里塞了一堆检查代码。每次malloc、free、数组访问,都会被监控。好处是快,只慢2-3倍;坏处是得重新编译。

2.2.1 怎么用

# 编译时加这两个flag
g++ -fsanitize=address -fno-omit-frame-pointer -g my_app.cpp -o my_app

# 直接运行,不用额外工具
./my_app

就这么简单。运行时如果检测到问题,程序会直接崩溃并打印出详细的调用栈。我特别喜欢这一点——不用像Valgrind那样等报告,出问题当场就崩,崩得明明白白。

2.2.2 能抓到什么

  • 堆内存越界:比如new了10个字节,你写了第11个
  • 栈内存越界:局部数组访问超了
  • 全局变量越界:这个Valgrind抓不到
  • Use-After-Free:释放了还在用,经典问题
  • Double-Free:释放两次

注意:ASan在MTK8676的Android系统上也能用。但有个坑——如果用了自定义的malloc实现(比如MTK的MallocDebug),ASan可能和它冲突。我遇到过编译过了,跑起来直接挂的情况。

2.2.3 我的使用习惯

我在项目中一般这么搭配:

  • 开发阶段:ASan常开,所有模块都加
  • 集成测试:ASan开在核心模块上,外围模块不开(怕影响性能)
  • 发布前:关掉ASan,用Valgrind做一次全量扫描

为什么还要Valgrind?因为ASan有漏检的情况。比如你malloc了100字节,但只初始化了前50字节,后面50字节读出来是垃圾——ASan不管这个,但Valgrind能抓到。

2.3 MTK自研MallocDebug:座舱专属的利器

这个工具是MTK为自家芯片平台量身定做的。说实话,我第一次用的时候觉得「这不就是个内存统计工具吗」,后来才发现我错了。

MallocDebug不是像Valgrind那样做全量检测,而是做内存分配追踪。它记录每次malloc/calloc/realloc/free的调用栈、大小、时间戳。然后你可以分析这些数据,找出谁在「吃内存」。

2.3.1 怎么开启

在MTK8676上,一般通过属性控制:

# 开启MallocDebug
adb shell setprop persist.vendor.malloc.debug 1

# 重启服务或进程
adb shell kill [pid]

# 抓取日志
adb logcat -b events | grep "MallocDebug"

嗯,这里要注意——开启后性能会有影响,大概慢5%-10%。对于座舱这种实时性要求高的系统,建议只在调试版本里开。

2.3.2 能做什么

功能 说明 适用场景
内存快照 抓取某个时刻所有存活的内存块 怀疑某个模块内存异常增长
分配统计 按调用栈汇总分配次数和大小 找出频繁分配的小对象
泄漏检测 进程退出时检查未释放的内存 确认泄漏点
碎片分析 分析堆内存碎片化程度 系统运行久了变慢

我最常用的是「分配统计」。有一次座舱的蓝牙模块内存一直涨,用MallocDebug一看,发现有个线程每秒分配了200个4字节的小对象,但只释放了199个。你想想看,一秒漏4个字节,一天就是345KB,一个月就是10MB——座舱跑几年,不崩才怪。

2.3.3 和Valgrind、ASan的对比

这三把刀各有各的用武之地:

  • Valgrind:最全面,但最慢。适合本地开发、小规模测试。
  • ASan:快,能抓越界和Use-After-Free。适合日常开发。
  • MallocDebug:轻量,能跑在真机上。适合长时间运行、压力测试。

我的建议:在MTK8676座舱项目上,我一般这么用——单元测试用ASan,集成测试用MallocDebug,发布前用Valgrind扫一遍关键模块。三管齐下,基本能把内存问题扼杀在摇篮里。

2.4 避坑指南

做内存泄漏检测这么多年,我踩过的坑能写一本书。这里挑几个典型的说说:

坑一:Valgrind在Android上跑不起来。 我曾经花了两天时间折腾Valgrind在MTK8676 Android系统上的移植,最后发现官方压根不支持。后来改用ASan + MallocDebug的组合,问题迎刃而解。

坑二:ASan和第三方库冲突。 有些闭源的第三方库,编译时没加ASan,你主程序加了,链接的时候就会出诡异的问题。解决方案是只对自研模块开ASan,第三方库不开。

坑三:MallocDebug的日志太多。 第一次开MallocDebug跑了一晚上,第二天发现logcat文件有2GB...后来我学会了用过滤条件,只抓特定进程的日志。

嗯,工具终究是工具。真正重要的,是你对内存管理的理解。工具能帮你找到问题,但能不能修好,还得看你的基本功。下一章我会讲怎么用这些工具去定位实际的内存泄漏案例,到时候咱们再细聊。