MCU-Malloc-Tracker 给每次 malloc 记账,崩溃后还能导出堆快照查泄漏

声明: MCU-Malloc-Tracker 是一个开源项目,作者为 Vanderhell(项目仓库)。本文是阅读该项目源码和文档后整理的学习笔记,用于理解 MCU 堆内存追踪的工程实现方式。本文作者不是该项目的开发者,未参与该项目的任何代码贡献。 文中所有工程细节均来自对开源代码的分析,不代表本文作者的设计决策。

项目仓库:github.com/Vanderhell/MCU-Malloc-Tracker(面向裸机 MCU 的 malloc/free 追踪库,查泄漏、找分配热点、导出崩溃快照)

追踪器整体流程

malloc 是从堆里动态申请内存的标准接口,配套的 free 负责归还。MCU 上的动态内存很有限,用错一点,问题要跑几天才暴露。

场景:设备随机崩溃,泄漏查不到

最常见的故障链条:某模块反复 malloc 却忘记 free,堆被悄悄吃光,之后某次分配失败,或者写穿边界,系统随机复位。排查的难点在现场留不住:设备不能随便接仿真器,故障后自动复位,等连上调试器时堆的状态已经没了。

这个库的思路是平时就把堆的账记下来:谁在哪个文件第几行分配了多大一块,分配顺序如何。出了事翻账本,泄漏点、分配热点、崩溃时的堆现场全部可查。

拦截:宏替换把 malloc 换成记账版

要拦下每一次 malloc,用的是 C 预处理器宏替换:

#define malloc(x) mt_malloc((x), __FILE__, __LINE__)

代码里的 malloc(256) 在编译期变成 mt_malloc(256, "driver.c", 42),文件路径和行号由编译器自动填好,业务代码一行不用改。库内部先调真实分配器拿到指针,再写一条记录。

宏拦截最怕递归:mt_malloc 内部调用真实 malloc,真实分配器若再触发宏展开,就会无限嵌套。库用一个 g_inside 标志做递归保护,进入追踪逻辑时置位,置位状态下直接放行到真实分配器。

账本:静态哈希表记下每次分配

记账表是一张静态数组,容量编译期固定(默认 512 项,必须是 2 的幂)。定位槽位用指针哈希:idx = hash(ptr) & (N-1)。用位与代替取模,MCU 上省掉除法开销,分配和释放都是平均 O(1)。每条记录包含指针、大小、文件哈希、行号、序号五样信息。

释放时不能把记录直接清空。哈希表靠连续探测找槽位,清空会打断探测链,后面的分配可能找不到。库把释放的槽位标记成墓碑(tombstone):查找时跳过,下次插入优先复用,代价是探测距离缓慢变长。表满时采用丢记录策略,真实分配照常成功,只是不记账,损失由 table_drops 计数如实暴露。

快照:确定性二进制带回 PC 解码

MCU 上通常没有可靠时钟,库不用时间戳,改用单调递增的序号记录分配顺序。同一段分配序列必然产生同一个内部状态,快照逐字节可复现,方便对比和回归验证。

mt_snapshot_write() 把当前所有存活记录导出成 MTS1 二进制:36 字节头加每条 24 字节记录,记录按指针升序排序,CRC32/IEEE 覆盖整份数据。文件名不存字符串,存 FNV1a-32 哈希,快照体积小、跨编译稳定;PC 端解码时用 --filemap 把哈希映射回源码路径。仓库自带的 demo 在本地编译运行后,解码器输出的 CRC 校验通过。

三条设计底线:确定性、零 malloc、诚实

底线 落实方式
确定性 序号代替时间戳,同一序列得到同一快照
零 malloc 全部静态表,追踪器自身从不申请内存
诚实 算不出的指标标 N/A,不编造数字

三条底线互相支撑:追踪器不申请内存,才有固定可预测的开销,能在任何 MCU 上稳定跑;状态完全确定,快照才可比对。碎片率是最典型的诚实案例:没有平台堆遍历钩子时,库直接报告 N/A;想要真实值,自己实现两个平台钩子即可,库只负责展示。

适用边界