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 无 无效操作码:非法指令
关键点:缺页异常属于“Fault”类型,意味着异常处理完成后,CPU会自动重新执行触发异常的指令(而非下一条指令)。这是缺页异常能够实现“按需调页”和“写时复制”等机制的基础。

4.2 IDT表结构与缺页异常门描述符

中断描述符表(Interrupt Descriptor Table, IDT)是x86架构下用于将中断/异常向量号映射到处理程序入口地址的核心数据结构。IDT中的每个表项称为一个“门描述符”(Gate Descriptor),大小为8字节。对于缺页异常,CPU使用中断门(Interrupt Gate)或陷阱门(Trap Gate)来存储处理程序信息。

IDT表结构与缺页异常门描述符 IDT表基址(IDTR.base) IDT[0] IDT[1] ... IDT[12] IDT[14] 缺页异常门描述符 IDT[15] ... IDT[255] 缺页异常门描述符(8字节)展开 Offset[15:0] Segment IST Type DPL P Offset[31:16] Offset = 处理程序入口地址(32位) | Segment = 代码段选择子 | IST = 中断栈表索引 Type = 0xE(中断门) | DPL = 描述符特权级 | P = 段存在位

IDT表通过LIDT指令加载到CPU的IDTR寄存器中。IDTR包含IDT表的基地址和限长(Limit)。当CPU收到向量号14的异常时,硬件自动执行以下步骤:

  1. 从IDTR中获取IDT基地址,计算 IDT基址 + 14 × 8 得到门描述符位置。
  2. 检查门描述符的DPL和当前CPL,确保异常处理程序有权限被调用。
  3. 读取门描述符中的Segment选择子和Offset,组合成处理程序的线性地址。
  4. 切换栈(如果涉及特权级变化),并将SS、ESP、EFLAGS、CS、EIP以及错误码压入栈中。
  5. 跳转到处理程序入口开始执行。

4.3 缺页异常处理程序的注册

在Linux内核启动初期,通过trap_init()函数完成IDT表的初始化,并将缺页异常处理程序page_fault注册到向量号14对应的门描述符中。注册过程涉及设置门描述符的各个字段,包括处理程序地址、代码段选择子、中断门类型、DPL等。

核心注册函数:Linux内核使用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)
缺页异常处理程序注册流程 trap_init() 内核启动初始化 set_intr_gate(14, page_fault) 设置IDT[14]门描述符 idt_setup_from_table() 写入IDT表 IDT[14] 门描述符最终内容 Offset = page_fault 函数地址 | Segment = __KERNEL_CS (0x10) Type = 0xE (中断门) | DPL = 0 (内核态) | P = 1 (存在) IST = 0 (使用普通栈) | 保留位 = 0 page_fault 汇编入口 当CPU收到向量号14时,自动通过IDT[14]跳转到page_fault入口

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)
注意:缺页异常的错误码格式与普通中断不同。错误码的低3位分别表示:位0(P)——页级保护违规;位1(W)——写访问违规;位2(U)——用户态访问违规。内核通过解析错误码来区分缺页类型,从而采取不同的处理策略。

4.5 缺页异常错误码解析

错误码(Error Code)是缺页异常处理的关键输入信息。CPU在触发异常时自动将错误码压入栈中,处理程序通过分析错误码判断缺页原因。

位 标志 含义 值为0时 值为1时
0 P (Present) 页级保护违规 页表项不存在(非驻留) 页表项存在但权限不足
1 W (Write) 访问类型 读访问 写访问
2 U (User) 特权级 内核态访问 用户态访问
3 RSVD 保留位覆盖 未覆盖保留位 覆盖了页表项中的保留位
4 I (Instruction) 指令获取 数据访问 指令获取(执行)
典型场景:错误码 = 0x06(二进制 110)表示:用户态写访问,且页表项不存在。这是最常见的缺页场景,对应“按需调页”或“写时复制”机制。

4.6 缺页异常处理流程总览

从硬件触发到内核处理,缺页异常的完整入口流程可以概括为以下步骤:

  1. 硬件检测:MMU在地址转换时发现页表项无效(P位=0)或权限不匹配,触发#PF异常。
  2. CPU自动处理:保存当前上下文(SS、ESP、EFLAGS、CS、EIP),并将错误码压入栈中。
  3. IDT查找:CPU根据向量号14从IDT中读取门描述符,获取处理程序入口地址。
  4. 特权级检查:如果当前CPL高于门描述符的DPL,则触发#GP异常;否则继续。
  5. 跳转到page_fault:CPU跳转到page_fault汇编入口,开始执行内核处理逻辑。
  6. 保存寄存器:汇编入口保存所有通用寄存器,准备调用C函数。
  7. 调用do_page_fault:将错误码和触发地址作为参数,调用C语言核心处理函数。
缺页异常入口处理流程 MMU检测页表无效 触发#PF异常 CPU保存上下文 压入错误码 IDT查找向量14 获取门描述符 跳转 page_fault SAVE_ALL 保存寄存器 准备C函数调用 call do_page_fault C语言核心处理 RESTORE_ALL 恢复上下文 iretq 返回用户态 重新执行触发指令 do_page_fault 返回后,CPU重新执行触发异常的指令 如果缺页已解决,指令正常执行;否则可能触发段错误或内核panic

4.7 总结

缺页异常处理入口是x86架构下异常处理机制与Linux内核内存管理子系统之间的桥梁。通过固定的中断向量号14、IDT表的结构化映射以及汇编/C语言处理程序的注册,内核能够高效地捕获并处理缺页事件。理解这一入口流程,是后续分析do_page_fault()核心处理逻辑和缺页异常细分类型的基础。