2.4 Handling & Error Code
[Home] [Top] [Previous] [Next]
2.4.1 Overview
当一个Interrupt/Exception发生时,CPU将以这个Interrupt/Exception的Vector Number为索引,到IDT中查找相应的Gate Descriptor。IDT中的Gate Descriptor有3种:Interrupt Gate Descriptor,Trap Gate Descriptor,Task Gate Descriptor。根据被设置的Gate Descriptor的不同,对ISR的调用方法和处理过程也不同。
当某些Exceptions发生时,会返回Error Code,以通知相应的ISR是那个Segment引起了这个Exception。
2.4.2 ISR Handling
一个ISR(Interrupt Service Routine)是一块专门用来处理某个Interrupt/Exception的程序。
如果一个Interrupt/Exception引用的是一个Interrupt Gate或Trap Gate,则当这个Interrupt/Exception发生时,不发生任务切换,其ISR仍然引用当前任务的Context。Interrupt/Trap Gate Descriptor中的Segment Selector引用的是一个放在GDT或当前LDT中的可执行代码段。Offset域引用的是其ISR的入口地址。
当CPU调用一个ISR时,会将EFLAGS,CS,EIP寄存器压栈,其中CS,EIP寄存器的内容事实上是ISR的返回地址。如果一个Exception发生时,存在一个Error Code,那么这个Error Code会在EIP之后被压栈。
如果ISR和被打断的任务具有相同的特权等级,则CPU不改变Stack,让ISR继续使用当前的Stack。
如果ISR的特权等级从数字上小于被打断任务的特权等级,也就是说如果ISR的权限大于被打断任务的权限,则会发生Stack Switch。为了能够在ISR结束之后,能够恢复使用原来的Stack,CPU会将SS和ESP寄存器的内容也压栈,并且这些内容都被压在新的Stack中,而不是Stack Switch之前的那个Stack。
问题是,CPU在Interrupt/Exception发生的时刻,发现特权等级不一致的时候,如何知道该使用那个栈?答案是通过当前任务的TSS。在TSS中存放着针对0,1,2三个特权等级的SS和ESP内容,当特权等级切换到某个特权等级的时候,CPU会从当前任务的TSS中找到相应特权等级的SS和ESP用来进行Stack Switch。
当ISR执行结束之后,需要继续执行之前被终止的任务,为了能够正确的做到这一点,ISR的返回必须使用IRET指令。IRET指令会自动从Stack中恢复EFLAGS寄存器的值,并根据Stack中的CS,EIP内容挑转到被中断的位置。如果之前在Interrupt/Exception发生的时候,进行了Stack Switch,此时IRET会根据栈中的SS,ESP的内容将Stack切换回去。
当通过一个Interrupt Gate或Trap Gate来处理一个Interrupt/Exception时,CPU会在将EFLAGS压栈之后,清除EFLAGS寄存器的TF flag。清除TF flag,是为了避免Instruction tracing影响中断的响应。随后的IRET指令会从栈中的EFLAGS中恢复TF flag的值。
在Interrupt Gate和Trap Gate之间的唯一区别是,当通过一个Interrupt Gate来处理一个Interrupt/Exception时,CPU会在将EFLAGS压栈之后,清除EFLAGS寄存器的IF flag。清除IF flag,是为了避免在当前ISR执行时受到其它Interrupts的影响。随后的IRET指令会从栈中的EFLAGS中恢复IF flag的值。而通过Trap Gate来处理Interrupt/Exception则不会影响IF flag的设置。
需要注意的是,在恢复EFLAGS的时候,只有CPL=0的时候,IOPL域才能够被恢复;只有CPL在数值上小于或等于IOPL时,IF位才能够被恢复。
下面是用C++实现的IRET指令:
inline void iret(void)
{
__asm__ __volatile__ ("iret": : :"memory");
}
Task Gate
如果调用Task Gate,会引起Context切换。如果Interrupts/Exceptions使用这种方式的话,会有如下几个优点:
ISR可以被放在一个独立的地址空间中,这种情况下,你需要通过设定一个独立的LDT。
但也必须注意它的缺点:由于在任务切换时,当时的机器状态需要全部被保存,所以它的性能要比Interrupt Gate和Trap Gate低。从而导致中断延迟越来越多。
2.4.3 Task Restart
为了能够保证一个被中断的Task能够在ISR运行结束后继续运行,除了Aborts之外的其他Exceptions,会保证在引起异常的精确指令位置来触发这个Exception。而所有的Interrupts则保证在一个指令结束后才会被触发。也就是说Interrupts/Exceptions都不会破坏指令的原子性。
由于Fault类的Exception是一种可恢复异常,其ISR就是为了处理造成异常的条件,等处理结束后,CPU会返回重新执行那条引起异常的指令。一个最常用的例子是Page-fault Exception——当系统引用一个Page时,如果这个Page不在RAM中,会引起Page-fault Exception,而其ISR所作的就是将这个Page以某种算法调入RAM,这时候,引起Exception的条件已经解除了,当ISR结束之后,重新去执行那条引用此Page的指令,就不会再引起任何异常。所以,对于Fault类Exception,当发生时,在Stack中保存的CS和EIP的内容应该是那条引起Exception的那条指令的位置。
而Trap类Exceptions用于在进行下一步操作之前的条件检查,比如INTO指令用于在执行下一条指令之前检查EFLAGS的OF flag,如果OF flag被设置,则引起一个Overflow Exception;再比如BOUND指令用于在进行数组操作之前检查一个对某个数组的索引是否越界,一旦越界,则会引起一个Bound Range Exceed Exception;而这些Exceptions的ISR被用作消除这些引起异常的条件,然后指令从下一条继续执行。另外,Trap类Exception也被用作调试目的,如INT 1和INT 3,一旦执行这两个指令,则会引起一个Debug Exception或Breakpoint Exception;这时候ISR的目的被用作暂时阻塞当前任务的运行,等阻塞解除后,任务可以从下一条指令继续进行。这里所说的"下一条",指的不是指令物理顺序,而是指令执行顺序的。比如一个JMP指令执行时发生Trap Exception异常,那么等ISR执行结束后,继续执行的是JMP指令要跳转去的位置的那条指令。
Abort类的Exception不支持可靠的Task Restart。Abort ISR被设计为用来收集关于Abort Exception发生时的诊断信息,然后尽可能优雅的当掉Application和系统。
Interrupts严格的支持被中断程序的Restart。被保存在Stack中的CS, EIP内容指向Interrupt发生时执行结束的那条指令之后的那条指令。如果当时被执行的指令有循环前缀(比如REP),则CPU会等到当前这次循环结束,相关寄存器被设置为下一次循环的内容之后,才会引发Interrupt。
2.4.4 Error Code
如果一个Exception Condition与一个段相关的时候,那么当这个Exception发生的时候,CPU会将一个Error Code压入ISR所使用的Stack中。
Error Code的格式如下图所示:
当一个带有Error Code的Exception发生时,一个Error
Code会被压入栈顶,但必须注意的是,IRET指令并不Pop,所以ISR程序在执行IRET之前必须明确的删除它。
当一个Exception是通过INT n指令产生的时候,即使对于那些产生Error Code的异常来说,CPU也不会将Error
Code压栈。