异常和中断处理

IA-32中异常和中断的处理

  1. 引导程序被读到内存后,开始执行引导程序,以装入操作系统内核,并对GDT,IDT等进行初始化,系统启动后,进入保护模式。
  2. IA-32中,每条指令执行后,下条指令的逻辑地址(虚拟地址)由CS和EIP指示 实地址模式下:指令地址=cs<<4+IP
  3. 每条指令执行过程中,CPU会根据执行情况判定是否发生了某种内部异常事件,并在某条指令执行结束时判定是否发生了外部中断请求(由此可见,异常事件和中断请求的检测都是在某一条指令执行过程中进行的,显然有硬件完成)
  4. 在CPU根据CS和EIP取下条指令之前,会根据检测的结果判断是否进入中断响应阶段(异常和中断的响应也都是在某一条指令执行过程中或执行结束时进行的,显然也由硬件完成)

IA-32中异常和中断响应过程

首先介绍一些名词,用下面的图片。
段描述符表:
IA-32处理器把所有段描述符按顺序组织成线性表 放在内存中,称为段描述符表。分为三类:全局描述符表GDT,局部描述符表LDT和中断描述符表IDT。GDT和IDT在整个系统中只有一张,而每个任务 都有自己私有的一张局部描述符表LDT,用于记录本任务中涉及的各个代码段、数据段和堆栈段以及本任务的使用的门描述符。

上篇文章也说过,中断描述符表(INterrupt Descriptor Table,IDT)是OS内核中的一个表。
异常和中断处理

• 为使能移植到绝大多数流行处理器平台, Linux简化了分段机制
• RISC对分段支持非常有限,因此Linux仅使用IA-32的分页机制,而对于分段,则通过在初始化时将所有段描述符的基址设为0来简化
若把运行在用户态的所有Linux进程使用的代码段和数据段分别称为用户代码段和用户数据段;把运行在内核态的所有Linux进程使用的代码段和数据段分别称为内核代码段和内核数据段,则Linux初始化时,将上述4个段的段描述符中各字段设置成下表中的信息
异常和中断处理
内核中的TSS段记录了每个进程的状态信息,例如,每个进程对应的页表,task和mm等结构信息,内核栈的栈顶信息:SS:ESP等

IA-32中异常和中断响应过程

  1. 确定中断类型号i,从IDTR指向的IDT中取出第i个表象的IDTi
  2. 根据IDTi中段选择符,从GDTR指向的GDT取出相应段描述符,得到对应的异常或中断处理程序所在段的 DPL、基地址等信息。Linux下中断门和陷阱门对应的即为内核代码段,所以DPL为0,基地址为0。
  3. 若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(CS中为异常处理程序或中断服务程序的基地址),IDTi中的偏移地址装入EIP,它们是异常处理程序或中断服务程序第一条指令的逻辑地址(Linux中段基址=0)
  9. 下个时钟周期开始,从CS:EIP所指处开始执行异常或中断处理程序!

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回到原来发生异常或中断的进程继续