中断,异常,以及中断描述符表--IDT
关于异常,中断
中断大多都是由外部硬件产生的,例如,键盘,硬盘,光驱等,产生中断后会向处理器发送事件请求信号,中断请求信号可能是数据的读写操作,也可能是控制外部设备,这种中断称为硬件中断,还有一种是软件中断,例如使用INT n
指令,中断可以在程序执行的过程中触发,每种架构都会对处理器的中断/异常将其归类,并使用数字对同一种类型的中断/异常进行标示(唯一的),这个标示称为中断向量,处理器通过向量号从IDT
(中断描述符表Interrupt Descriptor Table)索引出中断/异常处理程序的入口地址,向量号的数值范围是0-255,其中0-31号(共32个)向量被Intel作为异常向量(个别保留使用),剩余32-255供用户使用
在这篇文章中我们只关注与中断向量表,并使用Rust来实现IDT,硬件中断编程和异常处理会有单独的文章介绍
中断描述符表
IDT借助门描述符将中断/异常向量号与处理程序关联起来(就像GDT一样),IDT是一个门描述符数组,每个门描述符占8Byte(64位),第一个表项是有效的,为了使处理器达到最佳性能,将IDT按照8Byte边界对齐,处理器借助IDTR
寄存器定位出IDT的位置,使用IDT前需要使用LIDT
指令将IDT的线性地址(32位)和长度(16位)加载到IDTR
寄存器中(我们可以复用之前编写的DescriptorTablePointer
),LIDT指令只能在CPL=0时执行
IDT表项
IDT表项是由门描述符组成,可以使用陷进门,中断门,任务门等3类门(在IA-32e中没有任务门)
保护模式的IDT
中断门描述符和陷进门描述符都包含远跳转地址(段选择子和段内偏移),远跳转为处理器提供中断/异常处理程序的入口地址,以下是中断门描述符和陷进门描述符的结构
中断门描述符结构如下
|63 - 48|47|46 - 45|44|43|42|41|40|39-37|36 - 32|31 - 16 | 15 - 0 |
+--------+--+-------+--+--+--+--+--+-----+-----------+-----------+--------+
| Offset |P | DPL |0 |D |1 |1 |0 | 0 | reserved | Selector | Offset |
+--------+--+-------+--+--+--+--+--+-----+-----------+-----------+--------+
陷进门描述符结构如下
|63 - 48|47|46 - 45|44|43|42|41|40|39-37|36 - 32|31 - 16 | 15 - 0 |
+--------+--+-------+--+--+--+--+--+-----+-----------+-----------+--------+
| Offset |P | DPL |0 |D |1 |1 |1 | 0 | reserved | Selector | Offset |
+--------+--+-------+--+--+--+--+--+-----+-----------+-----------+--------+
中断门和陷进门不同的地方就是在对IF标志位的操作上,处理器执行通过中断门描述符执行程序时,处理器会复位IF标志位防止其他中断请求干扰当前中断程序的执行,处理器会在最后执行IRET指令还原保存的EFLAGS寄存器的值,陷进门不会对IF标志位进行操作
IDT索引过程如下
IA-32e模式的IDT
IA-32e模式的中断/异常处理机制和保护模式的处理机制相似,IA-32e中断发生时的栈空间保存方式由选择性保存(特权级变化时保存),改为无条件保存,IA-32e模式引入了全新的中断栈切换机制
中断门和陷进门描述符结构如下
| 127 - 96 | 95 - 64|
+--------------------------------------------------+----------------------+
| reserved | Segment Offset |
+--------------------------------------------------+----------------------+
| 63 - 48 |47|46-45|44|43-40|39-37|36|35|34-32|31 - 16| 15 - 0 |
+--------------+--+-----+--+-----+-----+--+--+-----+--------+-------------+
| SgemntOffset |P | DPL |0 | Type| 0 |0 |0 | IST |Selecotr|SegmentOffset|
+--------------+--+-----+--+-----+-----+--+--+-----+--------+-------------+
中断门和陷进门描述符都用8B(64位)扩展至16B(128位),高64位保存段内偏移(32-64),低64位用于IST功能
IST只有在IA-32e模式下有效,程序通过IST功能可以让处理无条件的进行栈切换,在IDT的任意一个门描述符都可以使用IST机制或原来的栈切换机制,IST复位时使用旧的栈切换机制,否则使用IST机制
IST位区域用于IST栈表索引,当确定目标IST后,处理器会强制将SS段寄存器赋值为NULL段选择子,并将中断栈地址加载到RSP寄存器中,最后将原SS,RSP,RFLAGS,CS和RIP值压入新栈中
中断堆栈帧
普通的函数通过CALL
指令调用,在调用前CPU会将当前的执行地址压入栈中,当函数使用RET
指令返回时,CPU会将上次执行的地址从栈中弹出并跳转
函数调用的示意图如下
函数调用后示意图如下
函数远调用示意图如下
函数远返回示意图如下
还记得我们之前写过set_cs
函数吗?其中我们用到了一个指令就是LRET
我们将段选择子压入栈中并使用LRET
指令完成了一个远返回,这样相当于间接设置了CS寄存器,用到的就是这个原理
对于异常/中断的处理,不仅仅只保存返回地址这么简单了,由于中断处理程序通常会在不同的上下文中运行,当一个异常发生时CPU将会执行以下步骤
- 对齐堆栈指针: 异常可能在执行任何指令时发生,所以栈指针也可能指向任意地址,一些CPU的指令要求栈指针必须以16字节边界上对齐,因此,在发生中断后CPU需要立即执行这种对齐
- 切换堆栈: 在特权级改变时将会切换堆栈(例如内核切换到用户),例如当用户模式程序中发生CPU异常时。可以使用中断堆栈表为特定中断切换配置堆栈(IST)
- 保存当前的栈帧(压入栈中):当一个异常/中断发生时,CPU会将当前的
SS
寄存器和RSP
寄存器得值压入栈中,这样从中断处理程序返回时,这可以恢复原始堆栈指针 - 将保存并更新RFLAGS:RFLAGS寄存器包含了各种控制和状态位,进入中断时,CPU更改IF标志位(中断门)并将旧值压入栈中
- 压入栈指针: 在跳转到异常/中断处理程序之前,CPU将会把
RIP
和CS
寄存器的值压入栈中,这与普通函数一样 - 保存错误码(如果有的话):对于一些特殊的异常(例如
#PF
)CPU会将用于描述错误信息的错误码压入栈中 - 调用异常处理程序:CPU从IDT中的相应字段读取中断处理程序功能的地址和段描述符,然后,通过将值加载到rip和cs寄存器中来调用此处理程序。
发生中断前后栈指针的变化如下
开始干活
拆分中断门/陷进门的结构
我们要创建IDT的结构,提供配套的操作方法以及辅助创建IDT的子结构
我们先明确以下I