跳到主要内容

MallocMonitor 使用指南

概述

MallocMonitor 是一个运行时内存调试工具,用于检测 C/C++ 程序中的内存泄漏和缓冲区溢出问题。它无需修改源代码即可透明运行。

主要特性

特性说明
内存泄漏检测跟踪所有内存分配,识别未释放的内存
缓冲区溢出检测检测内存写入越界违规
调用栈记录在分配时捕获调用栈,便于调试定位
零代码修改通过链接器符号包装实现,无需修改源码
C/C++ 支持完全兼容 C 和 C++ 程序

源码位置

platform/allwinner/eyesee-mpp/middleware/{chip}/MallocMonitor

技术原理

GCC 符号包装机制

MallocMonitor 利用 GCC 的 --wrap 链接器特性来拦截内存分配函数。该机制在链接阶段将标准内存函数替换为包装版本:

+------------------+         +------------------+         +------------------+
| Application | | Linker | | MallocMonitor |
| | | --wrap flag | | |
| malloc() ----->|-------->| Redirects to -->|-------->| __wrap_malloc() |
| free() ----->| | __wrap_xxx() -->|-------->| __wrap_free() |
| realloc() ----->| | | | __wrap_realloc()|
| calloc() ----->| +------------------+ +------------------+
+------------------+

整体架构图

                           MallocMonitor Architecture
┌──────────────────────────────────────────────────────────────────────────────┐
│ │
│ ┌────────────────────────────────────────────────────────────────────────┐ │
│ │ Application Layer │ │
│ │ │ │
│ │ malloc() ──┐ │ │
│ │ free() ──┼──► Standard Memory API Calls │ │
│ │ realloc() ─┤ │ │
│ │ calloc() ──┘ │ │
│ └────────────────────────────────┬───────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌────────────────────────────────────────────────────────────────────────┐ │
│ │ MallocMonitor Layer │ │
│ │ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────────────┐ │ │
│ │ │ __wrap_malloc │ │ __wrap_free │ │ __wrap_realloc/calloc │ │ │
│ │ └────────┬────────┘ └────────┬────────┘ └───────────┬─────────────┘ │ │
│ │ │ │ │ │ │
│ │ ▼ ▼ ▼ │ │
│ │ ┌─────────────────────────────────────────────────────────────────┐ │ │
│ │ │ Memory Tracking List │ │ │
│ │ │ ┌───────────┐ ┌───────────┐ ┌───────────┐ │ │ │
│ │ │ │ Node 1 │───►│ Node 2 │───►│ Node N │ │ │ │
│ │ │ │ • address │ │ • address │ │ • address │ │ │ │
│ │ │ │ • size │ │ • size │ │ • size │ │ │ │
│ │ │ │ • callstack│ │ • callstack│ │ • callstack│ │ │ │
│ │ │ └───────────┘ └───────────┘ └───────────┘ │ │ │
│ │ └─────────────────────────────────────────────────────────────────┘ │ │
│ └────────────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌────────────────────────────────────────────────────────────────────────┐ │
│ │ System Layer │ │
│ │ Real malloc/free Implementation │ │
│ └────────────────────────────────────────────────────────────────────────┘ │
│ │
└──────────────────────────────────────────────────────────────────────────────┘

内存分配流程

                    Memory Allocation Flow


┌─────────────────────────┐
│ Application calls │
│ malloc(size) │
└───────────┬─────────────┘


┌─────────────────────────┐
│ __wrap_malloc() │
│ intercepts the call │
└───────────┬─────────────┘

┌───────────────┼───────────────┐
│ │ │
▼ ▼ ▼
┌────────────┐ ┌────────────┐ ┌────────────────┐
│ Call real │ │ Capture │ │ Create tracking│
│ malloc() │ │ call stack │ │ node │
└────────────┘ └────────────┘ └───────┬────────┘


┌─────────────────────────┐
│ Store in tracking list: │
│ • Memory address │
│ • Allocation size │
│ • Call stack trace │
└───────────┬─────────────┘


┌─────────────────────────┐
│ Return pointer to │
│ application │
└─────────────────────────┘

内存释放流程

                Memory Deallocation Flow


┌─────────────────────────┐
│ Application calls │
│ free(ptr) │
└───────────┬─────────────┘


┌─────────────────────────┐
│ __wrap_free() │
│ intercepts the call │
└───────────┬─────────────┘

┌───────────────┼───────────────┐
│ │ │
▼ ▼ ▼
┌────────────┐ ┌────────────────┐ ┌────────────┐
│ Find node │ │ Check for │ │ Report │
│ in list │ │ buffer overflow│ │ if overflow│
└────────────┘ └────────────────┘ └────────────┘


┌────────────────────┐
│ Remove tracking │
│ node from list │
└─────────┬──────────┘


┌────────────────────┐
│ Call real free() │
└────────────────────┘

API 参考

函数概览

函数说明
ShowMallocStatistics()打印内存总体统计信息
ShowUnreleasedMemoryInfo()查看并打印当前未释放内存的信息,包括分配时的调用栈
CheckMemoryOverflow()查看当前存在内存写越界的内存节点信息
StartOverflowReportThread()启动检测线程,周期性检查溢出
StopOverflowReportThread()关闭检测线程

详细 API 说明

1. ShowMallocStatistics()

void ShowMallocStatistics(void);

功能:打印内存分配统计摘要,包括总分配次数、释放次数和当前内存使用情况。

使用示例

// 在程序任意位置检查内存统计
ShowMallocStatistics();

2. ShowUnreleasedMemoryInfo()

void ShowUnreleasedMemoryInfo(void);

功能:列出所有已分配但未释放的内存块,显示每个内存块的地址、大小和分配时的完整调用栈。

使用示例

// 程序退出或检查点时调用
ShowUnreleasedMemoryInfo();

3. CheckMemoryOverflow()

void CheckMemoryOverflow(void);

功能:扫描所有跟踪的内存块,检测缓冲区溢出违规。发现溢出时报告内存地址、大小和分配调用栈。

使用示例

// 周期性检查溢出
CheckMemoryOverflow();

4. StartOverflowReportThread()

int StartOverflowReportThread(int nIntervalMs);

功能:启动后台线程,自动周期性检查内存溢出。

参数

参数类型说明
nIntervalMsint检查间隔,单位毫秒

返回值:成功返回 0,失败返回非零值

使用示例

// 每 5 秒检查一次溢出
StartOverflowReportThread(5000);

5. StopOverflowReportThread()

int StopOverflowReportThread(void);

功能:停止后台溢出检查线程。

返回值:成功返回 0,失败返回非零值


集成指南

集成流程图

                     Integration Workflow

┌───────────────────┴───────────────────┐
▼ ▼
┌───────────────┐ ┌───────────────┐
│ Step 1 │ │ Step 2 │
│ Compile │ │ Configure │
│ MallocMonitor │ │ App Linker │
│ Module │ │ Flags │
└───────┬───────┘ └───────┬───────┘
│ │
▼ ▼
┌───────────────────────────────────────────────────────┐
│ Step 3 │
│ Enable MallocMonitor in Application Code │
└───────────────────────────┬───────────────────────────┘


┌───────────────────────────────────────────────────────┐
│ Step 4 │
│ Build and Run Application │
└───────────────────────────┬───────────────────────────┘


┌───────────────────────────────────────────────────────┐
│ Step 5 │
│ Analyze Memory Reports at Exit │
└───────────────────────────────────────────────────────┘

示例:tina-v821 sample_demux2adec2ao

本节以 tina-v821 平台的 sample_demux2adec2ao 为例演示集成过程。

步骤 1:编译 MallocMonitor 模块

编辑构建配置文件:

文件platform/allwinner/eyesee-mpp/middleware/sun300iw1/tina.mk

修改内容

# 修改前(被注释):
# make -C MallocMonitor -f tina.mk all

# 修改后(取消注释):
make -C MallocMonitor -f tina.mk all

步骤 2:配置应用程序链接参数

文件platform/allwinner/eyesee-mpp/middleware/sun300iw1/sample/tina.mk

修改 1:链接 MallocMonitor 库

# 修改前(被注释):
#LOCAL_STATIC_LIBS += libMallocMonitor

# 修改后(取消注释):
LOCAL_STATIC_LIBS += libMallocMonitor

修改 2:启用符号包装

# 修改前(被注释):
#LOCAL_LDFLAGS += -Wl,--wrap=malloc -Wl,--wrap=free -Wl,--wrap=realloc \
# -Wl,--wrap=calloc -Wl,--wrap=reallocarray \
# -Wl,--wrap=strdup -Wl,--wrap=strndup

# 修改后(取消注释):
LOCAL_LDFLAGS += -Wl,--wrap=malloc -Wl,--wrap=free -Wl,--wrap=realloc \
-Wl,--wrap=calloc -Wl,--wrap=reallocarray \
-Wl,--wrap=strdup -Wl,--wrap=strndup

步骤 3:在源码中启用 MallocMonitor

文件sample_demux2adec2ao.c

修改内容

// 修改前(被注释):
//#define ENABLE_MALLOC_MONITOR

// 修改后(取消注释):
#define ENABLE_MALLOC_MONITOR

步骤 4:编译并运行

# 编译应用程序
mkmpp

# 在目标设备上运行
./sample_demux2adec2ao

步骤 5:分析输出

应用程序退出时,MallocMonitor 会自动打印内存泄漏信息。


调用栈分析

理解输出格式

检测到内存泄漏时,MallocMonitor 输出详细的调用栈信息:

backtrace for unreleased mem[139](0xb7cae090,20)
[ 0]-> PC 0x001a1770: (+0x001a1770) from [./sample_demux2adec2ao]
[ 1]-> PC 0x001a0a42: (+0x001a0a42) from [./sample_demux2adec2ao]
[ 2]-> PC 0x00163d3e: (+0x00163d3e) from [./sample_demux2adec2ao]
[ 3]-> PC 0x00160b9c: (+0x00160b9c) from [./sample_demux2adec2ao]
[ 4]-> PC 0x00080802: (+0x00080802) from [./sample_demux2adec2ao]
[ 5]-> PC 0x00083216: (+0x00083216) from [./sample_demux2adec2ao]
[ 6]-> PC 0x0007f05c: (+0x0007f05c) from [./sample_demux2adec2ao]
[ 7]-> PC 0x0001c4ec: (+0x0001c4ec) from [./sample_demux2adec2ao]
[ 8]-> PC 0xb7f4958c: (+0x0001858c) from [/lib/ld-musl-riscv32.so.1]

输出解读

┌─────────────────────────────────────────────────────────────────────────────┐
│ backtrace for unreleased mem[139](0xb7cae090,20) │
│ │ │ │ │
│ │ │ └── Alloc size: 20 bytes │
│ │ └── Memory address: 0xb7cae090 │
│ └── Memory block index: 139 │
└─────────────────────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────────────────────┐
│ [ 0]-> PC 0x001a1770: (+0x001a1770) from [./sample_demux2adec2ao] │
│ │ │ │ │
│ │ │ └── Source: executable/library │
│ │ └── Offset address (use with addr2line) │
│ └── Runtime address │
└─────────────────────────────────────────────────────────────────────────────┘
字段说明用途
mem[139]跟踪链表中的内存块索引供参考
0xb7cae090运行时内存地址用于调试
20分配大小(字节)识别分配用途
PC 0x001a1770运行时程序计数器不能直接使用
(+0x001a1770)相对于二进制基址的偏移配合 addr2line 使用
[./sample_demux2adec2ao]二进制文件/库名称addr2line 中指定

使用 addr2line 定位源码

addr2line 工具将地址转换为源文件位置:

       addr2line Workflow


┌─────────────────────────────────┐
│ 1. Extract offset address │
│ from MallocMonitor output │
└───────────────┬─────────────────┘


┌─────────────────────────────────┐
│ 2. Run addr2line command: │
│ addr2line -e <binary> <addr>│
└───────────────┬─────────────────┘


┌─────────────────────────────────┐
│ 3. Get source file and line │
│ number from output │
└─────────────────────────────────┘

交叉编译版 addr2line 位置

prebuilt/rootfsbuilt/riscv/nds32le-linux-musl-v5d/bin/riscv32-linux-musl-addr2line

使用示例

riscv32-linux-musl-addr2line -e ./sample_demux2adec2ao 0x001a1770

调试符号配置

问题:缺少源码行号信息

如果没有调试符号(未加 -g 编译选项),addr2line 无法解析精确的源码位置:

无调试符号时的输出:
─────────────────────────────
CdxAllocFromUTF8
??:?
__stringSetTo8
StringContainer.c:?
__id3GetAlbumArt
Id3Base.c:?

??:?文件名:? 表示缺少调试信息。

解决方法:启用调试符号

通过 menuconfig 启用调试符号:

m menuconfig
# 选择: CONFIG_DEBUG=y

这会添加 -g3 编译选项,包含完整的符号表。

结果:完整的源码信息

启用调试符号后,addr2line 提供完整信息:

有调试符号时的输出:
─────────────────────────────────────────────────────────────────────────────
CdxAllocFromUTF8
/platform/allwinner/eyesee-mpp/middleware/sun300iw1/media/LIBRARY/libdemuxer/libcore/parser/base/id3base/CdxUtfCode.c:159
__stringSetTo8
/platform/allwinner/eyesee-mpp/middleware/sun300iw1/media/LIBRARY/libdemuxer/libcore/parser/base/id3base/StringContainer.c:32
__id3GetAlbumArt
/platform/allwinner/eyesee-mpp/middleware/sun300iw1/media/LIBRARY/libdemuxer/libcore/parser/base/id3base/Id3Base.c:945
Id3BaseExtraAlbumPic
/platform/allwinner/eyesee-mpp/middleware/sun300iw1/media/LIBRARY/libdemuxer/libcore/parser/base/id3base/Id3Base.c:1129
__Id3v2ParserGetMediaInfo
/platform/allwinner/eyesee-mpp/middleware/sun300iw1/media/LIBRARY/libdemuxer/libcore/parser/id3v2/CdxId3v2Parser.c:199
_Z13aw_demux_openP16CedarXDemuxerAPIP13CdxMediaInfoSP14CdxDataSourceS
/platform/allwinner/eyesee-mpp/middleware/sun300iw1/media/LIBRARY/libdemux/../../../media/LIBRARY/libdemuxer/libcore/parser/include/CdxParser.h:656
DemuxOpenParserLib
/platform/allwinner/eyesee-mpp/middleware/sun300iw1/media/component/Demux_Component.c:647
DemuxPreparePorts
/platform/allwinner/eyesee-mpp/middleware/sun300iw1/media/component/Demux_Component.c:1097
AW_MPI_DEMUX_CreateChn
/platform/allwinner/eyesee-mpp/middleware/sun300iw1/media/mpi_demux.c:314 (discriminator 3)
createDemuxChn
/platform/allwinner/eyesee-mpp/middleware/sun300iw1/sample/sample_demux2adec2ao/sample_demux2adec2ao.c:349


内存泄漏排查实例

排查流程

Memory Leak Debugging Flow


┌────────────────────────────────┐
│ 1. Enable MallocMonitor and │
│ run application │
└────────────────┬───────────────┘


┌────────────────────────────────┐
│ 2. Check exit report for │
│ unreleased memory info │
└────────────────┬───────────────┘


┌────────────────────────────────┐
│ 3. Extract offset addresses │
│ from call stack output │
└────────────────┬───────────────┘


┌────────────────────────────────┐
│ 4. Use addr2line to resolve │
│ source code locations │
└────────────────┬───────────────┘


┌────────────────────────────────┐
│ 5. Analyze source code to │
│ find missing free() calls │
└────────────────┬───────────────┘


┌────────────────────────────────┐
│ 6. Add proper free() calls │
│ to fix memory leak │
└────────────────────────────────┘

实际案例

从调用栈分析中,我们定位到泄漏源:

文件Id3Base.c 第 1129 行

问题代码

// 第 1129 行: 分配了内存但未释放
StringCtn* text = GenerateStringContainer();

// ... 处理代码 ...

// 缺少: EraseStringContainer(&text);

修复后代码

// 第 1129 行: 分配内存
StringCtn* text = GenerateStringContainer();

// ... 处理代码 ...

// 在函数末尾添加正确的清理代码
EraseStringContainer(&text);

注意事项

分析内存泄漏报告时:

  1. 全局单例:有些内存块是故意永不释放的(如配置缓存、单例对象)。这些在进程退出时由系统释放,不属于真正的泄漏。

  2. 误报过滤:在调查潜在泄漏前,先过滤掉已知的故意分配。

  3. 模式识别:相似的调用栈通常指向同一泄漏源,有助于优先修复。


快速参考

集成检查清单

□ 步骤 1: 在 tina.mk 中取消注释 MallocMonitor 编译项
□ 步骤 2: 在 LOCAL_STATIC_LIBS 中添加 libMallocMonitor
□ 步骤 3: 在 LOCAL_LDFLAGS 中添加 --wrap 标志
□ 步骤 4: 在源码中定义 ENABLE_MALLOC_MONITOR
□ 步骤 5: 启用 CONFIG_DEBUG=y 获取符号信息
□ 步骤 6: 编译并运行应用程序
□ 步骤 7: 使用 addr2line 分析输出

常用链接器标志

标志作用
-Wl,--wrap=malloc包装 malloc 函数
-Wl,--wrap=free包装 free 函数
-Wl,--wrap=realloc包装 realloc 函数
-Wl,--wrap=calloc包装 calloc 函数
-Wl,--wrap=reallocarray包装 reallocarray 函数
-Wl,--wrap=strdup包装 strdup 函数
-Wl,--wrap=strndup包装 strndup 函数

addr2line 常用命令

# 解析单个地址
riscv32-linux-musl-addr2line -e ./your_app 0x001a1770
# 解析并显示函数名
riscv32-linux-musl-addr2line -f -e ./your_app 0x001a1770
# 解析多个地址
riscv32-linux-musl-addr2line -e ./your_app 0x001a1770 0x001a0a42 0x00163d3e

常见问题

问题排查

问题原因解决方法
无调用栈输出缺少调试符号启用 CONFIG_DEBUG=y
输出显示 ??:?编译时未加 -g 选项使用调试符号重新编译
误报内存泄漏全局单例分配过滤已知的故意分配
地址解析失败使用了错误的二进制文件确保 addr2line 使用正确的二进制文件
符号缺失二进制文件已被 strip使用未 strip 的版本进行调试