在没有特权级变换的情况下,程序的转移中的一些参数和返回地址都是push进同一个堆栈,这种情况比较简单。而如果转移伴随着特权级变换,那么我们就会涉及到两个堆栈,外层堆栈(调用者堆栈)和内层堆栈(被调用者堆栈)。(特权级变化的时候,堆栈也要发生变化,这个是处理器的机制,其作用是为了避免高特权级的过程由于栈空间不足而崩溃。)
既然涉及到两个堆栈,那么我们从哪里取得其余堆栈的ss和esp呢?那就得使用到TSS(Task-State Stack)。TSS里面包含多个字段,现在只关心偏移4到偏移27的3个ss和3个esp。我们有四个特权级ring0,ring1,ring2 & ring3,为什么只有ring0,ring1,ring2 的ss 和 esp 呢?原来只有当转移时从外层到内层时(低特权级到高特权级),新的堆栈ss & esp才会从TSS中取得,而特权级从高到低转移时,新堆栈的ss & esp 则通过其他方式获取。
好,新堆栈问题解决后,我们看下转移过程。下面是CPU在整个过程中所做的工作:
1. 根据目标代码段的DPL(新的CPL)从TSS中选择切换到的对应的ss和esp。
2. 从TSS中读取新的ss和esp。在这个过程中如果发现ss、esp或者TSS界限错误都会导致无效TSS异常(#TS)。
3. 对ss描述符进行检验,如果发生错误,同样产生错误,同样产生#TS异常。
4. 暂时性地保存当前ss和esp的值。
5. 加载新的ss和esp。
6. 将刚刚保存起来的ss和esp的值压入新栈。
7. 从调用者堆栈中将参数复制到被调用者堆栈(新堆栈)中,复制参数的数目有调用门中Param Count一项来决定。如果Param Count是零的话,将不会复制参数。
8. 将当前的cs和eip压栈。
9. 加载调用门中指定的新的cs和eip,开始执行被调用者过程。
那么,正如call指令对应ret,调用门也面临返回的问题。ret基本上就是call的反过程,只是带参数的ret指令会同时释放事先被压栈的参数。由被调用者到调用者的返回过程中,处理器的工作包括一下步骤:
1. 检查保存的cs中的RPL以判断返回时是否要变换特权级。
2. 加载被调用者堆栈上的cs和eip(此时会进行代码段描述符和选择子类型和特权级检查)。
3. 如果ret指令含有参数,则增加esp的值以跳过参数,然后esp指向被保存过的调用者ss和esp。注意,ret的参数必须对应调用门中的Param Count的值。
4. 加载ss和esp,切换到调用者堆栈,被调用者的ss和esp被丢弃。在这里将会进行ss描述符、esp以及ss段描述符的检查。
5. 如果ret指令含有参数,增加esp的值以跳过参数(此时已经在调用者堆栈中)。
6. 检查ds、es、fs、gs的值,如果其中哪一个寄存器指向的段的DPL小于CPL(此规则不适用于一致代码段),那么一个空描述符会被加载到该寄存器。