malloc_debug 使用与原理整理
1. malloc_debug 是什么
malloc_debug 是 Android bionic libc 里的 native 内存调试能力。它通过在正常分配路径外面增加一层 shim,替换常见的内存分配接口,从而检测以下问题:
- 越界写,尤其是写到分配块前面或后面
- use after free
- 内存泄漏
- 非法指针传给 free、realloc、malloc_usable_size
- 分配调用链追踪和堆快照导出
它会替换这些接口:
- malloc
- free
- calloc
- realloc
- posix_memalign
- memalign
- aligned_alloc
- malloc_usable_size
- 32 位系统上还会替换 pvalloc 和 valloc
2. 使用方式
2.1 平台开发场景
对所有进程开启 backtrace:
bash
adb shell stop
adb shell setprop libc.debug.malloc.options backtrace
adb shell start
对指定进程开启,例如 ls:
bash
adb shell setprop libc.debug.malloc.options backtrace
adb shell setprop libc.debug.malloc.program ls
adb shell ls
对 zygote 及其派生进程开启:
bash
adb shell stop
adb shell setprop libc.debug.malloc.program app_process
adb shell setprop libc.debug.malloc.options backtrace
adb shell start
同时开启多个选项,例如 backtrace 和 guard:
bash
adb shell stop
adb shell setprop libc.debug.malloc.options "\"backtrace guard\""
adb shell start
说明:
- 外层引号是给宿主机 shell 用的。
- 内层引号是为了让设备端把 backtrace guard 当成一个整体参数。
2.2 环境变量方式
Android O 及以后:
bash
adb shell
export LIBC_DEBUG_MALLOC_OPTIONS=backtrace
ls
Pre-O 的老版本还可以通过 env_enabled 配合环境变量启用:
bash
adb shell
setprop libc.debug.malloc.env_enabled 1
setprop libc.debug.malloc.options backtrace
export LIBC_DEBUG_MALLOC_ENABLE=1
ls
2.3 应用开发场景
非 root 设备建议走 NDK 的 wrap.sh 方式。
root 设备可以直接设置 wrap 属性,例如:
bash
adb shell setprop wrap.<APP> '"LIBC_DEBUG_MALLOC_OPTIONS=backtrace logwrapper"'
同时开启多个选项:
bash
adb shell setprop wrap.<APP> '"LIBC_DEBUG_MALLOC_OPTIONS=backtrace\ leak_track\ fill logwrapper"'
示例:
bash
adb shell setprop wrap.com.google.android.googlequicksearchbox '"LIBC_DEBUG_MALLOC_OPTIONS=backtrace logwrapper"'
adb shell am force-stop com.google.android.googlequicksearchbox
如果设置后应用没有按预期启起来,建议检查:
bash
adb logcat -d | grep "malloc debug"
adb shell getprop | grep wrap
2.4 常用选项建议
排查越界写:
text
guard
rear_guard
front_guard
abort_on_error
backtrace
排查 use after free:
text
free_track
fill_on_free
backtrace
排查泄漏:
text
leak_track
backtrace
排查非法指针:
text
verify_pointers
backtrace
常见组合:
text
"guard backtrace"
"rear_guard backtrace abort_on_error"
"free_track backtrace"
3. 核心原理
3.1 总体思路
malloc_debug 的核心不是实时拦截每一次越界写,而是:
- 分配时在用户数据前后埋入固定模式的 guard 字节。
- free 或相关检查点到来时,对 guard 做完整比对。
- 如果比对失败,输出损坏字节、相对偏移和调用栈。
3.2 内存布局
只要启用了 front_guard 或 rear_guard,就会走带 Header 的特殊布局。
#mermaid-svg-d1rBqLNAK7y5PizI{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-d1rBqLNAK7y5PizI .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-d1rBqLNAK7y5PizI .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-d1rBqLNAK7y5PizI .error-icon{fill:#552222;}#mermaid-svg-d1rBqLNAK7y5PizI .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-d1rBqLNAK7y5PizI .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-d1rBqLNAK7y5PizI .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-d1rBqLNAK7y5PizI .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-d1rBqLNAK7y5PizI .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-d1rBqLNAK7y5PizI .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-d1rBqLNAK7y5PizI .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-d1rBqLNAK7y5PizI .marker{fill:#333333;stroke:#333333;}#mermaid-svg-d1rBqLNAK7y5PizI .marker.cross{stroke:#333333;}#mermaid-svg-d1rBqLNAK7y5PizI svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-d1rBqLNAK7y5PizI p{margin:0;}#mermaid-svg-d1rBqLNAK7y5PizI .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-d1rBqLNAK7y5PizI .cluster-label text{fill:#333;}#mermaid-svg-d1rBqLNAK7y5PizI .cluster-label span{color:#333;}#mermaid-svg-d1rBqLNAK7y5PizI .cluster-label span p{background-color:transparent;}#mermaid-svg-d1rBqLNAK7y5PizI .label text,#mermaid-svg-d1rBqLNAK7y5PizI span{fill:#333;color:#333;}#mermaid-svg-d1rBqLNAK7y5PizI .node rect,#mermaid-svg-d1rBqLNAK7y5PizI .node circle,#mermaid-svg-d1rBqLNAK7y5PizI .node ellipse,#mermaid-svg-d1rBqLNAK7y5PizI .node polygon,#mermaid-svg-d1rBqLNAK7y5PizI .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-d1rBqLNAK7y5PizI .rough-node .label text,#mermaid-svg-d1rBqLNAK7y5PizI .node .label text,#mermaid-svg-d1rBqLNAK7y5PizI .image-shape .label,#mermaid-svg-d1rBqLNAK7y5PizI .icon-shape .label{text-anchor:middle;}#mermaid-svg-d1rBqLNAK7y5PizI .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-d1rBqLNAK7y5PizI .rough-node .label,#mermaid-svg-d1rBqLNAK7y5PizI .node .label,#mermaid-svg-d1rBqLNAK7y5PizI .image-shape .label,#mermaid-svg-d1rBqLNAK7y5PizI .icon-shape .label{text-align:center;}#mermaid-svg-d1rBqLNAK7y5PizI .node.clickable{cursor:pointer;}#mermaid-svg-d1rBqLNAK7y5PizI .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-d1rBqLNAK7y5PizI .arrowheadPath{fill:#333333;}#mermaid-svg-d1rBqLNAK7y5PizI .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-d1rBqLNAK7y5PizI .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-d1rBqLNAK7y5PizI .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-d1rBqLNAK7y5PizI .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-d1rBqLNAK7y5PizI .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-d1rBqLNAK7y5PizI .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-d1rBqLNAK7y5PizI .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-d1rBqLNAK7y5PizI .cluster text{fill:#333;}#mermaid-svg-d1rBqLNAK7y5PizI .cluster span{color:#333;}#mermaid-svg-d1rBqLNAK7y5PizI div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-d1rBqLNAK7y5PizI .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-d1rBqLNAK7y5PizI rect.text{fill:none;stroke-width:0;}#mermaid-svg-d1rBqLNAK7y5PizI .icon-shape,#mermaid-svg-d1rBqLNAK7y5PizI .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-d1rBqLNAK7y5PizI .icon-shape p,#mermaid-svg-d1rBqLNAK7y5PizI .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-d1rBqLNAK7y5PizI .icon-shape .label rect,#mermaid-svg-d1rBqLNAK7y5PizI .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-d1rBqLNAK7y5PizI .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-d1rBqLNAK7y5PizI .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-d1rBqLNAK7y5PizI :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} Header
front guard
user data
rear guard
更精确一点,实际布局是:
text
Header | alignment padding | front guard | user data | rear guard
其中:
- Header 保存 tag、orig_pointer、size、usable_size
- alignment padding 用来保证返回给用户的指针对齐
- front guard 默认填充 0xaa
- rear guard 默认填充 0xbb
- user data 就是返回给调用者的实际可用区
3.3 偏移公式
假设:
- H = Header 起始地址
- S = sizeof(Header)
- A = 最小对齐字节数,32 位为 8,64 位为 16
- P = align(S, A)
- F = front_guard_bytes
- N = 用户申请大小,也就是 header->size
- R = rear_guard_bytes
那么:
-
Header
起点偏移:0
终点偏移:S - 1
-
对齐填充
起点偏移:S
终点偏移:P - 1
-
front guard
起点偏移:P
终点偏移:P + F - 1
-
user data
起点偏移:P + F
终点偏移:P + F + N - 1
-
rear guard
起点偏移:P + F + N
终点偏移:P + F + N + R - 1
3.4 64 位默认配置示意
在 64 位系统:
- Header 打包后通常是 28 字节
- 对齐后 P = 32
- 默认 front_guard = 32 字节
- 默认 rear_guard = 32 字节
所以默认布局可以画成:
text
偏移(相对 Header 起点)
0 ~ 27 Header
28 ~ 31 alignment padding
32 ~ 63 front guard, 默认 0xaa
64 ~ 64+N-1 user data
64+N ~ 95+N rear guard, 默认 0xbb
如果用户申请的是 N = 1382400,那么:
text
0 ~ 27 Header
28 ~ 31 padding
32 ~ 63 front guard
64 ~ 1382463 user data
1382464 ~ 1382495 rear guard
3.5 为什么日志显示 allocation1382400
日志里的 allocationindex 不是相对 Header 的偏移,而是相对 user pointer 的偏移。
所以:
- allocation0 是用户区第 1 个字节
- allocation1382399 是最后 1 个合法字节
- allocation1382400 是 rear guard 的第 1 个字节
这就是为什么看到:
text
allocation[1382400] = 0x83 (expected 0xbb)
可以直接判断成:
- 申请大小本身是 1382400
- 用户代码从 buffer 末尾之后继续写了至少 1 个字节
- 这是典型的尾部越界写,不等价于 realloc
4. guard 是怎么保存证据的
4.1 两份证据
guard 检测依赖两份"证据":
-
期望模板
内部会创建 cmp_mem 缓冲区,按固定值填满。
-
实际内存中的 guard 区
分配成功后,在 front guard 和 rear guard 位置写入对应模式字节。
4.2 判定方式
判定方式很直接:
- front guard 或 rear guard 在校验时与 cmp_mem 做 memcmp
- 完全一致则认为没有检测到破坏
- 任意字节不一致就报错
4.3 报错内容
一旦失败,会打印:
- 哪个 allocation 出问题
- 原始申请大小
- FRONT 还是 REAR guard 损坏
- 每个损坏字节的相对下标、实际值、期望值
- failure 发生时的 backtrace
- 如果开了 abort_on_error,则打印后直接 abort
5. 常见日志如何理解
5.1 FRONT GUARD
示例:
text
+++ ALLOCATION 0x12345678 SIZE 100 HAS A CORRUPTED FRONT GUARD
allocation[-32] = 0x00 (expected 0xaa)
说明:
- 写到了用户指针之前
- 这是 underrun,往前越界
- 负下标表示损坏点位于 user data 前面
5.2 REAR GUARD
示例:
text
+++ ALLOCATION 0x12345678 SIZE 100 HAS A CORRUPTED REAR GUARD
allocation[130] = 0xbf (expected 0xbb)
说明:
- 写到了用户区尾部之后
- 这是 overflow,往后越界
- 正下标超出 size 范围,说明写出了申请长度
5.3 USED AFTER FREE
示例:
text
+++ ALLOCATION 0x12345678 USED AFTER FREE
allocation[20] = 0xaf (expected 0xef)
说明:
- 该块已经 free
- free_track/fill_on_free 把内容填成了 0xef
- 之后又有人改写,说明存在 use after free
5.4 UNKNOWN POINTER
示例:
text
+++ ALLOCATION 0x12345678 UNKNOWN POINTER (free)
说明:
- 调用了 free 或 realloc
- 但这个指针不在 malloc_debug 当前跟踪的活动分配集合里
6. 示例说明
6.1 示例一:摄像头 YUV420 帧尾部越界
现象:
text
+++ ALLOCATION 0x798aa0e340 SIZE 1382400 HAS A CORRUPTED REAR GUARD
allocation[1382400] = 0x83 (expected 0xbb)
allocation[1382401] = 0x81 (expected 0xbb)
allocation[1382402] = 0x83 (expected 0xbb)
已知:
text
1280 * 720 * 3 / 2 = 1382400
这正好是 1280x720 的 YUV420 一帧大小。
结论:
- 这不是"因为重新分配了内存所以 guard 坏了"。
- 这表示某段写操作从用户区最后一个合法字节之后继续往后写。
- 第一个坏字节就是 allocation1382400,说明越界是从帧尾后第 1 字节开始的。
高概率原因:
- UV 平面长度计算错
- 最后一行循环边界多写一列或一行
- stride 当 width 用,或 width 当 stride 用
- NV12/NV21/I420 三种格式的平面布局混用
- 调用 libyuv 或驱动接口时 dst_stride、src_stride、height 组合不匹配
6.2 示例二:如何快速判断是前越界还是后越界
如果你看到:
text
allocation[-1]
allocation[-16]
allocation[-32]
优先怀疑:
- 指针回退写
- header 前面 memcpy/memset 长度写大了
- 数组下标写成负方向偏移
如果你看到:
text
allocation[size]
allocation[size+1]
allocation[size+32]
优先怀疑:
- 循环上界错误
- memcpy/memset 传入长度过大
- 像素格式大小或平面偏移计算错误
7. 推荐排查流程
7.1 先用小而准的选项组合
排查越界写,建议先用:
text
"rear_guard backtrace abort_on_error"
如果怀疑前后都可能越界:
text
"guard backtrace abort_on_error"
7.2 抓重点信息
看到日志后优先记录:
- pointer 地址
- size
- 第一个损坏下标
- 损坏字节模式
- backtrace
- 触发线程
7.3 优先检查的代码
- memcpy、memmove、memset 长度
- 图像/音视频 buffer 的 stride、width、height、plane size
- realloc 后旧长度和新长度处理
- for 循环边界,尤其是 <= 和 <
- 平面偏移计算,尤其是 Y/UV 起点
8. 常用选项简表
| 选项 | 用途 | 代价 |
|---|---|---|
| front_guard | 检测前越界 | 较低 |
| rear_guard | 检测后越界 | 较低 |
| guard | 前后越界一起查 | 较低到中等 |
| backtrace | 记录分配调用栈 | 较高 |
| backtrace_full | 更完整回溯,可跨 Java 帧 | 更高 |
| free_track | 保留已释放块并验证内容 | 中等到较高 |
| leak_track | 进程退出时输出未释放块 | 中等 |
| verify_pointers | 检测非法指针 | 较低 |
| abort_on_error | 发现错误直接中止 | 便于快速止损 |
| fill_on_alloc/fill_on_free | 用固定值填充,辅助定位未初始化和 use after free | 中等 |
9. 使用建议
- 越界问题优先开 rear_guard 或 guard,不要一上来就开所有选项。
- backtrace 很有用,但性能开销明显,线上长期开启要谨慎。
- 对摄像头、编解码、图像转换这类大 buffer 场景,rear_guard 往往最先发现问题。
- 如果你要追根因,第一现场最重要的是"第一个坏下标"和"调用栈"。
- 如果你已经能稳定复现,建议加 abort_on_error,让问题尽快在首次破坏时停住。
10. 一句话总结
malloc_debug 的本质是:
- 分配时在用户数据前后埋标记
- 校验时拿实际内存和期望模板逐字节比对
- 一旦发现差异,就把损坏方向、偏移、字节内容和回溯一起打出来
对于 native 内存问题,尤其是图像 buffer 越界、use after free、泄漏和非法 free,这是一套非常直接且有效的工具。