x86/x64体系探索及编程
上QQ阅读APP看本书,新人免费读10天
设备和账号都新为新人

第5章 了解Flags

在这一章里,我们来好好地了解eflags寄存器,它看似简单,其实还真的不像想象中那样简单。

Flags?Eflags?Rflags?它到底是多少位的?

在x86/x64上,除非使用Pentium 4和Athlon 64之前的处理器,否则Flags都应该被扩展为64位,因为从AMD的Athlon64处理器和Intel后期的Pentium 4处理器开始,都支持x86-64技术,后来慢慢将x86-64体系称为x64以对应于x86。

pushf                            ; 压入16位的flags(低16位)
pushfd                           ; 压入32位的eflags(低32位)
pushfq                           ; 压入64位的rflags

因此在x64机器统称为rflags似乎更合适,可是在legacy mode里flags还是被称为eflags。在上面的使用中,PUSHF是压入低16位,PUSHFD是压入低32位,而PUSHFQ是压入全部的64位。

它们的Mnemonic形式不同,可是opcode码是一样的,压入多少取决于operandsize(操作数的大小),在16位的operand size下,压入16位,在32位的operand size下,压入32位,在64位的operand size下,压入的是64位。与PUSHF/D/Q指令相对应的是POPF/D/Q指令,它们在助记符上有着同样的意义。

上面是32位下的eflags寄存器,在64位下的rflags寄存器高32位为保留位。按Intel的分类,在eflags寄存器里可以分为status flags(状态标志位)、control flags(控制标志位)和system flags(系统标志位)。

控制标志位

control flags只有一个DF(Direction Flags)标志位(bit10),它使用在LODSx,STOSx,MOVSx,SCASx,OUTSx,以及INSx这类串指令,指示串指令的指针方向。

DF标志所表达的意思是(以movsb指令为例)在一个循环里:

if (eflags.DF == 0)
{
       buffer[edi++]=source[esi++];        /* 指针 edi 和 esi 都是递增 */
}
else if (eflags.DF == 1)
{
      buffer[edi--]=source[esi--];        /* 指针 edi 和 esi 都是递减 */
}

当DF=0时,串指令中的edi和esi寄存器加1递增,DF=1时,edi和esi寄存器减1递减。在递增的情况下,源串和目标串指针应该指向起始点;在递减的情况下,源串和目标串指针应该指向终止点,这是软件设计者的职责。

5.1 Eflags中的状态标志位

status flags包括:OF(溢出标志),SF(符号位标志),ZF(零标志),AF(调整位标志),CF(进位标志),以及PF(奇偶位标志)。这些标志位反映了指令执行结果的状态值。

PF标志位

指令判断结果值的最低字节(byte 0),而设置相应的PF标志位,如下所示。

当最低字节(byte 0)中位为1值的数量是偶数PF标志被置位,否则被清0。

AF标志位

当运算时bit 3发生向上进位或借位时,AF标志被置位。AF标志位使用在BCD码的运算指令上,如下面使用AF标志位的例子。

mov al,8             ; al=0000 1000B
mov bl,9             ; bl=0000 1001B
add al,bl            ; al=0001 0001B,AF标志为1
aaa                     ; 使用 AF 标志进行调整,AX的结果是:00000001 00000111B

在上面的8+9式子里,bit 3向bit 4进1位,AF标志为1。AAA指令根据AF标志进行调整后,AX的值变成0107H(BCD码形式)。

5.1.1 signed数的运算

status flags标志位中有一部分用于表达signed(符号)数运算结果的状态,一部分用于表达unsigned(无符号)数运算结果的状态。而ZF标志位可以使用在signed和unsigned数上。

signed数运算中使用的标志位有:OF(溢出)标志和SF(符号)标志。

5.1.1.1 溢出位和符号位的产生

对于signed(符号数)的溢出,有两种情况。

① overflow(向上溢出):当结果值超出signed数的最大值时产生overflow。

② underflow(向下溢出):当结果值超出signed数的最小值时产生underflow。

当结果产生overflow或underflow时会对OF标志位置位。

overflow的产生

我们看看下面的2个正数相加的式子,为了计算方便,以4位的signed数为例。

  • 式子1:7+6。
  • 式子2:3+4。

如上面所示:式子2的运算是正确的。而在式子1中的+7与+6相加里,结果值却是-2,显然这是错误的。因为这个4位符号数的结果值超出了正数最大值7,而产生了overflow。因此,在这个计算结果中eflags.OF=1(溢出标志被置位),eflags.SF=1(符号标志位被置位)。

记录下来:两个正数相加,结果为负数时,产生了overflow。

underflow的产生

同样以4位数为例,再看看2个负数相加的式子。

  • 式子1:(-4)+(-8)
  • 式子2:(-4)+(-1)

在式子1中:(-4)+(-8)=(+4)两个负数相加结果为正数,显然是错误的。4位数的负数最小值是-8,而-4加上-8的值应为-12,它也超出了4位符号数的最小值,产生了underflow,这时eflags.OF=1,eflags.SF=0。

式子2中:(-4)+(-1)=(-5)这个值是正确的,这时eflags.OF=0,eflags.SF=1。值得注意的是,在这两个式子中都产生了进位。因此这两个式子中,CF标志位也被置位。

记录下来:两个负数相加,结果为正数时,产生了underflow溢出。

那么,当正数和负数相加时,情况又如何呢?

上面的2个正数与负数相加的式子中,它们的值都是正确的,OF标志都为0(没有溢出)。式子1中SF标志为0,式子2中的SF标志为1。

记录下来:正数和负数相加,不会产生溢出。

OF标志和SF标志也将影响到条件指令的执行,在x86上有下面几类条件指令族:Jcc指令家族,SETcc指令家族,LOOPcc指令家族,以及CMOVcc指令家族。这些指令助记符中cc代表一个条件码助记符。

5.1.1.2 signed数的比较操作

上面的OF、SF及ZF标志都用于signed数的比较。在执行cmp指令比较时,是对两个数进行相减操作,将比较的结果反映在标志位上。

-1>-2?4>-6?这两个比较式子如何反映在标志位上?

计算(-1)-(-2)和(4)-(-6)的结果,从eflags标志位上获得比较结果,如下所示。

在式子1中,-1减-2的结果是SF、OF以及ZF标志位都是0;式子2中,+4减-6的结果产生了overflow,因此OF标志与SF标志都为1。

对于这两个比较式子,我们知道前面的数都大于后面的数,因此得到的结论如下。

记录下来:当OF==SF时,比较结果是大于。

再看看-1>2和-3>6这两个比较式子,我们知道前面的数都小于后面的数,那么标志位上是什么呢?

在式子2的计算中,由于负数减正数结果值为正数而产生了underflow,因此OF标志被置位。可以看出,这两个式子中,ZF为0,SF与OF标志位都不相等。我们得到的结论是:

记录下来:当OF<>SF时,比较结果是小于。

signed数的条件码

基于SF标志、OF标志,以及ZF标志位,下面是用于signed数的条件码。

G (greater)                 :OF == SF 并且 ZF=0
L (less)                     :OF <> SF
GE (greater or euqal)     :OF == SF
LE (less or equal)         :OF <> SF或者ZF=1

在GE(大于等于)的情况下只需要判断OF是否等于SF标志,无论ZF是否为零都满足条件。而在L(小于)的情况下只需要判断OF不等于SF标志就可以了,也不需要判断ZF标志。

5.1.2 unsigned数的运算

ZF标志和CF标志被用在与unsigned数相关的运算里,在unsigned数的相关比较中不会使用OF和SF这两个标志位。

在x86上,尽管对于数的运算,指令会同时依据unsigned和signed数的结果对OF、SF、CF,以及ZF、AF和PF做相应的设置。可是,在unsigned与singed数与条件相关的指令中会做出相应的区分。

5.1.2.1 进位标志的产生

在相加运算中,由于向前进位而使用CF标志置位。在相减运算中,由于向前借位也会使CF标志置位。

-4加-8产生了进位,+4减-6产生了借位,这两个计算结果都会使CF标志置位。

5.1.2.2 unsigned数的比较及条件码

当unsigned数相减时,如果不够减则会产生借位(eflags.CF=1),表明是小于关系。下面是用于unsigned数的条件码。

A (Above)                   :CF=0并且ZF=0
B (below)                   :CF=1
AE (Above or euqal)       :CF=0
BE (below or equal)       :CF=1或者ZF=1

这与signed数的情形类似,AE(高于等于)和B(低于)的比较中都无需判断ZF标志。

5.2 IOPL标志位

eflags有两个位来表示IOPL(I/O Privilege Level)标志位,指示访问I/O地址空间所需要的权限,这个值仅在CPL=0权限下可以修改。IOPL标志还将影响到IF标志位,IF标志在具有IOPL所规定的权限内能被修改。

只有当CPL=0时,可以改变IOPL的值,当CPL<=IOPL时,可以改变IF标志位。

改变IOPL值可以使用popfd指令和iret指令,IF标志位还有专门的指令开/关中断:sti和cli指令。当使用popfd指令进行修改时,没有足够的权限时不能修改其值,但是并不会产生异常。

实验5-1:尝试改变IOPL和IF标志位

由于需要权限的改变,在我们的实例中,需要开启保护模式才能完成实验,因此,我们在setup.asm模块(common\setup.asm)里开启了保护模式,并没有使用分页机制。并在protected.asm模块里(topic05\ex5-1\protected.asm)进行这些实验。

代码清单5-1(topic05\ex5-1\protected.asm):

      pushfd                                                     ; get eflags
      or DWORD [esp],0x3000                                ; 将 IOPL=3
      popfd                                                      ; modify the IOPL
      ... ...
; 进入 ring 3 完成实验
      push user_data32_sel | 0x3
      push esp
      push user_code32_sel | 0x3
      push DWORD user_entry
      retf
      ... ...
      pushfd                                                     ; get eflags
      and DWORD [esp],0xffffcfff
      or DWORD [esp],0x0200                                ; 尝试将 IOPL 改为 0,IF 改为 1
      popfd                                                      ; 修改 eflags

在ring 3里尝试同时改变IOPL和IF的值,完整的源码在topic05\ex5-1\目录下。

上面这是在VMware里的测试结果(在真机下情况一样),结果显示:在CPL=0下将IOPL的权限改为3级,在CPL=3下,IF标志可以改为1(CPL<=IOPL),而IOPL的值不变(需要0级权限)。

I/O Bitmap

IOPL控制着程序的I/O地址空间访问权,只有在足够的权限下才能访问I/O地址,否则会产生#GP异常。其实这话说得不太完整,还与I/O位图相关。

如果当前CPL>IOPL(值大于),在TSS段中的I/O Bitmap有最后的决定权!

是的!即使当前运行的权限低于IOPL所规定的权限,也可以在TSS中的I/O Bitmap对某些port进行设置,达到可以访问I/O地址空间。当CPL>IOPL时,对port的I/O访问处理器将检查I/O Bitmap中相应的port位以决定这个I/O访问是否违例,当CPL<=IOPL时则无须检查I/O Bitmap。

I/O Bitmap中的每个bit对应于一个port,当这个bit被置位时(设为1),程序对port无访问权。当这个bit被清0时,port是可以访问的。

实验5-2:利用I/O Bitmap的设置来决定I/O空间访问权

为了完成这个实验,我们在TSS段中加入了I/O Bitmap(I/O位图),对common\setup.asm源码进行了相应的改动!原来是没有I/O Bitmap的。

同时,我们在protected.asm文件里增加了一个函数,用来设置I/O Bitmap的值,下面是这个函数的源码。

代码清单5-2(topic05\ex5-2\protected.asm):

;--------------------------------------------------------
; set_IO_bitmap(int port,int value):设置 IOBITMAP 中的值
; input:
;                esi - port(端口值),edi - value 设置的值
;---------------------------------------------------------
set_IO_bitmap:
      jmp do_set_IO_bitmap
GDT_POINTER dw 0
            dd 0
do_set_IO_bitmap:
      push ebx
      push ecx
      str eax                                           ; 得到 TSS selector
      sgdt [GDT_POINTER]                              ; 得到 GDT base
      and eax,0xfffffff8
      add eax,[GDT_POINTER+2]
      mov ebx,[eax+4]
      and ebx,0x00ff
      shl ebx,16
      mov ecx,[eax+4]
      and ecx,0xff000000
      or ebx,ecx
      mov eax,[eax]                                   ; 得到 TSS descriptor
      shr eax,16
      or eax,ebx
      movzx ebx,WORD [eax+102]
      add eax,ebx                                      ; 得到 IOBITMAP
      mov ebx,esi
      shr ebx,3
      and esi,7
      bt edi,0
      jc set_bitmap
      btr DWORD [eax+ebx],esi                        ; 清位
      jmp do_set_IO_bitmap_done
set_bitmap:
      bts DWORD [eax+ebx],esi                     ; 置位
do_set_IO_bitmap_done:
      pop ecx
      pop ebx
      ret

我们做实验的代码如下。

代码清单5-3(topic05\ex5-2\protected.asm):

;; 测试1:读 port 0x21
      in al,MASTER_OCW1_PORT              ; 尝试读 port 0x21
      mov esi,msg6
      call puts
;; 测试2:写 port 0x21
      mov al,0x0f
      out MASTER_OCW1_PORT,al                ; 尝试写 port 0x21

在topic05\ex5-2\目录下有全部的源代码,下面是测试的结果:

实验结果表明:在第1次读的时候,进入了#GP异常处理程序,这里的IOPL值是0,而我们的CPL权限是3,并且在开始的时候我们在I/O Bitmap中设置了port 0x21对应的位为1值,指示port为不可访问,所以产生了异常。

;; 现在重新开启I/O可访问权限
      mov esi,MASTER_OCW1_PORT
      mov edi,0                                        ; set port 0x21 IOBITMAP to 0
      call set_IO_bitmap
      iret

而在后来的写操作是成功的!因为,我们在#GP处理程序返回前重新开启了port为可访问(即:在I/O Bitmap中将port 0x21对应的bit清0),这时候对port 0x21的访问是成功的。

5.3 TF标志与RF标志

显然eflags.RF标志与eflags.TF标志是配合一起使用的,当TF标志被置位时,就代表开启了single-debug(单步调试)功能,处理器将进入single-debug状态。

什么时候开始进入single-debug?

答案是:当TF标志被置位,执行完下一条指令后,处理器进入#DB处理。这是因为single-debug属于trap类型的#DB异常。看看下面的指令序列。

; 开启 single debug功能
      pushfd
      bts dword [esp],8           ; eflags.TF=1
      popfd                           ; 更新 eflags 寄存器
      mov eax,1                     ;test 1
      mov eax,2                     ;test 2
      mov eax,3                     ;test 3
      mov eax,4                     ;test 4
      mov eax,5                     ;test 5

popfd指令执行完后,将更新TF标志为1,那么应该是在test 1之前还是之后呢?答案是test 1之后(test 2之前),是在TF被置位后的下一条指令执行完后产生#DB异常。

处理器在进入#DB异常处理程序之前,会将TF标志清0以防止在中断处理程序内发生single-deubg,这是显而易见的事情,RF标志也会被清0。在进入中断处理程序前,NT和VM标志都会得到清0。

那么,在压入单步调试#DB处理程序stack内的eflags寄存器中TF是原来的值(即为1),RF标志被清0。

看看上图:当popf指令执行完毕后,TF被置1,第1条mov指令执行完毕,产生#DB异常,CPU进入#DB处理程序后清TF和RF标志,而在#DB处理程序里,在iret指令返回前,应该将stack中的RF标志置为1,以确保返回到被中断的指令时能顺利执行。iret指令将pop回stack中原来的eflags值。

当第2条mov指令执行完毕后,CPU将清RF标志为0,处理器重新转入到#DB处理程序中执行。除非TF标志被清0,否则重复上一流程。

由于引发#DB异常的不止single-debug(单步调试)这一途径。#DB异常可以是Fault类型或是Trap类型,因此,在#DB异常处理程序中有责任去确保返回被中断的执行能够得到正常执行。通过设置stack中的eflags映像中的RF为1,让iret返回前更新eflags寄存器。

处理器会在每一条指令执行完毕后将RF标志清0,RF标志的值基本上恒为0。

实验5-3:实现一个single-debug例子

正如前面所列代码,我们是这样开启和测试TF、RF以及#DB处理程序的。

代码清单5-4(topic05\ex5-3\protected.asm):

; 开启 single debug 功能
        pushfd
        bts dword [esp],8                                ; eflags.TF=1
        popfd                                                 ; 更新 eflags 寄存器
        mov eax,1                                          ; test 1
        mov eax,2                                          ; test 2
        mov eax,3                                          ; test 3
        mov eax,4                                          ; test 4
        mov eax,5                                          ; test 5

我们测试了这5条mov指令产生single-debug的情况,为了方便观察演示,这5条mov指令只是给eax赋一个序号。我们需要实现自己的#DB处理程序,完整的源代码在topic05\ex5-3\protected.asm里。

上面是实验5-3的运行结果,每进入#DB处理程序一次就打印相关的信息。请留意画线标注的eax寄存器的值,这正是我们给eax寄存器进行mov操作后的值。说明在每执行一条mov指令后产生了#DB异常,这就是单步调试的结果。而eip是这些mov指令执行状态的当前值。其他的寄存器值都是不变的。

实际上,在这个例子里,RF标志置不置位对结果没影响,那是因为由single-debug引起的#DB属于trap类型的异常!它会返回到被中断指令的下一条指令。

然而不要以这个例子为准而认为不需要将RF置位,由其他中断源产生的#DB异常可能是fault类型的,fault类型的异常会返回中断指令再尝试重新执行。

5.4 NT标志

这个NT标志也牵扯着其他复杂的信息,NT标志被使用于处理器提供的task switch(任务切换)场景中,它是Nested Task(嵌套任务)标志位,当NT=1时,表示当前执行的任务被嵌套在另一个任务里(这是从任务的术语上来讲),当NT=0时,当前执行的任务没有被嵌套。NT标志一般由处理器自动维护,但是可以在任何权限下被软件修改。

什么时候NT标志被置为1?

在使用call指令进行task switch,以及发生interrupt/exception时的task switch,处理器从new task的TSS加载完eflags寄存器后,会将NT置1。

这个情景中的task switch是指:call调用一个TSS selector或者taskgate,以及interrupt/exception发生时,vector指向IDT中的task-gate。

当然,使用jmp一个TSS selector或task-gate也会产生任务切换,iret指令也可以产生任务切换,但它们不在上述将NT置为1的情景中。

在上述的task switch情景中,处理器会同时将旧任务的TSS selector写入新任务TSS段中的previous-link域中,以便可以切换回到旧任务。

什么时候NT标志被清为0?

其中一个情景是:当使用iret指令从被嵌套的任务(new)返回到原来的(old)任务时,处理器从stack中pop出eflags寄存器后会清NT为0(实际上是,先将stack中eflags寄存器image中的NT位清0,然后pop的时候,NT标志就为0)。

当执行iret指令时,处理器会检查当前的eflags.NT标志是否为1,为1时表示处于nested状态,执行完后NT被清为0。

这个情景中的返回是指:使用iret指令从interrupt/exception处理程序中返回时。注意:使用ret指令从一个过程的返回并不在其中。

当执行ret指令时,并不会清NT标志位(不改变stack中eflags寄存器image中的NT标志位,pop的时候NT标志为0),它并不需要去检查NT标志位是否为1值。

上述是Intel关于NT清0这一点的描述,可是AMD的描述似乎没有提及在stack中的eflags寄存器的image中的NT是否有被清0,似乎是pop出eflags寄存器后再将NT清0,但不管怎样,执行结果是完全一致的。

另一个情景是:使用jmp进行task切换时,处理器从新任务的TSS加载eflags完后,会将NT标志清为0,表示JMP指令执行的并不是嵌套任务。

在软件中可以由程序员自己手工去修改NT标志的值,通过修改在stack中eflags寄存器image的NT标志位,然后使用popf指令进行更新。

在long mode下的64位模式下并不支持TSS的task switch机制,因此,在64位模式下NT标志位是无效的。

5.5 AC标志

eflags中的AC标志是指Alignment Check(地址中的对齐检查),只有当同时开启CR0.AM和eflags.AC标志位时处理器才支持这项功能。

我们从下面的表格来看,什么情况下才是有效的alignment粒度。

当对上述的某一类数据类型的访问违反了它的粒度时,会产生#AC异常。

mov ax,WORD [0x10003]               ; 跨 WORD 类型边界
mov eax,DWORD [0x10005]             ; 跨 DWORD 类型边界
mov rax,QWORD [0x10007]             ; 跨 QWORD 类型边界
mov esi,0x10006
lodsd                                     ; 跨 bit string类型边界

上面几种情况下,都属于non-alignmnet行为。只有在权限级别3下的non-alignmnet行为才会产生#AC异常,在0、1及2级下不会产生#AC异常。

实验5-4:测试Alignment Check功能

#AC异常属于fault类型,#AC处理程序会返回到发生错误的指令继续执行,因此在我们的#AC处理程序中必须要修正这个错误:

代码清单5-5(topic05\ex5-4\protected.asm):

;-----------------------------------------------
; AC_handler():#AC handler
;-----------------------------------------------
AC_handler:
      jmp do_AC_handler
ac_msg1                db '---> Now,enter the #AC exception handler <---',10
ac_msg2                db 'exception location at 0x'
ac_location        dq 0,0
do_AC_handler:
      pusha
      mov esi,[esp+4+4*8]
      mov edi,ac_location
      call get_dword_hex_string
      mov esi,ac_msg1
      call puts
      call println
;; 现在 disable AC 功能
      btr DWORD [esp+12+4*8],18               ; 清elfags image中的AC标志
      popa
      add esp,4                                    ; 忽略 error code
      iret

#AC处理程序里在iret返回前,将stack中的eflags.AC清为0,iret执行完后,eflags.AC被清0。在ring0代码里先将CR0.AM置位,在ring3代码里,再将eflags.AC置位。

代码清单5-6(topic05\ex5-4\protected.asm):

; 开启 eflags.AC 标志
      pushf
      bts DWORD [esp],18
      mov ebx,[esp]
      popf
; test 1
      mov ax,WORD [0x10003]               ; 跨 WORD 类型边界
      push ebx
      popf
; test 2
      mov eax,DWORD [0x10005]             ; 跨 DWORD 类型边界
      push ebx
      popf
; teset 3
      mov esi,0x10006                       ; 跨 string 类型边界
      lodsd

由于在#AC处理程序会将AC标志清0,因此,每做一次测试前,再重新开启AC标志位。这个实验的完整源代码在topic05\ex5-4\目录下,运行的结果如下。

结果显示,共产生了3次#AC异常,就是我们所做的3个测试。

5.6 VM标志

eflags中的VM标志指示着处理器进入和离开virtual-8086模式,当VM=1时进入virtual-8086模式,VM=0时离开virtual-8086模式,VM标志不能被popfd指令修改,只有两种途径可以置eflags.VM标志位。

① 执行一个task switch(任务切换)时,包括:使用call/jmp指令执行一个TSS selector或task-gate,在TSS段中的eflags寄存器Figure-中VM被置1,处理器加载eflags时,VM为1,从而进入virtual-8086模式;当执行iret指令时,stack中的eflags.NT=1表示将进行任务切换,如果TSS段中的eflags.VM为1,也指示处理器进入virtual-8086模式。

② 当执行iret指令时,stack中的eflags映像的VM为1,也将指示处理器进入virtual-8086模式。

只有执行iret指令,stack中的eflags映像的VM标志为0时,才会离开virtual-8086模式,执行call/jmp进行任务切换(TSS段中的eflags.VM为0)这种情况并不能离开virtual-8086模式。

在64位模式下,处理器不支持virtual-8086模式,VM标志位也被忽略。

5.7 eflags寄存器的其他事项

处理器初始化后,eflags的值为00000002H,Bit 1为1,其他位为0,Bit 1是保留位,其值固定为1。某些标志位可以使用专门指令进行设置,对于CF标志,可以使用STC指令置位,CLC指令清位,CMC指令对CF标志进行取反。对于DF标志,可以使用STD指令置位,CLD指令清位。

STI指令可以对IF标志进行置位,CLI指令对IF标志进行清位,它需要足够的权限来修改IF标志位,这个足够的权限是:CPL<=IOPL。

可是这和popfd指令修改IF标志位不同,当不够权限时(CPL>IOPL),popfd指令修改IF标志不会产生#GP异常,而使用STI指令时会产生#GP异常。

实验5-5:测试sti指令

分别使用popfd指令和sti指令在权限不足的情况下进行测试,下面是实验的代码(在3级权限下)。

代码清单5-7(topic05\ex5-5\protected.asm):

;;测试 popfd 指令
      pushfd
      bts DWORD [esp],9
      popfd
;; 测试 sti 指令
      sti
      mov esi,msg7
      call puts                             ;打印成功信息
      call println;

而在我们的#GP处理程序里,我们需要修正这个错误,要么将IOPL权限降低,要么不继续执行引发错误的指令。

代码清单5-8(topic05\ex5-5\protected.asm):

mov eax,[esp]
cmp BYTE [eax],0xfb              ; 检查是否因为 sti 指令而产生 #GP 异常
jne do_GP_handler_done
inc eax                               ; 如果是,跳过产生 #GP 异常的 sti 指令,执行下一条指令
mov [esp],eax
mov esi,gp_msg3
call puts
do_GP_handler_done:
      iret

对OS来说降低IOPL权限似乎是一个比较危险的行为,示例中的#AC处理程序如果检测到发生#GP异常是由STI指令产生的(sti指令的opcode码为FB),那么就跳过STI指令继续向下执行。由于#GP异常属于fault类型,因此必须修正这个错误,否则将一直在#GP处理程序里转不出来。

实验的结果表明:在STI指令里发生了#GP处理程序,当跳过STI指令后得以继续往下执行。

当有下面的指令序列:

sti                       ; 在过程返回前开启 IF 标志
ret

在某个过程里,sti紧接着ret指令,sti指令执行后,如果在ret指令返回前发生了外部中断,这个外部中断将被暂缓响应。ret指令返回结束后,外部中断才被响应。

sti                        ; 第1次开启IF标志
sti                        ; 第2次开启IF标志
ret

Intel明确规定,在上面2次开启IF标志的序列中,ret返回前外部中断是可以被响应的。