x86启动中APIC与IOAPIC中断路由配置 - 第1节

1.1 中断向量:CPU 眼中的中断编号

在 x86 系统中,每个中断源都被分配一个唯一的 中断向量号(Vector Number),范围是 0~255。CPU 只通过这个 8 位编号来识别中断来源,而不关心中断是由硬件设备、外部控制器还是软件指令产生的。

向量范围 类型 典型用途
0~31 异常与 NMI 除零错(#DE)、页故障(#PF)、不可屏蔽中断等
32~255 可屏蔽中断(INTR) 外设中断、IPI、软件中断(INT n)
0x80 系统调用(Linux) 用户态进入内核的软中断入口
关键概念:向量号 0~31 被 Intel 保留给 CPU 内部异常和 NMI,固件和操作系统不能将这些向量分配给 IOAPIC 或外部设备。启动阶段必须确保 IDT 中这些入口有效,否则任何异常都会导致三重故障(Triple Fault)复位。

1.2 中断描述符表(IDT):中断处理的路由表

IDT 是 x86 保护模式与长模式下的一张 256 项 的描述符数组,每个描述符对应一个中断向量。CPU 收到中断向量后,直接以向量号为索引查找 IDT,获取处理程序的入口地址和属性。

IDT 描述符结构(保护模式 8 字节)

位域 宽度 说明
Offset[15:0] 16 bit 处理程序地址低 16 位
Selector 16 bit 代码段选择子(必须为可执行段)
IST(长模式) 3 bit 中断栈表索引,0 表示使用当前栈
Type 4 bit 描述符类型:中断门(0xE)、陷阱门(0xF)
DPL 2 bit 描述符特权级,软件中断需 CPL ≤ DPL
Present 1 bit 1 表示有效,0 触发 #GP
Offset[31:16] 16 bit 处理程序地址高 16 位
启动阶段要点:BIOS/UEFI 在进入保护模式前会设置临时的 IDT(通常只覆盖前 32 个异常向量)。Bootloader 或内核必须在切换至长模式或启用 APIC 前重新加载完整的 IDT,否则外部中断(向量 32+)会因描述符缺失导致 CPU 抛出 #GP 异常。

1.3 中断响应流程:从向量到处理函数

当 CPU 收到一个中断向量 N 时,硬件自动执行以下步骤:

  1. 保存现场:将 EFLAGS、CS、EIP(或 RIP)压栈;如果涉及特权级切换,同时保存 SS 和 ESP。
  2. 清除 IF:自动清除 EFLAGS 中的 IF 位,屏蔽后续可屏蔽中断(中断门行为)。
  3. 查 IDT:以向量号 N 为索引,从 IDTR 寄存器指向的 IDT 基址读取描述符。
  4. 权限检查:验证当前 CPL 是否 ≤ 描述符 DPL(软件中断)或描述符 DPL 是否 ≤ CPL(硬件中断)。
  5. 跳转处理:加载代码段选择子到 CS,偏移地址到 EIP/RIP,开始执行中断服务例程(ISR)。
常见陷阱:IDT 基址寄存器(IDTR)在系统复位后默认值为 0,此时任何中断都会导致 CPU 从地址 0 取指,通常引发 Triple Fault。固件必须在 第一条指令之后、开中断之前 完成 IDT 的初始化。

1.4 中断向量与 IDT 关系图

下图展示了中断源、向量号、IDT 表项与 ISR 之间的映射关系:

中断向量与 IDT 映射关系 中断源 定时器 键盘 磁盘 IPI 向量号 32 33 47 0xEF IDT 表项 Desc[32] → timer_isr Desc[33] → kbd_isr Desc[47] → disk_isr Desc[0xEF] → ipi_isr IDT 索引规则 • 向量号 = IDT 数组下标 • 每个描述符 8 字节(保护模式) • 或 16 字节(长模式) • IDTR 基址 + 向量号 × 8/16 • 硬件自动完成查表与跳转 ⚠ 未初始化 → Triple Fault

1.5 中断门 vs 陷阱门:IF 位行为差异

IDT 描述符的类型字段决定了中断处理期间是否自动清除 IF 位:

类型 Type 值 自动清除 IF 典型场景
中断门(Interrupt Gate) 0xE 是 硬件中断处理,防止嵌套
陷阱门(Trap Gate) 0xF 否 异常调试、系统调用(允许嵌套)
固件设计建议:在 APIC 初始化阶段,所有外部中断(向量 32+)应使用中断门,避免 ISR 执行期间被同级或更高优先级中断打断导致栈溢出。异常处理建议使用陷阱门,以便在调试时保留中断使能状态。

1.6 IDT 在启动阶段的典型初始化代码

以下代码展示在保护模式下使用 lidt 指令加载 IDT 的基本流程:

; 汇编示例:设置 IDT 并加载 IDTR
section .data
align 8
idt_start:
    ; 为每个向量预留 8 字节描述符空间
    times 256 dq 0          ; 256 个描述符,初始为 0

idt_descriptor:
    dw (idt_end - idt_start - 1)   ; IDT 限长
    dd idt_start                   ; IDT 基址(32 位)
idt_end:

section .text
global setup_idt
setup_idt:
    ; 1. 填充 IDT 描述符(以向量 32 为例)
    mov eax, timer_isr            ; 处理函数地址
    mov ebx, 0x0008               ; 代码段选择子(GDT 索引 1)
    mov ecx, 0x8E00               ; Present=1, DPL=0, Type=0xE(中断门)
    
    lea edi, [idt_start + 32*8]   ; 定位向量 32 的描述符
    mov [edi], ax                  ; Offset[15:0]
    shr eax, 16
    mov [edi+6], ax                ; Offset[31:16]
    mov [edi+2], bx                ; Selector
    mov [edi+4], cx                ; 属性 + 保留位
    
    ; 2. 加载 IDTR
    lidt [idt_descriptor]
    
    ; 3. 开中断
    sti
    ret
注意:上述代码仅为演示结构。实际固件中需要为每个使用的向量重复填充过程,并确保所有异常向量(0~31)至少指向一个错误处理函数,避免未处理异常导致系统崩溃。

1.7 本章知识结构总结

概念 核心要点 启动阶段注意事项
中断向量 0~255 编号,0~31 为异常保留 不要将外设中断分配到保留向量
IDT 256 项描述符表,CPU 硬件索引 必须在开中断前加载有效 IDTR
中断门 vs 陷阱门 中断门自动关 IF,陷阱门不关 硬件中断用中断门,异常用陷阱门
IDTR 加载 使用 lidt 指令,指定基址和限长 限长 = 256×8-1(保护模式)