- 文档
- 教程
- 操作系统
【免费下载链接】linux-insides
A book-in-progress about the Linux kernel and its insides.
中断描述符表(Interrupt Descriptor Table,IDT)是 x86_64 架构中连接中断/异常向量号与处理器入口的关键数据结构,也是 Linux 内核中断子系统一切工作的起点。本文以 linux-insides 仓库 X86/linux-x86-2.md 为骨架,结合 Interrupts 章节与 Initialization 章节的源码级讲解,系统梳理中断与异常的三大来源、异常分类、向量号分配、两类错误码格式、三种门描述符的 128 位布局,以及 Linux 内核中对应的 C 结构体与 Interrupt Stack Table(IST)机制。读完后你将能够独立读懂arch/x86/include/asm/traps.h与arch/x86/include/asm/desc_defs.h中的核心定义,理解中断从触发到处理器切换到处理程序入口的完整硬件流程。
中断与异常的三大来源
在 x86 体系下,所有打断处理器正常执行流的事件可以归结为三个一般性来源:
- 异常(Exceptions):同步(synchronous)事件,由处理器在执行指令时自行产生,例如除零、缺页。它们与当前指令的执行严格同步;
- 软件中断(Software interrupts):同步事件,由软件通过
int n等指令主动触发,历史上常用于系统调用入口; - 外部中断(External interrupts):异步(asynchronous)事件,由外部硬件设备(如网卡、键盘控制器)通过 Local APIC 或 I/O APIC 送达处理器,与当前执行的指令流没有时序关联。
从同步性角度理解:异常和软件中断都发生在指令流中的确定位置,处理器能够精确定位"是哪条指令";而外部中断可以在任意时刻到来,处理器无法把它归因于某一条正在执行的指令。这一区分决定了后续异常分类与处理方式的不同。
异常的分类:Fault、Trap 与 Abort
异常根据其可恢复性与报告时机被精确地划分为三类:
| 类型 | 报告时机 | 保存的%rip | 可恢复性 |
|---|---|---|---|
| Fault(故障) | 在引发异常的指令之前的边界上报 | 指向引发故障的那条指令 | 处理程序修正条件后,指令可被重新执行 |
| Trap(陷阱) | 在引发异常的指令之后的边界上报 | 指向引发陷阱指令的下一条指令 | 被中断的程序可以继续执行 |
| Abort(中止) | 无精确边界,不可精确定位 | 不确定 | 通常不允许可靠的程序重启 |
Fault 的典型例子是页故障(Page Fault):处理器在访问缺失页表项时报告异常,内核补上映射后重新执行原指令即可继续。Trap 的典型例子是int3断点与syscall返回路径。Abort 则是严重硬件错误(如机器检查#MC、双重故障#DF),此时系统状态已不可靠,通常无法安全恢复。
可屏蔽中断与非屏蔽中断
除了同步/异步的划分,中断还可按是否受IF标志位约束分为两类:
- 可屏蔽中断(Maskable interrupts):只有
RFLAGS.IF = 1时才会触发中断处理机制;若IF被清零,这些中断会一直被挂起(pending),直到IF重新置位。在 Linux 内核中,cli与sti两条指令分别对应清除与设置IF位,例如arch/x86/include/asm/irqflags.h中的native_irq_disable()/native_irq_enable()即封装了这两条指令; - 非屏蔽中断(Nonmaskable interrupts,NMI):不受
RFLAGS.IF影响,总是被处理器接受。但需要注意:一旦发生 NMI,处理器会屏蔽后续的 NMI,直到执行iret(在 64 位模式为iretq)指令后才解除屏蔽。
NMI 的特殊性在于它通常映射硬件级严重故障(如 ECC 内存错误、看门狗超时),因此它无法被软件推迟,Linux 内核为其准备了专用的NMI_STACKIST 栈与独立的嵌套处理逻辑,详见下文"非屏蔽中断(NMI)"一节。
中断向量号与 IDT
每个具体的异常或中断源都被分配一个固定的向量标识号(vector number,也常直接称为"中断向量")。中断处理机制正是通过这个向量号在 IDT 中定位对应的系统软件服务例程(即中断/异常处理程序):
- 处理器最多提供256 个唯一的中断向量(编号 0~255);
- 前 32 个向量(0~31)被保留给处理器预定义的异常与中断条件;
- 向量 32~255 未保留,操作系统可自由用于外部硬件中断、处理器间中断(IPI)等用途。
这 32 个保留向量的名称与编号在 Linux 内核的arch/x86/include/asm/traps.h中以下述枚举定义(原文代码,仓库未收录该内核源文件,此处作为规范说明):
/* Interrupts/Exceptions */ enum { X86_TRAP_DE = 0, /* 0, Divide-by-zero */ X86_TRAP_DB, /* 1, Debug */ X86_TRAP_NMI, /* 2, Non-maskable Interrupt */ X86_TRAP_BP, /* 3, Breakpoint */ X86_TRAP_OF, /* 4, Overflow */ X86_TRAP_BR, /* 5, Bound Range Exceeded */ X86_TRAP_UD, /* 6, Invalid Opcode */ X86_TRAP_NM, /* 7, Device Not Available */ X86_TRAP_DF, /* 8, Double Fault */ X86_TRAP_OLD_MF, /* 9, Coprocessor Segment Overrun */ X86_TRAP_TS, /* 10, Invalid TSS */ X86_TRAP_NP, /* 11, Segment Not Present */ X86_TRAP_SS, /* 12, Stack Segment Fault */ X86_TRAP_GP, /* 13, General Protection Fault */ X86_TRAP_PF, /* 14, Page Fault */ X86_TRAP_SPURIOUS, /* 15, Spurious Interrupt */ X86_TRAP_MF, /* 16, x87 Floating-Point Exception */ X86_TRAP_AC, /* 17, Alignment Check */ X86_TRAP_MC, /* 18, Machine Check */ X86_TRAP_XF, /* 19, SIMD Floating-Point Exception */ X86_TRAP_IRET = 32, /* 32, IRET Exception */ };内核中以X86_TRAP_*命名的这些宏贯穿始终:例如早期初始化用X86_TRAP_PF判断页故障,用X86_TRAP_NMI、X86_TRAP_DF为 NMI 和双重故障配置 IST 栈。向量号范围还有硬性校验,例如set_intr_gate等函数中的BUG_ON((unsigned)n > 0xFF),确保不会越界写入 IDT(详见 Initialization/linux-initialization-2.md)。
错误码(Error Codes):两类硬件格式
处理器在把控制权转移到异常处理程序时,会为部分异常通过异常机制压入一个错误码(error code),用于报告错误与状态信息。错误码具有两种格式:
- 大多数报错异常的通用格式(Selector 错误码);
- 页故障专用格式(Page-Fault 错误码)。
Selector 错误码格式
31 16 15 3 2 1 0 +-------------------------------------------------------------------------------+ | | | T | I | E | | Reserved | Selector Index | - | D | X | | | | I | T | T | +-------------------------------------------------------------------------------+各字段含义:
EXT(位 0):置 1 表示异常源在处理器外部;清 0 表示异常源在处理器内部;IDT(位 1):置 1 表示错误码的选择子索引字段引用的是IDT中的门描述符;清 0 表示该字段引用GDT或LDT中的描述符(具体由TI位决定);TI(位 2):置 1 表示选择子索引引用LDT中的描述符;清 0 表示引用GDT中的描述符;Selector Index(位 3~15):根据IDT与TI位的取值,指定在 GDT、LDT 或 IDT 中的索引。
这类错误码出现在#TS、#NP、#SS、#GP、#AC等与段选择子/权限相关的异常中。
Page-Fault 错误码格式
31 4 3 2 1 0 +-------------------------------------------------------------------------------+ | | | R | U | R | - | | Reserved | I/D | S | - | - | P | | | | V | S | W | - | +-------------------------------------------------------------------------------+各字段含义:
P(位 0,Present):清 0 表示页故障由"页不存在"引起;置 1 表示由"页保护违规"引起;R/W(位 1,Read/Write):清 0 表示引起故障的访问是内存读;置 1 表示是写;U/S(位 2,User/Supervisor):清 0 表示管理模式(CPL=0、1 或 2)下的访问引发故障;置 1 表示用户模式(CPL=3)下的访问引发故障;RSV(位 3,Reserved):置 1 表示处理器在页转换表项(page-translation-table entry)的保留字段中读到了 1,即页表项自身编码非法;I/D(位 4,Instruction/Data):置 1 表示引发页故障的访问是指令取指(instruction fetch)。
页故障发生时,除错误码外,处理器还会把故障线性地址写入cr2控制寄存器。Linux 早期缺页处理正是通过native_read_cr2()读取该值、再调用early_make_pgtable按需补建页表(详见 Initialization/linux-initialization-2.md)。
中断控制转移:三种门描述符
IDT 中每条表项(称为 gate,门)可以容纳三种门描述符之一:
- 任务门(Task Gate):包含一个 TSS 的段选择子,用于把异常/中断处理作为一个任务来切换执行;
- 中断门(Interrupt Gate):包含段选择子与偏移量,处理器用它把程序执行转移到中断处理程序所在代码段的处理过程;进入中断门时处理器会清除
IF标志,从而在处理期间屏蔽其它可屏蔽硬件中断; - 陷阱门(Trap Gate):同样包含段选择子与偏移量,用于把程序执行转移到异常处理程序所在代码段的处理过程;与中断门不同,陷阱门不修改
IF标志,处理期间仍可被可屏蔽中断打断。
注意:在 64 位长模式(long mode)下,IDT 中只能引用中断门与陷阱门(以及任务门在兼容性上的限制);x86 的硬件任务切换机制在长模式下被禁用,因此任务门在 x86_64 的 Linux 内核中已基本不再使用。
门描述符的 128 位通用格式
x86_64 下 IDT 表项是 16 字节(128 位)的门描述符,其通用格式如下:
127 96 +-------------------------------------------------------------------------------+ | | | Reserved | | | +-------------------------------------------------------------------------------- 95 64 +-------------------------------------------------------------------------------+ | | | Offset 63..32 | | | +-------------------------------------------------------------------------------+ 63 48 47 46 44 42 39 34 32 +-------------------------------------------------------------------------------+ | | | D | | | | | | | | Offset 31..16 | P | P | 0 |Type |0 0 0 | 0 | 0 | IST | | | | L | | | | | | | -------------------------------------------------------------------------------+ 31 16 15 0 +-------------------------------------------------------------------------------+ | | | | Segment Selector | Offset 15..0 | | | | +-------------------------------------------------------------------------------+各字段含义:
Selector(位 15:0 之上的段选择子字段):目标代码段的段选择子;Offset(Offset 15..0、Offset 31..16、Offset 63..32三段拼接):处理过程入口点的 64 位偏移量;DPL(Descriptor Privilege Level):描述符特权级,规定了通过软件指令(如int n)调用该门的特权下限;硬件中断与处理器异常会忽略该字段;P(Segment Present flag):段存在标志,必须置 1 才是有效描述符;引用P=0的门会触发#NP段不存在异常;IST(Interrupt Stack Table,位 32~34):中断栈表索引,详见下文专节;Type(位 40~43):门的类型,取值涵盖 LDT 段描述符、TSS 描述符、调用门(Call gate)、中断门(Interrupt gate)、陷阱门(Trap gate)与任务门(Task gate);- 其余保留位必须为 0。
仓库 Initialization/linux-initialization-2.md 中的示意图 IDT 门描述符 以图形方式展示了同样的位域布局,并指出:在 64 位模式中只有中断门与陷阱门可以合法出现在 IDT 中,二者唯一的区别是进入中断门时处理器会清除IF标志、进入陷阱门时保持IF不变。
Linux 内核中的 IDT 结构体表示
Linux 内核(仅x86_64)用如下打包结构体表示一个 IDT 门描述符,定义于arch/x86/include/asm/desc_defs.h(仓库未收录该内核源文件,此处引用自 Interrupts/linux-interrupts-1.md):
struct gate_struct64 { u16 offset_low; u16 segment; unsigned ist : 3, zero0 : 5, type : 5, dpl : 2, p : 1; u16 offset_middle; u32 offset_high; u32 zero1; } __attribute__((packed));位域布局与上文 128 位格式一一对应:offset_low/offset_middle/offset_high拼出 64 位处理程序偏移量,segment为目标代码段选择子,ist(3 位)、type(5 位)、dpl(2 位)、p(1 位)构成中间的控制字段。__attribute__((packed))保证该结构体在内存中严格按 16 字节紧凑排布,与硬件期望的 IDT 表项尺寸一致。
任务门描述符
任务门描述符不包含IST字段,其格式与中断门/陷阱门不同。Linux 内核中对应的结构体同样位于arch/x86/include/asm/desc_defs.h:
struct ldttss_desc64 { u16 limit0; u16 base0; unsigned base1 : 8, type : 5, dpl : 2, p : 1; unsigned limit1 : 4, zero0 : 3, g : 1, base2 : 8; u32 base3; u32 zero1; } __attribute__((packed));这个结构体同时被 LDT/TSS 描述符复用,其中的limit、base、g(粒度位)、type、dpl、p字段共同描述了目标段/任务的基址与界限。由于长模式禁用了硬件任务切换,该结构体在现代 x86_64 内核中主要用于维护 TSS 与 LDT 描述符本身(TSS 在 Linux 中承担了 IST 栈指针的载体职责,见下文)。
任务切换期间的异常
处理器在任务切换过程中加载段选择子时可能发生异常,访问 TSS 时也可能发生页故障。在这些情况下,硬件任务切换机制会先完成从 TSS 加载新任务状态,然后再触发相应的异常机制,从而保证新任务状态已就绪、异常处理程序可以安全接管。
但需要特别强调 x86_64 上的结论:在长模式(long mode)下,任务切换期间不可能发生此类异常,因为硬件任务切换机制本身已被禁用。这正是 Linux 内核在 x86_64 上可以完全绕开硬件任务切换、改用软件实现上下文切换(switch_to宏与__switch_to函数)的根本原因。
非屏蔽中断(NMI)
NMI 对应向量号 2。由于它不受IF标志约束、总是被处理器接受,Linux 内核必须为其设计专门的入口与嵌套保护。仓库 Interrupts/linux-interrupts-6.md 对 NMI 处理给出了完整的源码级讲解,要点如下:
- NMI 可以通过两种方式产生:外部硬件拉高 CPU 的 NMI 引脚;处理器在系统总线或 APIC 串行总线上收到投递模式为
NMI的消息; - 内核在
trap_init中通过set_intr_gate_ist(X86_TRAP_NMI, &nmi, NMI_STACK)把向量 2 关联到nmi入口并指定NMI_STACKIST 栈; - NMI 入口没有使用通用的
idtentry宏,而是在arch/x86/entry/entry_64.S中拥有独立的nmi汇编入口。其核心难题是嵌套 NMI:当 NMI 处理期间发生页故障或断点异常、执行iret返回时会重新使能 NMI,新的 NMI 可能覆盖正在执行的 NMI 的栈帧; - 为此内核在栈上维护了一个"NMI 正在执行"的临时变量:进入时压入 1,退出(
nmi_restore)时清零;同时复制一份原始栈帧,用于在嵌套发生时让第一个 NMI 感知并重做; - 处理器在进入
nmi入口时会自动压入SS/RSP/RFLAGS/CS/RIP五元组;汇编入口随后用paranoid_entry保存通用寄存器、读取MSR_GS_BASE判断来源模式并视需要执行swapgs;还会把cr2暂存到r12,因为 NMI 处理中可能发生页故障、破坏cr2的值; - 真正的处理逻辑在
do_nmi(arch/x86/kernel/nmi.c):经由nmi_nesting_preprocess/nmi_nesting_postprocess处理调试栈切换,nmi_enter/nmi_exit维护 lockdep 递归计数与 RCU 状态,最后通过nmi_handle分发本地 NMI、按get_nmi_reason()判断外部 NMI 的来源(SERR/IOCHK 等)。
中断描述符表的加载与早期初始化
IDT 的基址存放在IDTR寄存器中,由两条指令管理:
LIDT:把操作数(含基址与界限的描述符)加载进IDTR;SIDT:把IDTR的内容读出并存入操作数。
IDTR在 x86 上共 48 位:高 32 位是 IDT 基址(Base Address),低 16 位是 IDT 界限(Limit,即"表字节数减一")。仓库 Initialization/linux-initialization-2.md 中的示意图 IDT 描述符 展示了这个 48 位描述符的布局。内核中加载 IDT 的典型流程是:
- 在内核早期(进入长模式后、
x86_64_start_kernel中)调用idt_setup_early_handler():遍历NUM_EXCEPTION_VECTORS(值为 32)个向量,逐个调用set_intr_gate(i, early_idt_handler_array[i])初始化门描述符; set_intr_gate把向量号、处理程序地址封装进struct idt_data,再经idt_setup_from_table写入idt_table数组;- 最后调用
load_idt(&idt_descr)执行lidt指令,让处理器开始使用这张表。
idt_table是gate_desc的数组(extern gate_desc idt_table[]),其中gate_desc在x86_64下即前述 16 字节gate_struct64。早期阶段只装 32 个异常向量就足够了——此时硬件中断仍被禁用,不会到来;但页故障等异常随时可能发生(例如内核刚清空早期页表的 identity mapping 后,访问boot_params会触发缺页,do_early_exception会调用early_make_pgtable按需补页)。
Interrupt Stack Table(IST)
原文档中 IST 一节标注为TODO,但仓库其他章节(Interrupts/linux-interrupts-1.md、Initialization/linux-initialization-2.md)对该机制有完整描述,此处整合呈现:
- IST 是 x86_64 引入的、替代传统栈切换机制的设施。传统 x86 在响应中断时依据
CPL变化自动切换栈帧;IST 则是在启用后无条件切换到指定栈,且可以按中断逐个启用——并非所有中断都需要 IST,未启用 IST 的中断仍走传统切换路径; - 每个 CPU 最多提供 7 个 IST 指针,存放在该 CPU 的 TSS(Task State Segment)中。Linux 内核为其定义了若干具名 IST 索引:
#define DOUBLEFAULT_STACK 1 #define NMI_STACK 2 #define DEBUG_STACK 3 #define MCE_STACK 4即双重故障、NMI、调试、机器检查等"即使当前栈已损坏也必须能运行"的关键处理程序,各自独占一个专用栈;
- 启用方式:把 IDT 门描述符中的
IST字段(位 32~34)设为非 0 索引。例如set_intr_gate_ist(X86_TRAP_DB, &debug, DEBUG_STACK)为调试异常指定DEBUG_STACK,set_intr_gate_ist(X86_TRAP_NMI, &nmi, NMI_STACK)为 NMI 指定NMI_STACK,INTG(X86_TRAP_DF, double_fault)则让双重故障使用DOUBLEFAULT_STACK; - 执行流程:当
IST字段非 0 时,处理器把对应 IST 指针读入rsp;随后把旧ss、rsp、RFLAGS、cs、rip压入新栈,形成 40 字节的中断帧;若向量带错误码则压入错误码,否则压入哑错误码以保证栈布局一致;接着从门描述符加载段选择子到cs(并校验目标代码段的L位确为 64 位代码段),把偏移量加载到rip,从而进入处理程序。64 位模式下中断帧的每个单元固定为 8 字节:
+---------------+ | SS | 40 | RSP | 32 | RFLAGS | 24 | CS | 16 | RIP | 8 | Error code | 0 +---------------+- 与 per-CPU 栈的关系:除 IST 专用栈外,Linux 每个 CPU 还有自己的中断栈(
irq_stack,默认 16KB,IRQ_STACK_SIZE = PAGE_SIZE << IRQ_STACK_ORDER)与异常栈(exception_stacks,DOUBLEFAULT_STACK等即属于其中)。内核线程栈大小THREAD_SIZE同样由PAGE_SHIFT与THREAD_SIZE_ORDER推导(默认 16KB,启用CONFIG_KASAN时为 32KB)。IST 的意义在于:像双重故障这类异常发生时,当前内核栈可能已损坏到无法安全压栈,而 IST 提供了"无论如何都切到一块干净专用栈"的兜底能力。
中断/异常处理的完整流程小结
把上述内容串成一条完整的硬件处理链路:
- 中断或异常发生,处理器取向量号,乘以 16(每条 IDT 表项 16 字节)得到表内偏移,读出对应的门描述符;
- 校验门类型(中断门/陷阱门)与目标代码段的 64 位属性;
- 依据
IST字段选择栈:非 0 则读 TSS 中对应 IST 指针,为 0 则沿用当前栈; - 压入中断帧(
SS/RSP/RFLAGS/CS/RIP,必要时加错误码); - 从门描述符加载
cs与rip,跳转到处理程序入口(如early_idt_handler_array生成的桩代码,或正式内核中的idtentry宏展开体); - 处理程序完成工作后执行
iretq,无条件弹出被中断上下文的ss:rsp恢复原栈并恢复RFLAGS,继续执行被中断的代码(iretq不依赖CPL是否变化)。
总结与延伸阅读
本文以 X86/linux-x86-2.md 为主线,完整覆盖了:中断/异常的三大来源与同步性区分、Fault/Trap/Abort 三类异常、可屏蔽与非可屏蔽中断、0~255 向量空间与前 32 个保留向量、两类错误码格式(Selector 格式与 Page-Fault 格式)、三种门描述符及其 128 位布局、Linux 内核中的gate_struct64与ldttss_desc64结构体、长模式下任务切换异常的不可能性,并结合仓库其他章节补全了 NMI 嵌套处理与 IST 栈机制这两个原文档标注为 TODO 的主题。
想进一步深入,建议按以下顺序阅读仓库内相关章节:
- Interrupts/linux-interrupts-1.md:中断理论、
gate_desc/idt_table、IST 与 per-CPU 栈的完整背景; - Interrupts/linux-interrupts-2.md:早期
setup_idt、set_intr_gate系列函数与pack_gate/write_idt_entry的实现细节; - Interrupts/linux-interrupts-6.md:NMI 处理程序的汇编入口、嵌套 NMI 保护与
do_nmi分发逻辑; - Initialization/linux-initialization-2.md:0~31 号向量的完整表格、
early_idt_handler_array生成机制、do_early_exception与异常表(exception table)回退路径,以及文中引用的两张 IDT 示意图; - X86/linux-x86-1.md:同一 x86 基础章节中的分页(Paging)理论,理解页故障处理的前提。
说明:本文涉及的内核源码文件(如
arch/x86/include/asm/traps.h、arch/x86/include/asm/desc_defs.h、arch/x86/kernel/traps.c、arch/x86/entry/entry_64.S等)属于 Linux 内核主源码树,未收录于本仓库;仓库 X86/README.md 注明本章节对应 Linux 内核 v7.2.0,并给出了获取内核源码的git clone方式,如需核对上述定义可参考对应版本的内核源码。
- 文档
- 教程
- 操作系统
【免费下载链接】linux-insides
A book-in-progress about the Linux kernel and its insides.
相关推荐
Linux 内核揭秘:中断描述符表(IDT)——门描述符、错误代码与 x86_64 中断处理机制全解
Linux 内核揭秘:中断描述符表(IDT)——门描述符、错误代码与 x86_64 中断处理机制全解 中断描述符表(Interrupt Descriptor T
Linux 内核揭秘:中断描述符表(IDT)与内核内部系统数据结构深度解析
Linux 内核揭秘:中断描述符表(IDT)与内核内部系统数据结构深度解析 导读:本文是《Linux 内核揭秘》(linux insides zh)中"内核数据
Linux 内核初始化(linux-insides 第 2 部分):早期异常处理与中断描述符表(IDT)的构建
Linux 内核初始化(linux insides 第 2 部分):早期异常处理与中断描述符表(IDT)的构建 本文是 linux insides https:
文档教程操作系统
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考