- 故障:引起故障的指令被启动后但未执行结束时,CPU检测到的一类与指令执行相关的意外事件。这些事件有些可以恢复,有些不可以(如溢出、非法操作码)。
- 陷阱/自陷/陷入:当执行到陷阱指令(也称为自陷指令)时,CPU就调出特定的程序进行相应的处理,处理结束后返回到陷阱指令的下一条指令执行。
- 终止
- 可屏蔽中断:通过INTR引脚向CPU请求,可通过设置屏蔽字来屏蔽请求,若中断请求被屏蔽,则不会被送到CPU。
- 不可屏蔽中断:非常紧急的硬件故障,如:电源掉电,硬件线路故障等。通过NMI向CPU请求。一旦产生,就被立即送CPU,以便快速处理。这种情况下,中断服务程序会尽快保存系统重要信息,然后在屏幕上显示相应的消息或直接重启系统。

三、IA-32中的异常和中断处理
实地址模式(Real Mode)是Intel为80286及其之后的处理器提供的一种8086兼容模式。
寻址空间1MB(2^20),存储管理采用分段方式,每段最大地址空间64KB(2^16)。
物理地址=段地址*16+偏移地址,其中段地址在段寄存器中,即:
指令地址=CS<<4+IP(当时CS只有16位)。中断向量表位于0000H~03FFH。共256组,每组占四个字节,共1KB。
采用的是8086/8088的中断类型和中断方式。
中断向量表中每一项是对应中断服务程序或异常处理程序的入口地址,被称为中断向量(Interrupt Vector)。
实地址模式下没有分页管理机制!根据中断类型号查地址即可。
IA-32的保护模式并不像实地址模式那样将异常处理程序或中断服务程序的入口地址直接填入00000H -003FFH存储区,而是借助于中断描述符表来获得异常处理程序或中断服务程序的入口地址。
保护模式下,通过中断描述符表(Interrupt Descriptor Table,IDT)获得异常处理或中断服务程序入口地址,它是OS内核中的一个表,共有256个表项,每个表项占8个字节,共占用2KB。每一个表项是一个中断门描述符、陷阱门描述符或任务门描述符。
IDTR寄存器中存放 IDT在内存的首地址。
中断门描述符格式如图所示:
-
段选择符用来指示异常处理程序或中断服务程序所在段的段描述符在GDT中的位置,其RPL=0;
-
偏移地址则给出异常处理程序或中断服务程序第一条指令所在偏移量。
-
P:Linux总把P置1,表示段存在。(因为它从来不会把一个段交换到磁盘上, 而是以页面为单位交换。 )
-
DPL:访问本段要求的最低特权级。主要用于防止恶意应用程序通过 INT n 指令模拟非法异常而进入内核态执行破坏性操作。Linux把除了3、4、5、128外的绝大多数中断类型包括自定义中断类型,都设置为了0,因此漏洞较小。
-
DPL后一位为0,再后4位是TYPE。
-
TYPE:标识门的类型。TYPE=1110B为中断门;TYPE=1111B为陷阱门;TYPE=0101B为任务门。
IA-32中,每条指令执行后,下条指令的48位逻辑地址(虚拟地址)由16位段描述符CS和32位段偏移量EIP指示 。
每条指令执行过程中,CPU会根据执行情况判定是否发生了某种内部异常事件,并在每条指令执行结束时判定是否发生了外部中断请求。
(由此可见,异常事件和中断请求的检测都是在某一条指令执行过程中进行的,显然由硬件完成)
在CPU根据CS和EIP取下条指令之前,会根据检测的结果判断是否进入中断响应阶段
(异常和中断的响应也都是在某一条指令执行过程中或执行结束时进行的,显然也由硬件完成)
IA-32中中断检测过程


IA-32中异常和中断响应过程:
(1)确定中断类型号 i(硬件发现、中断控制器送CPU),从 IDTR寄存器指向的 IDT中取出第 i 个表项 IDTi。
(2)根据 IDTi 中段选择符,从 GDTR 指向的 GDT 中取出相应段描述符,得到对应异常或中断处理程序所在段的 DPL、基地址等信息。Linux下中断门和陷阱门对应的即为内核代码段,所以DPL为0,基地址为0。
(3)若CS寄存器低2位的当前特权级CPL<段描述符中的DPL或编程异常 IDTi 的 DPL<CPL,则发生13号异常。Linux下,前者不会发生。后者用于防止恶意程序模拟 INT n 陷入内核进行破坏性操作。
(4)若CPL≠DPL,则从用户态换至内核态,以使用内核栈。切换栈的步骤:
①读TR 寄存器,以访问正在运行的用户进程的TSS段;
②将TSS段中保存的内核栈的段选择符和栈指针分别装入寄存器 SS 和 ESP,然后在内核栈中保存原来用户栈的 SS 和 ESP。
(5)若是故障,则将发生故障的指令的逻辑地址写入 CS 和 EIP,以使处理后回到故障指令执行。其他情况下,CS 和 EIP 不变,使处理后回到下条指令执行。
(6)在当前栈中保存 EFLAGS、CS 和 EIP 寄存器的内容(断点和程序状态)。
(7)若异常产生了一个硬件出错码,则将其保存在内核栈中。
(8)将IDTi中的段选择符装入CS,IDTi中的偏移地址装入EIP,它们是异常处理程序或中断服务程序第一条指令的逻辑地址(Linux中段基址=0)。
下个时钟周期开始,从CS:EIP所指处开始执行异常或中断处理程序!
内核中的TSS段记录了每个进程的状态信息,例如,每个进程对应的页表、 task和mm等结构信息、内核栈的栈顶信息:
SS:ESP 等
IA-32中异常和中断返回过程:
中断或异常处理程序最后一条指令是从内核态返回用户态的IRET。CPU在执行IRET指令过程中完成以下工作:
(1)从栈中弹出硬件出错码(保存过的话)、EIP、CS和EFLAGS
(2)检查当前异常或中断处理程序的CPL是否等于CS中最低两位,若是则说明异常或中断响应前、后都处于同一个特权级,此时,IRET指令完成操作;否则,再继续完成下一步工作。
(3)从内核栈中弹出SS和ESP,以恢复到异常或中断响应前的特权级进程所使用的栈。
(4)检查DS、ES、FS和GS段寄存器的内容,若其中有某个寄存器的段选择符指向一个段描述符且其DPL小于CPL,则将该段寄存器清0。这是为了防止恶意应用程序(CPL=3)利用内核以前使用过的段寄存器(DPL=0)来访问内核地址空间。
执行完IRET指令后,CPU回到原来发生异常或中断的进程继续执行
四、Linux中的异常和中断处理
Linux利用陷阱门来处理异常,利用中断门来处理中断。
异常和中断对应处理程序都属于内核代码段,所以,所有中断门和陷阱门的段选择符(0x60)都指向 GDT 中的“内核代码段”描述符。
通过中断门进入到一个中断服务程序时,CPU 会清除 EFLAGS 寄存器中的 IF 标志,即关中断;通过陷阱门进入一个异常处理程序时,CPU 不会修改 IF 标志。也就是说,外部中断不支持嵌套处理,而内部异常则支持嵌套处理。
任务门描述符中不包含偏移地址,只包含 TSS 段选择符,这个段选择符指向 GDT 中的一个 TSS 段描述符,CPU 根据 TSS 段中的相关信息装载SS 和 ESP 等寄存器,从而执行相应的异常处理程序。
Linux中,将类型号为8的双重故障(#DF)用任务门实现,而且是唯一通过任务门实现的异常。
双重故障 TSS 段描述符在 GDT 中位于索引值为 0x1f 的表项处,即13位索引为0 0000 0001 1111,且其TI=0(指向 GDT),RPL=00(内核级代码),即任务门描述符中的段选择符为00F8H。
CPU负责对异常和中断的检测与响应,而操作系统则负责初始化 IDT 以及编制好异常处理程序或中断服务程序。Linux运用提供的三种门描述符格式,构造了以下5种类型的门描述符:
-
中断门:
DPL=0,TYPE=1110B。激活所有中断
-
系统门:
DPL=3,TYPE=1111B。激活4、5和128三个陷阱异常,分别对应指令into、bound和int $0x80三条指令。因DPL为3,CPL≤DPL ,故在用户态下可使用这三条指令
-
系统中断门:
DPL=3,TYPE=1110B。激活3号中断(即调试断点),对应指令int 3。因DPL为3,CPL≤DPL,故用户态下可使用int 3指令。
-
陷阱门:
DPL=0,TYPE=1111B。激活所有内部异常,并阻止用户程序使用INT n(n≠128或3)指令模拟非法异常来陷入内核态运行。
-
任务门:
DPL=0,TYPE=0101B。激活8号中断(双重故障)。
Linux内核在启用异常和中断机制之前,先设置好 IDT 的每个表项,并把IDT 首址存入 IDTR。系统初始化时,Linux完成对 GDT、GDTR、IDT和 IDTR 等的设置,以后一旦发生异常或中断,CPU就可通过异常和中断响应机制调出异常或中断处理程序执行。
Linux对异常的处理
异常处理程序发送相应的信号给发生异常的当前进程,或者进行故障恢复,然后返回到断点处执行。
例如,若执行了非法操作,CPU就产生6号异常(#UD),在对应的异常处理程序中,向当前进程发送一个SIGILL信号,以通知当前进程中止运行。
采用向发生异常的进程发送信号的机制实现异常处理,可尽快完成在内核态的异常处理过程,因为异常处理过程越长,嵌套执行异常的可能性越大,而异常嵌套执行会付出较大的代价。
并不是所有异常处理都只是发送一个信号到发生异常的进程。
例如,对于14号页故障异常(#PF),需要判断是否访问越级、越权或越界等,若发生了这些无法恢复的故障,则页故障处理程序发送SIGSEGV信号给发生页故障异常的进程;若只是缺页,则页故障处理程序负责把所缺失页面从磁盘装入主存,然后返回到发生缺页故障的指令继续执行。
所有异常处理程序的结构是一致的,都可划分成以下三个部分:
(1)准备阶段:在内核栈保存通用寄存器内容(称为现场信息),这部分大多用汇编语言程序实现。
(2)处理阶段:采用C函数进行具体处理。函数名由do_前缀和处理程序名组成,如 do_overflow 为溢出处理函数。
大部分函数的处理方式:保存硬件出错码(如果有的话)和异常类型号,然后,向当前进程发送一个信号。
当前进程接受到信号后,若有对应信号处理程序,则转信号处理程序执行;若没有,则调用内核abort例程执行,以终止当前进程。
(3)恢复阶段:恢复保存在内核栈中的各个寄存器的内容,切换到用户态并返回到当前进程的断点处继续执行。
Linux中异常对应的信号名和处理程序名如图所示:
异常处理在内核态信号处理在用户态
举例:
加上注释后,会出现如图结果,出现除法出错。如果是浮点数除法错,异常处理程序可以将结果用特殊值表示,而整数除法错则向当前进程发送一个SIGFPE信号,进程接受到信号后没有对应信号处理程序,调用内核abort例程执行,以终止当前进程。运行结果为“Floating point exception”。
去掉注释后会转信号处理程序执行,执行后可以继续运行完成。
Linux对中断的处理:
- 准备阶段(先行阶段)
- 处理阶段(具体的中断处理阶段)
- 恢复阶段
