6.2 中断
中断使我们可以在任意时刻修改程序的控制流。当程序正在执行时,外部事件(设备需要 CPU 来处理) 或者内部事件(除数为零,执行指令的特权级别不够,访问非权威地址)都会引起中断,而中断会使原本控制流程之外的代码被执行。这段代码叫作中断处理程序,中断处理程序一般是操作系统或者驱动程序的一部分。
在 [15] 中,Intel 把外部的异步中断和内部的同步异常进行了分离,不过这两种情况的处理方式是类似的。
每一个中断都会被标记一个固定的数值,这个数值就是它的身份标志。对于我们来说处理程序是怎么从中断 controller 中拿到这个中断编号的并不重要。
当第 n 个中断发生时,CPU 会检查内存中的中断描述符表(IDT)。和 GDT 类似,其地址和大小是在 idtr 中存储的,图 6-2 描述了这个特殊的寄存器:
Figure 6-2.idtr 寄存器
IDT 的每一个条目都有 16 字节,第 n 个条目对应第 n 个中断。该条目包含的信息中有例如中断处理器的地址之类。图 6-3 描述了中断描述符的格式。
Figure 6-3.中断描述符
DPL 描述符特权级别(Descriptor Privilege Level)
- 当前特权级别应该要小于等于 DPL 才能使用 int 指令调用中断处理器。否则 check 行为都不会进行。
1110 类型(中断门,interrupt gate,IF 标记会在中断处理器中被自动 clear 掉) 或者 1111 类型(陷阱门,trap gate,IF 标记不会被 clear)。
前 30 个中断是被保留的。也就是说你不能自己提供这几个中断的中断处理处理程序,但 CPU 会使用这些保留的中断来处理其内部的一些事件,例如非法的指令编码。除保留中断以外的中断可以被系统程序员使用。
当 IF 标记位被设置时,中断就会被处理;否则中断则会被忽略。
■Question 96 什么是 non-maskable 中断?这种中断和编码为 2 的中断和 IF flag 有什么关系?
应用程序代码在较低的权限下执行(ring3)。直接的设备控制只能在高级的特权级别下执行。当设备需要 CPU 响应并发送中断给 CPU 时,处理程序需要在高特权级别下进行执行,因此需要修改段选择器。
那栈的话呢?这时候栈也应该被切换啊。下面是我们在设置中断描述符的 IST 字段时需要的一些选项:
如果 IST 是 0,那么标准策略会被采用。当中断发生时,ss 寄存器被初始化为 0,新的 rsp 值被加载到 TSS 中。然后 ss 的 RPL 字段被设置为合适的特权级别。之后旧的 ss 和 rsp 值被保存到这个新的栈中。
如果 IST 是 1,那么在 TSS 中的七个 IST 的其中一个会被采用。之所以要创建 IST 是因为发生一些严重的错误(non-maskable 中断,双重错误等等)时,在一个已知的栈中执行是可以受益的。所以系统程序员即使是在 ring0 下也会创建一些栈,并使用它们来处理特定的中断。
有一个特殊的 int 指令,这条指令接收中断编号。并依据其描述符内容来调用一段中断处理程序。这个过程会忽略 IF flag:无论 IF 被设置成了什么值,中断处理程序都会被执行。为了控制 int 指令执行的特权代码,就是 DPL 字段的存在意义了。
在中断处理程序开始执行之前,一些寄存器被自动保存进栈。这些寄存器包括 ss,rsp,rflags,cs 和 rip。参见图 6-4 中的栈图。注意段选择器是如何使用零填充到 64 位的。
Figure 6-4.中断处理器开始时的栈情况
有时中断处理程序需要事件更多的信息。这时候一个中断错误码会被推到其栈顶。这个错误码包含了这种类型的中断的各种各样的信息。
很多中断在 Intel 的文档中都有特殊的助记符。例如,第 13 号中断被称为#GP(general protection)。你可以在表 6-1 中找到一些有趣的中断的简短描述。
Table 6-1.一些重要的中断
VECTOR | MNEMONIC | DESCRIPTION |
---|---|---|
0 | #DE | Divide error |
2 | Non-maskable external interrupt | |
3 | #BP | Breakpoint |
6 | #UD | Invalid instruction opcode |
8 | #DF | A fault while handling interrupt |
13 | #GP | General protection |
14 | #PF | Page fault |
并不是所有的二进制代码中的指令都被正确编码了。当 rip 没有指向一个有效的指令地址时,CPU 会产生一个 #UD 中断。
#GP 中断使用非常普遍。当你解引用一个禁用地址(没有指向任意已分配页)时就会触发这种中断,或者当执行需要更高特权级别的操作时也会触发这种中断,还有很多其它情况。
当访问一个内存页,且内存页的页表条目的 present flag 被清零时,就会产生 #PF 中断。该中断用来实现内存交换策略以及文件映射功能。中断处理程序将未命中的页从磁盘加载到内存中。
debugger 会强依赖于 #BP 中断。当 rflags 中的 TF 被标记时,这个中断就会在每一次执行指令时被触发,这样就允许程序的一步步执行了。显然,这种中断默认会被操作系统所处理。因此,给用户程序提供操作这种中断的接口就是操作系统的责任了,这样才能让程序员去编写它们自己的 debugger。
总结一下,当第 n 个中断发生时,在程序员看来会有下面的一些行为发生:
- 从 idtr 寄存器中取出 IDT 地址。
- 用 IDT 的第 128 × n 个字节开始定位到中断描述符。
- 从 IDT 条目中加载段选择器和中断处理程序的地址,并保存到 cs 和 rip 寄存器,可能还会切换特权级别。旧的 ss,rsp,rflags,cs 和 rip 像图 6-4 展示的一样保存到栈上。
- 对于某些中断来说,错误码会被推到中断处理程序的栈顶。这样可以提供中断原因的更多信息。
- 如果描述符类型字段将其定义为中断门(Interrupt Gate),那么 IF flag 就会被清零。如果是陷阱门(Trap Gate),则不会自动清零,这样允许嵌套的中断处理。
如果中断 flag 在中断处理程序开始之后没有被立即清零,我们没有任何办法保证中断处理甚至是其第一条指令被执行,因为这期间可能还会有其它的异步中断出现,这一点需要我们格外注意。
■Question 97 TF flag 会在进入中断处理器时自动被 clear 么?参考 [15]。
中断处理程序使用 iretq 指令来结束,该指令会恢复之前保存在栈里的所有寄存器,这些寄存器在图 6-4 中有所体现。而简单的 call 指令只会恢复 rip 寄存器。