第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返回前外部中断是可以被响应的。