纸上得来终觉浅,绝知此事还是得捣鼓捣鼓才行啊。理论与实践还是要两手抓两手都要硬嘛。
从理论上来说,在ret指令执行前,堆栈中应该已经准备好了目标代码段的CS、EIP,以及SS和ESP,另外还可能有参数。需要注意的是,这些既可以使处理器压入栈的,也可以使由我们自己压入栈的。ret指令执行前的堆栈可能如下图示:
这样,执行ret后就可以转移到低特权级的代码中 了。我们在前面写过的代码中添加可以添加一个ring3的代码段和一个ring3的堆栈段,如下:
LABEL_DESC_CODE_RING3: Descriptor 0,SegCodeRing3Len-1, DA_C+DA_32+DA_DPL3
……
LABEL_DESC_STACK3: Descriptor 0, TopOfStack3, DA_DRWA+DA_32+DA_DPL3
……
LABEL_DESC_VIDEO: Descriptor 0B8000h, 0ffffh, DA_DRW+DA_DPL3
……
SelectorCodeRing3 equ LABEL_DESC_CODE_RING3 - LABEL_GDT + SA_RPL3
……
SelectorStack3 equ LABEL_DESC_STACK3 - LABEL_GDT + SA_RPL3
……
; 堆栈段ring3
[SECTION .s3]
ALIGN 32
[BITS 32]
LABEL_STACK3:
times 512 db 0
TopOfStack3 equ $ - LABEL_STACK3 - 1
; END of [SECTION .s3]
……
; CodeRing3
[SECTION .ring3]
ALIGN 32
[BITS 32]
LABEL_CODE_RING3:
mov ax, SelectorVideo
mov gs, ax
mov edi, (80 * 14 + 0) * 2
mov ah, 0Ch
mov al, '3'
mov [gs:edi], ax
jmp $
SegCodeRing3Len equ $ - LABEL_CODE_RING3
; END of [SECTION .ring3]
这段ring3的代码一目了然,只是由于这段代码运行在ring3,而在其中要写显存而访问到VIDEO段,为了不会产生错误,我们把VIDEO段的DPL修改为了3。
另外,新段对应的描述符的属性加上了DA_DPL3,让它的DPL变成了3,相应的选择子的SA_RPL3将RPL也置为了3。初始化描述符的代码与前面相似,就不再重复了。这样,代码段和堆栈段就准备好了,下一步就是为retf指令准备SS、ESP、CS、EIP这些寄存器的目标值了,具体操作如下:
push SelectorStack3
push TopOfStack3
push SelectorCodeRing3
push 0
retf
至此需要修改的就改完了,我们将程序编译为test.com然后拷贝到pm.img中,可以运行程序看到如下结果(书本完整源码pmtest5a.asm):
看到最后红色的3就表明我们已经由ring0跳转到ring3了!历史性的特权级间跳转成功!
进入ring3之后就可以体验一下调用门了,先将调用门的描述符和选择子以及[SECTION .ring3]的代码稍作修改。
; 门 目标选择子,偏移,DCount, 属性
LABEL_CALL_GATE_TEST: Gate SelectorCodeDest, 0, 0, DA_386CGate+DA_DPL3
……
SelectorCallGateTest equ LABEL_CALL_GATE_TEST - LABEL_GDT + SA_RPL3
……
; CodeRing3
[SECTION .ring3]
ALIGN 32
[BITS 32]
LABEL_CODE_RING3:
mov ax, SelectorVideo
mov gs, ax
mov edi, (80 * 14 + 0) * 2
mov ah, 0Ch
mov al, '3'
mov [gs:edi], ax
call SelectorCallGateTest:0
jmp $
SegCodeRing3Len equ $ - LABEL_CODE_RING3
; END of [SECTION .ring3]
在进入死循环前,我们使用了调用门指令,修改相应的描述符和选择子是为了满足CPL和RPL都小于等于调用门DPL的条件。但是现在还不能运行,因为从低特权级到高特权级的转移需要TSS的帮助,下面我们就来准备一个TSS。
LABEL_DESC_TSS: Descriptor 0, TSSLen-1, DA_386TSS
……
SelectorTSS equ LABEL_DESC_TSS - LABEL_GDT
……
; TSS
[SECTION .tss]
ALIGN 32
[BITS 32]
LABEL_TSS:
DD 0 ; Back
DD TopOfStack ; 0 级堆栈
DD SelectorStack ;
DD 0 ; 1 级堆栈
DD 0 ;
DD 0 ; 2 级堆栈
DD 0 ;
DD 0 ; CR3
DD 0 ; EIP
DD 0 ; EFLAGS
DD 0 ; EAX
DD 0 ; ECX
DD 0 ; EDX
DD 0 ; EBX
DD 0 ; ESP
DD 0 ; EBP
DD 0 ; ESI
DD 0 ; EDI
DD 0 ; ES
DD 0 ; CS
DD 0 ; SS
DD 0 ; DS
DD 0 ; FS
DD 0 ; GS
DD 0 ; LDT
DW 0 ; 调试陷阱标志
DW $ - LABEL_TSS + 2 ; I/O位图基址
DB 0ffh ; I/O位图结束标志
TSSLen equ $ - LABEL_TSS
上面我们只初始化了0级堆栈,添加初始化TSS描述符的代码后,TSS就算是准备好了,我们需要在特权级转换之前加载它。
call DispReturn
mov ax, SelectorTSS
ltr ax
push SelectorStack3
push TopOfStack3
push SelectorCodeRing3
push 0
retf
到目前为止,我们已经成功地实现了由高特权级到低特权级或者由低特权级到高特权级的双向转换,并且熟悉了调用门这种经典门描述符的使用。
为了可以让程序顺利地返回实模式,我们将调用局部任务的代码加入到调用门的目标代码(SECTION .sdest)。最后,程序将由这里进入局部任务,然后原路返回实模式。
[SECTION .sdest]; 调用门目标段
[BITS 32]
LABEL_SEG_CODE_DEST:
mov ax, SelectorVideo
mov gs, ax ; 视频段选择子(目的)
mov edi, (80 * 12 + 0) * 2 ; 屏幕第 12 行, 第 0 列。
mov ah, 0Ch ; 0000: 黑底 1100: 红字
mov al, 'C'
mov [gs:edi], ax
; Load LDT
mov ax, SelectorLDT
lldt ax
jmp SelectorLDTCodeA:0 ; 跳入局部任务,将打印字母 'L'。
;retf
SegCodeDestLen equ $ - LABEL_SEG_CODE_DEST
; END of [SECTION .sdest]
最后的执行结果如下图(完整代码pmtest5.asm):
总结一下过程:一个是从高特权级到低特权级的跳转如何确定是跳转到哪一级呢?这个是由当时对应堆栈中的CS、EIP等决定的。那从低特权级到高特权级又是如何确定的呢?根据目标代码段的DPL(新的CPL)从TSS中选择应该切换至哪个SS和ESP。