汇编教程:控制转移(2)
2009-02-16 09:36:49 来源:WEB开发网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。源程序清单如下:
mov GISter>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位表示。(本文来自编程入门网:www.bianceng.cn)
类似地,通过如下段间直接转移指令实现从代码段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,即返回地址保存到堆栈。如无特权级变换,堆栈保持不变,返回地址就保存在原堆栈中;否则,返回地址保存在内层堆栈中。
更多精彩
赞助商链接