三亩地 三亩地SAN MU DI · CODE DIARY
ARTICLE DETAIL

日记详情

真实记录编程学习的某一天,欢迎挑你感兴趣的翻一翻。

IRQ 中断不用 add rsp, 8 跳过错误码,而 Page Fault需要

IRQ 中断不用 add rsp, 8 跳过错误码,而 Page Fault需要

问题的本质,在于中断/异常是否由CPU自动向栈中压入错误码,以及这个错误码对后续处理是否必须。


核心原因:CPU是否自动压栈错误码

IRQ(外部硬件中断)和 Page Fault(缺页异常)在CPU处理流程上有一个关键区别:

  • IRQ(中断)不会由CPU自动压入错误码,栈上只有返回地址(RIP+CS+RFLAGS),因此处理完IRQ后,直接执行iret返回即可,不需要跳过任何额外数据。

  • Page Fault(#PF,中断向量14)由CPU自动压入一个错误码(Error Code)到栈中,通常位于返回地址之后。因此,必须在返回前通过add rsp, 8等方式将栈指针推进,跳过这个错误码,否则iret会因栈布局错乱而出错。


详细对比

特性IRQ(外部中断)Page Fault(#PF)
触发源硬件设备(如定时器、键盘)内存访问异常(缺页、权限违规)
是否压入错误码❌ 否✅ 是(32位/64位均压入)
错误码内容不适用包含访问类型、页表层级、是否为写操作等
栈上布局RIPCSRFLAGSRIPCSRFLAGS错误码
返回前需要add rsp, 8❌ 不需要✅ 必须跳过错误码才能iret
是否必须处理通常由操作系统调度必须处理,否则触发三重故障

错误码(Error Code)的结构

#PF压入的错误码在 x86_64 下是一个 64 位的值(低 32 位有效),其位字段含义如下:

含义
0P(保护异常):0 = 页不存在,1 = 权限违规
1W(写操作):0 = 读,1 = 写
2U(用户模式):0 = 内核模式,1 = 用户模式
3RSVD(保留页表项):1 = 使用了保留位
4I/D(指令获取):1 = 指令获取
5+其他标志(某些 CPU 扩展)

内核在#PF处理函数中通过读取这个错误码,决定如何修复:分配新页、检查权限、或将异常转发给应用程序(如SIGSEGV)。

为什么 IRQ 不用跳,而 #PF 必须跳?

因为 IRQ 栈上根本不存在需要跳过的数据。如果错误地执行add rsp, 8,栈指针会指向错误的位置,iret时拿到的RIP是错的,CPU 会跳转到随机地址,几乎立刻触发#GP(通用保护异常),造成系统挂起或崩溃。

而对于#PF,如果不跳过错误码,iret会错误地尝试将错误码当作RIP来使用,导致栈展开错乱,系统崩溃。因此,跳过错误码是正确返回的前置条件


中断/异常处理的标准流程(x86_64)

  1. CPU 压栈(取决于异常类型):

    • 无错误码:仅压入RIPCSRFLAGS

    • 有错误码:再压入错误码(位于RFLAGS之后)

  2. 找到中断门 / 陷阱门(IDT 中的门描述符):

    • 如果是中断门RFLAGS.IF被清零,屏蔽后续中断

    • 如果是陷阱门IF不清零

  3. 切换到内核栈ISTTSS中的RSP0

  4. 执行内核处理函数(读取错误码,处理逻辑)

  5. 返回

    • 有错误码:add rsp, 8(跳过错误码)

    • 无错误码:直接iret

    • 恢复RFLAGS和用户态上下文


补充说明

  • IRETvsIRETQ:在 x86_64 中,始终使用iretq(64 位版本)返回。iret本身会根据当前模式自动选择操作数大小,但在长模式下建议显式使用iretq

  • 中断嵌套:某些 IRQ(如定时器中断)也可能在内核处理中触发#PF,此时栈上会叠加多层错误码,需要逐层处理。

  • MSR 与 RFLAGSSYSCALL会在进入内核时通过IA32_FMASK自动屏蔽RFLAGS中的IF,但硬件中断不涉及 MSR,而是通过RFLAGS.IF的位控制。


总结:

IRQ 不需要add rsp, 8,因为它根本没有被压入错误码;而#PF必须这样做,因为 CPU强制压入了错误码,跳过它是正确返回的必要前提。

← 返回列表