4. pt_regs 结构体:栈上布局与寄存器现场访问

当异常(中断、系统调用、数据中止等)发生时,CPU 会自动将当前执行上下文的关键寄存器保存到内核栈中。这个保存的寄存器集合在 Linux 内核中由 pt_regs 结构体描述。理解 pt_regs 在栈上的布局、寄存器保存顺序以及如何通过该结构体访问现场,是进行异常现场恢复与关键值分析的基石。

4.1 pt_regs 结构体定义与寄存器顺序

在 ARM64 架构中,pt_regs 结构体定义于 arch/arm64/include/asm/ptrace.h。它按照 CPU 硬件保存的顺序,依次存放了从 X0 到 X30 的通用寄存器、SP(栈指针)、PC(程序计数器)以及 PSR(程序状态寄存器)。

偏移量 (Offset) 寄存器 描述 异常恢复中的作用
0x00 X0 通用寄存器 0(通常为函数参数/返回值) 系统调用参数、错误码传递
0x08 X1 通用寄存器 1 系统调用参数
0x10 X2 通用寄存器 2 系统调用参数
... ... ... ...
0xE8 X29 帧指针 (FP) 回溯调用栈
0xF0 X30 链接寄存器 (LR) 异常返回地址
0xF8 SP 栈指针(异常发生时的栈顶) 恢复内核栈上下文
0x100 PC 程序计数器(异常发生时的指令地址) 异常返回点、指令流分析
0x108 PSR 程序状态寄存器(PSTATE) 恢复 CPU 状态(条件标志、异常掩码)
关键点: 寄存器在 pt_regs 中的顺序与 CPU 硬件自动压栈的顺序完全一致。X0 位于最低地址,PSR 位于最高地址。这种布局允许内核通过结构体偏移量直接访问任意寄存器。

4.2 pt_regs 在栈上的布局

当异常从内核态发生时,CPU 不会自动切换到其他栈,而是直接在当前内核栈的顶部压入寄存器。内核的异常入口汇编代码(如 entry.S)会预留出 pt_regs 大小的栈空间,然后按照硬件规范依次保存寄存器。

栈布局示意图说明: 下图展示了异常发生时,内核栈从高地址向低地址增长,pt_regs 结构体被压入栈顶的过程。SP 在保存完成后指向 pt_regs 的起始地址(即 X0 的位置)。
pt_regs 在 ARM64 内核栈上的布局 内核栈 (高地址 → 低地址) 栈增长方向 X0 (偏移 0x00) X1 (偏移 0x08) X2 ... X28 (偏移 0x10 - 0xE0) X29 / FP (偏移 0xE8) X30 / LR (偏移 0xF0) SP (偏移 0xF8) PC (偏移 0x100) PSR / PSTATE (偏移 0x108) SP → 说明: • 异常入口汇编代码 (如 kernel_entry) 先 sub sp, sp, #S_FRAME_SIZE 预留空间。 • 然后按照 X0→X30→SP→PC→PSR 的顺序 stp 指令保存,SP 最终指向 pt_regs 基址。

4.3 如何通过 pt_regs 访问现场

在异常处理函数(如 do_mem_abort、el0_sync_handler)中,内核会传递一个指向当前 pt_regs 的指针 struct pt_regs *regs。通过这个指针,可以读取或修改异常发生时的任意寄存器值。

4.3.1 访问寄存器成员

pt_regs 结构体为每个寄存器定义了明确的成员变量,访问方式直观且类型安全。

// 示例:在异常处理函数中读取关键寄存器
static int dump_exception_regs(struct pt_regs *regs)
{
    pr_err("异常现场信息:\n");
    pr_err("  X0  = 0x%016lx\n", regs->regs[0]);
    pr_err("  X1  = 0x%016lx\n", regs->regs[1]);
    // ... 访问 X2 到 X28 ...
    pr_err("  X29 = 0x%016lx (FP)\n", regs->regs[29]);
    pr_err("  X30 = 0x%016lx (LR)\n", regs->regs[30]);

    pr_err("  SP  = 0x%016lx\n", regs->sp);
    pr_err("  PC  = 0x%016lx\n", regs->pc);
    pr_err("  PSR = 0x%08x\n", regs->pstate);

    // 分析 PSR 中的异常掩码位
    if (regs->pstate & PSR_I_BIT)
        pr_err("  [IRQ 被屏蔽]\n");
    if (regs->pstate & PSR_F_BIT)
        pr_err("  [FIQ 被屏蔽]\n");

    return 0;
}

4.3.2 修改寄存器以影响异常返回

在某些场景下(如信号处理、系统调用重启),内核需要修改 pt_regs 中的值来改变异常返回后的执行流。

// 示例:修改 PC 以跳过当前指令(如模拟指令后)
void skip_current_instruction(struct pt_regs *regs)
{
    // ARM64 指令固定为 4 字节,跳过当前指令
    regs->pc += 4;
}

// 示例:修改 X0 以改变系统调用返回值
void set_syscall_return(struct pt_regs *regs, long retval)
{
    regs->regs[0] = retval;
}

// 示例:强制修改返回地址(用于调试或注入)
void override_return_address(struct pt_regs *regs, unsigned long new_pc)
{
    regs->pc = new_pc;
    // 注意:修改 PC 后,需要确保新地址处的上下文有效
}
重要警告: 直接修改 pt_regs 中的 PC、SP 或 PSR 是非常危险的操作。错误的修改可能导致内核崩溃、状态不一致或安全漏洞。只有在完全理解异常返回路径(eret 指令行为)的情况下,才应进行此类操作。

4.4 从 pt_regs 提取关键值的典型模式

在调试和分析异常时,以下模式用于从 pt_regs 中提取关键信息:

  • 获取异常指令地址: regs->pc 指向异常发生时的指令。对于数据中止,这通常是导致异常的加载/存储指令。
  • 获取栈回溯起点: regs->regs[29] (FP) 和 regs->regs[30] (LR) 可用于在异常上下文中进行栈回溯,打印调用链。
  • 获取系统调用号和参数: 在系统调用入口,X8 存放系统调用号,X0-X5 存放参数。这些值可通过 regs->regs[8] 和 regs->regs[0..5] 访问。
  • 分析 CPU 状态: regs->pstate 的位域指示了异常级别(EL)、条件标志(N/Z/C/V)、异常掩码(A/I/F)等。
分析目标 pt_regs 成员 典型用途
异常指令地址 regs->pc 定位崩溃点、反汇编分析
栈帧回溯 regs->regs[29] (FP)
regs->regs[30] (LR)
打印异常发生时的函数调用栈
系统调用参数 regs->regs[0..5]
regs->regs[8] (syscall no.)
审计系统调用、参数校验
CPU 异常级别 regs->pstate 的 [3:2] 位 判断异常来自 EL0 (用户态) 还是 EL1 (内核态)
数据中止地址 far_el1 (需从系统寄存器读取) 与 regs->pc 配合,分析内存访问错误
调试技巧: 在 kgdb 或 kdump 环境中,可以直接打印 struct pt_regs 的完整内容。使用 gdb 命令 p *regs 或 info registers 可以一次性查看所有保存的寄存器值,快速定位异常现场。

4.5 异常入口与 pt_regs 的构建流程

理解异常入口汇编如何构建 pt_regs,有助于在汇编级别进行调试。以下流程总结了从异常触发到 pt_regs 就绪的关键步骤:

  1. 异常触发: CPU 自动将 PSTATE 保存到 SPSR_EL1,将返回地址保存到 ELR_EL1,并跳转到异常向量表。
  2. 栈准备: 异常向量入口代码(如 el1_sync)首先调整 SP,预留 pt_regs 大小的栈空间。
  3. 寄存器保存: 使用 stp (Store Pair) 指令,按照 X0-X30、SP、PC、PSR 的顺序,将寄存器值写入栈上的 pt_regs 区域。
  4. 参数传递: 将 SP(指向 pt_regs 基址)作为第一个参数传递给 C 语言异常处理函数。
异常入口到 pt_regs 构建流程 1. 异常触发 CPU 硬件自动保存 2. 栈空间预留 sub sp, sp, #S_FRAME_SIZE 3. 寄存器压栈 stp x0, x1, [sp, #...] 4. 传递 pt_regs 指针 mov x0, sp → 调用 C 函数 关键汇编片段 (arch/arm64/kernel/entry.S): .macro kernel_entry, el, regsize=64 sub sp, sp, #S_FRAME_SIZE // 1. 预留 pt_regs 空间 stp x0, x1, [sp, #16 * 0] // 2. 保存 X0, X1 stp x2, x3, [sp, #16 * 1] // 继续保存 X2, X3 ... ... // 3. 最终 SP 指向 pt_regs 基址
实战要点: 在分析 vmcore (kdump) 或 ramdump 时,可以直接从内存中解析 pt_regs。通过找到异常栈的 SP 值,减去 sizeof(struct pt_regs),即可定位到 pt_regs 的起始位置,然后按照偏移量读取每个寄存器。这是离线分析异常现场的核心方法。