操作系统学习(十) 、代码段之间转移控制时的特权级检查
一、程序在代码段间的转移方式
对于将程序控制权从一个代码段转移到另一个代码段,目标代码段的选择符必须加载进代码段寄存器中。作为这个加载过程的一部分,处理器会检测目标代码段的段描述符并执行各种限长、类型和特权级检查。如果这些检查都通过了,则目标代码段选择符就会加载进CS寄存器,于是程序的控制权就被转移到新的代码段中,程序将从EIP寄存器指向的指令处开始执行。
程序的控制转移指令JMP、RET、CALL和IRET以及异常和中断机制来实现。异常和中断是一些特殊实现。JMP和CALL指令可以利用以下四种方法之一来引用另外一个代码段:
- 目标操作数含有目标代码的段选择符。
- 目标操作数指向一个调用门描述符,而该描述符中含有目标代码段的选择符。
- 目标操作数指向一个TSS(任务状态段),而该TSS中含有目标代码段的选择符。
- 目标操作数指向一个任务门,该任务门指向一个TSS,而该TSS中含有目标代码段的选择符
下面描述前两种引用类型。
二、直接调用或跳转到代码段
JMP、CALL和RET指令的近转移形式只是在当前代码段中执行程序控制转移,因此不会执行特权级检查。JMP、CALL或RET指令的远转移形式会把控制转移到另外一个代码段中,因此处理器一定会进行特权级检查。
当不通过调用门把程序控制权转移到另一个代码段时,处理器会验证4种特权级和类型信息,如下图所示:
- 当前特权级CPL,这里CPL是执行调用的代码段的特权级,即含有执行调用或跳转程序的代码段的CPL。
- 含有被调过程的目的代码段描述符中的描述符特权级DPL。
- 目的代码段的段选择符中的请求特权级RPL。
- 目的代码段描述符中的一致性标志C,它确定了一个代码段是非一致代码段还是一致代码段
处理器检查CPL、RPL和DPL的规则依赖于一致性标志C的设置状态。 当访问非一致代码段时(C=0),调用者的CPL必须等于目的代码段的DPL,否则将会产生一般保护异常。执行非一致代码段的段选择符的RPL对检查所起的作用有限。RPL在数值上必须小于或等于调用者的CPL才能使得控制转移成功完成。当非一致代码段的段选择符被加载进CS寄存器中时,特权级字段不会改变,即它任然是调用者的CPL。即使段选择符的RPl与CPl不同,这也是正确的。
当访问一致代码段时(C=1),调用者的CPL可以在数值上大于或等于目的代码段的DPL。仅当CPL<DPL,处理器才会产生一般保护异常。对于访问一致代码段,处理器忽略对RPL的检查。对于一致代码段,DPL表示调用者对代码段进行成功调用可以处于的最低数值特权级。
当程序控制被转移到一个一致代码段中,CPL并不改变,即使目的代码段的DPL在数值上小于CPL。这是CPL可能与当前代码段DPL不相同的唯一一种情况。同样,由于CPL没有改变,因此堆栈也不会切换。
大多数代码段都是非一致代码段。对于这些段,程序的控制权只能转移到具有相同特权级的代码段中,除非是通过一个调用门进行。
三、门描述符
为了对具有不同特权级的代码段提供受控的访问,处理器提供了称为门描述符的特殊描述符集,共有四种描述符:
- 调用门(Call Gate),类型 TYPE=12;
- 陷阱门(Trap Gate),类型 TYPE=15;
- 中断门(Interrupt, Gate),类型 TYPE=14;
- 任务门(Task Gate),类型 TYPE=5.
任务门用于任务切换,陷阱门和中断门时调用门的特殊类,专门用于调用异常和中断的处理程序。
调用门用于在不同特权级之间实现受控的程序控制转移。他们通常仅用于使用特权级保护机制的操作系统中。调用门描述符可以存放在GDT或LDT中,但是不能放在中断描述符表IDT中,一个调用门主要具有以下几个功能:
- 指定要访问的代码段
- 指定代码段中定义过程的一个入口点(就是代码段中程序的入口点)
- 指定访问过程的调用者需具备的特权级
- 若会发生堆栈切换,它会指定在堆栈之间需要复制的可选参数个数
- 指明调用门描述符是否有效。
调用门描述符格式如下所示:
调用门中的段选择符指定要访问的代码段。偏移地址指定段中入口点,这个入口点通常是指定过程的第一条指令。DPL字段指定调用门的特权级,从而指定通过调用门访问特定过程所要求的特权级。标志P指明调用门描述符是否有效。参数个数字段指明发生堆栈切换时从调用者堆栈复制到新堆栈中的参数个数。
四、通过调用门访问代码段
(一) 、通过调用门访问代码段的过程
为了访问调用门,我们需要为CALL或JMP指令的操作数提供一个远指针。该指针的段选择符用于指定调用门,而指针的偏移值虽然需要但是CPU并不会用它,该偏移值可以设置为任意值。
当处理器访问调用门时,它会使用调用门中的段选择符来定位目的代码段的段描述符。然后CPU会把代码段描述符的基地址与调用门中的偏移地址进行组合,形成代码段中指定程序入口点的线性地址。调用门调用过程如下图所示:
(二)、通过调用门进行转移控制时的特权级检查
通过调用门进行程序控制转移时,CPU会对以下4种不同的特权级进行检查,以确定控制转移的有效性。
- 当前特权级CPl
- 调用门选择符中的请求特权级RPL
- 调用门描述符中的描述符特权级DPL
- 目的代码段描述符中的DPL
另外,目的代码段描述符中的一致性标志C也将受到检查。
使用CALL指令和JMP指令分别具有不同的特权级检查规则,调用门描述符的DPL字段指明了调用程序能够访问调用门的特权级的最大数值(即最小特权级),即为了访问调用门,调用者程序的特权级CPL必须小于调用门的DPL,调用门选择符的RPL也需同调用者的CPL遵守同样的规则,即RPL必须小于或等于调用门的DPL。
如果调用者与调用门之间的特权级检查通过,CPU就会接着把调用者的CPL与代码段描述符的DPL进行比较检查。在这方面,CALL指令和JMP指令的检查规则就不同了。只有CALL指令可以通过调用门把程序控制转移到特权级更高的非一致性代码段中,即可以转移到DPL小于CPL的非一致性代码段中去执行。而JMP指令只能通过调用门把控制转移到DPL等于CPL的非一致代码段中。但CALL指令和JMP指令都可以吧控制转移到更高特权级的一致性代码段中,即DPL小于或等于CPL的一致性代码段中。
如果一个调用门把控制转移到了更高特权级的非一致代码段中,那么CPL就会被设置为目的代码段的DPL值,并且会引起堆栈切换。但是如果一个调用门或跳转到控制转移到更高级别的一致性代码段上,那么CPL并不会改变,并且不会引起堆栈切换。
调用门可以让一个代码段中的过程被不同特权级的程序访问。例如,位于一个代码段的操作系统代码可能含有操作系统自身和应用软件都允许访问的代码(比如处理字符I/O的代码)。因此可以为这些过程设置一个所有特权级代码都能访问的调用门。另外可以专门为仅用于操作系统的代码设置一下儿更高特权级的调用门。
五、堆栈切换
每当调用门用于把程序转移到一个更高级别的非一致性代码段时,CPU会自动切换到目的代码段特权级堆栈中去。执行栈切换操作的目的是为了防止搞特权级程序由于栈空间不足而引起崩溃,同时也为了防止低特权级程序通过共享的堆栈有意或无意地干扰搞特权级的程序。
每个任务最多可定义4
个栈。一个运行在特权级3的应用程序代码,其它用到的特权级分别为2、1和0。 如果一个系统只使用了3和0两个特权级,那么每个任务就只需设置两个栈,每个栈都位于不同的段中,并且使用段选择符和段中偏移指定。
当特权级3的程序在执行时,特权级3的堆栈的段选择符和栈指针会被分别存放在SS和ESP中,并且在发生堆栈切换时被保存在被调用过程的堆栈上。
特权级0、1、2的堆栈的初始指针值都存放在当前运行任务的TSS段中。TSS段中这些指针都是只读值,
在任务运行时CPU并不会修改他们。当调用更高特权级程序时,CPU才用他们来建立新的堆栈。当从调用过程返回时,相应的栈就不存在了。下一次再次调用该过程时,就又会再次使用TSS中的初始指针建立一个新栈。
操作系统需要负责为所有用到的特权级建立堆栈和堆栈段描述符,并且在任务的TSS中设置初始指针值。每个栈必须可读可写,并且具有足够的空间来存放以下一些信息:
- 调用过程的SS,ESP,CS和EIP寄存器的内容
- 被调过程的参数和临时变量所需使用的空间
- 当隐含调用一个异常或中断过程时标志寄存器EFLAGS和出错码使用的空间
由于一个过程可调用其它过程,因此每个栈必须有足够的空间来存放多份(多帧)上述信息。
当通过调用门执行一个过程调用而造成特权级CPL改变时,CPU就会执行以下步骤切换堆栈并开始在新的特权级是执行被调过程。
- 使用目的代码段的DPL(即新的CPL)从TSS中选择新栈的指针。从当前TSS中读取新栈的段选择符和栈指针。在读取栈段选择符、栈指针或栈段描述符的过程中,任何违反段界限的错误都将导致产生一个无效TSS异常。
- 检查栈段描述符特权级和类型是否有效,若无效同样产生一个无效TSS异常。
- 临时保存SS和ESP寄存器的当前值,把新栈段选择符和栈指针加载到SS和ESP中。然后把临时保存的SS和ESP内容压入新栈中。
- 把调用门描述符中指定参数个数的参数从调用过程栈复制到新栈中。调用门中参数个数最大值为31,如果个数为0张,则表示无参数,不需复制。
- 把返回指令指针(即当前CS和EIP的值)压入新栈,把新代码段(目的代码段)选择符加载到寄存器CS中,同时把调用门中的偏移地址(新指令指针)加载到EIP中,最后切换到目的代码段执行被调过程。
六、从被调过程返回
指令RET用于执行近返回(near return)、同特权级远返回(far return)和不同特权级的远返回。该指令用于从使用CALL指令调用的过程中返回。
- 近返回仅在当前代码段中转移程序的控制权,因此CPU仅进行界限检查。
- 对于相同特权级的远返回,CPU同时从栈中弹出返回代码段的选择符和返回指令指针。由于通常情况下这两个指针是CALL指令压入栈中的,因此他们应该是有效的,但CPU还是会执行特权级检查以应付当前过程可能修改I啊指针值或堆栈出现问题的情况。
- 会发生特权级改变的远返回仅允许返回到低特权级程序中,即返回到的代码段DPL在数值上要大于CPL。CPU会使用CS寄存器中选择符的RPL字段来确定是否要返回到低特权级。如果RPL的数值要比CPL大,就会执行特权级之间的返回操作。
当执行远返回到一个调用过程时,CPU会执行以下步骤:
- 检查保存的CS寄存器中RPL字段值,以确定在返回时特权级是否需要改变。
- 弹出并使用被调用过程堆栈是的值加载CS和EIP寄存器。再此过程中会对代码段描述符和代码段选择符的RPL进行特权级与类型检查。
- 若果RET指令包含一个参数个数操作数并且返回操作会改变特权级,那么就在弹出栈中CS和EIP值之后把参数个数值加载到ESP寄存器值中,以跳过被调用者栈上的参数。此时ESP寄存器指向原来保存的调用者堆栈的SS和ESP。(先把参数个数加载到ESP,一次出栈后ESP值指向调用者的ESP,因为出栈操作会改变ESP。)
- 把保存的SS和ESP值加载到SS和ESP寄存器中,从而切换回调用者的堆栈。此时被调用者堆栈的SS和ESP值被丢弃。
- 如果RET指令一个参数个数操作数,则把参数个数值加到ESP寄存器值中,以跳过调用者栈上的参数。
- 检查段寄存器DS、ES、FS和GS的内容。如果其中有指向DPL小于新CPL的段(一致代码段除外),那么CPU就会用NULL选择符加载这个段寄存器。