3. 寄存器基础回顾:x86-64 通用寄存器

在分析 Linux 内核崩溃(如 Oops、Panic)时,寄存器快照是定位内存访问越界的第一手证据。x86-64 架构提供了 16 个通用寄存器(General-Purpose Registers, GPRs),其中 rax、rbx、rcx、rdx、rsi、rdi、rbp、rsp、rip 是调试中最常使用的 9 个。理解它们的用途,能帮助我们从崩溃现场快速推断出“程序执行到哪里”、“访问了哪个地址”、“数据是什么”。

3.1 寄存器分类与核心功能

下表列出了 9 个关键寄存器的名称、主要用途以及在崩溃分析中的典型作用:

寄存器 主要用途 崩溃分析中的典型作用
rax 累加器;函数返回值 查看函数返回结果;若为非法地址,可能指示返回了错误指针
rbx 基址寄存器(被调用者保存) 常保存全局或局部基址;若被破坏,可能指向越界区域
rcx 计数器;第 4 个参数 循环计数或字符串操作长度;越界时可能为异常大或负数
rdx 数据寄存器;第 3 个参数 I/O 操作或乘除运算的高位数据
rsi 源索引;第 2 个参数 memcpy/memmove 的源地址;若指向只读或未映射区,触发 #PF
rdi 目标索引;第 1 个参数 memcpy/memset 的目标地址;越界写入的常见目标
rbp 栈帧基址指针 回溯调用栈的关键;若被覆盖,栈回溯会断裂
rsp 栈顶指针 当前栈位置;越界写入可能破坏栈上的返回地址或局部变量
rip 指令指针(程序计数器) 崩溃发生的精确指令地址;反汇编后直接定位越界指令
调试提示: 在 Linux 内核崩溃日志(如 dmesg 中的 Oops 信息)中,寄存器快照通常以 RIP: 0010:[<address>] 开头。优先检查 rip 指向的指令,然后查看 rdi/rsi 是否包含非法地址,这是定位内存访问越界的最快路径。

3.2 寄存器在函数调用中的角色(System V AMD64 ABI)

x86-64 Linux 遵循 System V AMD64 ABI 调用约定。函数参数通过寄存器传递,返回值通过 rax 传递。理解这一约定,能让我们在崩溃现场“读懂”函数调用的上下文。

  • 参数传递顺序: rdi → rsi → rdx → rcx → r8 → r9(前 6 个整数/指针参数)。
  • 返回值: rax(若为 64 位指针或整数)。
  • 被调用者保存(Callee-saved): rbx, rbp, r12–r15。这些寄存器在函数返回时必须恢复原值,因此常保存重要基址。
  • 调用者保存(Caller-saved): rax, rcx, rdx, rsi, rdi, r8–r11。函数调用后内容可能被破坏。
越界分析案例: 若崩溃时 rdi = 0xffff888012345678,rsi = 0x1000,且 rip 指向 memcpy 或 memset 指令,则表明目标地址 rdi 可能是一个已释放的 slab 对象或越界偏移后的地址,长度 rsi 可能过大导致访问了相邻内存。

3.3 寄存器与内存访问越界的关联模型

下面的 SVG 图展示了在内存访问越界场景下,寄存器如何与虚拟地址空间交互,最终触发页错误(#PF)并记录崩溃现场。

寄存器与内存访问越界关联模型 寄存器快照 rip = 0xffffffff8123456 rdi = 0xffff8880a0001000 rsi = 0x0000000000000200 rax = 0x0000000000000000 rbp = 0xffffc9000012f000 rsp = 0xffffc9000012ef80 CPU 执行 指令执行 mov (%rdi), %rax 或 rep movsb (%rsi), (%rdi) 访存 虚拟地址空间 有效映射区域 (rdi 指向合法对象) 越界区域 / 未映射 (rdi + offset 超出边界) 已释放 / 未分配 (use-after-free) 访问越界 #PF 页错误异常 CPU 自动压栈错误码 记录现场 Oops / Panic 日志 寄存器快照 + 栈回溯

3.4 关键寄存器在崩溃现场中的解读

当内核发生崩溃时,dmesg 或 kdump 会输出类似下面的寄存器快照。我们以一段典型的 Oops 信息为例进行解读:

BUG: unable to handle kernel paging request at ffff8880a0002000
IP: [<ffffffff81234567>] memcpy+0x17/0x30
PGD 8000000107f6f067 P4D 8000000107f6f067 PUD 8000000107f6f067 PMD 0
Oops: 0002 [#1] SMP NOPTI
CPU: 1 PID: 1234 Comm: test_app Not tainted 5.15.0-rc1 #1
Hardware name: QEMU Standard PC (i440FX + PIIX, 1996)
RIP: 0010:[<ffffffff81234567>] memcpy+0x17/0x30
RSP: 0018:ffffc9000012ef80  EFLAGS: 00010246
RAX: 0000000000000000  RBX: ffff8880a0001000  RCX: 0000000000000200
RDX: 0000000000000000  RSI: ffff8880b0002000  RDI: ffff8880a0002000
RBP: ffff8880a0001000  R8:  0000000000000000  R9:  0000000000000000
R10: 0000000000000000  R11: 0000000000000000  R12: ffff8880c0003000
R13: 0000000000000200  R14: 0000000000000001  R15: 0000000000000000
关键分析点:
  • RIP 指向 memcpy+0x17/0x30,说明崩溃发生在 memcpy 函数内部偏移 0x17 处。
  • RDI = 0xffff8880a0002000 是目标地址,RSI = 0xffff8880b0002000 是源地址。RDI 的值 0xffff8880a0002000 与 RBX/RBP 的 0xffff8880a0001000 相差 0x1000(4KB),这很可能是一个页边界偏移。
  • RCX = 0x200(512 字节)是复制的长度。如果目标对象实际大小小于 512 字节,则 RDI + RCX 会越过对象边界,访问到相邻内存甚至未映射页。
  • RSP = 0xffffc9000012ef80 是内核栈顶,可以结合栈回溯进一步确认调用链。

3.5 寄存器与内存访问越界的常见模式

根据寄存器值的特征,可以将内存访问越界归纳为以下几种常见模式:

模式 寄存器特征 典型场景
缓冲区溢出 rdi/rsi 指向栈或堆缓冲区,rcx/rsi 长度过大 memcpy/memset 长度参数未校验
Use-After-Free rdi/rsi 指向已释放的 slab 对象(地址在 slab 缓存范围内但已标记为 free) 释放后未置空指针,再次访问
空指针解引用 rdi/rsi = 0x0 或接近 0 的小地址 未检查 kmalloc 返回值
栈帧破坏 rbp 或 rsp 指向异常地址(如 0x41414141) 栈缓冲区溢出覆盖了返回地址或 rbp
越界偏移 rdi = base + offset,offset 超过对象大小 数组索引未做边界检查

3.6 小结:寄存器分析在越界定位中的位置

寄存器是 CPU 执行状态的“快照”。在内存访问越界分析中,寄存器提供了三个关键信息:

  1. 位置(Where): rip 告诉我们哪条指令触发了异常。
  2. 地址(What Address): rdi/rsi 告诉我们访问了哪个虚拟地址。
  3. 长度(How Much): rcx/rsi 告诉我们访问了多少字节。