glibc 堆内存分析 GDB 脚本
glibc 堆内存分析 GDB 脚本
以下 GDB 脚本类似于 GDB 的 GEF 插件中的 heap 相关命令,用于查询 glibc 堆内存信息,帮助分析堆损坏引发的 Core Dump 问题。
1、heap_arenas.gdb
# heap_arenas.gdb - 定义 gdb 自定义命令 heap_arenas,遍历 glibc 所有 arena 地址
# =================================================================
# 用法:
# 1) 进入 gdb 后先加载本脚本(仅定义命令,不立即执行):
# (gdb) source heap_arenas.gdb
# 2) 之后每次需要遍历 arena 时执行:
# (gdb) heap_arenas
# 3) 也可在启动时加载:
# gdb -q -x heap_arenas.gdb <程序> <core>
#
# 原理:
# glibc 的 arena(malloc_state) 通过 next 指针构成单向循环链表,
# main_arena 是链表头,遍历 next 直到回到 main_arena 即收集到全部 arena。
#
# 说明:
# - 第 1 个地址 = main_arena
# - 循环中不断读取"上次地址 + 0x868"处的 next 指针,去掉前导 0 后保存
# - 遇到地址回到 main_arena 时停止,main_arena 不重复保存
# - 若你的 glibc 中 next 偏移不是 0x868(不同版本可能不同),
# 修改下面 define 块内的这一行: set $next_off = 0x868 改为实际偏移(如 0x880)
# ===== 定义自定义命令 heap_arenas =====
define heap_arenas
# ===== 可配置: malloc_state.next 指针偏移 =====
set $next_off = 0x868
# ===== 第 1 步: 获取 main_arena 地址,作为第 1 个地址 =====
set $cur = (unsigned long)&main_arena
set $first = $cur
set $count = 1
printf "\n第 1 个地址 (main_arena) = 0x%lx\n", $first
# ===== 第 2 步: 循环读取 next 指针,直到回到 main_arena =====
set $done = 0
while !$done
# 读取 上次地址+0x868 处的 next 指针(%lx 自动去掉前导 0)
set $n = *(unsigned long*)($cur + $next_off)
printf " 0x%lx + 0x%lx -> 0x%lx\n", $cur, $next_off, $n
# 防御: next 为空指针则异常结束
if $n == 0
printf " next 为空指针,链表异常结束。\n"
set $done = 1
# 回到 main_arena 则停止(不重复保存第 1 个地址)
else
if $n == $first
printf " 回到 main_arena,遍历完成。\n"
set $done = 1
else
set $cur = $n
set $count = $count + 1
end
end
end
# ===== 汇总 =====
printf "\n共发现 %d 个 arena,首地址 0x%lx (main_arena)\n", $count, $first
end
# ===== 命令帮助文档 (gdb help heap_arenas 可查看) =====
document heap_arenas
遍历 glibc 所有 arena(malloc_state) 地址。
第 1 个地址为 main_arena,之后按 next 指针循环遍历,回到 main_arena 停止。
用法: 先 source heap_arenas.gdb 加载,再执行 heap_arenas。
end
2、heap_bins.gdb
# heap_bins.gdb
#
# 功能:遍历一个 arena 某个类型的所有 bin(fast / unsorted / small / large),
# 逐个分析每个 bin 的链表并定位断链位置。
# 用法:
# gdb -batch -x heap_bins.gdb <coredump> (交互式加载宏)
# gdb <coredump> (进入后执行 source heap_bins.gdb)
# 然后在 GDB 内执行: heap_bins <arena_addr> <bin_type>
# 例: heap_bins 0x7f862d055640 small
#
# bin 类型与偏移说明(glibc malloc_state 布局,64 位):
# fast bin : 首地址相对 arena 偏移 0x08,共 10 个,每个 bin header 占 8 字节
# unsorted : 首地址相对 arena 偏移 0x68,共 1 个,每个 bin header 占 16 字节
# small bin : 首地址相对 arena 偏移 0x78,共 63 个,每个 bin header 占 16 字节
# large bin : 首地址相对 arena 偏移 0x468,共 63 个,每个 bin header 占 16 字节
#
# 遍历逻辑(与既有脚本一致):
# fast → 单向链表(仅 fd 有效),逻辑与 heap_bin_fast.gdb 一致
# unsorted / small / large → 双向链表(fd/bk),逻辑与 heap_bin.gdb 一致
#
# 注意:
# 1) 下面的偏移/个数/槽大小均为可配置变量,位于脚本最前面,便于用户手动修改。
# 2) 纯 GDB 命令脚本无法捕获"解引用不可读地址"的异常(会报错中断),
# 脚本对 fd/bk 采用启发式检查(值为 0 或 < 0x10000 视为损坏),
# 并在每次解引用前打印当前位置,若 GDB 因野指针报错,
# 最后一行输出即为断链位置。
# ============================================================
# 可配置变量(请按实际 glibc 布局手动修改)
# ============================================================
# fast bin:首地址相对 arena 偏移、bin 个数、每个 bin header 字节数
set $fast_bin_offset = 0x08
set $fast_bin_count = 10
set $fast_bin_size = 8
# unsorted bin:首地址相对 arena 偏移、bin 个数、每个 bin header 字节数
set $unsorted_bin_offset = 0x68
set $unsorted_bin_count = 1
set $unsorted_bin_size = 16
# small bin:首地址相对 arena 偏移、bin 个数、每个 bin header 字节数
set $small_bin_offset = 0x78
set $small_bin_count = 63
set $small_bin_size = 16
# large bin:首地址相对 arena 偏移、bin 个数、每个 bin header 字节数
set $large_bin_offset = 0x468
set $large_bin_count = 63
set $large_bin_size = 16
# 遍历单个 bin 时的最大步数(防死循环)
set $heap_bins_max_steps = 1000000
# 字段偏移(与 glibc 源码一致):
# bin header(bin 槽):fd @ +0x0,bk @ +0x8(fast bin 槽仅 fd,8 字节)
# 被链接的 chunk(完整 malloc_chunk):
# offset 0x0 : prev_size
# offset 0x8 : size
# offset 0x10 : fd
# offset 0x18 : bk
# 输出 chunk 低 3 位 flags 的宏名拼接(0-7 全枚举,因 GDB 无字符串拼接函数)
# 用法: print_flags <flags值>
define print_flags
set $f = $arg0 + 0
printf " flags = 0x%x (", $f
if $f == 0
printf "NONE"
end
if $f == 1
printf "PREV_INUSE"
end
if $f == 2
printf "IS_MMAPPED"
end
if $f == 3
printf "PREV_INUSE | IS_MMAPPED"
end
if $f == 4
printf "NON_MAIN_ARENA"
end
if $f == 5
printf "PREV_INUSE | NON_MAIN_ARENA"
end
if $f == 6
printf "IS_MMAPPED | NON_MAIN_ARENA"
end
if $f == 7
printf "PREV_INUSE | IS_MMAPPED | NON_MAIN_ARENA"
end
printf ")\n"
end
# ============================================================
# 内部宏:遍历一个 fast bin(单向链表,逻辑与 heap_bin_fast.gdb 一致)
# 用法: heap_bins_walk_fast <bin_header_addr>
# ============================================================
define heap_bins_walk_fast
set $hdr = $arg0 + 0
set $max_steps = $heap_bins_max_steps
# fast bin 槽仅 8 字节,只有 fd(@ +0x0),无 bk
set $hdr_fd = *(unsigned long long*)($hdr + 0)
# ---------- 空链检查(空 bin 不显示任何数据,完全静默)----------
set $done = 0
if $hdr_fd == 0
# 空 bin(除 bin_header 外无任何 chunk)→ 不显示任何数据
set $done = 1
end
# ---------- 遍历 ----------
if !$done
printf " header.fd : 0x%llx\n", $hdr_fd
set $cur = $hdr_fd
set $prev = $hdr
set $step = 0
set $broken = 0
while $step < $max_steps && $broken == 0
# 启发式检查当前 chunk 地址
if $cur == 0
printf " RESULT: 发现断链!\n"
printf " 遍历到 chunk[%d] 时发现其前驱的 fd 为 0(NULL),链表未回到 bin header 即截断\n", $step
set $broken = 1
loop_break
end
if $cur < 0x10000
printf " RESULT: 发现断链!\n"
printf " chunk[%d] 地址为 0x%llx —— 疑似损坏指针(地址过小,非合法堆地址)\n", $step, $cur
set $broken = 1
loop_break
end
# 解引用前先打印当前位置(若 GDB 因野指针报错,最后一行即为断链点)
printf " 检查 chunk[%d] @ 0x%llx (prev=0x%llx) ...\n", $step, $cur, $prev
# 读取 chunk 字段:prev_size / size / fd(bk 在 fast bin 中无效,不读取)
set $c_prevsize = *(unsigned long long*)($cur + 0)
set $c_size_raw = *(unsigned long long*)($cur + 8)
set $c_fd = *(unsigned long long*)($cur + 16)
set $c_size = $c_size_raw & ~7
set $c_flags = $c_size_raw & 7
printf " prev_size = 0x%llx\n", $c_prevsize
printf " size = 0x%llx (不含低3位 flags)\n", $c_size
print_flags $c_flags
printf " fd = 0x%llx (bk 无效,不检查)\n", $c_fd
# 当前 chunk 的 fd 指回 bin header → 链尾,单向链完整
if $c_fd == $hdr
printf " RESULT: fast bin 链表完整,无断链。\n"
printf " 共 %d 个 chunk(不包含 header 0x%llx),fd 单向链正确\n", $step + 1, $hdr
set $broken = 1
loop_break
end
# 启发式检查下一个地址(fd 指向的值)
if $c_fd == 0
printf " RESULT: 发现断链!\n"
printf " 断链点 = chunk[%d] @ 0x%llx\n", $step, $cur
printf " 该 chunk 的 fd=0(NULL),链表未回到 bin header(0x%llx) 即截断\n", $hdr
set $broken = 1
loop_break
end
if $c_fd < 0x10000
printf " RESULT: 发现断链!\n"
printf " 断链点 = chunk[%d] @ 0x%llx\n", $step, $cur
printf " 该 chunk 的 fd=0x%llx —— 疑似损坏指针(地址过小,非合法堆地址)\n", $c_fd
set $broken = 1
loop_break
end
# 前进一步
set $prev = $cur
set $cur = $c_fd
set $step = $step + 1
end
if $broken == 0
printf " RESULT: 达到最大遍历步数 %d,链表可能存在环或过长\n", $max_steps
end
end
end
# ============================================================
# 内部宏:遍历一个普通 bin(双向链表,逻辑与 heap_bin.gdb 一致)
# 用法: heap_bins_walk_normal <bin_header_addr>
# ============================================================
define heap_bins_walk_normal
set $hdr = $arg0 + 0
set $max_steps = $heap_bins_max_steps
# 普通 bin 槽 16 字节:fd @ +0x0, bk @ +0x8
set $hdr_fd = *(unsigned long long*)($hdr + 0)
set $hdr_bk = *(unsigned long long*)($hdr + 8)
# bin header 是"虚构"的 malloc_chunk:链首 chunk->bk 与链尾 chunk->fd
# 回指 bin_at 地址(= bin header 地址 - 0x10)
set $vhdr = $hdr - 0x10
# ---------- 空链检查(空 bin 不显示任何数据,完全静默)----------
set $done = 0
# 判空条件(两种形态任一满足即为空):
# 1) fd/bk 均指向 bin_at(= bin_header - 0x10)——glibc 真实空 bin 初始化值
# 2) fd/bk 均为 0(测试构造或异常形态)
if ($hdr_fd == $hdr - 0x10 && $hdr_bk == $hdr - 0x10) || ($hdr_fd == 0 && $hdr_bk == 0)
# 空 bin(除 bin_header 外无任何 chunk)→ 不显示任何数据
set $done = 1
end
if $hdr_fd == 0 && $hdr_bk != 0
printf " RESULT: 发现断链!header.fd=0 但 header.bk=0x%llx(非空)\n", $hdr_bk
printf " → header 的 fd/bk 不一致,链表结构损坏\n"
set $done = 1
end
if $hdr_fd != 0 && $hdr_bk == 0
printf " RESULT: 发现断链!header.fd=0x%llx(非空)但 header.bk=0\n", $hdr_fd
printf " → header 的 fd/bk 不一致,链表结构损坏\n"
set $done = 1
end
# ---------- 遍历 ----------
if !$done
printf " header.fd : 0x%llx\n", $hdr_fd
printf " header.bk : 0x%llx\n", $hdr_bk
printf " bin 虚拟chunk头(vhdr=bin_header-0x10) : 0x%llx\n", $vhdr
set $cur = $hdr_fd
set $prev = $vhdr
set $step = 0
set $broken = 0
while $step < $max_steps && $broken == 0
# 启发式检查当前 chunk 地址
if $cur == 0
printf " RESULT: 发现断链!\n"
printf " 遍历到 chunk[%d] 时发现其前驱的 fd 为 0(NULL),链表未回到 header 即截断\n", $step
set $broken = 1
loop_break
end
if $cur < 0x10000
printf " RESULT: 发现断链!\n"
printf " chunk[%d] 地址为 0x%llx —— 疑似损坏指针(地址过小,非合法堆地址)\n", $step, $cur
set $broken = 1
loop_break
end
# 解引用前先打印当前位置(若 GDB 因野指针报错,最后一行即为断链点)
printf " 检查 chunk[%d] @ 0x%llx (prev=0x%llx) ...\n", $step, $cur, $prev
# 读取 chunk 全部字段:prev_size / size / fd / bk
set $c_prevsize = *(unsigned long long*)($cur + 0)
set $c_size_raw = *(unsigned long long*)($cur + 8)
set $c_fd = *(unsigned long long*)($cur + 16)
set $c_bk = *(unsigned long long*)($cur + 24)
set $c_size = $c_size_raw & ~7
set $c_flags = $c_size_raw & 7
printf " prev_size = 0x%llx\n", $c_prevsize
printf " size = 0x%llx (不含低3位 flags)\n", $c_size
print_flags $c_flags
printf " fd = 0x%llx\n", $c_fd
printf " bk = 0x%llx\n", $c_bk
# 检查当前 chunk 的 bk 是否指向前驱
if $c_bk != $prev
printf " RESULT: 发现断链!\n"
printf " 断链点 = chunk[%d] @ 0x%llx\n", $step, $cur
printf " 该 chunk 的 bk=0x%llx,应指向 prev=0x%llx → bk 不指向前驱\n", $c_bk, $prev
set $broken = 1
loop_break
end
# 当前 chunk 的 fd 指回虚拟 chunk 头(vhdr)→ 链尾
if $c_fd == $vhdr
if $hdr_bk != $cur
printf " RESULT: 发现断链!\n"
printf " 断链点 = chunk[%d] @ 0x%llx(链尾)\n", $step, $cur
printf " 该 chunk 的 fd 指回虚拟chunk头(0x%llx) 正确,但 header.bk=0x%llx 应指向链尾 0x%llx\n", $vhdr, $hdr_bk, $cur
printf " → header.bk 不指向链尾 chunk\n"
else
printf " RESULT: 链表完整,无断链。\n"
printf " 共 %d 个 chunk(不包含 header 0x%llx),fd/bk 全部互指正确\n", $step + 1, $hdr
end
set $broken = 1
loop_break
end
# 启发式检查下一个地址(fd 指向的值)
if $c_fd == 0
printf " RESULT: 发现断链!\n"
printf " 断链点 = chunk[%d] @ 0x%llx\n", $step, $cur
printf " 该 chunk 的 fd=0(NULL),链表未回到虚拟chunk头(0x%llx) 即截断\n", $vhdr
set $broken = 1
loop_break
end
if $c_fd < 0x10000
printf " RESULT: 发现断链!\n"
printf " 断链点 = chunk[%d] @ 0x%llx\n", $step, $cur
printf " 该 chunk 的 fd=0x%llx —— 疑似损坏指针(地址过小,非合法堆地址)\n", $c_fd
set $broken = 1
loop_break
end
# 前进一步
set $prev = $cur
set $cur = $c_fd
set $step = $step + 1
end
if $broken == 0
printf " RESULT: 达到最大遍历步数 %d,链表可能存在环或过长\n", $max_steps
end
end
end
# ============================================================
# 主宏:遍历一个 arena 某个类型的所有 bin
# 用法: heap_bins <arena_addr> <bin_type>
# bin_type: fast | unsorted | small | large(不区分大小写)
# ============================================================
define heap_bins
# $arg0 : arena 地址(如 0x7f862d055640)
# $arg1 : bin 类型(字符串,脚本通过首字符 ASCII 码识别:
# f=102/F=70 fast, u=117/U=85 unsorted, s=115/S=83 small, l=108/L=76 large)
set $arena = $arg0 + 0
set $bin_type_valid = 0
set $bin_is_fast = 0
set $bin_offset = 0
set $bin_count = 0
set $bin_size = 0
# --- fast ---
if "$arg1"[0] == 102 || "$arg1"[0] == 70
set $bin_offset = $fast_bin_offset
set $bin_count = $fast_bin_count
set $bin_size = $fast_bin_size
set $bin_is_fast = 1
set $bin_type_valid = 1
printf "==== heap_bins: arena=0x%llx, type=fast, 共 %d 个 bin ====\n", $arena, $bin_count
end
# --- unsorted ---
if "$arg1"[0] == 117 || "$arg1"[0] == 85
set $bin_offset = $unsorted_bin_offset
set $bin_count = $unsorted_bin_count
set $bin_size = $unsorted_bin_size
set $bin_type_valid = 1
printf "==== heap_bins: arena=0x%llx, type=unsorted, 共 %d 个 bin ====\n", $arena, $bin_count
end
# --- small ---
if "$arg1"[0] == 115 || "$arg1"[0] == 83
set $bin_offset = $small_bin_offset
set $bin_count = $small_bin_count
set $bin_size = $small_bin_size
set $bin_type_valid = 1
printf "==== heap_bins: arena=0x%llx, type=small, 共 %d 个 bin ====\n", $arena, $bin_count
end
# --- large ---
if "$arg1"[0] == 108 || "$arg1"[0] == 76
set $bin_offset = $large_bin_offset
set $bin_count = $large_bin_count
set $bin_size = $large_bin_size
set $bin_type_valid = 1
printf "==== heap_bins: arena=0x%llx, type=large, 共 %d 个 bin ====\n", $arena, $bin_count
end
if $bin_type_valid == 0
printf "错误: 未知 bin 类型。用法: heap_bins <arena_addr> <fast|unsorted|small|large>\n"
end
# ---------- 遍历该类型所有 bin ----------
if $bin_type_valid == 1
set $b = 0
while $b < $bin_count
set $bin_hdr = $arena + $bin_offset + $b * $bin_size
# 先判断该 bin 是否为空(fast: fd==0;普通: fd==0 且 bk==0),
# 空 bin(除 bin_header 外无任何 chunk)→ 不显示任何数据,完全静默
set $bin_hdr_fd = *(unsigned long long*)($bin_hdr + 0)
set $bin_empty = 0
if $bin_is_fast
if $bin_hdr_fd == 0
set $bin_empty = 1
end
else
set $bin_hdr_bk = *(unsigned long long*)($bin_hdr + 8)
# 判空条件(两种形态任一满足即为空):
# 1) fd/bk 均指向 bin_at(= bin_header - 0x10)——glibc 真实空 bin 初始化值
# 2) fd/bk 均为 0(测试构造或异常形态)
if ($bin_hdr_fd == $bin_hdr - 0x10 && $bin_hdr_bk == $bin_hdr - 0x10) || ($bin_hdr_fd == 0 && $bin_hdr_bk == 0)
set $bin_empty = 1
end
end
if !$bin_empty
printf " --- bin[%d] @ 0x%llx ---\n", $b, $bin_hdr
if $bin_is_fast
heap_bins_walk_fast $bin_hdr
else
heap_bins_walk_normal $bin_hdr
end
end
set $b = $b + 1
end
end
printf "==== heap_bins 检查结束 ====\n"
end
printf "heap_bins.gdb 已加载。用法: heap_bins <arena_addr> <fast|unsorted|small|large>\n"
3、heap_bin.gdb
# heap_bin.gdb
#
# 功能:分析 glibc ptmalloc 某个 bin 的双向链表,定位断链位置。
# 用法:
# gdb -batch -x heap_bin.gdb <coredump> (交互式加载宏)
# gdb <coredump> (进入后执行 source heap_bin.gdb)
# 然后在 GDB 内执行: heap_bin <bin_header_addr>
#
# 说明:
# bin header 地址可从 gdb 中 `p &main_arena->bins[0]`(unsorted bin)
# 或 `p &main_arena->bins[n]` 获取;也可从 malloc_state 偏移计算。
#
# 遍历逻辑(与 Python 版 bin_check.py 一致):
# 1) 从 header.fd 取第一个 chunk,检查其 bk 是否指向虚拟 chunk 头(bin_at = header-0x10)
# 2) 沿 fd 逐个遍历,检查每个 chunk 的 bk 是否指向前一个 chunk
# 3) 最后一个 chunk 的 fd 应指回虚拟 chunk 头(bin_at = header-0x10),
# 且 header.bk 应指向该 chunk
# 4) 任一步 fd/bk 不互指、或指针疑似损坏(0/过小)→ 立即中断并输出断链描述
#
# 记录内容:
# - bin header 地址、header.fd、header.bk
# - 每个遍历到的 chunk:地址、prev_size、size(不含低 3 位 flags)、
# 低 3 位 flags(按宏名 | 拼接)、fd、bk
# flags 宏定义:
# bit0 (0x1) PREV_INUSE
# bit1 (0x2) IS_MMAPPED
# bit2 (0x4) NON_MAIN_ARENA
#
# 字段偏移(与 glibc 源码一致):
# bin header(bin 槽,只有 fd/bk 两个字段,16 字节):
# offset 0x0 : fd
# offset 0x8 : bk
# 被链接的 chunk(完整 malloc_chunk):
# offset 0x0 : prev_size
# offset 0x8 : size
# offset 0x10 : fd
# offset 0x18 : bk
# (chunk 的 fd 字段在 chunk 被分配后复用为用户数据指针)
#
# 注意:纯 GDB 命令脚本无法捕获"解引用不可读地址"的异常(会报错中断)。
# 脚本对 fd/bk 采用启发式检查(值为 0 或 < 0x10000 视为损坏),
# 并在每次解引用前打印当前位置,若 GDB 因野指针报错,
# 最后一行输出即为断链位置。
# 输出 chunk 低 3 位 flags 的宏名拼接(0-7 全枚举,因 GDB 无字符串拼接函数)
# 用法: print_flags <flags值>
define print_flags
set $f = $arg0 + 0
printf "flags = 0x%x (", $f
if $f == 0
printf "NONE"
end
if $f == 1
printf "PREV_INUSE"
end
if $f == 2
printf "IS_MMAPPED"
end
if $f == 3
printf "PREV_INUSE | IS_MMAPPED"
end
if $f == 4
printf "NON_MAIN_ARENA"
end
if $f == 5
printf "PREV_INUSE | NON_MAIN_ARENA"
end
if $f == 6
printf "IS_MMAPPED | NON_MAIN_ARENA"
end
if $f == 7
printf "PREV_INUSE | IS_MMAPPED | NON_MAIN_ARENA"
end
printf ")\n"
end
define heap_bin
# heap_bin <bin_header_addr> [max_steps]
# $arg0 : bin header 地址(十六进制,如 0x7f0000000000)
# $arg1 : 可选,最大遍历步数(默认 1000000,防死循环)
set $hdr = $arg0 + 0
set $max_steps = 1000000
if $argc > 1
set $max_steps = $arg1 + 0
end
# 读取 bin header 的 fd / bk(header 是 bin 槽:fd @ +0x0, bk @ +0x8)
set $hdr_fd = *(unsigned long long*)($hdr + 0)
set $hdr_bk = *(unsigned long long*)($hdr + 8)
# 空 bin(除 bin_header 外无任何 chunk)→ 输出一行 "no chunks"
# 判空条件(两种形态任一满足即为空):
# 1) fd/bk 均指向 bin_at(= bin_header - 0x10)——glibc 真实空 bin 初始化值
# (ptmalloc_init 中 bin->fd = bin->bk = bin,bin 即 bin_at 地址)
# 2) fd/bk 均为 0(测试构造或异常形态)
if ($hdr_fd == $hdr - 0x10 && $hdr_bk == $hdr - 0x10) || ($hdr_fd == 0 && $hdr_bk == 0)
printf "no chunks\n"
else
printf "==== heap_bin: 分析 bin 双向链表 ====\n"
printf "bin header : 0x%llx\n", $hdr
printf "header.fd : 0x%llx\n", $hdr_fd
printf "header.bk : 0x%llx\n", $hdr_bk
# bin header 是"虚构"的 malloc_chunk:bin_at 地址 = fd 槽地址 - 0x10
# 因此链表中第一个 chunk->bk 与最后一个 chunk->fd 回指的
# 是 bin_at 地址(= 传入的 bin header 地址 - 0x10),而非传入地址本身
set $vhdr = $hdr - 0x10
printf "bin 虚拟chunk头(vhdr=bin_header-0x10) : 0x%llx\n", $vhdr
# ---------- 空链检查(fd/bk 不一致 = 结构损坏)----------
set $done = 0
if $hdr_fd == 0 && $hdr_bk != 0
printf "RESULT: 发现断链!header.fd=0 但 header.bk=0x%llx(非空)\n", $hdr_bk
printf " → header 的 fd/bk 不一致,链表结构损坏\n"
set $done = 1
end
if $hdr_fd != 0 && $hdr_bk == 0
printf "RESULT: 发现断链!header.fd=0x%llx(非空)但 header.bk=0\n", $hdr_fd
printf " → header 的 fd/bk 不一致,链表结构损坏\n"
set $done = 1
end
# ---------- 遍历 ----------
if !$done
set $cur = $hdr_fd
# 第一个 chunk 的 bk 应指向"虚拟"的 bin_at 地址(vhdr),而非 bin 槽地址
set $prev = $vhdr
set $step = 0
set $broken = 0
while $step < $max_steps && $broken == 0
# 启发式检查当前 chunk 地址
if $cur == 0
printf "RESULT: 发现断链!\n"
printf " 遍历到 chunk[%d] 时发现其前驱的 fd 为 0(NULL),链表未回到 header 即截断\n", $step
set $broken = 1
loop_break
end
if $cur < 0x10000
printf "RESULT: 发现断链!\n"
printf " chunk[%d] 地址为 0x%llx —— 疑似损坏指针(地址过小,非合法堆地址)\n", $step, $cur
set $broken = 1
loop_break
end
# 解引用前先打印当前位置(若 GDB 因野指针报错,最后一行即为断链点)
printf " 检查 chunk[%d] @ 0x%llx (prev=0x%llx) ...\n", $step, $cur, $prev
# 读取 chunk 全部字段:prev_size / size / fd / bk
set $c_prevsize = *(unsigned long long*)($cur + 0)
set $c_size_raw = *(unsigned long long*)($cur + 8)
set $c_fd = *(unsigned long long*)($cur + 16)
set $c_bk = *(unsigned long long*)($cur + 24)
# size 不含低 3 位 flags
set $c_size = $c_size_raw & ~7
set $c_flags = $c_size_raw & 7
# 记录 chunk 字段
printf " prev_size = 0x%llx\n", $c_prevsize
printf " size = 0x%llx (不含低3位 flags)\n", $c_size
print_flags $c_flags
printf " fd = 0x%llx\n", $c_fd
printf " bk = 0x%llx\n", $c_bk
# 检查当前 chunk 的 bk 是否指向前驱
if $c_bk != $prev
printf "RESULT: 发现断链!\n"
printf " 断链点 = chunk[%d] @ 0x%llx\n", $step, $cur
printf " 该 chunk 的 bk=0x%llx,应指向 prev=0x%llx → bk 不指向前驱\n", $c_bk, $prev
set $broken = 1
loop_break
end
# 当前 chunk 的 fd 指回虚拟 chunk 头(vhdr)→ 链尾
if $c_fd == $vhdr
if $hdr_bk != $cur
printf "RESULT: 发现断链!\n"
printf " 断链点 = chunk[%d] @ 0x%llx(链尾)\n", $step, $cur
printf " 该 chunk 的 fd 指回虚拟chunk头(0x%llx) 正确,但 header.bk=0x%llx 应指向链尾 0x%llx\n", $vhdr, $hdr_bk, $cur
printf " → header.bk 不指向链尾 chunk\n"
else
printf "RESULT: 链表完整,无断链。\n"
printf " 共 %d 个 chunk(不包含 header 0x%llx),fd/bk 全部互指正确\n", $step + 1, $hdr
end
set $broken = 1
loop_break
end
# 启发式检查下一个地址(fd 指向的值)
if $c_fd == 0
printf "RESULT: 发现断链!\n"
printf " 断链点 = chunk[%d] @ 0x%llx\n", $step, $cur
printf " 该 chunk 的 fd=0(NULL),链表未回到虚拟chunk头(0x%llx) 即截断\n", $vhdr
set $broken = 1
loop_break
end
if $c_fd < 0x10000
printf "RESULT: 发现断链!\n"
printf " 断链点 = chunk[%d] @ 0x%llx\n", $step, $cur
printf " 该 chunk 的 fd=0x%llx —— 疑似损坏指针(地址过小,非合法堆地址)\n", $c_fd
set $broken = 1
loop_break
end
# 前进一步
set $prev = $cur
set $cur = $c_fd
set $step = $step + 1
end
if $broken == 0
printf "RESULT: 达到最大遍历步数 %d,链表可能存在环或过长\n", $max_steps
end
end
printf "==== heap_bin 检查结束 ====\n"
end
end
printf "heap_bin.gdb 已加载。用法: heap_bin <bin_header_addr> [max_steps]\n"
4、heap_bin_fast.gdb
# heap_bin_fast.gdb
#
# 功能:分析 glibc ptmalloc 某个 fast bin 的单向链表,定位断链位置。
# 用法:
# gdb -batch -x heap_bin_fast.gdb <coredump> (交互式加载宏)
# gdb <coredump> (进入后执行 source heap_bin_fast.gdb)
# 然后在 GDB 内执行: heap_bin_fast <bin_header_addr> [max_steps]
#
# 说明:
# bin header 地址可从 gdb 中 `p &main_arena->fastbinsY[n]` 获取,
# 也可从 malloc_state 偏移计算。
#
# fast bin 与普通 bin 的区别(遍历逻辑不同):
# 1) fast bin 是【单向链表】(后进先出 LIFO),只通过 fd 链接,
# bin_header 和各 chunk 的 bk 指针均无效,不读取也不检查;
# 2) bin header(fastbinsY[n] 槽)只有 8 字节,仅 fd 有效(@ +0x0);
# 3) header.fd 指向第一个 chunk(最近释放的 chunk);
# 4) 沿 fd 逐个遍历,直到最后一个 chunk 的 fd 指回 bin header 本身;
# 5) 遍历过程中 fd 出现 0 / 过小(疑似损坏)→ 立即中断并输出断链描述。
#
# 记录内容:
# - bin header 地址、header.fd
# - 每个遍历到的 chunk:地址、prev_size、size(不含低 3 位 flags)、
# 低 3 位 flags(按宏名 | 拼接)、fd
# flags 宏定义:
# bit0 (0x1) PREV_INUSE
# bit1 (0x2) IS_MMAPPED
# bit2 (0x4) NON_MAIN_ARENA
#
# 字段偏移(与 glibc 源码一致):
# fast bin 槽(只有 fd 一个字段,8 字节):
# offset 0x0 : fd
# 被链接的 chunk(完整 malloc_chunk):
# offset 0x0 : prev_size
# offset 0x8 : size
# offset 0x10 : fd
# offset 0x18 : bk(fast bin 中无效)
#
# 注意:纯 GDB 命令脚本无法捕获"解引用不可读地址"的异常(会报错中断)。
# 脚本对 fd 采用启发式检查(值为 0 或 < 0x10000 视为损坏),
# 并在每次解引用前打印当前位置,若 GDB 因野指针报错,
# 最后一行输出即为断链位置。
# 输出 chunk 低 3 位 flags 的宏名拼接(0-7 全枚举,因 GDB 无字符串拼接函数)
# 用法: print_flags <flags值>
define print_flags
set $f = $arg0 + 0
printf "flags = 0x%x (", $f
if $f == 0
printf "NONE"
end
if $f == 1
printf "PREV_INUSE"
end
if $f == 2
printf "IS_MMAPPED"
end
if $f == 3
printf "PREV_INUSE | IS_MMAPPED"
end
if $f == 4
printf "NON_MAIN_ARENA"
end
if $f == 5
printf "PREV_INUSE | NON_MAIN_ARENA"
end
if $f == 6
printf "IS_MMAPPED | NON_MAIN_ARENA"
end
if $f == 7
printf "PREV_INUSE | IS_MMAPPED | NON_MAIN_ARENA"
end
printf ")\n"
end
define heap_bin_fast
# heap_bin_fast <bin_header_addr> [max_steps]
# $arg0 : bin header 地址(十六进制,如 0x7f0000000000)
# $arg1 : 可选,最大遍历步数(默认 1000000,防死循环)
set $hdr = $arg0 + 0
set $max_steps = 1000000
if $argc > 1
set $max_steps = $arg1 + 0
end
# 读取 fast bin 槽的 fd(槽只有 8 字节:fd @ +0x0,无 bk 槽)
set $hdr_fd = *(unsigned long long*)($hdr + 0)
# 空 bin(除 bin_header 外无任何 chunk)→ 输出一行 "no chunks"
if $hdr_fd == 0
printf "no chunks\n"
else
# fast bin 链尾 chunk 的 fd 直接回指 bin header 本身(fast bin 无 vhdr 概念)
printf "==== heap_bin_fast: 分析 fast bin 单向链表 ====\n"
printf "bin header : 0x%llx\n", $hdr
printf "header.fd : 0x%llx\n", $hdr_fd
# ---------- 遍历 ----------
set $done = 0
if !$done
set $cur = $hdr_fd
set $prev = $hdr
set $step = 0
set $broken = 0
while $step < $max_steps && $broken == 0
# 启发式检查当前 chunk 地址
if $cur == 0
printf "RESULT: 发现断链!\n"
printf " 遍历到 chunk[%d] 时发现其前驱的 fd 为 0(NULL),链表未回到 bin header 即截断\n", $step
set $broken = 1
loop_break
end
if $cur < 0x10000
printf "RESULT: 发现断链!\n"
printf " chunk[%d] 地址为 0x%llx —— 疑似损坏指针(地址过小,非合法堆地址)\n", $step, $cur
set $broken = 1
loop_break
end
# 解引用前先打印当前位置(若 GDB 因野指针报错,最后一行即为断链点)
printf " 检查 chunk[%d] @ 0x%llx (prev=0x%llx) ...\n", $step, $cur, $prev
# 读取 chunk 字段:prev_size / size / fd(bk 在 fast bin 中无效,不读取)
set $c_prevsize = *(unsigned long long*)($cur + 0)
set $c_size_raw = *(unsigned long long*)($cur + 8)
set $c_fd = *(unsigned long long*)($cur + 16)
# size 不含低 3 位 flags
set $c_size = $c_size_raw & ~7
set $c_flags = $c_size_raw & 7
# 记录 chunk 字段
printf " prev_size = 0x%llx\n", $c_prevsize
printf " size = 0x%llx (不含低3位 flags)\n", $c_size
print_flags $c_flags
printf " fd = 0x%llx (bk 无效,不检查)\n", $c_fd
# 当前 chunk 的 fd 指回 bin header → 链尾,单向链完整
if $c_fd == $hdr
printf "RESULT: fast bin 链表完整,无断链。\n"
printf " 共 %d 个 chunk(不包含 header 0x%llx),fd 单向链正确\n", $step + 1, $hdr
set $broken = 1
loop_break
end
# 启发式检查下一个地址(fd 指向的值)
if $c_fd == 0
printf "RESULT: 发现断链!\n"
printf " 断链点 = chunk[%d] @ 0x%llx\n", $step, $cur
printf " 该 chunk 的 fd=0(NULL),链表未回到 bin header(0x%llx) 即截断\n", $hdr
set $broken = 1
loop_break
end
if $c_fd < 0x10000
printf "RESULT: 发现断链!\n"
printf " 断链点 = chunk[%d] @ 0x%llx\n", $step, $cur
printf " 该 chunk 的 fd=0x%llx —— 疑似损坏指针(地址过小,非合法堆地址)\n", $c_fd
set $broken = 1
loop_break
end
# 前进一步
set $prev = $cur
set $cur = $c_fd
set $step = $step + 1
end
if $broken == 0
printf "RESULT: 达到最大遍历步数 %d,链表可能存在环或过长\n", $max_steps
end
end
printf "==== heap_bin_fast 检查结束 ====\n"
end
end
printf "heap_bin_fast.gdb 已加载。用法: heap_bin_fast <bin_header_addr> [max_steps]\n"
5、heap_chunk.gdb
# heap_chunk.gdb — 查询 glibc ptmalloc 某个 chunk 的信息
#
# 用法:
# gdb -batch -x heap_chunk.gdb <coredump> (加载宏)
# gdb <coredump> (进入后执行 source heap_chunk.gdb)
# 然后在 GDB 内执行: heap_chunk <addr>
#
# <addr> 为用户数据地址(chunk 首地址 + 0x10),可以是十六进制地址、
# 指针变量(如 p)或表达式。
#
# 注意:GDB 的自定义命令按空白拆分参数(括号不分组)。因此 <addr> 必须
# 是单个 token,推荐三种写法:
# 1) 指针变量名: heap_chunk p
# 2) convenience 变量: set $u = p + 0x10
# heap_chunk $u
# 3) 无空格表达式/地址: heap_chunk fake_chunk+0x10
# heap_chunk 0x5555555592a0
# 切勿在参数中写含空格的表达式(如 `p + 0x10`),会被拆成多个参数。
#
# 输出内容:
# chunk addr : chunk 首地址(= 用户数据地址 - 0x10)
# user data addr : 用户数据地址(chunk 首地址 + 0x10)
# pre_size : 前一个 chunk 的大小(仅当前一个 chunk 空闲时有意义)
# size : 本 chunk 大小(已屏蔽低 3 位 flags)
# flags : 低 3 位 flags 按宏名拼接(0 显示为 NONE)
# fd / bk : 空闲 chunk 在 bin 链表中的前驱/后继指针
# (chunk 被分配后该区域为用户数据,fd/bk 无意义)
# ... : 整个 chunk 的内存转储(x/gx 风格,从 chunk 首地址开始,
# 长度恰好覆盖整个 chunk:size + 头部 0x10 字节)
#
# 字段偏移(与 glibc 源码 malloc_chunk 一致,64 位):
# offset 0x0 : prev_size
# offset 0x8 : size(低 3 位为 flags)
# offset 0x10 : fd(用户数据区起点 = chunk 首地址 + 0x10)
# offset 0x18 : bk
#
# flags 宏定义:
# bit0 (0x1) PREV_INUSE —— 前一个 chunk 正在使用
# bit1 (0x2) IS_MMAPPED —— 本 chunk 由 mmap 分配
# bit2 (0x4) NON_MAIN_ARENA —— 本 chunk 属于非主分配区(非 main arena)
#
# 说明:
# gdb 11.x 的 x/ 命令计数只接受数字字面量,x/(表达式) 或 x/$变量 会报语法
# 错误(已在本机 gdb 11.2 实测确认),因此内存转储无法用"计算出的长度"直接
# 拼接进 x/ 命令。为保证任意 gdb 版本可用,这里改用循环 printf 按 x/gx
# 完全相同的格式逐字输出整个 chunk。
# 输出 chunk 低 3 位 flags 的宏名拼接(0-7 全枚举,因 GDB 无字符串拼接函数)
# 用法: heap_chunk_flags <flags值>
define heap_chunk_flags
set $f = $arg0 + 0
if $f == 0
printf "NONE"
end
if $f == 1
printf "PREV_INUSE"
end
if $f == 2
printf "IS_MMAPPED"
end
if $f == 3
printf "PREV_INUSE | IS_MMAPPED"
end
if $f == 4
printf "NON_MAIN_ARENA"
end
if $f == 5
printf "PREV_INUSE | NON_MAIN_ARENA"
end
if $f == 6
printf "IS_MMAPPED | NON_MAIN_ARENA"
end
if $f == 7
printf "PREV_INUSE | IS_MMAPPED | NON_MAIN_ARENA"
end
printf "\n"
end
define heap_chunk
# heap_chunk <addr>
# $arg0 : 用户数据地址(chunk 首地址 + 0x10)
if $argc < 1
printf "用法: heap_chunk <addr>\n"
printf " <addr> 为用户数据地址(chunk 首地址 + 0x10)\n"
else
# 计算 chunk 首地址(64 位:头部 prev_size + size 共 0x10 字节)
set $hc_chunk = $arg0 - 0x10
# 读取 chunk 头部字段
set $hc_pre_size = *(unsigned long long*)($hc_chunk + 0)
set $hc_size_raw = *(unsigned long long*)($hc_chunk + 8)
# size 不含低 3 位 flags
set $hc_size = $hc_size_raw & ~7
set $hc_flags = $hc_size_raw & 7
# fd / bk(空闲 chunk 的 bin 链表指针;分配后为用户数据)
set $hc_fd = *(unsigned long long*)($hc_chunk + 16)
set $hc_bk = *(unsigned long long*)($hc_chunk + 24)
# 打印基本信息
printf "chunk addr: 0x%llx\n", $hc_chunk
printf "user data addr: 0x%llx\n", $arg0
printf "pre_size: 0x%llx\n", $hc_pre_size
printf "size: 0x%llx (不含低3位 flags)\n", $hc_size
printf "flags: "
heap_chunk_flags $hc_flags
printf "fd: 0x%llx\n", $hc_fd
printf "bk: 0x%llx\n", $hc_bk
printf "\n"
# 整个 chunk 的内存转储(x/gx 风格:每行两个 8 字节字)
# 总字节数 = size + 头部 0x10,字数 = 总字节 / 8
set $hc_cnt = ($hc_size + 0x10) / 8
printf "整个 chunk 内存转储 (x/gx, 共 %d 个8字节字):\n", $hc_cnt
set $hc_i = 0
while $hc_i < $hc_cnt
printf "0x%016llx:\t0x%016llx", $hc_chunk + 8 * $hc_i, *(unsigned long long*)($hc_chunk + 8 * $hc_i)
set $hc_i = $hc_i + 1
if $hc_i < $hc_cnt
printf "\t0x%016llx", *(unsigned long long*)($hc_chunk + 8 * $hc_i)
set $hc_i = $hc_i + 1
end
printf "\n"
end
end
end
printf "heap_chunk.gdb 已加载。用法: heap_chunk <addr> (addr 为用户数据地址 = chunk首地址+0x10)\n"

浙公网安备 33010602011771号