2.关于实例三的说明 有些步骤的实现方法已在前面的实例中做过介绍,下面就任务内无特权级变换的转移和使用局部描述符LDT等作些说明: (1)实模式下初始化LDT 演示任务使用了局部描述符表LDT,本实例中该LDT在实模式下初始化(当然,也可以在使用LDT前的保护模式初始化)。为了简便,LDT中各描述符的界限和属性值在定义时预置,利用一个子程序设置各段的段基地址。为方便起见,在定义时把各段的段值安排在相应描述符的段基地址低16位字段中。由于实例中各段在实模式下定位(这是因为程序是从实模式下启动执行的),所以把段值乘以16就是对应的段基地址。 (2)装载LDTR寄存器 在使用LDT之前,还要装载局部描述符表寄存器LDTR。本实例中的如下两条指令用于装载LDTR: mov ax,LDT_SEL lldt ax LLDT指令是专门用于装载LDTR的指令。该指令的操作数是对应LDT段描述符的选择子。根据该选择子,处理器从GDT中取出相应的LDT段描述符,在进行合法性等检查后,LDT段描述符的基地址和界限等信息被装入LDTR的高速缓冲寄存器中。由于要引用GDT,所以不能在实模式下装载LDTR。在“操作系统类指令”一文中将对LLDT指令作详细说明。 (3)利用段间转移指令JMP实现任务内无特权级的转移 在本实例中进入保护方式后,特权级是0。通过如下段间直接转移指令实现从代码段K到代码段L的转移: JUMP16 CodeL_Sel,Virtual2 其中,选择子CodeL_Sel是对应代码段L的描述符的选择子。该描述符在LDT中,所以选择子中的描述符表指示位TI为1。描述符特权级是0,表示对应代码段的特权级是0,选择子中的请求特权级RPL也是0。目标代码段不是一致代码段,所以在CPL=DPL,RPL<=DPL的情况下,顺利进行相同特权级的转移:目标代码段的选择子CodeL_Sel被装入CS,对应描述符中的信息被装入高速缓冲寄存器中,偏移量Virtual2被装入指令指针寄存器。由于是16位代码段,所以偏移用16位表示。 类似地,通过如下段间直接转移指令实现从代码段L转移到代码段K: JUMP16 CodeK_Sel,Virtual3 其中,选择子CodeK_Sel是对应代码段K的描述符选择子。由于描述符在GDT中,所以选择子中的TI位是0。 (4)利用段间调用指令CALL实现任务内无特权级变换的转移 在代码段L中,通过段间直接调用指令CALL调用了在代码段C中的两个子程序,这些调用都是无特权级变换的转移。例如,利用如下指令调用了显示字符串子程序DispMsg: CALL16 CodeC_Sel,DispMsg 其中,CodeC_Sel是代码段C的选择子,DispMsg表示子程序的入口。描述代码段C的描述符在LDT中,描述符特权级DPL是0,所以使用的选择子CodeC_Sel的请求特权级RPL是0,描述符表指示位TI为1。目标代码段C不是一致代码段,所以在CPL=DPL,RPL<=DPL的情况下,顺利进行相同特权级的转移:当前CS和IP压入堆栈,目标代码段的选择子CodeC_Sel被装入CS,对应描述符中的信息被装入高速缓冲寄存器中,16位偏移DispMsg被装入指令指针IP。由于是16位段,所以偏移用16位表示,压入堆栈的是字而不是双字。 (5)段间返回指令RET实现任务内无特权级变换的转移 段间返回指令RET从堆栈的栈顶弹出返回地址(由选择子和偏移)构成。弹出选择子内的RPL=CPL,并且对应DPL=CPL,RPL<=DPL是当然的,所以能顺利进行相同特权级的转移。 3.别名技术 在上述实例三中,使用了两个描述符来描述演示任务的LDT段。段描述符LDTable被安排在GDT中,它是系统段描述符,把段LDTSeg描述成演示任务的局部描述符表LDT。描述符ToLDT被安排在LDT中,它是数据段描述符,把段LDTSeg描述成一个普通数据段。描述符LDTable被装载到LDTR,描述符ToLDT被装载到某个数据段寄存器。为什么要这样处理呢?根据实例三的功能要求,需要访问演示任务的局部描述符表LDT段,以取得代码段L的段界限值,这需要通过某个段寄存器进行,但不能把系统段描述符的选择子装载到段寄存器,所以采用两个描述符来描述段LDTSeg。 这种为了满足对同一个段实施不同方式操作的需要,而用多个描述符加以描述的技术称为别名技术。在保护方式程序设计中,常常要采用别名技术。例如:用两个具有不同类型值的描述符来描述同一个段。再如,用两个具有不同DPL的描述符来描述同一个段。 <三>任务内不同特权级的变换 在一个任务内可以存在四种特权级,所以常常会发生不同特权级之间的变换。例如,外层的应用程序调用内层操作系统的例程,以获得必要的诸如存储器分配等系统服务。内层操作系统的例程完成后,返回到外层应用程序。 在同一任务内,实现特权级从外层到内层变换的普通途径是使用段间调用指令CALL,通过调用门进行转移;实现特权级从内层向外层变换的普通途径是使用段间返回指令RET。注意,不能用JMP指令实现任务内不同特权级的变换。 1.通过调用门的转移 当段间转移指令JMP和段间调用指令CALL所含指针的选择子指示调用门描述符时,就可以实现通过调用门的转移。但只有CALL指令能变换到内层的特权级,JMP指令只能转移到同级的代码。 调用门描述符转移的入口点包含目标地址的段及偏移量的48位全指针。在执行通过任务门的段间转移指令JMP或段间调用指令CALL时,指令所含指针内的选择子用于确定调用门,而偏移被丢弃;把调用门内的48位全指针作为目标地址指针进行转移。 处理器采用与访问数据段相同的特权级规则控制对门描述符的访问。调用门描述符的DPL规定了访问该门的最外层特权级,在取出调用门内的48位全指针,把它作为目标地址指针向目标代码段转移之前,要进行特权级检查。只有在相同级或者更内层特权级的程序才可访问调用门,即CPL<=调用门的DPL。同时,还要求指示门的选择子的RPL必须满足RPL<=调用门的DPL的条件。检测通过后,才开始向目标代码段转移的步骤。其中还要检测目标描述符是否为代码段描述符,调用门内的选择子指示的描述符必须是代码段描述符。此外,在装载代码段描述符高速缓冲寄存器之前调整代码段选择子的RPL=0,也即调用门中代码段选择子的RPL被忽略。 在装载CS高速缓冲寄存器时,还要对目标代码段描述符进行保护检测。检测过程中的DPL不再是调用门的DPL,而是调用门内选择子所指示的目标代码段描述符的DPL。段间调用指令CALL和段间转移指令JMP所做的检测不一样。 对于使用调用门的段间转移指令JMP,检测条件与段间直接转移相同。由于已置RPL=0,所以可认为 RPL<=DPL的条件总能满足。所以,对于普通的非一致代码段,当CPL=DPL时,发生无特权级变换的转移;对于一致代码段,在满足CPL>=DPL时也发生无特权级变换的转移;其它情形就引起异常。 对于使用调用门的段间调用指令CALL,情形就不同了。由于已置RPL=0,所以可认为RPL<=DPL的条件总能满足。对于一致代码段,在满足CPL>=DPL时发生无特权级变换的转移。对于非一致代码段,当CPL=DPL时,仍发生无特权级变换的转移;当CPL>DPL时,就发生向内层特权级变换的转移,将调用门中的选择子和偏移装入CS和指令指针EIP中,并使CPL保持等于DPL,同时切换到内层堆栈。 综上所述,使用段间调用指令CALL,通过调用门可以实现从外层程序调用进入内层程序(JMP指令只能实现无特权级变换的转移);通过调用门也可实现无特权级变换的转移。需要注意的是,JMP指令和CALL指令都不能实现向外层特权级的转移否则会引起异常。 当然,CALL指令在最后把目标代码段的指针装入CS和EIP之前,要把原CS和EIP,即返回地址保存到堆栈。如无特权级变换,堆栈保持不变,返回地址就保存在原堆栈中;否则,返回地址保存在内层堆栈中。 2.堆栈切换 在使用CALL指令通过调用门向内层转移时,不仅特权级发生变换,控制转移到一个新的代码段,而且也切换到内层的堆栈段。从本教程第五篇的任务状态段TSS的格式可见,TSS中包含有指向0级、1级和2级堆栈的指针。在特权级发生向内层变换时,根据变换到的特权级使用TSS中相应的堆栈指针对SS及ESP寄存器进行初始化,建立起一个空栈。 在建立起内层堆栈后,处理器先把外层堆栈的指针SS及ESP寄存器的值压入内层堆栈,以使得相应的向外层返回可恢复原来的外层堆栈。然后,从外层堆栈复制以双字为单位的调用参数到内层堆栈中,调用门中的DCOUNT字段值决定了复制参数的数量。这些被复制的参数是主程序通过堆栈传递给子程序的实参,在调用之前被压入外层堆栈。通过复制栈中的参数,使内层的子程序不需要考虑堆栈的切换,而容易地访问主程序传递过来的实参。最后,调用的返回地址被压入堆栈,以便在调用结束时返回。下图为在向内层变换时,建立内层堆栈,并从外层堆栈复制2个双字参数到内层堆栈的示意图。图中每项是双字,可见的段寄存器内的选择子被扩展成32为存入堆栈,高16位为0。对于16位的使用调用门的段也是如此。 需要注意的是,无论是否通过调用门,只要不发生特权级变换,就不会切换堆栈。 3.向外层返回 与使用CALL指令通过调用门向内层变换相反,使用RET指令实现向外层返回。段间返回指令RET从堆栈中弹出返回地址,并且可以采用调整ESP的方法,跳过相应的在调用之前压入堆栈的参数。返回地址的选择子指示要返回的代码段的描述符,从而确定返回的代码段。选择子的RPL确定返回后的特权级,而不是对应描述符的DPL,这是因为,段间返回指令RET可能使控制返回到一致代码段,而一致代码段可以在DPL规定的特权级以外的特权级执行。需要注意的是,RET指令所使用的返回地址的选择子只能使用代码段描述符,而不能使用任何系统段描述符或门描述符,当然,更不能使用数据段描述符,否则会引起异常。与CALL指令相对应,RET指令也不能向内层返回。 段间返回指令完成返回的步骤如下: (1)RET指令先从堆栈中弹出返回地址。如果弹出地址的选择子的RPL规定相对于CPL更外层的特权级,那么就引起向外层返回。 (2)为向外层返回,跳过内层堆栈中的参数,再从内层栈中弹出指向外层堆栈的指针,并装入SS及 ESP,以恢复外层堆栈。 (3)调整ESP,跳过在相应的调用之前压入到外层堆栈的参数。即返回指令不但弹出内层栈的参数,而且也弹出外层栈的参数。 (4)然后,检查数据段寄存器DS、ES、FS及GS,以保证寻址的段在外层是可访问的,如果段寄存器寻址的段在外层是不可访问的,那么装入一个空选择子,以避免在返回时发生保护空洞。 (5)返回外层继续执行。 上述五步是对带立即数的段间返回指令而言的,立即数规定了堆栈中要跳过的参数的字节数。对于无立即数的段间返回指令缺少第二步和第三步。若RET指令不需要向外层返回,那么就只有(1)和(5)两步。对于有通过堆栈传递参数的子程序,必须使用带立即数的返回指令返回,否则返回时会装载错误的外层栈指针。 若不使用带立即数的返回指令,可以在返回前把外层栈的栈指针存入内层栈中的用于保存返回地址上方两个双字的区域中,由外层返回的过程可知,这可正确恢复外层栈的指针,但在外层程序中,必须人为调整外层栈指针,以便废除在外层栈中压入的参数。在使用C调用约定的程序中可使用此方法。但这会增加代码的长度和处理时间,使代码效率变低。正因为如此,在Windows 9X下,新增加了一种STDCALL的调用约定,它正是为了适应Intel系列处理器的体系结构而产生的。 <四>演示任务内特权级变换的实例(实例四) 下面给出一个演示任务内特权级变换的实例。该实例演示在任务内通过调用门从外层特权级变换到内层特权级;也演示通过段间返回指令从内层特权级变换到外层特权级;还演示通过调用门的无特权级变换的转移。实例使用了任务状态段TSS,这是因为任务内特权级变换时要使用的内层堆栈指针存放在TSS中。 1.实现步骤 该实例的实现步骤为: (1)实方式下初始化; (2)切换到保护模式; (3)设置TR和LDTR。由于在任务内发生特权级变换时要切换堆栈,而内层堆栈的指针存放在当前任务的TSS中,所以在进入保护模式后设置任务状态段寄存器TR。由于演示任务使用了局部描述符表,所以设置LDTR; (4)经调用门进入32位过渡代码段; (5)建立返回3级演示代码段的环境; (6)利用RET指令转移到3级的演示代码段。为了演示外层程序通过调用门调用内层程序,要使CPL>0。实例先通过段间返回指令RET从特权级0变换到特权级3的演示代码段。在特权级3下,通过调用门调用1级的子程序。随着执行段间返回指令RET,又回到3级的演示代码段; (7)在3级的演示代码段中,经调用门转移到0级的32位过渡代码段; (8)直接转0级的临时代码段; (9)准备返回实模式; (10)切换回实模式; (11)实模式下的恢复工作。 2.源程序组织和清单 实例四由如下部分组成: (1)全局描述符表GDT。GDT含有演示任务的TSS段描述符和LDT段描述符,此外还含有临时代码段的描述符、规范数据段描述符和视频缓冲区段描述符。 (2)演示任务的LDT段。它含有除临时代码段外的其它代码段的描述符和演示任务各级堆栈段描述符,还含有3个调用门。 (3)演示任务的TSS段。 (4)演示任务的0级、1级和3级堆栈段。 (5)显示子程序段。32位代码段,特权级1。 (6)演示代码段。32位代码段,特权级3。 (7)过渡代码段。32位段,特权级0。 (8)临时代码段。16位段,特权级0。 (9)实模式下的数据和代码段。 该实例的逻辑功能是显示演示代码段执行时的当前特权级CPL。源程序清单如下: INCLUDE 386SCD.INCGDTSeg SEGMENT PARA USE16 GDT LABEL BYTE DUMMY Desc <> Normal Desc <0ffffh,,,ATDW,,> VideoBuf Desc <07fffh,8000h,0bh,ATDW+DPL3,,>EFFGDT LABEL BYTE DemoTSS Desc <DemoTssLen-1,DemoTSSSeg,,AT386TSS,,> DemoLDTD Desc <DemoLDTLen-1,DemoLDTSeg,,ATLDT,,> TempCode Desc <0ffffh,TempCodeSeg,,ATCE,,>GDTLen = $-GDT GDNum = ($-EFFGDT)/(SIZE Desc) Normal_Sel = Normal-GDT Video_Sel = VideoBuf-GDT DemoTSS_Sel = DemoTSS-GDT DemoLDT_Sel = DemoLDTD-GDT TempCode_Sel = TempCode-GDT GDTSeg ENDS DemoLDTSeg SEGMENT PARA USE16 DemoLDT LABEL BYTE DemoStack0 Desc <DemoStack0Len-1,DemoStack0Seg,,ATDW+DPL0,D32,> DemoStack1 Desc <DemoStack1Len-1,DemoStack1Seg,,ATDW+DPL1,D32,> DemoStack3 Desc <DemoStack3Len-1,DemoStack3Seg,,ATDW+DPL3,,> DemoCode Desc <DemoCodeLen-1,DemoCodeSeg,,ATCE+DPL3,D32,> T32Code Desc <T32CodeLen-1,T32CodeSeg,,ATCE,D32,> EchoSubR Desc <EchoSubRLen-1,EchoSubRSeg,,ATCER+DPL1,D32,>DemoLDNum = ($-DemoLDT)/(SIZE Desc) DemoStack0_Sel = DemoStack0-DemoLDT+TIL+RPL0 DemoStack1_Sel = DemoStack1-DemoLDT+TIL+RPL1 DemoStack3_Sel = DemoStack3-DemoLDT+TIL+RPL3 DemoCode_Sel = DemoCode-DemoLDT+TIL+RPL3 T32Code_Sel = T32Code-DemoLDT+TIL Echo_Sel1 = EchoSubR-DemoLDT+TIL+RPL1 Echo_Sel3 = EchoSubR-DemoLDT+TIL+RPL3 ToT32GateA Gate <T32Begin,T32Code_Sel,,AT386CGate,> ToT32GateB Gate <T32End,T32Code_Sel,,AT386CGate+DPL3,> ToEchoGate Gate <EchoSub,Echo_Sel3,,AT386CGate+DPL3,>DemoLDTLen = $-DemoLDT ToT32A_Sel = ToT32GateA-DemoLDT+TIL ToT32B_Sel = ToT32GateB-DemoLDT+TIL ToEcho_Sel = ToEchoGate-DemoLDT+TILDemoLDTSeg ENDS DemoTSSSeg SEGMENT PARA USE16 DD 0 DD DemoStack0Len DD DemoStack0_Sel DD DemoStack1Len DD DemoStack1_Sel DD 0 DD 0 DD 0 DD 0 DD 0 DD 0 DD 0 DD 0 DD 0 DD 0 DD 0 DD 0 DD 0 DD 0 DD 0 DD 0 DD 0 DD 0 DD 0 DD DemoLDT_Sel DW 0 DW $+2 DW 0ffffh DemoTSSLen = $DemoTSSSeg ENDS DemoStack0Seg SEGMENT DWORD STACK USE32 DemoStack0Len = 512 DB DemoStack0Len DUP(?)DemoStack0Seg ENDS DemoStack1Seg SEGMENT DWORD STACK USE32 DemoStack1Len = 512 DB DemoStack1Len DUP(?)DemoStack1Seg ENDS DemoStack3Seg SEGMENT DWORD STACK USE16 DemoStack3Len = 512 DB DemoStack3Len DUP(?)DemoStack3Seg ENDS EchoSubRSeg SEGMENT PARA USE32 ASSUME CS:EchoSubRSegMessage DB 'CPL=',0 EchoSub PROC FAR cld push ebp mov ebp,esp mov ax,Echo_Sel1 mov ds,ax mov ax,Video_Sel mov es,ax mov edi,1996 mov esi,OFFSET Message mov ah,4eh EchoSub1: lodsb or al,al jz EchoSub2 stosw jmp EchoSub1EchoSub2: mov eax,[ebp+8] and al,3 add al,'0' mov ah,4eh stosw pop ebp retfEchoSub ENDPEchoSubRLen = $EchoSubRSeg ENDS DemoCodeSeg SEGMENT PARA USE32 ASSUME CS:DemoCodeSegDemoBegin PROC FAR CALL32 ToEcho_Sel,0 CALL32 ToT32B_Sel,0 DemoBegin ENDPDemoCodeLen = $DemoCodeSeg ENDS T32CodeSeg SEGMENT PARA USE32 ASSUME CS:T32CodeSegT32Begin PROC FAR mov ax,DemoStack0_Sel mov ss,ax mov esp,DemoStack0Len push DWORD PTR DemoStack3_Sel push DemoStack3Len push DWORD PTR DemoCode_SEL push OFFSET DemoBegin retf T32Begin ENDPT32End PROC FAR JUMP32 TempCode_Sel,<OFFSET ToReal>T32End ENDPT32CodeLen = $T32CodeSeg ENDSTempCodeSeg SEGMENT PARA USE16 ASSUME CS:TempCodeSegVirtual PROC FAR mov ax,DemoTSS_Sel ltr ax mov ax,DemoLDT_Sel lldt ax JUMP16 ToT32A_Sel,0 ToReal: mov ax,Normal_Sel mov ds,ax mov es,ax mov fs,ax mov gs,ax mov ss,ax mov eax,cr0 and al,11111110b mov cr0,eax JUMP16 <SEG Real>,<OFFSET Real>Virtual ENDPTempCodeLen = $TempCodeSeg ENDSRDataSeg SEGMENT PARA USE16 VGDTR PDesc <GDTLen-1,> SPVar DW ? SSVar DW ? RDataSeg ENDSRCodeSeg SEGMENT PARA USE16 ASSUME CS:RCodeSeg,DS:RDataSegStart PROC mov ax,RDataSeg mov ds,ax cld CALL InitGDT mov ax,DemoLDTSeg mov fs,ax mov si,OFFSET DemoLDT mov cx,DemoLDNum CALL InitLDT mov SSVar,ss mov SPVar,sp lgdt QWORD PTR VGDTR cli mov eax,cr0 or al,1 mov cr0,eax JUMP16 <TempCode_Sel>,<OFFSET Virtual>Real: mov ax,RDataSeg mov ds,ax lss sp,DWORD PTR SPVar sti mov ax,4c00h int 21hStart ENDPInitGDT PROC push ds mov ax,GDTSeg mov ds,ax mov cx,GDNum mov si,OFFSET EFFGDTInitG: 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 retInitGDT ENDPInitLDT PROCILDT: 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 ILDT retInitLDT ENDPRCodeSeg ENDS END Start  
|