9.8 异常条件(Exception Conditions)
以下几节详细的讨论可能的异常条件。每个类型都将分为错误(fault)、陷阱(trap)、或中止(abort)来讲述。这种分类有助于系统程序员重想引起异常的子程序。
错误: 当发生错误时,保存的CS和EIP将指向了引起错误异常的指令。
陷阱: 当发生陷阱时,保存的CS和EIP将指向引起陷阱的指令的下一条指令,且这下一条指令是动态的一下条。如果陷阱是在一条改变程序控制流的指令期间发生时,保存的CS和EIP则指向了控制改变后的程序的指令。例如,如果在执行一条JMP指令期间发生一个陷阱,压入堆栈的CS和EIP将是JMP的目的地指令,而不是在JMP下面的指令。
中止: 中止是不能精确定位引起异常的指令或不能重起指令的异常。中止用来报告严重的错误,如硬件出错或系统表的不一致性或错误。
9.8.1 中断0——除法错(Divide Error)
除法错误发生在当DIV或IDIV指令的除数为0时。
9.8.2 中断1—— 调试异常(Debug Exceptions)
处理器在很多情况下都会引发这个中断。这个异常是错误还是陷阱取决于以下条件:
指令地址断点错误(Intruction address breakpoint fault)
数据地址断点陷阱(Data address breakpoint trap)
通用错误(General detect fault)
单步陷阱(Single-step trap)
任务切换断点陷阱(Task-switch breakpoint trap)
处理器在这种情况下不会压入出错码。异常处理程序可以检察调试寄存器来决定是什么条件引起的异常。关于调试和调试寄存器,参看第12章。
9.8.3 中断3——断点(Breakpoint)
INT 3指令引发这个陷阱。INT3是一条一字节长的指令,这样就可以很容易地用断点操作码来替代代码段中操作码。操作系统或调试系统可以用一个数据段的别名来指向代码段,这样可以很方便地在任何地方放入INT3指令,来引起异常,从而进行一些进一步的操作。调试器经常在一个任务的关键地方,使用断点来显示寄存器、变量。
保存的CS:EIP值,指向了断点异常指令的下一个字节。如果一个调试器用调试操作码来替代了正常的指令,它必须要把保存的EIP减去1个字节的值。关于调试的信息,请参看第12章。
9.8.4 中断4——溢出(Overflow)
只有当处理器遇到INTO指令且OF(溢出位)标志设置时,处理器才引发这个异常。因为有符号数和无符号数使用同样的算数指令,处理器不能确定到底是哪一种操作,所以当发生溢出时,处理器并不自动引发异常。取而代之的是,如果被作为符号数处理的话,且有可能超出表示范围的话,处理器设置OF位。当对符号数操作时,仔细的程序员和编译器将自已测试OF位或使用INTO指令。
9.8.5 中断5——越界(Bounds Check)
当处理器执行BOUND指令时,且操作数超出了界限时,处理器将引发这个错误。程序可以使用BOUND指令来检察一个有符号的到指定内存区域的数组索引。
9.8.6 中断6——非法操作码(Invalid Opcode)
当执行部件遇到一条非法操作码的指令时,引发这个错误。(只有当执行到时,才会引发该异常,也就是说指令预取时取到一条非法操作数的指令时,并不会引发异常)。处理器不会压入出错码。异常可以在同一个任务中处理。
当指定类型的操作码的操作数非法时,也会引起该异常。例如,一条段间JMP去访问一个寄存器操作数、或LES指令访问一个寄存器源操作数。
9.8.7 中断7——协处理器不可用(Coprocessor Not Available)
当以下两种情况其中之一发生时,引发这个异常:
处理器则到一条ESC(escape)指令,且CR0(control register zero)中的EM(emulate)位设置时。
处理器遇到一条WAIT指令或和条ESC指令,且CR0中的MP(monitor coprocessor)位和TS(task switched)位都设置时。
关于协处理器,请检看第11章。
9.8.8 中断8——双重错(Double Fault)
通常,当处理器刚要调用一个先前的异常的异常处理程序时又检测到一个异常,两个异常可以被连续地处理。但是,如果处理器不能串行的处理他们,处理器引发一个双重错异常。当两个错误被声明为一个双重错误时,80386把异常分为3类:良性异常(benign exceptions)、贡献异常(contributory exceptions)、和页错误(page faults),图9-3显示了这种分类。
表9-4显示了引起双重错误的异常组合。
处理器总是压入一个出错码到双重出错的异常处理程序。但是,出错码总是0。出错的指令是不可重起的。如果在将要调用双重异常处理程序时,又发生了一个异常,处理器将停机。
9.8.9 中断9——协处理器段超出(Coprocessor Segment Overrun)
处理器在保护模式执行时,当向协的中间部分传送一个协处理器的操作数到NPX时,如果检测到一个页错误或段错误时,处理器引发这个异常。
9.8.10 中断10——非法TSS(Invalid TSS)
当在任务切换时,发现新的TSS非法时,处理器发生中断10。图9-5显示了非法的TSS的情况。出错码被压入堆栈,以帮助检察错误引发的原因。EXT位指示了当前的异常是否是由程序外界引起的。也就是通过一个任务门引起的外部中断切换到这个非法的TSS。
这个错误可能发生原先的任务中或发生在新的任务的上下文中。直到处理器完全检测了新TSS的存在后,异常发生在原先任务的上下文中。当新任务的TSS存在性被检测后,任务切换就算完成了。也就是说更新了TR。如果任务切换是由于CALL指令或中断,新任务的TSS中的返回链将被设置为当前的TSS(旧的TSS)。任一个在这时之后发生的错误将在新的任务中处理。
为了能在一个任务中正确的处理这个异常,异常10应该是通过任务门在一个新的任务中处理的。
9.8.11 中断11——段不存在(Segment Not Prosent)
当处理器发现一个描述符的存在位为0时,处理器引发异常11。处理器可能在以下情况下引发该错误:
当加载CS,DS,ES,FS,GS,寄存器时,但加载SS寄存器引发堆栈错误。
当用LLDT指令加载LDT寄存器时。在任务切换时加载LDT寄存器,则引发非法
TSS异常。
- 当使用一个不存在的门描述符时。
这样的错误是可以重起的。如果异常处理程序把段的存在位设置且返回后,被中断的程序将继续执行。
如果段不存在异常发生在任务切换过程中,任务切换的步骤没有完全完成。在任务切换的过程中,处理器首先加载所有的段寄存器,然后检察它们内容的有效性。如果一个段不存在异常被检测到,那么剩下的段寄存器的值将是未经过检测的,所以可能是不可用来访问内存的。异常处理程序不应该在没有引起另一个异常前依赖于此时的CS,SS,DS,ES,FS和GS。异常处理程序应该在恢复新任务之前首先检测所在段寄存器。否则,通用保护异常可能会随后发生,以致错误检测将更加困难。有3种方法来处理这种情况:
1、 在一个任务中处理段不存在异常。当切换回被中断的任务时,处理器将从TSS加载时检测寄存器的有效性。
2、 压入和弹出所有段寄存器。每一条POP指令将引起处理器检测段寄存器的新内容。
3、 细查TSS段中存储的每一个段寄存器映象,模拟处理器在加载段寄存器时的检测。
这个异常压入一个出错码到堆栈上。EXT位指出是否是外部事件引起的段不存在的异常。如果出错码访问的是IDT的项,I位将被设置,也就是说一条INT指令访问了一个不存在的门。
操作系统通常使用“段不存在”异常来实现基于段的虚拟内存。但是,一个门描述中的不存位,通常不是指段的不存在(因为门不一定要对应着一个段)。门描述符的不存在可以被操作系用来引发一个特别重要的异常。
9.8.12 中断12——堆栈异常(Stack Exception)
堆栈异常通常发生在以下两种情况下:
在使用SS寄存器来访问内存时,如果发生了任何的界限违例。这包括了基于堆栈的指令,如POP,PUSH,ENTER,还有LEAVE,当然还有其它的一些隐式使用SS的内存访问(例如,MOV AX, [BP+6])。当堆栈太小而不能容纳指定的局部变量时,ENTER指令将引起这个异常。
当加载一个选择子到SS寄存器时,且该选择子指向一个标识为不存在但有效的描述符。这种情况可能发生在任务切换中、段间CALL指令、段间返回、LSS指令、或者一条向SS加载的MOV或POP指令。
当处理器发现堆栈异常时,它会压入一个出错码到异常处理程序的堆栈上。如果异常是由堆栈段不存在或在段间CALL指令间时的新堆栈溢出的话,出错码包含了出问题的段的选择子(异常处理程序可以测试描述符的存在位来确定是哪个异常发生的)。否则出错码为0。
造成这种异常的指令在所有情况下都是可重起的。被压入入异常处理程序堆栈的返回地址指向了一条需要重起的指令处,一般来说就是引起异常的指令。但是,在一个任务切换过程中,加载不存在的堆栈段寄存器时,它指向了新任务的第一条指令。
当堆栈错误在任务切换时发生的话,段寄存器不能再用来内存访问了。在任务切换中,选择子是在描述符被检察之前加载到寄存器里的,所以可能并不能用来作内存访问。堆栈异常处理程序不应在未引起另一个异常之前依赖于CS,SS,DS,ES,FS和GS中的值。异常处理程序应该要在重起任务前检测所有的段寄存器的值。否则,通用保护异常错误将会使以后的错误调试更加困难。
9.8.13 中断13——通用保护异常(General Protection Exception)
所有保护模范规则的违例,如果没有引起另一个异常,将引起一个通用保护异常。这些包括(但不局限于):
1、 当使用CS,DS,ES,FS,或GS,做内存访问时的段界限超出。
2、 访问描述符表时的界限超出。
3、 向一个不可执行的段作控制转移。
4、 向一个只读段或一个代码段写入数据。
5、 从只执行的代码段内读取数据。
6、 用一个指向只读描述符的选择子加载SS寄存器(除非选择子来自于任务切换过程时的TSS段中,这种情况将引发一个TSS异常)
7、 把系统段描述符加载到SS,DS,ES,FS,或GS。
8、 把一个不可读的执行代码段描述符加载到DS,ES,FS或GS中。
9、 将代码段描述符加载到SS寄存器。
10、 DS,ES,FS,和GS中包含一个空选择子(null selector)来访问内存时。
11、 向一个正忙的任务切换。
12、 违反特权级规则。
13、 把一个PG=1而PE=0的值加载到CR0内。
14、 通过中断门或陷阱门从V86模式转移到不是特权级0时。
15、 执行一个长度大于15个字节的指令(只可能发生在达多的前缀用于一条指令前时)。
通用保护异常是一个错误。在响应这个异常时,处理器压入一个出错码到异常处理程序的堆栈上。如果在加载一个描述符时,发生异常,出错码包含了指向此描述符的选择子。否则,出错码为空。出错码中的选择子可能来自以下:
1、 指令操作数。
2、 一个指令操作数中的门,选择子在门中。
3、 在任务切换过程中,在TSS中的选择子。
9.8.14 中断14——缺页异常(Page Fault)
当启用了分页后(PG=1),当处理器将线性地址转换到物理地址时,检测到以下情况中的一个将引发一个异常:
所需要的页目录或页表项的存在位为0时。
当前子程序没有足够的权限来访问指定的页面。
处理器为缺页异常处理程序提供以下两种信息以便异常的诊断和从异常中恢复:
- 一个在堆栈上的出错码。为缺页异常提供的出错码和一般的异常的出错码格式有所不同(见图9-8)。出错码告诉异常处理程序以下三件事:
1、引起异常的原因是由于页不存在还是没有足够的权限来访问指定的页。
2、异常发生时,处理器是处于用户模式还是处于超级用户模式。
3、在访问内存时,是读操作还是写操作。
- CR2 (控制寄存器2)。处理器把引起异常的线性地址放在CR2中(如图9-9)。异常处理程序可以使用这个线性地址来定位页目录项和页表项。如果在这个异常处理过程中,允许另一个缺页异常产生的话,异常处理程序应该负责把CR2压入到堆栈中。
9.8.14.1 在任务切换中的缺页异常(Page Fault During Task Switch)
在任务切换时,处理器可能访问以下4个段:
1、 把当前的任务状态写入到它的任务状态段中。
2、 读取GDT来定位新任务的TSS描述符。
3、 读取新任务的TSS,以便检测段描述符的类型。
4、 可能会读取新任务的LDT,以便来检测存储在新任务TSS中段寄存器。
当访问他们中的任意一个段时,都可能出现缺页异常。在后两种情况下,异常算发生在新任务的上下文里。保存的指针指向新任务的下一条指令,而不是引起任务切换的指令。如果操作系统的设计允许在任务切换时发生缺页异常,缺页异常错误应该通过一个任务门来处理。
9.8.14.2 缺页错误内的不一致堆栈指针(Page Fault with Inconsistent Stack)
为了保证在缺页异常中不会让处理器使用非法的堆栈指针(SS:ESP),应该特别注意。在80386早期写的软件常常使用一对指令还改变堆栈,如:
MOV SS,AX
MOV SP,StackTop
在80386下,第二条指令要访问内存,可能会在SS改变后而在SP改变前发生缺页异常。这时,堆栈的两部分SS:SP(或,对于32位程序来说,SS:ESP)将不一致。
如果在处理缺页异常时,发生了堆栈切换到一个定义好的堆栈(也就是说处理程序是一个任务或是一个特权级更高的子程序)的话,处理器就不会使用不一致的堆栈指针。即使这样,如果缺页异常是在一个陷阱门或中断门时处理的,且缺页异常处理程序和发生缺页异常的程序是在同一特权级的话,处理器会使用当前的(非法的)堆栈指针。
在实现分页和缺页异常处理程序在同一任务内(用陷阱门或中断门)的系统里,同特权级的软件应该用新的LSS指令,而不要使用一对上面那样的指令,来初始化堆栈。当缺页异常处理程序在特权级0(正常情况下应该是)执行时,问题则只局限在特权级0的代码,一般说来是操作系统内核。
9.8.15 中断16——协处理器错(Coprocessor Error)
当处理器从ERROR#引脚发现一个80287或80387发送的一个报告时,处理器引发这个异常。80386只在一定情况下的ESC指令或者当遇到WAIT指令且MSW中的EM位为0时(没有摸拟)执行前才检测这个引脚。关于协处理器的信息,请参看第11章。