Protection 5 ---- Priviliege Level Checking 2
CPU不仅仅在程序访问数据段和堆栈段的时候进行权限级别检查,当程序控制权转换的时候也会进行权限级别检查。程序控制权转换的情况很多,各种情况下检查的方式以及涉及到的检查项都是不同的。这篇文章主要描述了各种代码控制权转换过程中涉及到的各种检查并且配以相应的示例,示例代码是根据《Task》中的代码修改的,托管在https://github.com/activesys/learning_cpu/tree/master/x86/protection_5
很多指令都可以引起代码控制权的转换,例如call, jmp, int, lcall, ljmp, sysenter, sysexit以及syscall, sysret等等,但是不同的指令会引起不同类型的控制权转换,即使相同的指令接不同类型的选择子也会引起不同类型的控制权转换,总结起来有下面几种:
短跳转:这种跳转是在段内的控制权转换,不进行权限级别检查。长跳转:这种跳转是段间的控制权转换,进行权限级别检查,这篇文章主要关注的就是这类控制权转换。中断和异常:中断和异常引起的控制权转换在学习中断和异常的时候再描述。任务切换:任务切换引起的控制权转换已经在《Task》中描述了。sysenter,sysexit以及syscall,sysret指令实现的快速系统调用引起的控制权转换。在lcall或者ljmp指令后面接代码段选择子来实现段间的程序控制权转换,这个时候CPU要实施权限级别检查,检查涉及到CPL, RPL, DPL以及代码段描述符中的C位。C标志位的不同导致了代码段分为nonconforming和conforming,针对这两种类型的代码段的权限级别检查也是不同的。
nonconforming在跳转到nonconforming代码段的时候,CPU要求CPL == DPL && CPL >= RPL。当段选择子成功的加载到%cs之后,CPL并不改变。这样看来要访问nonconforming代码段必须是同级别的代码,即使是高权限级别代码访问低权限级别代码也是不行的。
为了验证对nonconforming代码段访问过程中的权限级别检查,我们必须添加两个nonconforming代码段,一个DPL==0,一个DPL==3,同时还有这两个段的“配套设置”:数据段,堆栈段和作为屏幕输出的扩展段:
从运行的结果可以看出从kernel代码跳转到了DPL==0的nonconforming代码段。
接下来试验一下CPL==3的时候跳转到DPL==3的nonconforming代码段,在user.s中加入长跳转:从结果上看控制权是从kernel代码转移到user代码,这时候CPL==3,然后通过lcall转移到了nonconforming代码段。
没有通过权限级别检查上面的例子都是通过的权限级别检查的,再来看看不能通过检查的情况,也就是当CPL!=DPL的时候,首先在CPL==0的代码中访问DPL==3的nonconforming代码段,在kernel.s加入lcall长跳转:
结果是在kernel中触发了#GP。
再来看看在CPL==3的代码中访问DPL==0的nonconforming代码段,在user.s中调用lcall长跳转:
从运行结果中可以看出在CPL==3的时候访问DPL==0的nonconforming代码段触发了#GP。conforming控制权切换到conforming代码段的时候进行的权限级别检查与nonconforming是不同的,conforming代码段描述中的DPL表示的是能够访问该代码段的最高权限级别,例如DPL==0,那么CPL==0~3都可以访问,但是如果DPL==3,那么只有CPL==3的代码段才可以访问。控制权转移到conforming代码段之后CPL并不改变,例如从CPL==3的代码段转换到DPL==0的conforming代码段,转换之后CPL仍然是3。
为了验证切换到conforming代码段时进行的权限级别检查,我们添加了三个代码段描述符:
从结果中可以看出成功的切换到了DPL==0的conforming代码段。
再来看看从CPL==3的代码段切换至DPL==3的conforming代码段,在user.s中加入如下代码:
从结果中可以看出从CPL==3的代码段成功的切换到了DPL==3的conforming代码段。
没有通过权限级别检查conforming代码段描述符中的DPL表示的是能够访问该代码段的CPL的最高权限,那么在CPL==0的代码段中访问DPL==3的conforming代码段必然会触发异常,为了做这个验证,在kernel.s加入如下代码:
我们再来试验一下在CPL==3的情况下访问DPL==0的conforming代码段,在user.s中加入下面代码:
怎么会触发异常了呢?按照conforming代码段的权限级别检查规则,这个测试应该是成功的,具体原因在哪里呢?其实这个#GP不是代码控制权转换过程中的权限检查产生的,而是进入conforming代码段之后加载段寄存器时的权限检查产生的,因为conforming代码段的控制权转换过程中,CPL不变,所以进入conforming代码段之后CPL仍然是3,但是要加载的代码段,堆栈段等段都是DPL==0的,这样就触发了#GP。
还记得最开始的时候我们准备了一段DPL==0,CPL==3的conforming代码段吗,现在可以派上用场了,它与DPL==0的conforming代码段的区别就是内部加载的段寄存器都是DPL==3的段,这样就不会触发#GP了。为了验证我们的猜测,在user.s中加入如下代码:
call gate到目前为止似乎不能够在不同权限级别之间进行切换,因为无论是nonconforming代码段还是conforming代码段,在发生控制权转换的过程中CPL都是不变的。为了实现权限级别切换,CPU提供了Call-Gate描述符。但是权限切换是有严格要求的,不是所有的情况都能够实现权限切换。
先来看看call-gate机制:
这是Intel官方文档中关于call-gate机制的描述,长跳转指令通过门选择子以及偏移量来选择门描述符,这里的偏移量CPU不会使用,但是必须提供,所以可以是任何值。门描述符中有段选择子以及偏移量,通过段选择子获得段描述符其中的段基址,然后与门描述符中的偏移量一同计算出实际的代码段。
这样通过call-gate来进行控制权转换的过程中进行权限级别检查时涉及的标志位就是:
CPLcall-gate的RPL门描述符的DPL目标代码段描述符的DPL目标代码段描述符的C标志位通过call-gate访问代码段的时候lcall和ljmp导致的权限检查是不同的:
这是Intel官方文档中给出的通过call-gate访问代码段的时候的权限检查规则,当访问nonconforming代码段的时候lcall和ljmp的检查规则是不一致的。
从表中可以看出只有lcall命令可以从低权限级别访问高权限级别的nonconforming代码段,对于conforming代码段,lcall和ljmp都可以实现从低权限级别到高权限级别的访问。但是只有lcall从低权限级访问高权限级别的nonconforming代码段的时候才会发生CPL改变,CPL变成nonconforming代码段的DPL,访问conforming代码段CPL仍然是不变的。
原来在user.s中访问DPL==0的nonconforming代码段是会触发#GP的,现在可以通过call-gate实现这样的访问。为了实现call-gate机制要在GDT中添加一个call-gate描述符:
从运行结果可以看出,代码实现了从CPL==3的代码段转换到了DPL==0的nonconforming代码段,如果不使用call-gate机制是会触发#GP的,这说明了call-gate的作用。
参考《Intel? 64 and IA-32 Architectures Software Developer’s Manual Volume 3 (3A, 3B & 3C): System Programming Guide》《自己动手写操作系统》《x86/x64体系探索与编程》