4. 缺页异常处理入口:x86架构下的中断向量号、IDT表、异常处理程序注册
缺页异常(Page Fault)是x86架构下由MMU(内存管理单元)在地址转换过程中检测到页表项无效时,向CPU触发的一种异常。CPU通过中断描述符表(IDT)查找对应的异常处理程序入口,从而进入内核的缺页异常处理流程。本节将详细解析缺页异常的中断向量号、IDT表结构以及异常处理程序的注册机制。
4.1 缺页异常的中断向量号
在x86架构中,每个异常或中断都被分配一个唯一的向量号(Vector Number),范围从0到255。缺页异常对应的向量号是14(0x0E),属于保留异常类别。
| 向量号 | 助记符 | 异常类型 | 错误码 | 描述 |
|---|---|---|---|---|
| 0x0E (14) | #PF | Fault(可重入) | 有(32位错误码) | 缺页异常:页表项无效或权限不足 |
| 0x0D (13) | #GP | Fault/Trap | 有 | 通用保护异常:段或页权限违规 |
| 0x06 (6) | #UD | Fault | 无 | 无效操作码:非法指令 |
4.2 IDT表结构与缺页异常门描述符
中断描述符表(Interrupt Descriptor Table, IDT)是x86架构下用于将中断/异常向量号映射到处理程序入口地址的核心数据结构。IDT中的每个表项称为一个“门描述符”(Gate Descriptor),大小为8字节。对于缺页异常,CPU使用中断门(Interrupt Gate)或陷阱门(Trap Gate)来存储处理程序信息。
IDT表通过LIDT指令加载到CPU的IDTR寄存器中。IDTR包含IDT表的基地址和限长(Limit)。当CPU收到向量号14的异常时,硬件自动执行以下步骤:
- 从IDTR中获取IDT基地址,计算 IDT基址 + 14 × 8 得到门描述符位置。
- 检查门描述符的DPL和当前CPL,确保异常处理程序有权限被调用。
- 读取门描述符中的Segment选择子和Offset,组合成处理程序的线性地址。
- 切换栈(如果涉及特权级变化),并将SS、ESP、EFLAGS、CS、EIP以及错误码压入栈中。
- 跳转到处理程序入口开始执行。
4.3 缺页异常处理程序的注册
在Linux内核启动初期,通过trap_init()函数完成IDT表的初始化,并将缺页异常处理程序page_fault注册到向量号14对应的门描述符中。注册过程涉及设置门描述符的各个字段,包括处理程序地址、代码段选择子、中断门类型、DPL等。
set_intr_gate()或set_trap_gate()宏来设置IDT表项。对于缺页异常,使用set_intr_gate(14, page_fault)进行注册。
// arch/x86/kernel/traps.c 中的注册代码片段
void __init trap_init(void)
{
/* ... 其他异常初始化 ... */
// 注册缺页异常处理程序
set_intr_gate(14, page_fault);
/* ... 其他初始化 ... */
}
// set_intr_gate 宏定义(简化)
#define set_intr_gate(n, addr) \
do { \
struct idt_data data; \
data.vector = n; \
data.segment = __KERNEL_CS; \
data.bits.type = GATE_INTERRUPT; \
data.bits.p = 1; \
data.bits.dpl = 0; \
data.addr = addr; \
idt_setup_from_table(idt_table, &data, 1); \
} while (0)
4.4 缺页异常处理程序的入口汇编
注册到IDT的page_fault入口通常是用汇编语言编写的,负责保存CPU上下文、提取错误码、调用C语言处理函数do_page_fault(),并在返回时恢复上下文。x86架构下,缺页异常会向栈中压入一个32位错误码,该错误码包含页表项访问违规的详细信息。
// arch/x86/entry/entry_64.S 中的缺页异常入口(简化)
ENTRY(page_fault)
// 保存所有通用寄存器
SAVE_ALL
// 从栈中取出错误码
movq ORIG_RAX(%rsp), %rdi // 错误码作为第一个参数
movq RIP(%rsp), %rsi // 触发异常的指令地址作为第二个参数
// 调用C语言处理函数
call do_page_fault
// 恢复寄存器并返回
RESTORE_ALL
iretq
END(page_fault)
4.5 缺页异常错误码解析
错误码(Error Code)是缺页异常处理的关键输入信息。CPU在触发异常时自动将错误码压入栈中,处理程序通过分析错误码判断缺页原因。
| 位 | 标志 | 含义 | 值为0时 | 值为1时 |
|---|---|---|---|---|
| 0 | P (Present) | 页级保护违规 | 页表项不存在(非驻留) | 页表项存在但权限不足 |
| 1 | W (Write) | 访问类型 | 读访问 | 写访问 |
| 2 | U (User) | 特权级 | 内核态访问 | 用户态访问 |
| 3 | RSVD | 保留位覆盖 | 未覆盖保留位 | 覆盖了页表项中的保留位 |
| 4 | I (Instruction) | 指令获取 | 数据访问 | 指令获取(执行) |
4.6 缺页异常处理流程总览
从硬件触发到内核处理,缺页异常的完整入口流程可以概括为以下步骤:
- 硬件检测:MMU在地址转换时发现页表项无效(P位=0)或权限不匹配,触发#PF异常。
- CPU自动处理:保存当前上下文(SS、ESP、EFLAGS、CS、EIP),并将错误码压入栈中。
- IDT查找:CPU根据向量号14从IDT中读取门描述符,获取处理程序入口地址。
- 特权级检查:如果当前CPL高于门描述符的DPL,则触发#GP异常;否则继续。
- 跳转到page_fault:CPU跳转到
page_fault汇编入口,开始执行内核处理逻辑。 - 保存寄存器:汇编入口保存所有通用寄存器,准备调用C函数。
- 调用do_page_fault:将错误码和触发地址作为参数,调用C语言核心处理函数。
4.7 总结
缺页异常处理入口是x86架构下异常处理机制与Linux内核内存管理子系统之间的桥梁。通过固定的中断向量号14、IDT表的结构化映射以及汇编/C语言处理程序的注册,内核能够高效地捕获并处理缺页事件。理解这一入口流程,是后续分析do_page_fault()核心处理逻辑和缺页异常细分类型的基础。