堆栈
复习长跳转和短跳转,如果调用或者跳转指令实在段间而不是段内进行的,称之为长 far jmp/call , 反之,如果在段内则是短的 near jmp / call !
那长的和短的 call 或者jmp 有什么分别?
对于jmp,仅仅是结果不同罢了,短跳转对应段内,而长跳转对应段间; 而call则稍微复杂些,因为call 指令会影响堆栈的,长调用和短调用对堆栈的影响是不同的!
只考虑32位情况! 对于短调用来说,call指令执行时下一条指令的eip压栈,到ret指令执行,eip 会被弹出!长调用跟着个类似!因为是长转移,所以cs也要压栈!
特权级的转移
所以call 一个调用门也是长调用,但是堆栈发生切换了!也就是说call指令执行前后的堆栈已经不再是同一个。
所以这就是问题所在!我们在堆栈A 压入的参数和返回地址,等需要的时候,堆栈已经切换到堆栈B 了!
intel 提供了一种机制! 将堆栈A 的诸多内容复制到堆栈B中!
这里只涉及到两个堆栈!
事实上,每一个任务最多都可能在4个特权级间的转移!所以,每个任务实际上需要4个堆栈!
但是我们只有一个ss和esp,那么当发生堆栈切换,我们该从哪里获得其余堆栈的ss和esp呢?
所以这就引申出来了新鲜的东东,那就是TSS TASK STATE STACK, 它也是一个数据结构,里面报告多个字段,32位的TSS 如图所示
从图中能出来,TSS 包含了很多字段,包含了3个ss 和 3 个 esp。 当发生堆栈切换,内层的ss和esp就是从这里取得的!
比如,我们当前所在的是ring3,当转移至ring1时,堆栈将被自动切换到由ss1和esp1指定的位置。 由于只是在由外层到内层(低特权级到高特权)切换到新堆栈才会从TSS 中取得的,所以TSS 中没有最外层的ring3的堆栈信息!
转移过程的工作:
- 根据目标代码段的DPL(新的CPL)从TSS 中选择应该切换至哪个SS和esp
- 从TSS 中读取新的ss和esp。在这过程中,如果发现ss、esp或者TSS 界限错误都会导致无效TSS 异常(#TS)。
- 对ss描述符进行检验,如果发生错误,同样产生#TS 异常!
- 暂时性的保存当前ss和esp的值!
- 加载新的ss和esp
- 将刚刚保存起来的ss和esp 的值压入新栈。
- 从调用者堆栈中讲参数复制到被调用者堆栈(新堆栈)中,复制参数的数目由调用门中param count一项来决定!如果param count为0 ,将不会复制参数!
- 将当前的cs 和ip 压栈!
- 加载调用门中指定的新的cs和eip,开始执行被调用者的过程!
在第七步,终于明白了paramcount的作用了! 但是param count只有5位,最多只能复制31个参数! 如果参数超过31个怎么办? 这时候可以让其中的某一个参数变成指向一个数据结构的指针。或者通过保存在新堆栈里的ss和esp来访问旧堆栈的参数!
call ret 指令,书上也讲了ret, ret能实现短返回和长返回,这个没啥好说的!但是它也实现带有特权级变化的长返回!
由被调用者到调用者的返回过程中,处理器的工作包含了以下步骤:
- 检查保存的cs中RPL 以判断返回时是否要变化特权级。
- 加载被调用者堆栈上的cs和eip (此时会进行代码段描述符和选择子类型和特权级检验)。
- 如果ret指令含有参数,则增加esp 的值以跳过参数,然后esp 将指向被保存过的调用者ss和esp。注意,ret 的参数必须对应调用门中的param count的值!
- 加载ss和esp,切换到调用者堆栈,被调用者的ss和esp被丢弃。在这里将会进行ss描述符、esp以及ss段描述符的检验!
- 如果ret指令含有参数,增加esp的值以跳过参数(此时已经在调用者堆栈中)
- 检查ds es fs gs的值,如果其中哪一个寄存器指向的段的DPL 小于CPL(此规则不适用于一致代码段),那么一个空描述符会被加载到该寄存器。
综上所述,使用调用门的过程分为两个部分:
- 从低特权级到高特权级,通过调用门和call指令来实现;
- 另一部分则是从高特权级到低特权级别,通过ret来是实现!
进入ring3
在ret指令执行之前,堆栈中肯定准备好了目标代码段的cs eip ss esp ! 因为一般都是压栈的!
执行ret之后就可以转移到低特权级代码中!
代码在pmtest4.asm 基础上修改!
我们要增加一个低特权级别的段,也就是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
....
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]
.....
; 初始化堆栈段描述符(Ring3)
xor eax, eax
mov ax, ds
shl eax, 4
add eax, LABEL_STACK3
mov word [LABEL_DESC_STACK3 + 2], ax
shr eax, 16
mov byte [LABEL_DESC_STACK3 + 4], al
mov byte [LABEL_DESC_STACK3 + 7], ah
.....
; 初始化Ring3描述符
xor eax, eax
mov ax, ds
shl eax, 4
add eax, LABEL_CODE_RING3
mov word [LABEL_DESC_CODE_RING3 + 2], ax
shr eax, 16
mov byte [LABEL_DESC_CODE_RING3 + 4], al
mov byte [LABEL_DESC_CODE_RING3 + 7], ah
.....
push SelectorStack3
push TopOfStack3
push SelectorCodeRing3
push 0
retf
....
; 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]
从代码看出 LABEL_DESC_CODE_RING3 设置为 DA_DPL3 ,堆栈LABEL_DESC_STACK3 也是 DA_DPL3。
LABEL_DESC_VIDEO 显存也设置为 DA_DPL3!
段选择子也是SA_RPL3!
注意一下,因为代码是运行在DPL3 ,所以为了要访问显存,也要把LABEL_DESC_VIDEO 设置为DPL3!
在代码的第392行 让程序不再继续执行。之所以这样做,是为了验证一下ring0 到 ring3的转移是否成功!!
如果屏幕上出现红色的3,并且按住不动,不在返回dos,说明转移成功!!!
就这样,我们把ring3的代码段,堆栈段都准备好了!
push SelectorStack3
push TopOfStack3
push SelectorCodeRing3
push 0
retf
所以就把ring3 段的选择子都压入堆栈中,执行retf!!
编译运行,我们能看到红色的3!