目录
1 概述
1.1 什么是中断和异常
1. 中断和异常是特殊的控制转移方法,当中断或异常发生时,处理器在结束当前指令的执行后,将控制转移到一个处理中断或异常的程序,在处理完成后通过一条IRET指令返回被中断的程序
2. 中断是异步的
① 中断由一个外部事件引起,与正在执行的指令没有关系
② 中断通常用于指示IO设备的一次操作已经完成
3. 异常是同步的
① 异常是在指令执行期间检测到的不正常或非法的条件
② 异常与所执行的指令有直接的联系
1.2 中断和异常处理时机
1. 中断和异常通常在两条指令之间进行处理
2. 但是80386中有些指令执行时间相对较长,而且执行时间不固定(e.g. 串操作指令,movs等)。为了保证系统有良好的中断响应,在串操作指令中,80386允许在每次重复操作后响应中断
1.3 中断向量
1. 每种中断和异常,都有一个与之相联系的中断向量,用8位二进制数表示。因此,中断向量的范围为0 ~ 255
2. 处理器通过中断向量从中断描述符表(IDT)中为给定的中断或异常选择处理程序
3. 分配给异常的中断向量为0 ~ 31,是80386处理器体系结构确定的
4. 中断可以使用的中断向量号其实是0 ~ 255范围内的任何值(通过设置中断控制器实现),但是为了避免与异常使用的中断向量冲突,最好分配在32 ~ 255范围内
1.4 中断和异常的屏蔽简介
1. 在正常情况下,中断和异常可以在任意两条指令之间发生,但是处理器提供了相应机制屏蔽中断和某些异常
2. 只有调试相关的异常可以屏蔽,其他异常(e.g. 段异常、页异常)不能被屏蔽
2 中断
80386支持可屏蔽中断INTR和不可屏蔽中断NMI这两种类型的中断,处理器有2个引脚分别对应这两种类型的中断
说明:此处的屏蔽与不可屏蔽,是指是否可以被EFLAGS寄存器中的IF位屏蔽
2.1 INTR中断
1. INTR中断是可屏蔽中断,可以有任意的中断向量号。中断控制器在发出INTR中断信号的同时,还会向处理器发送中断向量号
以8059A中断控制器为例,会在CPU向8259A发送第2个INTA(Acknowledge)信号期间,将一个代表中断向量号的8位数据送到数据总线上供CPU读取
关于8059A中断控制器的详细内容,可参考X86汇编语言从实模式到保护模式08:中断和动态时钟显示chapter 1.3及chapter 2.5.5
2. INTR中断可以用EFLAGS寄存器中的IF位进行屏蔽
IF(Interrupt-enable Flag) 中断允许标志位 |
|
3. 当EFLAGS寄存器中的IF位为0时,如果有INTR中断产生,信号会被送到80386处理器,但是处理器不会响应该中断请求。此时,该中断信号被挂起(pending),直到IF位被置为1或外部硬件撤销该中断请求
这里的中断挂起,存在于3个层面,
① 中断控制器层面,中断控制器中的中断请求寄存器的相关位会一直为1,表示该中断还没有被处理
② 处理器层面,INTR引脚被持续拉高,表示INTR中断请求信号一直有效
③ 产生中断的外设层面,该外设的中断请求寄存器的相关位也会一直为1,表示该中断还没有被处理(该位一般在中断处理程序中被清零)
2.2 NMI中断
1. NMI中断是不可屏蔽中断,即EFLAGS寄存器中的IF位对NMI中断不起屏蔽作用
2. 处理器在响应NMI中断时,不从外部硬件获取中断向量号,而是由处理器固定分配中断向量号。在80386中,NMI的中断向量号为2
3. 由于NMI不可屏蔽,所以仅限于系统出现某种紧急事件时使用。例如系统中出现总线超时、电源故障等
产生不可屏蔽中断的中断源一般通过与非门连接到处理器的NMI引脚,一旦有一个中断源生效,则可以触发处理器的NMI中断
4. NMI处理程序屏蔽另一个NMI
当处理器正在处理一个NMI中断请求的过程中,如果又发生了另一个紧急事件;或者两种紧急事件同时在两条指令之间发生。仅在这两种情况下,一种事件的NMI处理程序可以屏蔽另一个NMI中断请求,从而避免发生中断嵌套。当然,这种情况是极少发生的
3 异常
3.1 异常分类
根据引起异常的程序是否可被恢复,将异常分为故障(Fault)、陷阱(Trap)和终止(Abort)
3.1.1 故障(Fault)
1. 故障是在引起异常的指令之前,将异常情况通知给系统的一种异常
2. 当处理器跳转到故障处理程序时,所保存的断点(CS : EIP)指向引起故障的指令。也就是说,在80386中故障是可以重新执行的
一旦在故障的异常处理程序中将导致故障的原因排除,通过iret指令就可以再次执行引起异常的指令
3. 典型的故障示例是缺页异常(Page Fault),操作系统依赖该机制实现虚拟内存的分配
① 当程序访问尚未建立映射的虚拟地址时,会触发缺页异常
② 在缺页异常的处理程序中,会分配物理页,并填充页表建立虚拟地址到物理页的映射(这其中还包括权限检查,以及可能的读取磁盘的操作)
③ 当缺页异常处理程序返回时,会再次执行引起缺页异常的指令,由于此时已经建立映射,指令可以成功执行
4. 故障的检测时机与状态恢复
① 故障的检测可以在一条指令开始之前,也可以在一条指令的执行期间进行
② 如果在指令执行的中间检测到故障,则中止故障指令,并将指令的源操作数恢复为指令执行之前的值。这样在故障恢复后,重新执行故障指令时,就可以有与前一次执行完全相同的输入条件
3.1.2 陷阱(Trap)
1. 陷阱是在引起异常指令之后,将异常情况通知给系统的一种异常
2. 当处理器跳转到故障处理程序时,所保存的断点(CS : EIP)指向引起陷阱的指令的下一条应该执行的指令
当陷阱的异常处理程序通过iret指令返回时,则继续执行后面的指令
3. 在一个陷阱通知系统之前,引起陷阱的指令会被执行完成,因为该指令可能修改了寄存器或内存
4. 典型的陷阱示例是调试指令(INT3 / INTO / BOUND)及软中断指令(INT n)
说明:如何理解"下一条应该执行的指令"
① "下一条应该执行的指令"不一定是汇编程序中的下一条指令,例如使用loop前缀的循环指令。在循环条件仍满足的情况下,如果循环指令触发陷阱,则返回后仍然执行循环中的指令
② 之所以提出这点区别,是要说明不能总是能够根据陷阱保存的断点,反推确定产生异常的指令
3.1.3 终止(Abort)
1. 终止是系统在出现严重情况(e.g. 硬件故障或系统表中出现非法值)时,通知给系统的一种异常
2. 引起中止的指令是无法确定的,产生终止时,正在执行的程序不能被恢复执行
3. 系统接收到终止异常后,处理程序需要重建各种系统表,并可能需要重新启动操作系统
3.2 异常小结
说明1:某些异常会提供出错码,向异常处理程序提供附加信息
说明2:通过INT n指令触发的软中断属于陷阱类型异常,因此无法通过EFLAGS中的IF位屏蔽
3.2.1 异常0:除法出错
1. 由DIV和IDIV指令触发
2. 产生该故障的场景,
① 除数为0
② 相除后得到的商太大,结果操作数无法容纳
3.2.2 异常1:排错异常
1. 排错异常有故障类型的,也有陷阱类型的。可以通过访问DR6寄存器(Debug Register),确定排错异常的原因及类型
2. 在一条指令中,可以检测不止一个排错异常,并使得DR6中同时有多位被置为1
3. EFLAGS寄存器中的RF位,用于控制是否产生调试异常
RF(Restart Flag) 重启标志位 |
当RF = 1,则禁用指令断点产生调试异常 |
说明:80386调试寄存器简介
80386共有DR0 ~ DR7共8个调试寄存器,其中,
① DR0 ~ DR3可以分别设置4个断点的线性地址
② DR4 ~ DR5保留未使用
③ DR6是断点状态寄存器
④ DR7是断点控制寄存器(包括断点类型、断点长度、断点使能)
3.2.3 异常3:单字节INT3
1. INT3是一条特别的单字节INT n指令,用来支持程序断点
2. INT3指令被看成是一种陷阱,而不是一个中断
3.2.4 异常4:溢出
INTO指令提供的是一个条件陷阱,
1. 如果执行INTO指令时,EFLAGS寄存器中的OF位为1,则产生陷阱
2. 如果执行INTO指令时,EFLAGS寄存器中的OF为为0,则不产生陷阱,继续执行后面的指令
3.2.5 异常5:边界检查
1. 如果BOUND指令发现被测试的值超出了规定的范围,则发生边界检查故障
2. BOUND指令格式如下,
;目的操作数是寄存器,包含数组的索引
;源操作数指向内存位置,那里存储了两个成对出现的字或双字
;分别是数组索引的下限和上限
bound r16, m16
bound r32, m32
3.2.6 异常6:无效操作码
1. 从[CS : EIP]指定的内存位置开始,如果连续一个或多个字节所包含的内容不表示80386指令集中的任何一条指令,则发生无效操作码故障
2. 发生无效操作码故障的场景
① 操作码字段的内容不是合法的80386指令代码
② 指令要求使用存储器操作数,但是使用寄存器操作数
③ 不能被加锁的指令使用了LOCK前缀
3.2.7 异常7:设备不可用
1. 在不包括80387协处理器的系统中,可以通过异常7来软件模拟浮点运算
2. 发生设备不可用故障的场景
① 在执行浮点指令时,CR0中的EM位或TS位为1
② 在执行WAIT指令时,CR0中的TS位及MP位均为1
3.2.8 异常8:双重故障
1. 双重故障是指处理器在检测到一个异常时,已有另一个异常正通知给系统,此时会向系统通知一个双重故障
2. 双重故障属于终止异常,因此断点(CS : EIP)保存的地址不可能指向引起双重故障的指令
3. 双重故障通常指示系统中出现的严重问题,例如段描述符表、页表或中断描述符表出现问题
3.2.9 异常10:无效TSS
1. 当正从TSS中装入选择子时,如果发生除了不存在异常以外的段异常时,则产生无效TSS故障
2. 无效TSS故障会提供一个出错码,包含有引起异常的段选择子
3.2.10 异常11:段不存在
1. 当处理器加载除SS之外的段寄存器时,如果发现要访问的段描述符中P位 =0,则产生段不存在故障
2. 段不存在故障会提供一个出错码,包含有引起异常的段选择子
说明:段异常出错码
① 当出错码中包含段选择子时,直接将引起异常的段选择子的Index字段和TI字段保存在出错码中。如果出错码中不包含段选择子,则Index部分为0
② 当处理某一异常或外部中断时,如果又发生了异常,此时会将EXT位置为1
③ 当从IDT中读取表项并产生异常时,会将IDT位置为1
3.2.11 异常12:栈段异常
当处理器检测到用SS段寄存器寻址发生问题时,产生段故障,分为如下3类,
1. 由SS段寄存器寻址栈段时界限超出范围,将引起段故障,且提供的出错码为0
2. 跨特权级调用(e.g. 调用门)或中断处理中,高特权级栈的界限超出范围,将引起段故障,且出错码包含有高特权级栈的栈选择子
3. 加载SS段寄存器时,如果对应的段描述符P位 = 0,将引起段故障,且出错码包含有引起异常的段选择子
说明1:区分高特权级栈故障与栈段不存在故障
① 高特权级栈故障与栈段不存在故障的出错码均为非零,不能通过出错码直接区分这两种情况
② 通过检查出错码中段选择子对应的段描述符的P位,可以区分上述两种情况
说明2:栈段扩展操作
① 在支持栈段扩展的系统中,如果栈段故障的出错码为0,则表示需要对栈段进行扩展
② 需要注意的是,这里是指分段机制控制的线性地址空间中的栈段扩展
3.2.12 异常13:通用保护
1. 通用保护异常属于故障类型
2. 通用保护异常会提供一个依赖于被检测条件的出错码,以便异常处理程序判断异常类型
3.2.13 异常14:页异常
1. 同时满足如下2个条件时产生页故障,
① 分页机制被启用,即CR0中的PG位为1
② 指令在进行内存访问时,被访问的线性地址所在的页未驻留在内存,或用不适当的访问类型访问内存页(e.g. 对只读内存进行写入操作)
2. 处理器将引起页故障的线性地址保存在CR2,并提供一个出错码,指示引起页故障的存储器访问类型,页异常处理程序中会根据该出错码进行不同的处理
说明1:页故障出错码
① P位表示页异常是否由页不存在导致
- P = 0,表示页异常由于访问不存在的页导致
- P = 1,表示页异常由于访问类型违反保护权限导致
② W位表示引起页异常的访问类型
- W = 0,表示进行读访问
- W = 1,表示进行写访问
③ U位表示引起页异常的访问特权级
- U = 0,表示访问来自特权级0、1、2(系统级)执行的程序
- U = 1,表示访问来自特权级3(用户级)指向的程序
说明2:Linux 0.11页异常处理程序示例
① 进入页异常处理程序时的栈状态
此时栈顶指向页异常错误码
② 页异常处理程序入口
文件:mm/page.s
在调用do_no_page和do_wp_page函数之前,向栈中压入了EDX和EAX寄存器,其中分别保存了引发页异常的线性地址和页异常错误码,他们将作为传递给do_no_page和do_wp_page函数的参数
需要注意的是,按照约定,函数参数从右向左压栈
③ 处理缺页异常
文件:mm/memory.c
④ 处理页保护异常
文件:mm/memory.c
4 中断和异常的优先级
1. 80386所识别的中断及异常优先级如上图所示
2. 当检测到多于一个中断或异常时,将优先级最高的中断或异常通知给系统,其他优先级较低的异常被废弃,而优先级较低的中断则保持pending
5 屏蔽中断和异常
某些条件及处理器标志将引起中断及排错故障被忽略或屏蔽,在这种情况下,中断保持pending,而排错异常被废弃。具体场景如下,
1. 如果EFLAGS寄存器中的IF位为0,则INTR中断被屏蔽
2. 当EFLAGS寄存器中的IF位为0时执行STI指令(也就是开中断),则在STI指令及下一条指令执行期间屏蔽INTR中断
3. 如果EFLAGS寄存器中的RF位为1,则屏蔽排错异常
4. 当系统正在处理一个NMI中断时,屏蔽任何新的NMI中断
5. 以SS寄存器为目的寄存器的MOV或POP指令,将在该指令及下面一条指令的执行期间屏蔽各种中断及排错异常
说明1:设置栈时屏蔽中断和异常
① 80386中设置栈时,需要将栈指针加载到SS和ESP寄存器。如果使用2条指令完成,则需要确保这2条指令的执行不被打断,以确保栈设置的正确性
② 80386中也提供了LSS指令,该指令可以在一条指令中,将存储器中的一个48位全指针分别加载到SS和ESP寄存器
说明2:Linux 0.11使用LSS指令设置栈指针示例
文件:boot/head.s
6 中断和异常的转移方法
6.1 中断描述符表(IDT)
在保护模式下,处理器使用的是中断描述符表IDT(Interrupt Descriptor Table)管理中断
1. 中断描述符表共256个表项(与256个异常向量对应),每个描述符8B,因此需要2KB内存
2. 当中断或异常发生时,处理器以中断向量作为索引,从中断描述符表中获取一个8B的门描述符
3. 每个表项是一个门描述符,可以是中断门、陷阱门或任务门
4. 可以存储在线性地址空间中的任何位置,通过IDTR寄存器保存其线性基地址与表界限(与GDT类似)
说明1:中断门和陷阱门描述符只允许安装在IDT内,任务门描述符可以安装在GDT、LDT和IDT中
说明2:中断描述符表寄存器IDTR
① IDTR中存储的是IDT的线性基地址和表界限,16位表界限最大为64KB,但实际最大只使用2KB
② 系统复位时,IDTR的基地址部分为0,表界限部分为0xFFFF
③ 与GDT类似,系统中只有一个IDT
④ 为了提高高速缓存的工作性能,建议IDT的基地址8B对齐
6.2 中断描述符
如上文所述,中断描述符表的每个表项中可以部署中断门、陷阱门或任务门,因此中断描述符的格式就是系统段或门的描述符格式(其中DT位 = 1)
6.2.1 Selector & Offset
1. 包含中断 / 异常处理程序的48位全指针
2. 如果是任务门,则不使用Offset字段,只使用Selector字段记录任务的TSS段选择子
关于任务门描述符,可参考X86汇编语言从实模式到保护模式17:协同式任务切换 chapter 3
6.2.2 P位
1. 标识门描述符是否有效,P = 1时,门描述符有效;P = 0时,门描述符无效
2. 使用无效的门描述符将引起异常
6.2.3 DPL
1. 标识门描述符的特权级,用于进行门级检查,此时要求
CPL <= 门描述符DPL
2. 只有使用INT n或INTO指令触发异常时,才会进行门级检查,以避免应用程序通过INT n指令使用外部设备的中断向量触发异常
对于其他异常或中断,不进行门级检查,忽略DPL
6.2.4 Type
1. Type字段标识中断描述符类型
2. 如上文所述,部署在中断描述符表中的,只能是下面5种门描述符
说明:在中断门 / 陷阱门 / 任务门描述符中,不使用DwordCount字段(该字段仅在调用门中使用)
6.3 通过中断门和陷阱门转移
转移目的地:当前任务全局部分中的中断 / 异常处理程序
6.3.1 选择中断描述符
1. 当中断或异常发生时,处理器以中断向量作为索引,从中断描述符表中获取一个8B的门描述符
2. 如果索引的中断描述符是一个中断门或陷阱门,则表示将控制转移到当前任务的一个处理程序(此时不会发生任务的切换)
3. 中断门或陷阱门中的Selector字段用于访问GDT或LDT中的段描述符,该描述符必须指向一个可执行的存储器段
门描述符中的Offset字段则标识处理程序入口点在段内的偏移量
6.3.2 门级检查
1. 门级检查仅在使用INT n或INTO指令触发异常时进行,以避免应用程序使用分配给外部设备的中断向量
2. 对于其他的异常或中断,不进行门级检查
6.3.3 段级检查
1. 中断门或陷阱门描述符中的Selector必须指向一个存在的、可执行的存储器段,处理器需要检查该存储器段的描述符中的DPL字段,以确定是否可以跳转
2. 段级检查中,要求只能向同级或更高优先级跳转,即
CPL >= 跳转目标段描述符DPL
6.3.4 特权级与栈切换
1. 如果中断 / 异常处理需要跳转到更高优先级处理,则根据保护模式的要求,所使用的栈段也需要随之切换
2. 新的栈段初值[SS : ESP]从当前TSS段中获取,而旧的栈段值[SS : ESP]被压入新栈
6.3.5 EFLAGS寄存器压栈与设置
1. 当完成特权级与栈的切换后,将当前EFLAGS寄存器值压栈
2. 将当前EFLAGS寄存器中的NT位清零,表示处理器通过IRET指令返回时,是返回到同一任务而不是一个嵌套任务
3. 将当前EFLAGS寄存器中的TF位清零,表示处理器不允许单步执行。这样做是为了避免进入中断 / 异常处理程序后,每执行一条指令都反复触发异常,重新进入异常处理程序
4. 如果是通过中断门进行转移,将当前EFLAGS寄存器的IF位清零,表示处理器屏蔽INTR中断,不允许中断嵌套
如果是通过陷阱门进行转移,则IF位保持不变,从而在异常处理程序中也可以响应中断
说明:除了对IF位的处理不同,中断门和陷阱门是完全相同的。根据该特性,中断门更适合处理中断,陷阱门更适合处理异常
6.3.6 保存返回地址
1. 通过将适当的[CS : EIP]寄存器值压栈,保存中断 / 异常处理程序的返回地址
2. 这里的"适当"是指根据中断 / 异常的特性,压栈当前指令还是下一条应执行指令
6.3.7 控制流转移
1. 将门描述符中的Selector装入CS寄存器
2. 将门描述符中的偏移量装入EIP寄存器
说明:关于门描述符中Selector的RPL字段
① 由于门描述符中的Selector字段是引用一个可执行存储器段的选择子,因此他的最后2位是RPL字段
② 此处的RPL字段的特权级可以低于跳转目标代码段的特权级(e.g. 目标跳转代码段的DPL = 0,但是此处RPL = 3),这样就可以实现对一致性代码段的支持(即在跳转到高特权级代码段时,CPL仍然保持低特权级)
6.3.8 出错码压栈
1. 处理器异常处理的最后一部分,是根据需要将出错码压栈
2. 出错码为16位,但是为了确保栈边界对齐,实际压栈的出错码具有32位的值,其中高16位的值未做定义。出错码压栈后的状态如下图所示,最终的栈顶指向出错码(如果有的话)
3. 出错码由异常处理程序负责出栈
由于出错码并非所有异常都给出,因此在调用IRET指令进行中断返回之前,对于有出错码的异常,异常处理程序需要先从栈中弹出出错码
4. 如果通过任务门处理异常,且对应的异常有出错码,那么该出错码会被压入新任务的栈中
6.3.9 IRET返回
1. 由于在通过中断门或陷阱门进行转移时,EFLAGS寄存器的NT位被清零,所以在中断 / 异常处理程序中调用IRET指令时,将从当前栈的栈顶获取返回信息
因此,为了IRET指令可以正确返回,在进行中断 / 异常处理程序中需要保持栈平衡,并且将错误码弹出
2. IRET指令从栈顶弹出返回地址[CS : EIP],然后弹出EFLAGS寄存器。根据弹出的CS的RPL字段和CPL,判断中断 / 异常返回是否切换了特权级
如果切换了特权级,则再从栈中取出保存的低特权级栈段[SS : ESP],并进行栈段的切换
6.4 通过任务门转移
转移目的地:其他任务
1. 如果通过中断向量索引到的中断描述符是一个任务门,则表示要转移到不同任务的处理程序
2. 通过任务门的转移,会将EFLAGS寄存器的NT位置为1,从而在调用IRET指令时,可以返回嵌套的任务
6.5 通过中断/陷阱门和任务门处理中断/异常的比较
1. 使用中断/陷阱门和使用任务门是两种不同的解题思路,
① 使用中断/陷阱门,是由当前任务之内的一个过程来处理中断/异常
② 使用任务门,是由另外一个任务来处理中断/异常
2. 由当前任务内的过程来处理中断/异常,可以很快地转移到处理程序,但是处理程序需要负责保存和恢复寄存器的内容(即保存和恢复中断现场)
此时在中断/异常处理程序中访问被打断的任务状态比较容易
3. 由另外一个任务来处理中断/异常,要花费较长的时间,但是保存和恢复寄存器内容的开销已作为任务切换的一部分处理
此时在中断/异常处理程序中访问被打断的任务状态比较负责,但是隔离效果较好
说明:中断/异常处理程序的部署
① 一般将中断/异常处理程序部署在任务的全局空间,以便被所有任务共享
② 如果各个任务要求有不同的处理程序,则全局中断/异常处理程序中保存一个各任务处理程序的入口表,并为引起异常的任务调用相应的处理程序