代码段间转移控制时的特权级检查(JMP或者CALL指令)
在保护模式下,JMP
或CALL
指令可以用以下四种方法之一来引用另外一个代码段:
1. 目标操作数含有目标代码段的段选择子和偏移
2. 目标操作数指向一个调用门描述符
3. 目标操作数指向一个TSS
4. 目标操作数指向一个任务门
后两种涉及任务的切换。本文仅对前两种进行讨论。
1. 直接调用或跳转到另一个代码段
JMP
、CALL
、RET
指令的近转移只是在当前代码段中执行程序的控制转移,因此不会执行特权级检查。JMP
、CALL
、RET
指令的远转移形式会把控制转移到另外一个代码段中,因此处理器一定会执行特权级检查。
当目标代码段的描述符中的C
字段为1时,表示这是一个一致代码段;为0时,表示这是一个非一致代码段。
1.1 转移到一致代码段(C=1)
要求调用者的CPL在数值上大于等于目标代码段的DPL。仅当CPL的数值小于DPL时,处理器才会产生一般保护异常。处理器忽略对RPL的检查。
CPL并不改变,由于CPL没有改变,因此堆栈也不会切换。
1.2 转移到非一致代码段(C=0)
要求调用者的CPL的数值必须等于目标代码段的DPL,RPL在数值上小于等于目标代码段的DPL;
当目标代码段的段选择子被加载进CS寄存器中时,CS的RPL字段在数值上等于调用者的CPL.
下图摘自《Intel Architecture Software Developer’s Manual Volume 2: Instruction Set Reference》中的CALL指令,对于我们理解这个过程很有帮助。
注意画红框的部分:第一行表示把目标代码段的选择子传到CS寄存器;第三行表示用当前特权级(不改变)修正CS寄存器的RPL字段。
1.3. 总结
我把以上检查规则总结为下面的表格:
大多数代码段都是非一致代码段。对于这些代码段,程序的控制权只能转移到具有相同特权级的代码段中,除非转移是通过一个调用门进行,见下文。
2. 通过调用门进行控制转移
2.1. 调用门描述符的格式
调用门用在不同特权级之间实现受控的程序控制转移,通常仅用于使用特权级保护机制的操作系统中。本质上,它只是一个描述符,一个不同于代码段和数据段的描述符,可以安装在GDT或者LGT中,但是不能安装在IDT(中断描述符表)中。
注意:Linux Kernel 0.12 中并没有用到调用门。
上图就是调用门描述符的格式(图片来自赵炯的《Linux内核完全剖析》)。
- 调用门描述符给出了代码段的选择子,有了段选择子,就可以访问GDT或者LDT得到代码段的基地址。
- 调用门描述符中给出了偏移量,因此通过调用门进行控制转移时,不使用指令中给出的偏移量。
- TYPE字段用于标识门的类型,
1100
表示调用门。 - 描述符中的P位是有效位,通常是
1
。当它为0
时,调用这样的门会导致处理器产生异常。 - DPL字段指明调用门的特权级,从而指定通过调用门访问特定过程所要求的特权级。
- 参数个数字段指明在发生堆栈切换时从调用者堆栈复制到新堆栈中的参数个数。
- 门调用的操作过程如下图所示(图片来自赵炯的《Linux内核完全剖析》)。
为了访问调用门,我们需要为CALL或者JMP指令的操作数提供一个远指针。该指针中的选择子用于指定调用门,而指针中的偏移值虽然需要,但是CPU不会使用它。该偏移值可以设置为任意值。当处理器访问调用门时,它会使用调用门中的段选择子来定位目标代码段的段描述符。然后CPU会把代码段描述符中的基地址和调用门中的偏移值进行组合,形成代码段中指定程序入口点的线性地址。
2.2. 通过调用门访问代码段时的特权级检查
通过调用门进行控制转移时,CPU会检查以下字段:
1. 当前特权级CPL
2. 调用指令中的调用门选择子的RPL
3. 调用门描述符中的DPL
4. 目标代码段描述符中的DPL
5. 目标代码段描述符中的一致性标志C
不再啰嗦,一张图看懂。
需要说明的是:如果通过调用门把控制转移到了更高特权级的非一致代码段中,那么CPL就会被设置为目标代码段的DPL值,并且会引起堆栈切换。
关于堆栈的切换过程,我会在下篇博文中总结,敬请关注……