继推出80386之后,Intel又推出了80386、Pentium和Pentium PRO。这些处理器都具有实模式和保护模式两种工作方式。前面已介绍过,实模式与8086兼容,可以运行DOS及以其为平台的几乎所有软件;但在实模式下,处理器不能发挥自身的优越性能,不能支持多用户、多任务操作系统的运行。为了充分发挥处理器的功能,同时使DOS及以其为平台的软件继续有效地运行,从80386开始增加了虚拟8086模式。本文将介绍虚拟8086模式。这里下载本文源代码。
虚拟8086模式是保护模式下的一种工作方式,也称为V8086模式,或者简称为V86模式。在虚拟8086模式下,处理器类似于8086。寻址的地址空间是1M字节;段寄存器的内容作为段值解释;20位存储单元地址由段值乘以16加偏移构成。在V86模式下,代码段总是可写的,这与实模式相同,同理,数据段也是可执行的,只不过可能会发生异常。所以,在虚拟8086模式下,可以运行DOS及以其为平台的软件。但V86模式毕竟是虚拟8086的一种方式,所以不完全等同于8086。
当标志寄存器中的VM位为1时,处理器就处于V86模式。此时,其当前特权级由处理器自动设置为3。
8086程序可以直接在V86模式下运行,而V86模式受到称为V86监控程序的控制。V86监控程序和在V86模式下的8086程序构成的任务称为虚拟8086任务,或者简称为V86任务。V86任务形成一个由处理器硬件和属于系统软件的监控程序组成的“虚拟8086机”。V86监控程序控制V86外部界面、中断和I/O。硬件提供该任务最低端1M字节线性地址空间的虚拟存储空间,包含虚拟寄存器的TSS,并执行处理这些寄存器和地址空间的指令。
80386把V86任务作为与其它任务具有同等地位的一个任务。它可以支持多个V86任务,每个V86任务是相对独立的。所以,通过V86模式这种形式,运行8086程序可充分发挥处理器的能力和充分利用系统资源。
保护模式和V86模式之间的切换情形如下图所示。图中左面部分为V86任务。从图中可见,V86模式与保护模式的切换可发生在V86任务之内,这种切换是V86模式下的8086程序与保护模式下的监控程序之间的转换;V86模式与保护模式的切换可发生在任务之间,这种切换是V86任务与其它任务的切换。此外,V86监控程序与其它任务之间的切换是普通的任务切换。
由于80386没有提供直接改变VM标志的指令,并且只有当前特权级CPL=0时,对VM的改变才有效,所以V86模式与保护模式的切换不能简单地通过改变VM位而进行。下面介绍V86模式与保护模式之间的切换,也就是如何进入和离开V86模式。为了方便,先介绍如何离开V86模式。
在V86模式下,如果处理器响应中断/异常,那么就会退出当前V86任务的V86模式。
在V86模式下,处理器对中断/异常的响应处理不同于真正的8086,而仍然采用保护模式下对中断/异常响应处理的方法。所以,在V86模式下,不是根据位于线性地址空间最低端的中断向量表内的对应中断向量转入处理程序,而是根据中断描述符表IDT内的对应门描述符的指示转入处理程序。
如果对应的门描述符是386中断门或386陷阱门,那么就发生在当前V86任务内从V86模式到保护模式的转换。80386要求执行这种中断/异常处理程序时的CPL必须等于0。
由于V86模式下的CPL=3,而转换到保护模式后的CPL=0,所以这种转换包含了特权级的变换。在转入处理程序之前,处理器先将V86模式下的段寄存器GS、FS、DS及ES压入0级堆栈,并在进入保护模式下的处理程序之前装入空选择子。为保持使堆栈对齐,把段寄存器压入堆栈时一律按32位值压入,低16位是段寄存器的值,高16位为空。于是,转换后的0级堆栈如下图所示。其中,段寄存器SS和CS的值也是V86模式下的段值。图(a)是没有出错码的情形;图(b)是有出错码的情形。
在这种V86任务内从V86模式转换到保护模式的过程中,为了保证中断/异常处理程序工作于特权级0,对目标代码段描述符特权级进行检查,如果由目标代码段描述符特权级决定的CPL不等于0,将引起通用保护异常。此外,标志寄存器EFLAGS中的VM位被清0,从而使得中断/异常处理在保护模式下进行,也即离开V86模式。
这种情况下,相应的中断/异常处理在当前V86任务之内进行。中断异常处理程序可以检查保存在堆栈中的EFLAGS映象,根据VM位的值来确定被中断程序的工作模式。如果VM=1,那么被中断的程序工作于V86模式,是8086程序;否则,被中断的程序工作于保护模式,是V86监控程序。
如果对应的门描述符是任务门,那么就发生从当前V86任务到其它任务的切换,也就离开了当前V86任务的V86方式。象普通任务切换一样,V86模式的各通用寄存器、段寄存器、指令指针和标志寄存器EFLAGS等保存到原V86任务的386TSS中。被保存的段寄存器的内容是V86模式下的段值。被保存的EFLAGS内的VM=1。
这种情况下,相应的中断异常处理在另一个任务内进行。目标任务可以是普通任务,也可以是另一个V86任务。如果目标任务TSS内的EFLAGS字段内的VM=1,那么就转入另一个V86任务的V86模式。
与离开V86模式的两条途径相对应,有两条进入V86模式的途径。
通过在中断/异常处理结束时使用IRET指令返回被中断的程序继续执行。指令IRET的执行步骤如下所示:
(1)若NT=1,则进行任务切换,然后转步骤6;
(2)否则从堆栈中弹出EIP、CS和EFLAGS;
(3)若VM=1且CPL=0,则恢复外层堆栈及其它段寄存器,然后转步骤6;
(4)若无特权级变换则转步骤6;
(5)否则恢复外层堆栈;
(6)结束
尽管上述步骤不够细致和没包括异常情况,但还是体现了指令IRET执行时所处理的三种情形。第一种情形是当前EFLAGS中的NT=1,也即嵌套任务返回,那么就进行任务切换,指向目标任务TSS的选择子在当前任务TSS的连接字段。NT=0表示当前中断/异常处理程序与被中断程序属于同一任务,于是就从堆栈弹出EIP、CS和EFLAGS。第二和第三中情形是NT=0的条件下产生的。第二种情形是弹出的EFLAGS中VM=0,表示被中断的程序是普通保护模式程序,那么就考虑特权级变换,如果向外层返回,那么就恢复外层堆栈指针。不允许向内层返回,否则将会引起通用保护异常。前文中介绍的IRET指令的动作只考虑了情形一和情形二,并不是IRET指令的完整动作。
第三种情形是弹出的EFLAGS中VM=1且当前正运行程序的CPL=0,表示被中断的程序是V86模式下的8086程序,当前是从同一V86任务下的中断/异常处理程序返回。由于V86模式的特权级是3,所以要进行堆栈切换,也即从堆栈中弹出3级堆栈的指针(ESP和SS)。此外,还从堆栈中弹出段寄存器ES、DS、FS和GS。在这种情形下,弹到各段寄存器(包括CS和SS)的内容都作为段值,而非选择子。这种处理动作对应于上述第一种离开V86模式的情形,有关堆栈操作与上图所示的堆栈内容相符。当然,如果产生异常时提供出错码,那么异常处理程序在利用IRET指令返回时,必须确保堆栈指针指向如上图所示保存EIP的单元。简单的实现方法是,异常处理程序在执行IRET前,先从堆栈中弹出出错码。
利用指令IRET处理的这第三种情形,可以方便地从V86任务下的中断/异常处理程序返回到V86模式下的8086程序。利用这条途径还可以直接进入V86模式。为此,先在0级堆栈中形成如上图(a)所示的栈顶。对应EIP值是V86模式下要执行的8086程序入口点的16位偏移;对应CS值是V86模式下要执行的8086程序入口点的段值;对应EFLAGS值中的VM位必须是1;对应SS和ESP的值是要执行的8086程序的堆栈指针;对应ES、DS、FS和GS的值是相应的段值。然后,在CPL=0和NT=0的情况下,执行IRET指令。实际上,这种进入V86模式的途径是,先建立一个V86模式下执行的8086程序被中断而离开V86模式的环境,然后再返回。需要注意的是,若当前正执行程序的CPL不为0,则再执行IRET指令时不会进入V86模式,但也不产生异常,EFLAGS中的VM位被处理器自动清0。
不能通过RET指令进入V86模式,因为它不改变EFLAGS的内容。任何不能修改EFLAGS中VM位的指令均不能切换到V86模式。
通过任务切换的途径,可以从其它任务进入V86任务内的V86模式。
利用在前文介绍的任务切换的方法可以进行任务切换。如果目标任务由386TSS描述,并且其中EFLAGS字段内的VM位为1,那么在切换到目标任务时,也就进入了V86模式。在切换到V86模式时,CPL被规定为3。目标任务TSS中的各段寄存器字段被解释为8086可以接受的段值,而不是选择子。任务切换时也将装载LDTR和CR3。
程序在V86模式下执行时,EFLAGS寄存器中的NT位被忽略,所以,不能在V86模式下用IRET指令完成一个任务切换,并使其工作于V86以外的工作模式。在V86模式下执行IRET指令时,将弹出 IP、CS及FLAGS寄存器,以恢复被中断的程序,而不考虑NT位的值。
如果利用这条途径建立V86任务并进入V86模式,那么主要是把对应386TSS中EFLAGS字段内的VM位置1,把8086程序的有关段值填入对应386TSS中的相应段寄存器字段。此外,如果V86监控程序需要用到LDT,那么还要填写LDTR字段;如果需要采用分页机制,那么还要填写CR3字段(当新任务为V86模式的任务时,只装入段寄存器,而没有装入描述符投影寄存器的动作)。
下面给出一个用于演示进入和离开V86模式的实例。该实例的逻辑功能是,以驻留方式结束程序,退出时已处于V86模式。该实例演示内容包括:两种方式进入V86模式和两种方式离开V86模式; V86模式下的8086程序如何调用实模式下的软中断处理程序。
为了便于演示,本实例含有三个任务:临时任务,V86任务和INTFF任务。在实模式下做必要的初始化工作后切换到保护模式,也即进入临时任务,开始演示。演示分两个阶段:第一阶段进入V86任务的V86模式,并驻留退出;第二阶段进入INTFF任务,切换到临时任务,并返回实模式。
第一阶段的演示步骤如下:
(1)开始临时任务后,作切换到V86任务的准备;
(2)切换到V86任务,由于V86任务TSS中的EFLAGS字段内的VM=1,所以伴随着任务切换就进入了V86模式。
(3)进入V86任务的V86模式后,显示提示信息,驻留结束,出现DOS提示符,第一阶段至此结束。
在V86模式下,可进行各种操作,运行其它8086程序。如果8086程序引起通用保护异常,那么在屏幕上显示提示信息,并中止该8086程序。如果在8086程序中执行“INT 0FFH”指令,开始第二阶段。第二阶段的演示步骤如下:
(1)进入INTFF任务后,显示提示信息,切换到临时任务;
(2)在临时任务内切换到实模式;
(3)在实模式下中止发出“INT 0FFH”的程序
源程序由如下几部分组成:
(1)全局描述符表GDT;
(2)中断描述符表IDT(只适用于V86任务);
(3)INTFF任务的TSS段、LDT段、0级堆栈段和代码段;
(4)V86任务的TSS段、LDT段、0级堆栈段、3级堆栈段及数据段,通用保护异常处理程序段和其它中断/异常处理程序段,V86模式下的8086程序段;
(5)临时任务的TSS段和代码段;
(6)实模式下的初始化代码段及有关过程。
源程序清单如下:
;名称:ASM11.ASM
;功能:演示进入和离开V86方式
;编译:TASM ASM11.ASM
;连接:TLINK ASM11.OBJ
;============================================================================
INCLUDE 386SCD.INC
;============================================================================
GDTSeg SEGMENT PARA USE16 ;全局描述符表数据段(16位)
;----------------------------------------------------------------------------
;全局描述符表
GDT LABEL BYTE
;空描述符
DUMMY Desc <>
;规范段描述符及选择子
Normal Desc <0ffffh,,,ATDW,,>
Normal_Sel = Normal-GDT
;显示缓冲区数据段描述符及选择子
Video Desc <07fffh,8000h,0bh,ATDW,,>
Video_Sel = Video-GDT
;----------------------------------------------------------------------------
EFFGDT LABEL BYTE
;V86任务TSS段描述符及选择子
V86TSS Desc <V86TSSLen-1,V86TSSSeg,,AT386TSS,,>
V86TSS_Sel = V86TSS-GDT
;V86任务局部描述符表的描述符及选择子
V86LDT Desc <V86LDTLen-1,V86LDTSeg,,ATLDT,,>
V86LDT_Sel = V86LDT-GDT
;IntFF任务TSS段描述符及选择子
IntFFTSS Desc <IntFFTSSLen-1,IntFFTSSSeg,,AT386TSS,,>
IntFFTSS_Sel = IntFFTSS-GDT
;IntFF任务局部描述符表的描述符及选择子
IntFFLDT Desc <IntFFLDTLen-1,IntFFLDTSeg,,ATLDT,,>
IntFFLDT_Sel = IntFFLDT-GDT
;临时任务的任务状态段描述符及选择子
TempTSS Desc <TempTSSLen-1,TempTSSSeg,,AT386TSS,,>
TempTSS_Sel = TempTSS-GDT
;临时任务代码段描述符及选择子
TempCode Desc <0ffffh,TempCodeSeg,,ATCE,,>
TempCode_Sel = TempCode-GDT
;----------------------------------------------------------------------------
GDNum = ($-EFFGDT)/(SIZE Desc) ;需特殊处理的描述符数
GDTLen = $-GDT ;全局描述符表长度
GDTSeg ENDS ;全局描述符表段定义结束
;============================================================================
IDTSeg SEGMENT PARA USE16 ;V86任务使用的中断描述符表
IDT LABEL BYTE
;对应0--12号中断/异常的中断门描述符
REPT 13
Gate <,TPCode_Sel,,AT386IGate+DPL3,>
ENDM
;通用保护故障处理程序门描述符
Gate <GPBegin,GPCode_Sel,,AT386TGate+DPL3,>
;对应14--254号中断/异常的中断门描述符
REPT 241
Gate <,TPCode_Sel,,AT386IGate+DPL3,>
ENDM
;对应255号中断的任务门描述符
Gate <,IntFFTSS_Sel,,ATTaskGate+DPL3,>
;----------------------------------------------------------------------------
IDTLen = $-IDT
IDTSeg ENDS ;中断描述符表段定义结束
;============================================================================
;IntFF任务的TSS段
IntFFTSSSeg SEGMENT PARA USE16
DD 0 ;链接字
DD 0 ;0级堆栈指针(实例不使用)
DW 0,0 ;0级堆栈选择子(实例不使用)
DD 0 ;1级堆栈指针(实例不使用)
DW 0,0 ;1级堆栈选择子(实例不使用)
DD 0 ;2级堆栈指针(实例不使用)
DW 0,0 ;2级堆栈选择子(实例不使用)
DD 0 ;CR3
DW IntFFBegin,0 ;EIP
DD 0 ;EFLAGS
DD 0 ;EAX
DD 0 ;ECX
DD 0 ;EDX
DD 0 ;EBX
DD IntFFStackLen ;ESP
DD 0 ;EBP
DD 0 ;ESI
DD 0 ;EDI
DW Normal_Sel,0 ;ES
DW IntFFCode_Sel,0 ;CS
DW IntFFStack_Sel,0 ;SS
DW Normal_Sel,0 ;DS
DW Normal_Sel,0 ;FS
DW Normal_Sel,0 ;GS
DW IntFFLDT_Sel,0 ;LDTR
DW 0 ;调试陷阱标志
DW $+2 ;指向I/O许可位图
DB 0ffh ;I/O许可位图结束标志
;----------------------------------------------------------------------------
IntFFTSSLen = $
IntFFTSSSeg ENDS
;============================================================================
;IntFF任务的LDT段
IntFFLDTSeg SEGMENT PARA USE16
FLDT LABEL BYTE
;0级堆栈段描述符及选择子
IntFFStack Desc <IntFFStackLen-1,IntFFStackSeg,,ATDWA,,>
IntFFStack_Sel = IntFFStack-FLDT+TIL
;代码段描述符及选择子
IntFFCode Desc <IntFFCodeLen-1,IntFFCodeSeg,,ATCER,,>
IntFFCode_Sel = IntFFCode-FLDT+TIL
;----------------------------------------------------------------------------
IntFFLDNum = ($-FLDT)/(SIZE Desc)
IntFFLDTLen = $
IntFFLDTSeg ENDS
;============================================================================
;IntFF任务的堆栈
IntFFStackSeg SEGMENT PARA USE16
IntFFStackLen = 512
DB IntFFStackLen DUP(0)
IntFFStackSeg ENDS
;============================================================================
;IntFF任务的代码段
IntFFCodeSeg SEGMENT PARA USE16
ASSUME CS:IntFFCodeSeg
;----------------------------------------------------------------------------
IntFFMess DB 'Return to real mode.'
IntFFMessLen = $-IntFFMess
;----------------------------------------------------------------------------
IntFFBegin PROC FAR
mov si,OFFSET IntFFMess
mov ax,Video_Sel
mov es,ax
mov di,0
mov ah,17h
mov cx,IntFFMessLen
cld
INext: mov al,BYTE PTR cs:[si]
inc si
stosw
loop INext
JUMP16 TempTSS_Sel,0
IntFFBegin ENDP
;----------------------------------------------------------------------------
IntFFCodeLen = $
IntFFCodeSeg ENDS
;============================================================================
;V86任务的TSS段
V86TSSSeg SEGMENT PARA USE16
DD 0 ;链接字
DD V86Stack0Len ;0级堆栈指针
DW V86Stack0_Sel,0 ;0级堆栈选择子
DD 0 ;1级堆栈指针(实例不使用)
DW 0,0 ;1级堆栈选择子(实例不使用)
DD 0 ;2级堆栈指针(实例不使用)
DW 0,0 ;2级堆栈选择子(实例不使用)
DD 0 ;CR3
DW V86Begin,0 ;EIP
DD IOPL3 OR VMFL ;EFLAGS(IO特权级为3,VM=1)
DD 0 ;EAX
DD 0 ;ECX
DD 0 ;EDX
DD 0 ;EBX
DD V86Stack3Len ;ESP
DD 0 ;EBP
DD 0 ;ESI
DD 0 ;EDI
DW V86CodeSeg,0 ;ES
DW V86CodeSeg,0 ;CS
DW V86Stack3Seg,0 ;SS
DW V86CodeSeg,0 ;DS
DW V86CodeSeg,0 ;FS
DW V86CodeSeg,0 ;GS
DW V86LDT_Sel,0 ;LDTR
DW 0 ;调试陷阱标志
DW $+2 ;指向I/O许可位图
DB 4000h/8 DUP(0) ;I/O许可位图
DB 0ffh ;I/O许可位图结束标志
;----------------------------------------------------------------------------
V86TSSLen = $
V86TSSSeg ENDS
;============================================================================
;V86任务的LDT段
V86LDTSeg SEGMENT PARA USE16
VLDT LABEL BYTE
;----------------------------------------------------------------------------
;V86任务线性地址空间中最低端1M字节段的描述符及描述符
VAllMem Desc <0ffffh,,,ATDWA,0fh,>
VAllMem_Sel = VAllMem-VLDT+TIL
;V86任务0级堆栈段描述符及选择子
V86Stack0 Desc <V86Stack0Len-1,V86Stack0Seg,,ATDWA,,>
V86Stack0_Sel = V86Stack0-VLDT+TIL
;V86任务数据段描述符及选择子
V86Data Desc <V86DataLen-1,V86DataSeg,,ATDR,,>
V86Data_Sel = V86Data-VLDT+TIL
;V86任务中断/异常处理程序代码段描述符及选择子
TPCode Desc <TPCodeLen-1,TPCodeSeg,,ATCE,,>
TPCode_Sel = TPCode-VLDT+TIL
;V86任务通用保护异常处理程序代码段描述符及选择子
GPCode Desc <GPCodeLen-1,GPCodeSeg,,ATCE,,>
GPCode_Sel = GPCode-VLDT+TIL
;----------------------------------------------------------------------------
V86LDNum = ($-VLDT)/(SIZE Desc)
V86LDTLen = $
V86LDTSeg ENDS
;============================================================================
;V86任务的0级堆栈
V86Stack0Seg SEGMENT PARA USE16
V86Stack0Len = 512
DB V86Stack0Len DUP(0)
V86Stack0Seg ENDS
;============================================================================
;V86任务的3级堆栈
V86Stack3Seg SEGMENT PARA USE16
V86Stack3Len = 1024
DB V86Stack3Len DUP(0)
V86Stack3Seg ENDS
;============================================================================
;V86任务数据段
V86DataSeg SEGMENT PARA USE16
GPErrMess DB '......General Protection Error......'
GPErrMessLen = $-GPErrMess
V86DataLen = $
V86DataSeg ENDS
;============================================================================
;定义部分代表堆栈单元的符号
Perr EQU <WORD PTR [BP+0]>
Pip EQU <WORD PTR [bp+4]>
Pcs EQU <WORD PTR [bp+8]>
Pflag EQU <WORD PTR [bp+12]>
Psp EQU <WORD PTR [bp+16]>
Pss EQU <WORD PTR [bp+20]>
Pes EQU <WORD PTR [bp+24]>
Pds EQU <WORD PTR [bp+28]>
Pfs EQU <WORD PTR [bp+32]>
Pgs EQU <WORD PTR [bp+36]>
;============================================================================
;V86任务下的中断/异常处理程序代码段
TPCodeSeg SEGMENT PARA USE16
ASSUME CS:TPCodeSeg
;----------------------------------------------------------------------------
TPBegin PROC FAR
Count = 0
REPT 256 ;对应256个入口
IF Count EQ 21h
Ent21H LABEL BYTE ;在第21H项处定义标号Ent21H
ENDIF
push bp
mov bp,Count ;置中断向量号到BP
jmp Process ;都转统一的处理程序
Count = Count+1
ENDM
Process: push bp ;保存BP
mov bp,sp ;堆栈指针送BP
push eax
push ebx ;保存EAX、EBX
;在V86堆栈顶形成返回点的现场
mov ax,VAllMem_Sel ;转载描述最低1M字节线性地址
mov ds,ax ; 空间的描述符选择子
xor eax,eax
mov ax,Psp ;修改在V86任务0级堆栈中保存
sub ax,3*2 ; 的3级堆栈的指针,减3个字
mov Psp,ax ; 即在栈顶空出3个字
xor ebx,ebx
mov bx,Pss ;使EBX指向V86堆栈顶
shl ebx,4
add ebx,eax
mov ax,Pip ;把保存在0级堆栈中的返回地址
mov WORD PTR [ebx],ax ;的偏移部分送V86堆栈
mov ax,Pcs
mov WORD PTR [ebx+2],ax ;段值部分送V86堆栈
mov ax,Pflag
mov WORD PTR [ebx+4],ax ;标志值送V86堆栈
;用对应的中断向量值代替返回地址
mov bx,[bp] ;取中断向量号
shl bx,2 ;乘4
mov ax,[bx] ;取实模式下对应中断向量的偏移
mov Pip,ax ;代替0级堆栈中的EIP
mov ax,[bx+2] ;取实模式下对应中断向量的段值
mov Pcs,ax ;代替0级堆栈中的CS
pop ebx ;恢复现场
pop eax
pop bp
pop bp
;从保护方式返回V86方式
;先转入对应的中断处理程序,再返回中断发生处
iretd
TPBegin ENDP
;----------------------------------------------------------------------------
TPCodeLen = $
TPCodeSeg ENDS
;============================================================================
;V86任务下的通用保护故障处理程序代码段
;----------------------------------------------------------------------------
GPCodeSeg SEGMENT PARA USE32
ASSUME CS:GPCodeSeg
;----------------------------------------------------------------------------
GPBegin PROC FAR
mov ax,V86Data_Sel
mov ds,ax
mov si,OFFSET GPErrMess
mov ax,Video_Sel
mov es,ax
mov di,0
mov ah,17h
mov cx,GPErrMessLen
cld
GNext: lodsb
stosw
loop GNext
add esp,4
mov ax,4c01h
JUMP16 TPCode_Sel,Ent21H
GPBegin ENDP
;----------------------------------------------------------------------------
GPCodeLen = $
GPCodeSeg ENDS
;============================================================================
;V86方式执行的8086程序段
V86CodeSeg SEGMENT PARA USE16
ASSUME CS:V86CodeSeg,DS:V86CodeSeg
Message DB 'V86 is OK!',0dh,0ah,24h
V86Begin PROC FAR
mov ah,9
mov dx,OFFSET Message
int 21h
mov ax,RCodeSeg
sub ax,GDTSeg
mov dx,OFFSET TSRLine+15
shr dx,4
add dx,ax
add dx,10h
mov ax,3100h
int 21h
V86Begin ENDP
V86CodeSeg ENDS
;============================================================================
TempTSSSeg SEGMENT PARA USE16 ;临时任务的TSS段
TSS <>
DB 0ffh ;I/O许可位图结束标志
TempTSSLen = $
TempTSSSeg ENDS
;============================================================================
TempCodeSeg SEGMENT PARA USE16 ;临时任务的代码段
ASSUME CS:TempCodeSeg
;----------------------------------------------------------------------------
Virtual PROC FAR
mov ax,Normal_Sel
mov ds,ax
mov es,ax
mov fs,ax
mov gs,ax
mov ss,ax
mov ax,TempTSS_Sel ;装载TR
ltr ax
JUMP16 V86TSS_Sel,0 ;直接切换到演示任务
ToDos: clts
mov eax,cr0 ;准备返回实模式
and al,11111110b
mov cr0,eax
JUMP16 <SEG Real>,<OFFSET Real>
Virtual ENDP
;----------------------------------------------------------------------------
TempCodeSeg ENDS
;============================================================================
RDataSeg SEGMENT PARA USE16 ;实方式数据段
RDataSeg ENDS
;============================================================================
RCodeSeg SEGMENT PARA USE16
ASSUME CS:RCodeSeg,DS:RCodeSeg
;----------------------------------------------------------------------------
VGDTR PDesc <GDTLen-1,> ;GDT伪描述符
VIDTR PDesc <IDTLen-1,> ;IDT伪描述符
NORVIDTR PDesc <> ;用于保存原IDTR值
SPVar DW ? ;用于保存实方式下的SP
SSVar DW ? ;用于保存实方式下的SS
;----------------------------------------------------------------------------
Start PROC
mov ax,RCodeSeg
mov ds,ax
cld
call InitGDT ;初始化全局描述符表GDT
call InitIDT ;初始化中断描述符表IDT
mov ax,V86LDTSeg
mov fs,ax
mov cx,V86LDNum
mov si,OFFSET VLDT
call InitLDT
mov ax,IntFFLDTSeg
mov fs,ax
mov cx,IntFFLDNum
mov si,OFFSET FLDT
call InitLDT
mov SSVar,ss
mov SPVar,sp
lgdt QWORD PTR VGDTR ;装载GDTR并切换到保护方式
sidt QWORD PTR NORVIDTR ;保存IDTR
cli ;关中断
lidt QWORD PTR VIDTR ;装载IDTR
mov eax,cr0
or al,1
mov cr0,eax
JUMP16 <TempCode_Sel>,<OFFSET Virtual>
Real: mov ax,cs
mov ds,ax
lss sp,DWORD PTR SPVar ;又回到实方式
lidt QWORD PTR NORVIDTR
sti
mov ax,4c00h
int 21h
Start ENDP
;----------------------------------------------------------------------------
TSRLine LABEL BYTE
;----------------------------------------------------------------------------
InitGDT PROC
push ds
mov ax,GDTSeg
mov ds,ax
mov cx,GDNum
mov si,OFFSET EFFGDT
InitG: mov ax,[si].BaseL
movzx eax,ax
shl eax,4
shld edx,eax,16
mov WORD PTR [si].BaseL,ax
mov BYTE PTR [si].BaseM,dl
mov BYTE PTR [si].BaseH,dh
add si,SIZE Desc
loop InitG
pop ds
mov bx,16
mov ax,GDTSeg
mul bx
mov WORD PTR VGDTR.Base,ax
mov WORD PTR VGDTR.Base+2,dx
ret
InitGDT ENDP
;----------------------------------------------------------------------------
;入口参数:FS:SI=第一个要初始化的描述符,CX=要初始化的描述符数
;----------------------------------------------------------------------------
InitLDT PROC
mov ax,WORD PTR FS:[si].BaseL
movzx eax,ax
shl eax,4
shld edx,eax,16
mov WORD PTR fs:[si].BaseL,ax
mov BYTE PTR fs:[si].BaseM,dl
mov BYTE PTR fs:[si].BaseH,dh
add si,SIZE Desc
loop InitLDT
ret
InitLDT ENDP
;----------------------------------------------------------------------------
InitIDT PROC
push ds
mov ax,IDTSeg
mov ds,ax
mov cx,256-1
mov si,OFFSET IDT
mov ax,OFFSET TPBegin
IIDT1: cmp cx,256-1-13
jz IIDT2
mov [si],ax
IIDT2: add si,8
add ax,7
loop IIDT1
pop ds
mov bx,16
mov ax,IDTSeg
mul bx
mov WORD PTR VIDTR.Base,ax
mov WORD PTR VIDTR.Base+2,dx
ret
InitIDT ENDP
;----------------------------------------------------------------------------
RCodeSeg ENDS
END Start
用于演示INTFF任务的源程序如下:
;功能:用于演示V86模式下的INTFF任务
;编译:TASM INTFF.ASM
;连接:TLINK INTFF.OBJ
;====================================================================
Text SEGMENT
ASSUME cs:Text,ds:Text
;---------------------------------------
Start PROC
int 0ffh
mov ax,4c00h
int 21h
Start ENDP
;---------------------------------------
Text ENDS
END Start
为了方便地书写IDT表,采用了重复汇编。从采用重复汇编方式定义的IDT表可见,除对应通用保护异常的13号陷阱门描述符和255号任务门描述符外,其它中断门描述符内的偏移均未设定。为此,在实模式下初始化时,把相应的入口偏移填入这些门描述符。从源程序可见,这些处理程序的入口偏移也用重复汇编书写,并且字节数相同,所以间隔等长。
实例从临时任务切换到V86任务时进入V86模式。在V86任务的TSS中,EFLAGS字段内的VM=1,所以伴随着任务切换,就进入了V86模式。为此,TSS中对应各段寄存器字段内的初始值都是V86模式下要执行的8086程序各段的段值,而非选择子。由于在发生中断/异常时要进入V86任务的特权级0,所以初始化了0级堆栈指针。V86任务使用局部描述符表LDT,所以TSS中对应字段填有相应的选择子。
实例对V86模式下响应中断和执行软中断指令“INT n”的处理方法是,转实模式下的对应中断处理程序。具体步骤如下:
1)在V86模式堆栈(V86任务的3级堆栈)顶形成返回点的现场;
2)用实模式下对应的中断向量值代替返回地址;
3)从保护模式返回V86模式。由于堆栈中保存的EFLAGS内的VM=1,所以在保护模式下执行IRET指令时,返回V86模式;由于在0级堆栈中保存的返回地址(段值和偏移)已被修改成实模式下的中断向量,所以这时的返回也就是转入实模式下的对应中断处理程序;由于在V86堆栈顶已安排了返回地址,所以在执行实模式下对应中断处理程序时,遇到IRET指令就返回到V86任务的被中断处。这种处理方法似乎绕了个弯。但就是利用这个方法有效地调用了DOS功能,方便地显示了提示信息“V86 is OK.”,顺利地实现了驻留退出。
这种处理方法没有充分考虑异常,更没有考虑异常时的出错码。
在演示的第一阶段和第二阶段,不会发生任何异常。但在这两个阶段之间,由于允许执行其它程序,可能引起异常。为了简单,实例只考虑了通用保护异常,而未考虑其它异常。该可能发生异常的阶段是在V86模式下。如果发生通用保护异常,那么就转入V86任务的通用保护异常处理程序。通用保护异常处理程序在保护模式下显示提示信息 “......General Protection Error......”,然后再转21H号中断处理程序,通过设置入口参数AX=4C01H,中止引起通用保护异常的程序。
为了演示以任务切换方式离开V86模式,在实例中安排了这一任务,并称之为INTFF任务。由于中断描述符表IDT中的255(0FFH)号描述符是任务门描述符,所以当在V86模式下执行软中断指令“INT 0FFH”时便从V86模式切换到INTFF任务。INTFF任务的TSS是初始化好的,在INTFF任务下不发生特权级变换,不使用局部描述符表LDT。INTFF任务先显示提示信息 “Return to real mode.”,然后再切换到临时任务。
在V86模式下,CPL=3,执行特权指令时,或者要引起出错码为0的通用保护故障,或者要引起非法操作码故障。
在V86模式下,输入/输出指令的敏感条件有所变化。指令CLI和STI的敏感条件不变,由于CPL=3,所以如果IOPL<3,那么执行CLI或STI指令将引起通用保护故障。输入/输出指令IN、INS、OUT或OUTS的敏感条件仅仅是当前V86任务TSS内的I/O许可位图,而忽略EFLAGS中的IOPL。输入/输出指令IN、INS、OUT或OUTS是否可以执行与CPL是否小于IOPL无关,而直接由I/O许可位图对应位决定,如果输入/输出指令所使用I/O地址对应的I/O许可位图内的各位都位0,则输入输出指令可正常执行,否则引起通用保护故障。
此外,在V86模式下,指令PUSHF、POPF、INT n和IRET却对IOPL敏感。也就是说,在V86模式下,当IOPL<3时,执行指令PUSHF、POPF、INT n及IRET会引起出错码为0的通用保护故障。
采取上述措施的目的是使操作系统软件可以支持一个“虚拟EFLAGS”寄存器。