malloc_debug_notes

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 的核心不是实时拦截每一次越界写,而是:

  1. 分配时在用户数据前后埋入固定模式的 guard 字节。
  2. free 或相关检查点到来时,对 guard 做完整比对。
  3. 如果比对失败,输出损坏字节、相对偏移和调用栈。

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

那么:

  1. Header

    起点偏移:0

    终点偏移:S - 1

  2. 对齐填充

    起点偏移:S

    终点偏移:P - 1

  3. front guard

    起点偏移:P

    终点偏移:P + F - 1

  4. user data

    起点偏移:P + F

    终点偏移:P + F + N - 1

  5. 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 检测依赖两份"证据":

  1. 期望模板

    内部会创建 cmp_mem 缓冲区,按固定值填满。

  2. 实际内存中的 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,这是一套非常直接且有效的工具。

相关推荐
郭小李1 小时前
spring @PathVariable 注解用法
java·后端·spring
用户3126874877201 小时前
Java 锁全景:synchronized、ReentrantLock、ReadWriteLock、StampedLock
java
鱼宵1 小时前
CSV 怎么做销售报表?Stream 第 9 课:读洗聚排打五段管道一次跑通
java·报表·stream
jaysee-sjc1 小时前
【苍穹外卖】Day12:工作台数据看板与 Excel 报表导出
java·开发语言·数据库·spring boot·spring·微信·excel
w_zero_one1 小时前
算法--桶思想
java·开发语言·算法
MetaLite1 小时前
Spring 同一个类里调用 @CacheEvict,为什么缓存没清掉?
java·spring·缓存
sunburn-2 小时前
数据库聚合查询完整教程:从基础函数到实战统计
java·数据库·mysql·数据库开发
wangchunyu1142 小时前
JAVA 类加载机制与双亲委派模型
java·开发语言
小蒜学长2 小时前
基于 Spring Boot 的英语单词背诵系统的设计与实现(代码+数据库+LW)
java·数据库·spring boot·后端