操作系统实践之第二章(特权级变换*理论篇)

来源:互联网 发布:秋瓷炫长相知乎 编辑:程序博客网 时间:2024/05/22 12:00

在IA32的分段机制中,特权级总共有4个特权级别,从高到低分别是0、1、2、3。数字越小表示对应的特权级越大。

较为核心的代码和数据,将被放在特权级较高的层级中(0:内核代码;1/2:服务;3:应用程序)。处理器将用这样的机制来避免低特权级的任务在不被允许的情况下访问位于高特权级的段。如果处理器检测到一个访问请求是不合法的,将会产生常规保护错误(#GP)。

处理器在判别任务是否拥有对应访问权限时,通过CPL、DPL、RPL这三种特权级进行检验。

1.   CPL(Current Privilege Level)

CPL是当前执行程序或任务的特权级。他被存储在CS和SS的第0位和第1位上。在通常情况下,CPL等于代码所在的段的特权级。当程序转移到不同特权级的代码段时,处理器将改变CPL。

在遇到一致代码段时,情况稍微有点特殊,一致代码段可以被相同或者更低特权级的代码访问。当处理器访问一个与CPL特权级不同的一致代码段时,CPL不会被改变。

2.   DPL(Descriptor Privilege Level)

DPL表示段或门的特权级。它被存储在段描述符或者门描述符的DPL字段中,正如我们先前看到的那样。当当前代码段试图访问一个段或者门时,DPL将会和CPL以及段或门选择子的RPL相比较,根据段或门类型的不同,DPL将会被区别对待,下面介绍一下各种类型的段或者门的情况。

数据段:DPL规定了可以访问此段的最低特权级。例如,一个数据段的DPL是1,那么只有运行在CPL为0或1的程序才有全访问它。

非一致代码段(不使用调用门的情况下):DPL规定访问此段的特权级,只有CPL和该段的DPL相等时才可访问。

调用门:DPL规定了当前执行的程序或任务可以访问此调用门的最低特权级(这与数据段的规则是一致的)。

一致代码段和通过调用门访问的非一致代码段:DPL规定了访问此段的最高特权级。例如,一个一致代码段的DPL是2,则只有CPL为2、3的程序可以访问该段。

TSS:DPL规定了可以访问该TSS的最低特权级,与数据段的规则一致。

3.   RPL(Requested Privilege Level)

RPL是通过选择子的第0位和第1位表现出来的。处理器通过检查RPL和CPL来确认一个访问请求是否合法。即便提出访问请求的段有足够的特权级,如果RPL不够也是不行的。也就是说,如果RPL的数字比CPL大(数字越大特权级越低),那么RPL将会起决定性作用,反之亦然。

操作系统过程往往用RPL来避免低特权级应用程序访问高特权级段内的数据。当操作系统过程(被调用过程)从一个应用程序(调用过程)接收到一个选择子时,将会把选择子的RPL设置成调用者的特权级。于是,当操作系统用这个选择子去访问相应的段时,处理器将会用调用过程的特权级(已经被存到RPL中),而不是操作系统更高的操作系统过程的特权级(CPL)进行特权检验。这样,RPL就保证了操作系统不会越俎代庖地代表一个程序去访问一个段,除非这个程序本身是有权限的。

直接使用jmp和call虽然也能进行特权级直接的跳转,但是这种跳转是非常有限的。对于非一致代码段,只能在相同特权级代码段之间转移;遇到一致代码段也只能从低到高跳转,而且CPL不会改变。如果想要自由地进行不同特权级之间的转移,显然还需要其他方法,就是运用门描述符或者TSS。

门描述符的结构如下:

可以看到,门描述符和我们前面看的段描述符有较大的不同,它主要定义了目标代码对应段的选择子、入口地址的偏移和一些属性。在第5个字节中各项内容的含义与前面用过的段描述符中的含义相同,以便识别描述符的类型。此处的S位将是0,因为这是一个门描述符。

现在我们唯一还不清楚的就是第4个字节中的“Param Count”这个成员。其实这部分是用来指定从调用者堆栈复制到被调用者堆栈的参数数目的。如果现在还不是太懂没什么关系,后面会有详细的介绍。

门描述符的结构直观来看,是由一个选择子和一个偏移所指定的线性地址,程序正是通过这个地址进行转移的。门描述符可以分为四种:调用门,中断门,陷阱门,任务门。其中,中断门和陷阱门是特殊的调用门。

我们想要用门的形式来实现不同特权级代码之间的转移,就必须要了解使用调用门进行转移的特权级检验规则。

假设我们想由代码A转移到代码B,用用一个调用门G,即调用门G中的目标选择子指向代码B的段。过程中,我们涉及了几个要素:CPL,RPL,代码B的DPL(记做DPL_B),调用门G的DPL(DPL_G)。当A访问G这个调用门时,规则相当于访问一个数据段,要求CPL和RPL都小于或者等于DPL_G。即要求CPL和RPL需要在更高的特权级上。

除此之外,系统还将比较CPL和DPL_B。如果是一致代码段的话,要求DPL_B<=CPL;如果是非一致代码段的话,call指令和jmp指令又有所不同。在用call指令时,要求DPL_B<=CPL;在用jmp指令时,只能是DPL_B=CPL。综上所述,规则如下:

其中,红框框出的部分是为了确保可以访问调用门;绿框框出的部分则是在不同代码段中call和jmp指令所需满足的转移规则。

由上可见,通过调用门和call指令,我们可以实现从低特权级到高特权级的转移,无论目标代码是一致的还是非一致的。

自己用代码实现特权级间的转换显然是一件有趣的事情,但是到目前为止我们只能实现从低特权级到高特权级的转换,更无奈的是,我们的代码一直以来都是运行在最高特权级下的……看样子在实践之前我们还是不得不先学会如何降低特权级才行。

特权级转移的复杂之处,不但在于严格的特权级检验,还在于特权级变换的时候,堆栈也会随之发生变化。处理器的这种机制避免了高特权级的过程由于栈空间不足而崩溃。而且,如果不同特权级共享同一个堆栈的话,高特权级的程序可能因此受到有意或无意的干扰,很容易导致程序甚至操作系统发生意外。

那么不同特权级之间转换的时候堆栈又是如何随之变化的呢?此时,正如上面所说,将发生堆栈的切换;那么堆栈在切换的时候又会发生什么呢?我们在堆栈A中压入参数和返回时的地址,等到需要使用它们的时候堆栈已经变成B了,这又如何是好?Intel提供的解决方案是将A中的诸多内容复制到堆栈B中。如下图示:

上图展示了两个堆栈间的联系情况。需要说明的是,SSESP只有在特权级由低到高转换时才会被压栈。实际上,由于每一个任务最多可能在4个特权级间转移,所以,每个任务实际上需要4个堆栈。可是,我们只有一个SS和一个ESP,那么当发生堆栈切换后,我们又该去哪里获得其余堆栈的SS和ESP呢?实际上,这里还有另一位主人公TSS(Task-State Stack),它是一个数据结构,里面包含了多个字段,32位的TSS如下:

由上可见,TSS包含很多个字段,这里我们只关注偏移4到偏移27的3个SS和3个ESP。当发生堆栈切换时,内层的SS和ESP就是从这里取得的。

例如,我们当前在ring3,当转移至ring1时,堆栈将被自动切换到SS1和ESP1指定的位置。由于只是在由外层到内层(低特权级到高特权级)切换时新堆栈才会从TSS中取得,所以TSS张没有位于最外层的ring3的堆栈信息。如果是从内层到外层的情况是可以从内层堆栈中得到外层SSESP的相应信息的,不需要用到TSS(因为相应的SSESP在高特权级的堆栈中存着)

到现在为止,堆栈相关的问题解决的也差不多了,那就整理一下转移的具体过程。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,开始执行被调用者过程。

到现在终于把Param Count的作用搞清楚了。需要说明的是,Param Count只有5位,也就是说,最多只能复制31个参数。如果参数多于31个,可以让其中的某个参数变成指向一个数据结构的指针,或者通过保存在新堆栈里的SS和ESP来访问旧堆栈中的参数。

正如call指令对应ret,调用门也面临返回的问题。实际上,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(此规则不使用于一致代码段),那么一个空描述符将会被加载到该寄存器。

上述过程如下图所示:

综上所述,使用调用门的过程实际上分为两个部分,一部分是从低特权级到高特权级,通过调用门和call指令来实现;另一部分则是从高特权级到低特权级,通过ret指令来实现。是的,ret指令就可以实现由高特权级到低特权级的转移。




0 0
原创粉丝点击