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"
posted @ 2026-09-29 10:28  CJXUNOO  阅读(3)  评论(0)    收藏  举报