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 |
__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
sp_el0 保存用户栈指针,并将 SP 切换到当前进程的内核栈。来自 EL1 的异常不切换栈,直接使用当前内核栈。
3.4 异常入口完整流程 SVG 图
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 进入缺页中断的核心处理逻辑。