上下文保存 中断_IA-32的中断和异常处理
在os的内容中,我们很多次说到中断和异常,不过都没有详细的介绍,这里会有一个详细的介绍。
1.什么是中断和异常(INTERRUPT AND EXCEPTION),它们在什么场景下产生?
你在图书馆看书,突然一个电话来了,说你家狗丢了,那么你肯定要去找狗。可能你很快找到了,回去继续看书,也有可能没有那么容易找到,你就不回去看书了。这个比如对应到计算机中,你是处理器,你家狗丢了这个通知就是中断和异常的信号,你去找狗就是中断和异常的处理。你回去继续看书,就是中断和异常处理完成后继续执行之前的任务,不能回去看书,就是一些中断和异常会导致用户程序被os kill。
每种类型的中断和异常,我们都会进行编号,称为vector number。处理器支持从0到255的编号,最多256种情况。其中前32个是cpu自己用的,剩下的交给开发者。下面的表6-1显示对应编号的具体中断和异常类型:
那么在计算机中,中断和异常在哪些情况下会产生呢?
1.中断:
1.外部的硬件产生(比如有一个网络I/O来了)。cpu有对应的引脚来接受中断信息,分别为INTR和NMI。从INTR通知的中断信号是可以屏蔽的,通过EFLAGS中的IF标志来控制。NMI的中断必须处理,并且NMI对应的中断号默认是2.
2.程序自己通过调用INT指令产生。比如INT 35 就表示强制执行35号中断的处理程序。
2.异常
处理器的异常有以下三个来源:
1.指令执行的时候产生的错误。对于每种错误处理器都会有相应的编号。异常通常被分为这三种不同的类型:faults,traps和aborts
2.使用INTO, INT 3, 和 BOUND命令产生。不过使用INT n指令来产生指令是有限制的。如果INT n指令指定的vertor number是处理器定义的异常。这种情况如果是硬件正常产生通常是会有错误代码的,但是如果是指令产生这种异常,处理器在处理的时候是不会将错误代码进行压栈的。这样在进行异常处理的时候,异常处理程序仍然会去对错误代码进行出栈操作,因为没有错误代码,所以pop的时候就会将eip设置为错误数据,这样返回的时候执行点就不是之前的了。
3.机器检查的异常。P6和Pentium系列的处理器提供了内部和外部的机器检查机制来检查内部芯片硬件的执行和总线事务。如果这种检查发现了错误,那么处理器会发起一个vector 18的机器检查异常并且返回一个错误代码。
上面说到异常会被分为faults,traps,aborts.这是根据这些异常产生的方式和异常处理后重新开始执行指令的地点来分类的。
- Faults — 一个fault异常通常是可以修复的,并且如果被修复了,那么程序可以重新得到执行从而不受影响。对于fault,执行完异常对应的处理程序后,返回地址是产生fault的指令。
- Traps — 一个trap异常是在运行一个trapping指令的时候立刻产生的。对应的返回地址是trapping指令后的那条指令。
- Aborts—这种异常产生的时候一般是不会有详细的错误指令地址的,并且程序也不会得到重新执行,可以理解为发生了一个无法挽回的错误。
不可屏蔽的中断NMI (NONMASKABLE INTERRUPT)
NMI可以通过下面2种方式产生:
- 外部的硬件通过NMI引脚产生
- 处理器在系统总线上收到相应的NMI消息
不管是这2种中的哪一种,处理器都会立刻调用NMI中断对应的中断处理程序。并且在处理程序执行期间保证没有其他的中断产生,包括NMI中断。当然,这2种方式产生的NMI中断不会被EFLAGS中的IF标志位屏蔽。
还有一种方式,也就是通过INTR引脚产生可屏蔽中断,不过中断的vector number是2。这种模拟的方式并不是真正的NIM中断,所以在进行相应处理的时候,处理器对应的专门用来支持NMI中断的硬件并不会工作,
当NMI中断在处理的过程中,处理器会阻塞其他的NMI中断直到中断处理程序的iret调用完成(也就是说中断处理程序已经执行完成了)。所以可以看出NMI中断处理程序是不允许嵌套执行的。另外,如果iret指令执行的时候产生了一个错误,是不会影响NMI中断被打开的。
EFLAGS中的IF标记位可以用来开启或者屏蔽INTR引脚的中断,不过不会影响NMI中断的接受,处理器产生的异常也不会受到影响。可以分别使用STI(set interrupt-enable flag )和CLI (clear interrupt-enable flag) 来设置和清除IF标志位。下面的三种情况也会对IF标志位产生影响。
- PUSHF指令会将EFLAGS中的内容保存到栈中,POPF会对应的将栈中的数据加载到EFLAGS中,这也许会改变IF的值。
- 任务切换的时候,POPF和IRET会被调用,所以IF指令也许会被修改
- 当一个中断处理程序是使用中断gate调用的时候,IF是自动被清除的,所以可屏蔽中断就都会被屏蔽了。
当我们在切换栈的时候,可能会使用如下的2个指令来完成操作:
MOV SS, AX
MOV ESP, StackTop
但是问题在于,如果第一条指令执行完成后,发生了中断或者异常,那么在中断或者异常的处理程序执行的过程中,对应的栈的地址空间就是异常的了。为了解决这个问题,处理器在修改ss寄存器的命令后禁止中断,debug异常,single-step trap ,直到下一条指令执行完毕。当然如果使用LSS命令来修改SS寄存器的值就不会出现这个问题了。
中断和异常的优先级
如果有多个中断或者异常被挂起,处理器会按照一个先后顺序来进行处理。下面的表6-2给出了各种异常和中断的优先级。
上面表中的分类等级在各个架构中都是统一的,不过在各个分类中不同的中断或者异常的优先级各个架构就会不一样了。处理器会首先处理最高等级的异常或者中断。低等级别的异常会被丢掉,低等级别的中断会被挂起。当中断处理程序返回到程序或者任务产生异常或者中断的地方,被丢掉的异常会再次产生,中断描述符表IDT( INTERRUPT DESCRIPTOR TABLE )
接收到中断后,处理器应该怎么处理呢?首先每种类型的中断和异常对应的处理程序可以在IDT中找到。和GDT和LDT类型,IDT也是一个数组,其中的元素就是8byte大小的门描述符(gate descriptor)。你可以把IDT放在任何位置,然后将对应的线性地址基地址记载IDTR寄存器中就可以了,下面的图6-1展示了2者的关系和应用。
IDT中包含3中类型的门描述符:
1.任务门描述符 Task-gate descriptor
2.中断门描述符 Interrupt-gate descriptor
3.陷阱门描述符 Trap-gate descriptor
下面的图6-2显示了各种描述符的内容格式:
下面我们来看下具体的中断和异常处理的过程。
处理器对异常和中断处理程序的调用,类似于使用CALL指令调用一个过程或者任务一样。首先根据中断或者异常编号在IDT中找到对应的描述符。如果是trap-gate 或者Interrupt-gate,那么就类似于使用CALL命令调用一个过程。如果是task-gate,则会进行上下文切换,执行task-gate对应的任务,类似于CALL调用一个task gate。图6-3显示了这个过程:
如果中断和异常处理过程的权限和目前代码的权限不同:
1.从当前任务的TSS中获取相应权限的栈信息,在新的栈上,处理器将被中断的过程的段选择子和栈指针信息入栈
2.处理器将当前过程的EFLAGS, CS, 和 EIP 寄存器的值入栈
3.如果有错误代码,那么也入栈
如果相同:
1.将EFLAGS, CS, 和 EIP 寄存器的值入栈
2.如果有错误代码也入栈
下面的图6-4显示了这2种不同情况:
不管是通过interrupt gate还是trap gate来访问异常或者中断的处理程序,处理器都会在保存了EFLAGS 寄存器值后将TF标志位清空。清除 TF标志位可以防止指令调制影响中断的响应。 IRET指令会将TF的值重新恢复。
interrupt gate还是trap gate唯一区别在于对于IF标志位的使用。interrupt gate在调用的时候会清除IF标志位,但是trp gate不会。
如果异常或者中断的处理程序是task gate,那么在进行异常或者中断处理的时候会产生任务的切换,下面是使用任务来作为中断或者异常处理的优势:
- 被中断的过程或者任务的上下文会被自动保存
- 新的任务有新的栈可以使用,可以防止原有栈的问题导致系统出现故障
- 可以定义新的地址空间,做到任务的分离。
当然劣势也很明显,就是任务切换太耗费资源,所以效率很慢。
下面的图6-5是调用过程的图示。