在 x86_64 架构下#PFPage Fault处理函数的汇编层面处理错误码主要分两个核心部分保存现场时将它作为参数传递以及恢复现场时在iretq之前正确地跳过它。此外错误码本身包含了触发异常的关键信息。第一部分进入处理函数——保存错误码并传递当 CPU 触发#PF时它会自动完成以下操作压栈将错误码压入当前栈中。此时栈顶布局为[rsp] → 错误码 (Error Code) [rsp8] → RIP [rsp16] → CS [rsp24] → RFLAGS [rsp32] → RSP (如果发生了特权级切换) [rsp40] → SS (如果发生了特权级切换)跳转通过 IDT中断描述符表中的#PF门描述符跳转到内核注册的处理函数入口。在汇编入口处需要保存通用寄存器并将错误码作为参数传递给后续的 C 语言处理函数。; 汇编入口page_fault_handler_entry page_fault_handler_entry: ; 1. 保存所有通用寄存器以便在 C 函数中恢复 push rax push rbx push rcx push rdx push rsi push rdi push rbp push r8 push r9 push r10 push r11 push r12 push r13 push r14 push r15 ; 2. 保存当前栈指针到 rbp方便后续定位 mov rbp, rsp ; 3. 将错误码作为第一个参数传递给 C 函数 ; 注意错误码此时位于 [rbp 15*8] 处 ; 因为我们已经压入了 15 个 8 字节的通用寄存器 mov rdi, [rbp 15*8] ; rdi Error Code ; 4. 将出错的虚拟地址CR2作为第二个参数 mov rsi, cr2 ; rsi Faulting Address push rsi ; 保存 CR2 到栈上可选 ; 5. 调用 C 语言编写的核心处理函数 call do_page_fault ; void do_page_fault(unsigned long error_code, unsigned long address) ; 6. C 函数返回后恢复之前保存的寄存器 ; 注意此时 rsp 需要恢复到保存通用寄存器之后的位置 mov rsp, rbp pop r15 ; ... (按相反顺序弹出所有寄存器) pop rax ; 7. 跳过错误码并返回 add rsp, 8 iretq第二部分do_page_faultC 函数对错误码的解析C 函数do_page_fault会收到错误码并解析其位字段以决定处理方式错误码 Bit含义处理逻辑Bit 0 (P)0 页不存在1 权限违规若为 0分配物理页并建立映射若为 1检查权限。Bit 1 (W)0 读操作1 写操作确定访问类型用于 COW写时复制或只读页错误。Bit 2 (U)0 内核态1 用户态决定是内核错误panic还是用户态错误SIGSEGV。Bit 3 (RSVD)1 使用了保留位通常意味着页表损坏直接触发panic。Bit 4 (I/D)1 指令获取若为 1表示 CPU 尝试从此地址执行指令需特殊处理。第三部分返回前正确处理错误码在汇编中关键在于add rsp, 8指令的位置。它必须紧跟在iretq之前执行为什么是add rsp, 8因为错误码是 CPU 在进入处理程序时压入的它不属于iretq的正常返回栈帧。iretq期望栈顶是RIP如果不跳过iretq会尝试将错误码当作RIP使用导致系统崩溃。错误码的常见陷阱栈偏移计算错误在多级栈帧中如果你在通用寄存器压栈和call指令之间操作了栈那么错误码在栈上的偏移量会发生改变。忘记处理CR2在 64 位模式下出错的地址存放在CR2寄存器中。在调用do_page_fault之前必须将CR2的值也作为参数传递。