3. 中断与异常基础:x86架构的中断描述符表(IDT)、中断门与陷阱门、异常处理流程
中断与异常是操作系统响应硬件事件与软件错误的根本机制。在 x86 架构中,CPU 通过中断描述符表(IDT)将中断向量号映射到对应的处理程序入口。理解 IDT 的结构、门描述符的类型以及异常处理流程,是分析系统调用全链路的前提。
3.1 中断描述符表(IDT)
IDT 是一个由 256 个 8 字节描述符组成的数组,每个描述符对应一个中断向量号(0~255)。CPU 在响应中断或异常时,根据向量号从 IDT 中取出对应的门描述符,解析出处理程序的段选择子与偏移量,进而跳转执行。
关键寄存器:IDTR(Interrupt Descriptor Table Register)存储 IDT 的基地址与限长。使用
lidt 指令加载。
向量号分配:0~31 为 Intel 保留的异常与 NMI;32~255 为用户定义的中断(包括系统调用使用的
int 0x80 或 syscall 指令)。
3.2 中断门与陷阱门
IDT 中的描述符类型决定了 CPU 在跳转时的行为差异。最常用的是中断门(Interrupt Gate)和陷阱门(Trap Gate)。两者都包含目标代码段选择子、偏移量、DPL 和存在位,但关键区别在于对 IF(Interrupt Flag) 的处理。
| 特性 | 中断门(Interrupt Gate) | 陷阱门(Trap Gate) |
|---|---|---|
| IF 标志 | 进入时自动清除 IF(禁止嵌套中断) | 不修改 IF(允许嵌套中断) |
| 典型用途 | 硬件中断处理(如时钟、键盘) | 异常处理、系统调用(int 0x80) |
| 返回指令 | iret / iretq |
iret / iretq |
| DPL 检查 | CPL ≤ DPL 时允许触发 | CPL ≤ DPL 时允许触发 |
注意:Linux 内核在 x86 上使用陷阱门实现系统调用(
int 0x80),因为系统调用处理期间需要保持中断使能,避免丢失硬件中断。对于时钟中断等硬件中断,则使用中断门自动关闭中断,防止重入。
3.3 异常处理流程
当 CPU 执行指令时遇到错误(如除零、缺页、无效操作码),会触发异常。异常分为三类:故障(Fault)、陷阱(Trap)、中止(Abort)。Linux 内核为每个异常注册了专用的处理函数。
3.3.1 异常分类与典型示例
| 类别 | 行为 | 典型向量 | Linux 处理函数 |
|---|---|---|---|
| 故障(Fault) | 返回指令重新执行 | #PF (14) 缺页异常 | do_page_fault() |
| 陷阱(Trap) | 返回指令下一条 | #BP (3) 断点异常 | do_int3() |
| 中止(Abort) | 严重错误,不恢复 | #MC (18) 机器检查 | do_machine_check() |
3.3.2 异常处理硬件流程
- CPU 自动压栈:将 SS、RSP、RFLAGS、CS、RIP 压入内核栈;若涉及权限切换(CPL 变化),还会压入 SS 和 RSP。
- 错误码压栈:部分异常(如缺页 #PF)会压入错误码,用于指示异常原因。
- 查 IDT:CPU 根据向量号从 IDT 中取出门描述符,获取处理程序入口地址。
- 跳转执行:CPU 跳转到处理程序,执行内核注册的异常处理函数。
- 返回:处理完成后执行
iret或iretq,恢复现场并返回用户态。
关键点:异常处理程序运行在中断上下文中,不能睡眠、不能访问用户空间数据(除非明确处理)。缺页异常是唯一可以“修复”并重新执行指令的故障类型。
3.4 中断与异常在系统调用中的作用
在 x86 传统实现中,系统调用通过 int 0x80 指令触发陷阱门,CPU 自动完成以下操作:
- 从用户态(Ring 3)切换到内核态(Ring 0)。
- 压栈用户态寄存器(SS、RSP、RFLAGS、CS、RIP)。
- 根据向量号 0x80 查找 IDT,跳转到
entry_INT80_32或entry_SYSCALL_64(x86-64 使用syscall指令,但原理类似)。
现代 x86-64 优化:Linux 内核在 x86-64 上优先使用
syscall/sysret 指令,而非 int 0x80。但 IDT 仍然保留 0x80 条目用于兼容 32 位程序。下一章将详细对比两种系统调用入口。
理解 IDT、中断门与陷阱门、异常处理流程,是后续分析系统调用从 int 0x80 到 syscall 演进的基础。这些机制共同构成了 CPU 与内核之间的契约接口,确保控制权安全、高效地转移。