[自制操作系统] 第10回 认识保护模式之深入浅出特权级

目录
一、前景回顾
二、什么是特权级检查
三、门
四、如何进行特权级检查
五、调用门的跳转执行流程
六、调用门的跳转权限检查

 

一、前景回顾

  我们在前面讲过保护模式较之于实模式的三大特点:分页机制、特权级和分时机制。现在分页机制的坑已经填好了,接下来我们开始填特权级的坑。

二、什么是特权级检查

  首先我们来看看什么是特权级,CPU为了计算机的安全,将程序拥有的权力划分为了4个等级,也就是特权级,特权级按照权力大小划分为了0、1、2、3级,数字越小,权力越大。我们操作系统内核所占的特权级就是0级,处于至高无上的地位,而用户的应用程序处于3级,最低特权级。不过在Linux中,实际只用上了0级和3级,即我们常说的用户态便处于3级,内核态处于0级。

  特权级检查,说到底就是起到一个保护作用,操作系统为了避免用户程序随意访问内核,修改内核数据,所以在“访问者”访问“受访者”那一刹那,体现在实际中就是:段寄存器中的值被重新加载段选择子时(因为我们已经进入保护模式),检查访问者的特权级和受访者的特权级是否匹配。

  在特权级中我们要认识一些特定术语:

  1、CPL:处理器当前的特权级。

  2、DPL:段或者门的特权等级,位于段描述符或者门描述符的DPL字段。

  3、RPL:请求特权级,位于选择子的RPL字段。

  书上其实关于特权级的介绍还是比较详细,只不过对于初学者来说,不是那么好容易理解。我前前后后看了好几遍才算是大体理解了。所以我尝试用我自己的语言来描述一下特权级检查,重点就是CPL、DPL和RPL之间的关系。

  首先CPL是用来描述处理器当前的特权级的,我们的处理器只用上了0和3特权级,也就是说如果处理器在执行用户态程序时,处理器此时的特权级就是3级,如果处理器此时处于内核态,那么处理器的特权级就是0级。

  DPL描述的是段描述符或者门描述符的访问门槛,也就是说如果处理器的特权级低于目标段或者目标门的访问门槛,那么处理器就无法访问段描述符或者门描述符描述的那段内存区域。我相信这个也是比较好理解的。

  重点就是RPL,RPL表示的是请求资源的能力。这个其实是最不好理解的,咋一看RPL其实跟CPL应该是没什么区别的,因为书上都说,当处理器从一个特权级的代码段A转移到另一个特权级的代码段B上运行时(这里假设特权检测通过),处理器的CPL就会变更为代码段B的DPL,也就是目标代码段描述符的DPL将保存在代码段寄存器CS的RPL位,其实还是有区别的。

  这里我自己的理解是:RPL其实并不是指的处理器的请求资源能力,它应该是描述的段的请求资源的能力。我还是举例子来说明,如果用户通过中断门从用户态进入内核态(这个是很常见的低特权级进入高特权级的方式),此时CS段寄存器的CPL和RPL都应该是0,因为此时我们已经进入了内核态,无所不能了。如果用户程序有坏心思,想将代码段C(DPL为0)中存放的内核代码拷贝到指定的数据缓冲区中,这个缓冲区本质上是一个数据段D。重点来了,不管这个数据段是来自用户空间还是内核空间,当段选择子加载到DS寄存器时,该段数据段的RPL都会被操作系统修改为用户进程的CPL,也就是3。此时我们来看看特权检查的规则:

  请求某特权级为DPL级的资源时,参与特权检查的不只是CPL,还要加上RPL,CPL和RPL的特权必须同时大于等于受访者的特权DPL:

  数值上CPL≤DPL并且RPL≤DPL

  显然该数据段就无法接收内核代码,因为它的RPL为3,没有这个能力。关于特权级更详细的介绍我不在赘述,感兴趣的朋友可以参考原书《操作系统真象还原》p229~p251。

三、门

  “门结构”的存在是为了实现从低特权级到高特权级的转变,CPU中实现特权级变换有四种门。门结构就是记录一段程序起始地址的描述符。

  

  他们的使用方式有些许不同:

  1、任务门:任务门以任务段TSS位单位,用来实现任务切换。但是现代操作系统很少使用,我们后面也不用。

  2、中断门:以int指令主动发中断的形式实现从低特权级到高特权级转移,Linux系统调用便是使用此门实现的。

  3、陷阱门:以int3指令主动发中断的形式实现从低特权级到高特权级转移,一般是在编译器调试用。我们也不用。

  4、调用门:call或jmp指令后接调用门选择子作参数,以调用函数例程的方式实现从低特权级到高特权级转移。

四、如何进行特权级检查

  还是照搬书上的总结,当受访者为数据时:

  CPL <= 目标数据段DPL && RPL <=目标数据段DPL,即数据段不允许被比本数据段特权级更低的代码段访问。

  当受访者为代码时,分为如下三种情况:

  1、无门结构且目标为非一致性代码段:CPL = RPL = 目标代码段DPL

  2、无门结构且目标为一致性代码段:CPL >= 目标代码段DPL && RPL >= 目标代码段DPL

  3、有门结构:DPL_GATE >= CPL >= DPL_CODE && RPL <= DPL_GATE

五、调用门的跳转执行流程

  这里以调用门的跳转执行流程为例讲解门的使用,先看图:

 

  调用门的门描述符依旧是占据4个字节大小,被放置在GDT表中。用户程序中通过“call 调用门选择子”来访问调用门,处理器通过选择子中的高13位索引号加上GDT基址得到门描述符,在门描述符中又得到内核中被调用例程所在代码段的选择子以及偏移,又根据得到的选择子在GDT表中索引得到内核中被调用例程所在代码段的基址,将偏移量和基址结合就得到内核中被调用例程的地址。

六、调用门的跳转权限检查

  援引书中的一个例子,能够更好地说明调用门的特权级检查:

  假设当前处理器正在 DPL为3的代码段上运行,即正在运行用户程序,故处理器当前特权级CPL为 3。此时用户进程想获取安装的物理内存大小,该数据存储在操作系统的数据段中,该段DPL为0。由于当前运行的是用户程序,CPL为3,所以无法访问DPL为0的数据段。于是它使用调用门向系统救助。调用门是操作系统安装在全局描述符表GDT中的,为了让用户进程可以使用此调用门,操作系统将该调用门描述符的DPL设为3。该调用门只需要一个参数,就是用户程序用于存储系统内存容量的缓冲区所在数据段的选择子和偏移地址。调用门描述符中记录的就是内核服务程序所在代码段的选择子及在代码段内的偏移量。用户进程用“call 调用门选择子”的方式使用调用门,此调用门选择子是由操作系统提供的,该选择子的 RPL为3,此时如果用户伪造一个调用门选择子也没用,因为此选择子是用来索引门描述符的,并不用来指向缓冲区的选择子,调用门选择子中的高13位索引值必须要指向门描述符在GDT中的位置,选择子中低2位的RPL伪造也没意义,因为此时CPL为3,是短板,以它为主。此时处理器便进行特权级检查,CPL为 3,RPL为3,门描述符DPL为3,即数值上(CPL≤DPL && RPL≤DPL)成立,初步检查通过。接下来还要再将CPL与门描述符中选择子所对应的代码段描述符DPL比较,这是调用门对应的内核服务程序的DPL,为叙述方便将其记作DPL_CODE。由于DPL_CODE是内核程序的特权级,所以DPL_CODE为0,CPL为3,即数值上满足CPL≥DPL_CODE,CPL比目标特权级低,检查通过,该用户程序可以用调用门,于是处理器的当前特权级CPL的值用DPL_CODE代替,记录在CS.RPL中,此时CPL变为0。接下来,处理器便以0特权级的身份开始执行该内核服务程序,由于该服务程序的参数是用户提交的缓冲区所在的数据段的选择子及偏移量,为避免用户将缓冲区指向了内核的数据区,安全起见,在该内核服务程序中,操作系统将这个用户所提交的选择子的RPL变更为用户进程的CPL,也就是指向缓冲区所在段的选择子的 RPL变成了3。前面说过,参数都是内核在0级栈中获得的,虽然用户进程将缓冲区的选择子及偏移量压在了3特权级栈中,但由于调用门的特权级变换,参数已经由处理器在固件一级上自动复制到0特权级栈中了。用户的代码段寄存器 CS 也在特权级发生变化时,由处理器自动压入到0特权级栈中,所以操作系统需要的参数都可以在自己的0特权级栈中找到。用户缓冲区的选择子修改过后,接下来内核服务程序将用户所需要的内存容量大小写到这个选择子和用户提交的偏移量对应的缓冲区。如果用户程序想搞破坏,所提交的这个缓冲区选择子指向的目标段不是用户进程自己的数据段,而是内核数据段或内核代码段,由于目标段的DPL为0,虽然此时已在内核中执行,CPL为0,但选择子RPL已经被改为3,数值上不满足CPL≤DPL && RPL≤DPL,往缓冲区中的写入被拒绝,处理器引发异常。如果用户程序提交的缓冲区选择子确实指向用户程序自己的数据段,DPL则为3,数值上满足CPL≤DPL && RPL≤DPL,往缓冲区中的写入则会成功。如果中断服务程序内部再有访问内核自己内存段的操作,还会按照数值上(CPL≤DPL && RPL≤DPL)的策略进行新一轮的特权检测。通常,如果不是用户程序向内核提交缓冲区地址来接收数据的话,内核不会主动访问用户的内存段,多是访问自己的数据段或代码段,内核服务程序中若访问内核自己的内存段,由于内存段的DPL为0,所以段选择子的RPL也必须为0。

  好了,本回到此结束了,一两句话是讲不清楚特权级的,要想搞懂还是得认真看书。预知后事如何,请看下回分解。

张贴在2