theme: juejin
一、中断和异常概述
中断和异常是指系统中的某些地方出现的需要处理器注意的事件。当中断或异常发生时,典型的结果是迫使处理器将控制从当前正在执行的程序或任务转移到一个特殊的软件例程或任务中去。该例程被称作中断处理程序,或者异常处理程序。处理器响应中断或异常时所采用的行动被称作中断或异常处理。
中断包括硬件(产生的)中断和软件(产生的)中断。
硬件中断是指在处理器外部发生的事件,这些事件是由处理器外部的硬件(比如外围硬件设备)发出的中断信号引起的,以请求处理器提供服务。硬件中断完全是随机产生的,与处理器的执行并不同步。
软件中断是由 INT n 指令引发的,其中 n 是中断号。
异常是指处理器的内部中断,表示在执行指令时遇到了错误的情况,比如除数为零的错误。这类错误还包括违反保护条件、页故障和机器内部故障等。以上所列出的错误都是处理器内部产生的,它们被称为异常或异常中断。
当处理器接收到中断信号或者检测到异常时,当前正在运行的程序或任务被挂起,处理转而去执行中断或异常处理程序。当中断或异常处理程序执行完成后,处理器恢复执行被中断的程序或任务。当被中断的程序或任务恢复执行时,会从被中断的位置或该位置的下一条指令继续执行,而不会有任何连续性损失。
二、异常和中断向量
为了帮助处理异常和中断,每一个架构定义的异常及需要处理的中断条件都被分配了一个唯一的标识号,被称为向量号。处理器以向量号为索引,到中断描述符表(Interrupt Descriptor Table,IDT )中查找对应的异常或中断处理程序。中断描述符表中保存了各个异常和中断处理程序的入口点。
向量号的允许范围为 0 ~ 255,其中 0 ~ 31号向量是Intel 64及 IA-32架构保留的,用于架构定义的异常和中断。这些架构预定义的向量号并没有全部用到,未分配的向量号是系统保留的,不能用作其它用途。
向量号 32 ~ 255 是专门给用户定义的中断使用的。这些中断通常分配给外部 I/O 设备,从而使这些设备能够利用外部硬件中断机制发送中断给处理器。
下表列出了 Intel 处理器在保护模式下的异常和中断,包括类型和来源,以及是否会生成错误码。
向量 | 助记 | 描述 | 类型 | 错误码 | 来源 |
---|---|---|---|---|---|
0 | #DE | Divide Error | Fault | No | DIV and IDIV instructions. |
1 | #DB | Debug Exception | Fault/Trap | No | Instruction, data, and I/O breakpoints; single-step; and others. |
2 | — | NMI Interrupt | Interrupt | No | Nonmaskable external interrupt. |
3 | #BP | Breakpoint | Trap | No | INT3 instruction. |
4 | #OF | Overflow | Trap | No | INTO instruction. |
5 | #BR | BOUND Range Exceeded | Fault | No | BOUND instruction. |
6 | #UD | Invalid Opcode (Undefined Opcode) | Fault | No | UD instruction or reserved opcode. |
7 | #NM | Device Not Available (No Math Coprocessor) | Fault | No | Floating-point or WAIT/FWAIT instruction. |
8 | #DF | Double Fault | Abort | Yes (zero) |
Any instruction that can generate an exception, an NMI, or an INTR. |
9 | Coprocessor Segment Overrun (reserved) | Fault | No | Floating-point instruction. | |
10 | #TS | Invalid TSS | Fault | Yes | Task switch or TSS access. |
11 | #NP | Segment Not Present | Fault | Yes | Loading segment registers or accessing system segments. |
12 | #SS | Stack-Segment Fault | Fault | Yes | Stack operations and SS register loads. |
13 | #GP | General Protection | Fault | Yes | Any memory reference and other protection checks. |
14 | #PF | Page Fault | Fault | Yes | Any memory reference. |
15 | — | (Intel reserved. Do not use.) | No | ||
16 | #MF | x87 FPU Floating-Point Error (Math Fault) | Fault | No | x87 FPU floating-point or WAIT/FWAIT instruction. |
17 | #AC | Alignment Check | Fault | Yes (Zero) |
Any data reference in memory. |
18 | #MC | Machine Check | Abort | No | Error codes (if any) and source are model dependent. |
19 | #XM | SIMD Floating-Point Exception | Fault | No | SSE/SSE2/SSE3 floating-point instructions. |
20 | #VE | Virtualization Exception | Fault | No | EPT violations |
21 | #CP | Control Protection Exception | Fault | Yes | RET, IRET, RSTORSSP, and SETSSBSY instructions can generate this exception. When CET indirect branch tracking is enabled, this exception can be generated due to a missing ENDBRANCH instruction at target of an indirect call or jump |
22-31 | — | Intel reserved. Do not use. | |||
32-255 | — | User Defined (Non-reserved) Interrupts | Interrupt | External interrupt or INT n instruction. |
当中断和异常发生时,NMI 和异常的向量是由处理器自动给出的;硬件中断的向量是由 I/O 中断控制器芯片送给处理器的;软中断的向量是由指令中的操作数给出的。
三、中断来源
中断来源有 2 种:
- 外部中断(硬件中断)
- 软件生成的中断
3.1 外部中断
处理器使用引脚来接收外部(硬件)中断,但是外部中断信号一般不直接发送到这些引脚上,是通过 PIC(Programmable Interrupt Controller) 或 APIC (Advanced Programmable Interrupt Controller)来中继处理的。
在老的处理器中,当 I/O 接口发出中断请求时,会被 PIC 设备(如 8259A)收集,并发送到处理器;在较新的处理器中,使用 APIC 来处理 I/O 请求。APIC 包括 2 种独立的设备:Local APIC 和 I/O APIC。
主要的中断引脚被称为 LINT[1:0] 引脚,它们与 Local APIC 相连接;当处理器没有或者关闭 Local APIC 功能时,它们被配置为 INTR 和 NMI 引脚。
在较新的处理器中,每个逻辑处理器都有独立的 Local APIC,Local APIC 会连接到处理器的中断引脚,同时 Local APIC 也会连接到系统的 I/O APIC。 I/O APIC 负责接收外部中断,然后把接收到的外部中断通过系统总线传递给 Local APIC。在多处理器系统中,处理器之间也可以通过系统总线来传递中断。
MP系统中,支持超线程技术的处理器,其 Local APIC 和 I/O APIC 功能如下图所示:
3.2 可屏蔽硬件中断
任何通过 INTR引脚或 Local APIC 传递到处理器的外部中断都被称为可屏蔽硬件中断。
EFLAGS 寄存器中的 IF 标志位,可用于整体打开或关闭所有可屏蔽硬件中断。
3.3 软件产生的中断
INT n 指令允许在软件内部生成中断,其中操作数 n 为向量号。
从 0 ~ 255 范围内的任何中断向量都可以用作该指令的参数。但是,当使用处理器预定义的 NMI 向量(比如向量号2)作为参数时,处理器并不会按照正常生成的 NMI 向量来处理。此时 NMI 中断处理程序会被调用,但是处理器内部处理NMI 的硬件并没有被激活。
通过 INT n 产生的中断,不能被 EFLAGS 寄存器中的 IF 标志所屏蔽。
四、异常来源
异常来源有三种:
- 处理器检测到的程序错误异常
- 软件生成的异常
- 机器检查异常
4.1 程序错误异常
当处理器在执行应用程序或操作系统程序时,检测到程序错误发生,就会产生一个或多个异常。
Intel 64 和 IA-32 架构为每一个处理器可探测到的异常定义了一个向量号。
异常分为三类:
- 故障(Faults)
- 陷阱(Traps)
- 终止(Aborts)
4.2 软件产生的异常
软件可以使用 INTO、INT1、 INT3 和 BOUND 指令来生成异常。这些指令允许在指令流中的某些点执行异常条件检查。
比如,INTO 指令在执行时,将检查 EFLAGS 寄存器中的 OF 标志位,如果被置位(为 ”1“),则引发 4 号异常;否则,什么也不做。
又比如,INT3 是断点指令,这条指令在程序调试时很有用。当程序运行不正常时,可以在程序某个地方设置一个检查点,也称断点,来查看寄存器、内存以及标志寄存器的内容,这条指令就是为了这个目的而设置的。INT3 是单字节指令,其机器指令码为 CC,当需要设置断点时,可以将断点处那条指令的第一个字节改成 0xcc
,原字节予以保存。当处理器执行到 INT3 指令时,即发生 3 号中断,去执行相应的中断处理程序。
注意,INT3 和 INT 3 不是一回事,INT1 和 INT 1 也不是一回事。INT n 指令的操作码是 CD
;而 INT1的操作码为 F1
,INT3 的操作码为 CC
。
4.3 机器检查异常
处理器提供了一种对内部芯片硬件和总线事务操作进行检查的机制,当检测到有错误发生时,就会引发此类异常(向量 18)。
五、异常分类
根据异常的性质和严重性,异常分为以下三种,并分别实施不同的处理:
- 故障(Faults)。故障通常是可以纠正的,比如,当处理器执行一个访问内存的指令时,发现那个段或者页不在内存中,此时可在异常处理程序中予以纠正(分配内存或者执行磁盘的换入换出操作),返回时,程序可以重新启动并不失连续性。为了做到这一点,当故障发生时,处理器把机器状态恢复到引起故障的那条指令之前的状态,在进入异常处理程序时,压入栈中的返回地址(CS 和 RIP 的内容)是指向引起故障的那条指令的,而不像通常那样指向下一条指令。如此一来,当中断返回时,将重新执行引起故障的那条指令,而且不在出错(如果异常已经妥善处理)。
- 陷阱(Traps)。陷阱异常通常在执行了截获陷阱条件的指令后立即产生,如果陷阱条件成立的话。陷阱通常用于调试目的,比如单步中断指令 INT3 和溢出检测指令 INTO。陷阱中断允许程序或者任务从中断处理过程返回之后继续进行而不失连续性。当此类异常发生时,在转入处理程序之前,处理器在栈中压入陷阱截获指令的下一条指令的地址。
- 终止(Aborts)。终止标志着最严重的错误,诸如硬件错误、系统表(GDT、LDT等)中的数据不一致或者无效。这类异常总是无法精确地报告引起错误的指令的位置,在这种错误发生时,程序或者任务都不可能重新启动。一个比较典型的终止类异常是”双重故障“(中断号为 8),当发生一次异常后,处理器在转入该中断的处理程序时,又发生另外的异常(如该中断处理程序所在的段不在内存中,或者栈溢出)。对于中断处理程序来说,很难从栈中获取有关如何纠正此类错误的明确信息,往往是发生极为重大的错误时才伴随着这种异常,所以再继续执行引起此异常的程序或任务已相当困难,操作系统只能把该任务从系统中抹去。
六、不可屏蔽中断
不可屏蔽中断 NMI (nonmaskable interrupt)可通过 2 种方式产生:
- 外部硬件激活了 NMI 引脚
- 处理器在总线上接收到了 NMI 模式的消息
当处理器从以上2种方式中收到 NMI 中断时,会立即调用向量为 2 的 NMI 中断程序进行处理。处理器完成本次 NMI 处理之前,不会接收其它任何中断(包括 NMI 中断)。
NMI 中断不能被 EFLAGS 寄存器的 IF 标志所屏蔽。
七、启用和禁用中断
处理器根据自身当前状态以及 EFLAGS 寄存器中 IF (Interrupt Flag)及 RF (Resume Flag)标志位的状态,来决定是否禁用中断。
7.1 可屏蔽硬件中断
可屏蔽硬件中断可通过 EFLAGS 寄存器中 IF 标志位来控制开启与禁用。当 IF 标志被清除时,处理器会禁止可屏蔽硬件中断;当 IF 标志被置位时,会启用可屏蔽硬件中断。分别使用 CLI
和 STI
指令来清除和设置 IF 标志。
IF 标志既不会影响 NMI 中断,也不会影响处理器生成的异常。
7.2 指令断点
EFLAGS 寄存器中的 RF (Resume Flag)标志用来控制处理器对于断点条件的处理。当 RF 置位时,在断点处不会产生调试异常(#DB);当 RF 位被清除时,在断点处会产生调试异常。
7.3 屏蔽栈切换时产生的中断和异常
由于处理器中与栈相关的寄存器有2个 -- %ss 及 %esp,所以当我们要进行栈的切换时,通常需要使用一对指令来完成此功能,例如:
mov %ax, %ss
mov StackTop, %esp
第一条指令是加载栈段寄存器,第二条指令是加载栈指针寄存器。如果在第一条指令执行完成,第二条指令尚未执行之前产生中断或者异常,那么在进入中断或异常处理程序后,其栈空间的逻辑地址就会出现错误。
为了解决这种情况,处理器在执行 MOV 到 SS 或 POP 到 SS 指令之后,在下一条指令执行之前,会禁止中断或异常,以保证栈切换的完整性。
八、并发事件的优先级
事件是有优先级的,如果在指令边界(指令执行之间)有多个事件待处理,处理器以预定义的顺序为他们提供服务。
下表展示了不同事件类别之间的优先级:
Priority | Description |
---|---|
1 (Highest) | Hardware Reset and Machine Checks - RESET - Machine Check (#MC) |
2 | Trap on Task Switch - T flag in TSS is set (#DB) |
3 | External Hardware Interventions - FLUSH - STOPCLK - SMI - INIT |
4 | Traps on the Previous Instruction - Trap-class Debug Exceptions (#DB due to TF flag set or data/I-O breakpoint) |
5 | Nonmaskable Interrupts (NMI) |
6 | Maskable Hardware Interrupts |
7 | Fault-class Debug Exceptions (#DB due to instruction breakpoint) |
8 | Faults from Fetching Next Instruction - Code-Segment Limit Violation (#GP) - Code Page Fault (#PF) |
9 (Lowest) | Faults from Decoding the Next Instruction - Control protection exception due to missing ENDBRANCH at target of an indirect call or jump (#CP) - Instruction length > 15 bytes (#GP) - Invalid Opcode (#UD) - Coprocessor Not Available (#NM) |
处理器首先处理最高优先级的事件,较低优先级的异常被丢弃, 较低优先级的中断保持待处理状态。当事件处理程序返回到程序或任务中产生原始事件的点继续执行时,可能会重新生成被丢弃的异常。
九、实模式下的中断向量表(IVT)
处理器可以识别 256 个中断,理论上需要有 256 段程序。在实模式下,处理器要求将这些程序的入口点集中存放到内存中从物理地址 0x00000开始,到 0x003ff 结束,共 1KB 的空间内,这就是所谓的中断向量表(Interrupt Vector Table, IVT)。
每个中断在中断向量表中占用 4 个字节,分别是中断处理程序的偏移地址和段地址。中断 0 的入口点位于物理地址 0x00000 处,也就是逻辑地址0x0000:0x0000处;中断 1 的入口点位于物理地址 0x00004 处,也就是逻辑地址0x0000:0x0004 处。其它中断依次类推。
十、中断描述符表(IDT)和中断描述符
在实模式下,在物理内存的最低 1KB 处,使用中断向量表(IVT)定义了 256 个程序的入口。
在保护模式下,处理器对中断的管理是相似的,但并非使用中断向量表来保存中断程序入口,而是使用中断描述符表(Interrupt Descriptor Table,IDT)。在这个表里,保存的是和中断处理程序有关的门描述符,包括中断门、陷阱门和任务门。
在x86(32位保护模式)模式下,中断门、陷阱门和任务门描述符的格式如下:
可以看到,x86模式下,每个描述符的大小为 8 字节。
在x86_64模式下,仅保留了中断门和陷阱门,线性地址由 32 位扩展为 64 位,门描述符大小也做了相应的扩展,由 8 字节扩展成 16 字节,其格式如下:
门描述符中各字段解释如下:
0-15
位 - 中断处理程序入口点针对段选择子基地址的偏移量,低 15 位16-31
位 - 目标代码段的段选择子IST
- 中断栈表索引,x86_64
架构中新增加的特殊机制DPL
- 描述符特权级别;P
- 段存在标志;TYPE
- 门类型,其中1110
表示中断门,1111
表示陷阱门48-63
位 - 中断处理程序入口点针对段选择子基地址的偏移量,第二部分,16 ~ 31 位64-95
位 - 中断处理程序入口点针对段选择子基地址的偏移量,第三部分,32 ~ 63 位96-127
位 - 保留,未使用
中断门和陷阱门区别不大,但还是有一些不同之处。通过中断门进入中断处理程序时,RFLAGS 寄存器的 IF 位被处理器自动清零,已禁止嵌套的中断,当中断返回时,将从栈中恢复 RFLAGS 寄存器的原始状态。陷阱中断的优先级较低,当通过陷阱门进入中断处理程序时,RFLAGS 寄存器的 IF 位保持不变,以允许其它中断优先处理。
和实模式下的中断向量表不同,中断描述符表不要求必须位于内存的最低端,这也就意味着中断描述符表在内存中的位置是不固定的。在处理器内部,有一个中断描述符表寄存器(Interrupt Descriptor Table Register,IDTR),保存着中断描述符表在内存中的线性基地址和界限。在 x86 模式下,IDTR 是一个 48 位的寄存器;在 x86_64模式下,IDTR 被扩展到 80 位。
IDTR 格式如下:
IDTR 的低 16 位,表示中断描述符表的大小限制,其单位是字节。
x86(x86_64)架构提供了 2 个指令来操作 IDTR:LIDT 和 SIDT。LIDT 指令用于把 IDT 的线性地址及大小加载到 IDTR 中去;而 SIDT 指令的效果跟 LIDT 相反,它把 IDTR 寄存器中的内容保存到指定的地方去。
当中断或异常发生时,处理器从 IDTR 中获取到 IDT 在内存中的位置,然后用向量号乘以 8 (x86架构)或16 (x86_64架构)去访问 IDT,从中取得对应的描述符。
十一、中断栈表(IST)
中断栈表机制( Interrupt Stack Table,IST)是x86_64架构下新增的一种机制,仅在x86_64架构下中断及异常处理需要栈切换时使用。中断栈表是一种可选机制,是否启用由 IDT 中门描述的 IST 字段决定。 IST 字段共 3 位,提供了8 种可能性。当 IST 字段为 0 时,表示不启用 IST 机制。IST 机制最多可以提供 7 个 IST 指针(1 ~ 7),每个指针 64 位大小,这些指针保存在任务状态段( Task-State Segment, TSS )中。当启用了 IST 机制,当栈切换时,TSS 中对应的 IST 指针会被加载到 RSP 中。
64位模式下,TSS 格式如下所示:
十二、错误码
有些异常产生时,处理器会在异常处理程序的栈中压入一个错误码,这和特定的中断向量有关。错误码格式如下:
EXT 位: 外部事件(External event) 指示位。置位时,表示异常是由硬件中断等外部事件引发的; 清除时,表示由软中断(INT n, INT3, 或者 INTO)等引发。
IDT 位: 描述符位置(Descriptor location) 指示位。置位(为 ”1“)时,表示错误码中的段选择子索引部分(位 3 ~ 15)是指向中断描述符(IDT)的; 否则,表示段选择子索引部分指向 LDT 或 GDT。
TI 位: 表(GDT/LDT) 指示位。该位仅在 IDT 位为 ”0“ 的情况下才有意义。此位为 ”0“ 时,表示段选择子索引部分指向的是 GDT;否则,指向 LDT。
注意,当通过 iret
指令从中断处理程序返回时,处理器并不会自动弹出错误码,需要中断处理程序主动移除错误码。
另外,缺页异常(Page-Fault Exception)和控制保护异常(Control Protection Exception)的错误码格式与通用错误码格式是不一致的,需要特别处理。
最后,对于通过引脚( INTR 或者 LINT[1:0] )产生的外部异常,以及通过 INT n 指令产生的异常,即使有错误码存在也不会被压入栈中。
十三、x86_64模式下中断和异常处理过程
中断和异常是随机产生,不可预测的。当中断和异常发生时,处理器可能在执行特权级别为 0 的任务(内核态),也可能在执行特权级别为 3 的任务(用户态)。当处理器将控制转移到中断处理程序时,如果中断处理程序的特权级别和当前特权级别一致,则不用进行栈切换;如果当前进程的特权级别比中断处理程序的级别低,比如从用户态转移到内核态,那么将会发生栈切换。
注意,当中断或异常产生时,处理器不允许程序执行从高特权级向低特权级转移。如果中断处理程序的特权级比当前程序的低,比如从内核态转移到用户态,就会引发通用保护异常(#GP)。
当中断和异常发生时:
- 如果特权级没有变化
- 直接把旧的 SS、RSP、RFLAGS、CS 及 RIP 压入当前栈
- 如果特权级有变化
- 新的
SS
选择子被强制为 NULL,并且新SS
选择子的 RPL 字段被设置为新的 CPL。 - 如果门描述符中的
IST
字段不是0
,处理器会根据IST
字段的值,把对应的IST
指针加载到RSP
寄存器;否则,新的RSP
需要从当前任务的 TSS 获取。
- 新的
在64位模式下,中断栈帧压入的大小固定为 8 字节,因此我们将得到以下堆栈:
如果中断向量有对应的错误码,会把错误码也压入栈。否则,为了保证栈格式的一致性,操作系统应该向栈中压入一个错误码占位符。接下来,要把门描述符中的段选择子字段加载到 CS
寄存器,并且目标代码段必须是 64 位模式代码段(CS.L = 1)。最后,把门描述符中的偏移字段加载到RIP
寄存器,这是中断处理程序的入口点。所有这些都完成之后,中断处理程序开始从入口处执行;当中断处理程序执行完成后,必须通过iret
指令把控制权交还给被中断的进程。iret
指令从栈中弹出旧的寄存器值,以恢复被中断的进程。
中断处理过程示意如下:
十四、参考资料
1、Intel 开发者手册:Intel 64 and IA-32 Architectures Software Developer Manuals Volume 3A Chapter 6 Interrupt and Exception Handling
2、《x86汇编语言:从实模式到保护模式》17.1 中断和异常
3、Linux inside -- interrupts-1
这是一个从 https://juejin.cn/post/7368653223798161449 下的原始话题分离的讨论话题