3、缺页中断入口:异常向量表配置、el0_sync/el1_sync入口分析、保存现场与栈切换

缺页异常(Page Fault)在 ARM64 架构中属于同步异常(Synchronous Exception),其入口由异常向量表(Exception Vector Table)统一管理。当 CPU 在 EL0(用户态)或 EL1(内核态)访问无效地址时,硬件自动跳转到对应的向量条目,内核随即执行保存现场、栈切换、异常类型判定等操作。本章深入分析入口阶段的完整流程。

3.1 ARM64 异常向量表结构

ARM64 架构要求每个异常等级(EL)维护一张 2048 字节的向量表,共 16 个条目,每个条目 128 字节。向量表基址由 VBAR_ELn 寄存器指定。Linux 内核在启动阶段为 EL1 和 EL0 分别配置向量表。

偏移量 异常类型 描述
0x000 EL1t Sync 使用 SP_EL1 的同步异常(当前 EL)
0x080 EL1t IRQ 使用 SP_EL1 的 IRQ
0x100 EL1t FIQ 使用 SP_EL1 的 FIQ
0x180 EL1t SError 使用 SP_EL1 的 SError
0x200 EL1h Sync 使用 SP_EL1 的同步异常(当前 EL,h 模式)
0x280 EL1h IRQ 使用 SP_EL1 的 IRQ
0x300 EL1h FIQ 使用 SP_EL1 的 FIQ
0x380 EL1h SError 使用 SP_EL1 的 SError
0x400 EL0 64-bit Sync 来自 EL0 的 64 位同步异常
0x480 EL0 64-bit IRQ 来自 EL0 的 64 位 IRQ
0x500 EL0 64-bit FIQ 来自 EL0 的 64 位 FIQ
0x580 EL0 64-bit SError 来自 EL0 的 64 位 SError
0x600 EL0 32-bit Sync 来自 EL0 的 32 位同步异常
0x680 EL0 32-bit IRQ 来自 EL0 的 32 位 IRQ
0x700 EL0 32-bit FIQ 来自 EL0 的 32 位 FIQ
0x780 EL0 32-bit SError 来自 EL0 的 32 位 SError
关键点:缺页异常(数据中止、指令中止)属于同步异常,在 EL1 下命中 EL1h Sync(偏移 0x200),在 EL0 下命中 EL0 64-bit Sync(偏移 0x400)。内核通过 __vectors 和 __vectors_end 标签定义向量表。
向量表配置验证:在启动汇编 head.S 中,内核通过 msr vbar_el1, x0 将向量表基址写入 VBAR_EL1。可通过 cat /proc/cpuinfo 或 dmesg | grep "Vector" 确认向量表地址。

3.2 el0_sync 与 el1_sync 入口分析

当异常来自 EL0 时,CPU 自动切换到 EL1 并使用 SP_EL1,然后跳转到 el0_sync 入口。当异常来自 EL1 时,CPU 保持在 EL1,跳转到 el1_sync 入口。两个入口的职责不同:el0_sync 需要处理用户态缺页并可能返回用户态;el1_sync 处理内核态缺页,若无法修复则触发内核 panic。

/* arch/arm64/kernel/entry.S 简化入口代码 */
.align 11
ENTRY(vectors)
    /* EL1t Sync */
    ventry  el1_sync_invalid
    ventry  el1_irq_invalid
    ventry  el1_fiq_invalid
    ventry  el1_error_invalid

    /* EL1h Sync */
    ventry  el1_sync          /* 内核态同步异常入口 */
    ventry  el1_irq
    ventry  el1_fiq_invalid
    ventry  el1_error_invalid

    /* EL0 64-bit Sync */
    ventry  el0_sync          /* 用户态同步异常入口 */
    ventry  el0_irq
    ventry  el0_fiq_invalid
    ventry  el0_error_invalid

    /* EL0 32-bit Sync */
    ventry  el0_sync_compat
    ventry  el0_irq_compat
    ventry  el0_fiq_invalid
    ventry  el0_error_invalid
END(vectors)

el0_sync 和 el1_sync 内部首先读取 ESR_EL1(Exception Syndrome Register)寄存器,提取异常类型(EC, Exception Class)字段,判断是否为数据中止(EC=0x24)或指令中止(EC=0x20)。若是,则调用 do_mem_abort 进入缺页处理核心路径。

注意:内核态缺页(el1_sync)通常意味着内核访问了无效地址,可能是内存越界或空指针解引用。内核会尝试通过 fixup 机制修复,若失败则触发 panic。用户态缺页(el0_sync)则通过缺页中断正常处理。

3.3 保存现场与栈切换

保存现场是异常入口的核心任务,目的是将 CPU 寄存器完整保存到栈上,以便异常处理完成后精确恢复执行。ARM64 内核使用 pt_regs 结构体保存现场,该结构体包含 31 个通用寄存器、SP、PC、PSTATE 等。

/* arch/arm64/kernel/entry.S 保存现场宏 */
.macro  kernel_entry, el, regsize = 64
    .if     \el == 0
    /* 从 EL0 进入,需要切换栈 */
    mrs     x21, sp_el0          /* 保存用户栈指针 */
    ldr_this_cpu    tsk, __entry_task, x20
    msr     sp_el0, task_stack_page(tsk)  /* 切换到内核栈 */
    .endif

    /* 保存通用寄存器到栈上 */
    stp     x0, x1, [sp, #16 * 0]
    stp     x2, x3, [sp, #16 * 1]
    stp     x4, x5, [sp, #16 * 2]
    /* ... 保存 x0~x29 ... */
    stp     x30, x21, [sp, #16 * 15]  /* x30=LR, x21=SP_EL0 */

    /* 保存 ELR_EL1 和 SPSR_EL1 */
    mrs     x22, elr_el1
    mrs     x23, spsr_el1
    stp     x22, x23, [sp, #16 * 16]

    /* 保存 ESR_EL1 和 FAR_EL1 */
    mrs     x24, esr_el1
    mrs     x25, far_el1
    stp     x24, x25, [sp, #16 * 17]
.endm
栈切换机制:当异常来自 EL0 时,CPU 自动使用 SP_EL1(内核栈)。内核通过 sp_el0 保存用户栈指针,并将 SP 切换到当前进程的内核栈。来自 EL1 的异常不切换栈,直接使用当前内核栈。

3.4 异常入口完整流程 SVG 图

ARM64 缺页异常入口流程 缺页异常入口:从硬件触发到保存现场 硬件触发缺页异常 异常向量表查找(VBAR_EL1) 异常来源判定 el0_sync 入口 来自用户态(EL0) el1_sync 入口 来自内核态(EL1) 保存现场(kernel_entry 宏) 保存 pt_regs 到栈 栈切换(仅 EL0 入口) 用户栈 → 内核栈 读取 ESR_EL1 判定异常类型 EC=0x24/0x20 → 缺页处理 说明: • EL0 入口需切换栈 • EL1 入口不切换栈 • 两者最终都调用 do_mem_abort

3.5 保存现场数据结构:pt_regs

pt_regs 是 ARM64 内核保存异常现场的核心数据结构,定义在 arch/arm64/include/asm/ptrace.h。其布局与栈上保存顺序严格对应。

struct pt_regs {
    union {
        struct user_pt_regs user_regs;
        struct {
            u64 regs[31];   /* x0 ~ x30 */
        };
    };
    u64 sp;                 /* 栈指针 */
    u64 pc;                 /* 程序计数器(ELR_EL1) */
    u64 pstate;             /* 处理器状态(SPSR_EL1) */
    /* 以下为内核扩展字段 */
    u64 orig_x0;            /* 系统调用时保存原始 x0 */
    u64 syscallno;          /* 系统调用号 */
    u64 unused;             /* 对齐填充 */
};
字段 寄存器来源 说明
regs[0..30] x0 ~ x30 通用寄存器,x30 为链接寄存器(LR)
sp sp_el0(EL0 入口)或 sp(EL1 入口) 异常发生时的栈指针
pc ELR_EL1 异常返回地址
pstate SPSR_EL1 异常发生时的 PSTATE
orig_x0 x0 系统调用场景保留原始 x0
调试技巧:在缺页异常处理函数中,可通过 struct pt_regs *regs 参数访问异常现场。例如 regs->pc 获取触发缺页的指令地址,regs->sp 获取用户栈指针。使用 dump_stack() 可打印内核栈回溯。

3.6 入口阶段关键寄存器与作用

寄存器 全称 入口阶段作用
ESR_EL1 Exception Syndrome Register 记录异常类型(EC)、异常原因(ISS)
FAR_EL1 Fault Address Register 记录触发缺页的虚拟地址
ELR_EL1 Exception Link Register 记录异常返回地址(触发缺页的指令地址)
SPSR_EL1 Saved Program Status Register 保存异常发生时的 PSTATE
SP_EL0 Stack Pointer EL0 EL0 入口时保存用户栈指针
SP_EL1 Stack Pointer EL1 内核栈指针,异常入口自动使用
入口阶段总结:缺页异常入口的核心任务可归纳为三步:
1. 向量跳转:硬件根据异常等级和类型跳转到 el0_sync 或 el1_sync。
2. 保存现场:通过 kernel_entry 宏将 CPU 寄存器完整保存到栈上,形成 pt_regs 结构。
3. 栈切换:来自 EL0 的异常需要从用户栈切换到内核栈,来自 EL1 的异常直接使用当前内核栈。
完成上述步骤后,内核调用 do_mem_abort 进入缺页中断的核心处理逻辑。