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)并记录崩溃现场。
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 执行状态的“快照”。在内存访问越界分析中,寄存器提供了三个关键信息:
- 位置(Where): rip 告诉我们哪条指令触发了异常。
- 地址(What Address): rdi/rsi 告诉我们访问了哪个虚拟地址。
- 长度(How Much): rcx/rsi 告诉我们访问了多少字节。