段级保护
代码段之间转移控制时的特权级检查
对于将程序控制权从一个代码段转移到另一个代码段,其目标代码段的段选择符必须加载进代码段寄存器(CS)中。作为这个加载过程的一部分,处理器会检测目标代码段的段描述符并执行各种限长、类型和特权级检查。如果这些检查都通过了,则目标代码段的段选择符就会加载进CS寄存器,于是程序就被转移到新代码段中,程序将从EIP寄存器指向的指令处开始执行。
程序的控制转移使用指令JMP,RET,INT和IRET以及异常和中断机制来实现。下面主要说明JMP、CALL和RET指令的实现方法。JMP或CALL指令可以利用以下4种方法之一来引用另一个代码段。
- 目标操作数含有目标代码段的段选择符。
- 目标操作数指向一个调用门描述符,而该描述符中含有目标代码段的选择符。
- 目标操作数指向一个TSS,而该TSS中含有目标代码的选择符。
- 目标操作数指向一个任务门,该任务门指向一个TSS,而该TSS中含有目标代码段的选择符。
下面描述前两种引用类型。
1、直接调用或跳转到代码段
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没有改变,因此堆栈也不会切换。
大多数代码段都是非一致代码段。对于这些段,程序的控制权只能转移到具有相同特权级的代码段中,除非转移是通过一个调用门进行。
2、门描述符
为了对具有不同特权级的代码段提供受控访问,处理器提供了称为门描述符的特殊描述符。共有4种门描述符:
- 调用门,类型TYPE=12
- 陷阱门,类型TYPE=15
- 中断门,类型TYPE=14
- 任务门,类型TYPE=5
任务门用于任务切换,陷阱门和中断门是调用门的特殊类,专门用于处理异常和中断的处理程序,这里仅说明调用门的使用方法。
调用门用于在不同特权级之间实现受控的程序转移。他们通常仅用于使用特权级保护机制的操作系统中。调用门描述符可以存放在GDT或LDT中,但是不能放在中断描述符表IDT中。一个调用门主要具有以下功能:
- 指定要访问的代码段。
- 在指定代码段中定义过程(程序)的一个入口值。
- 指定访问过程的调用者须具备的特权级。
- 若会发生对转切换,它会指定在堆栈之间需要复制的可选参数个数。
- 指明调用门描述符是否有效。
调用门中的段选择符字段指定要访问的代码段。偏移值字段指定段中入口点。这个入口点通常是制定过程的第一条指令。DPL字段指定调用门的特权级,从而制定通过调用门访问过程所要求的特权级。参数个数字段指明在发生堆栈切换时从调用者堆栈复制到新堆栈中的参数个数。Linux内核中并没有用到调用门。
3、通过调用门访问代码
为了访问调用门,我们需要为CALL或JMP指令的操作数提供一个远指针。该指针中的段选择符用于指定调用门,而该指针的偏移值虽然需要但CPU并不会用它。该偏移值可以设置为任意值。当处理器访问调用门时,它会使用调用门中的段选择符来定位目的代码段的段描述符。然后CPU会把代码段描述符的基地址与调用门中的偏移值进行组合,形成代码段中指定程序入口点的线性地址。
指令 | 特权级检查规则 |
CALL |
CPL小于或等于调用门的DPL;RPL小于或等于调用门的DPL 对于一致性或非一致性代码段都只要求DPL小于或等于CPL |
JMP |
CPL小于或等于调用门的DPL;RPL小于或等于调用门的DPL 对于一致性代码段要求DPL小于或等于CPL;对于非一致性代码段要求DPL等于CPL |
只有CALL指令可以通过调用门把程序控制转移到特权级更高的非一致性代码段中,即可以转移到DPL小于CPL的非一致性代码段中去执行。而JMP指令只能通过调用门把控制转移到DPL等于CPL的非一致性代码段中。但CALL和JMP指令都可以把控制转移到更高特权级的一致性代码段中,即转移到DPL小于或等于CPL的一致性代码段中。
如果一个调用把控制转移到了更高特权级的非一致性代码段中,那么CPL就会被设置为目的代码段的DPL值,并且会引起堆栈切换。但是如果一个调用或跳转把控制转移到更高级别的一致性代码段上,那么CPL并不会改变,并且也不会引起堆栈切换。
4、堆栈切换
每当调用门用于把程序控制转移到一个特权级更高级别的非一致性代码段时,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和出错码使用的空间。
由于一个过程可调用其他过程,因此每个站必须有足够大的空间来容纳多帧(多套)上述信息。
当通过调用门执行一个过程调用而造成特权级改变时,CPU就会执行以下步骤切换堆栈并开始在新的特权级上执行被调用过程:
- 使用目的代码的DPL(即新的CPL)从TSS中选择新栈的指针。从当前TSS中读取新栈的段选择符和栈指针。在读取栈段选择符、栈指针或栈段描述符过程中,任何违反段界限的错误都将导致产生一个无效的TSS异常。
- 检查栈段描述符的特权级是否有效,若无效则同样产生一个无效TSS异常。
- 临时保存SS和ESP寄存器的的当前值,把新栈的段选择符和栈指针加载到SS和ESP中。然后把临时保存的SS和ESP内容压入新栈中。
- 把调用门描述符中指定的参数个数的参数从调用过程复制到新栈中。调用门中参数个数值最大为31,如果个数为0,则表示无参数,不需复制。
- 把返回指令指针(及当前CS和EIP内容)压入新栈。把新(目的)代码段选择符加载到CS中,同时把调用门中偏移值(新指令指针)加载到EIP中。最后开始执行被调用过程。
5、从被调用过程返回
指令RET用于执行近返回,同特权级远返回和不同特权级的远返回。该指令用于从使用CALL指定调用的过程中返回。近返回仅在当前代码段中转移程序控制权,因此CPU仅进行界限检查。对于相同特权级的远返回,CPU同时从堆栈中弹出返回代码段的选择符和返回指令指针。由于通常情况下这两个指针是CALL指令压入栈中的,因此他们应该是有效的。但是CPU还是会执行特权级检查以应付当前过程可能修改指针值或者堆栈出先问题时的情况。
会发生特权级改变的远返回仅允许返回到低特权级程序中,即返回到的代码段DPL在数值上要大于CPL。CPU会使用CS寄存器中选择符的RPL字段来确定是否要求返回到低特权级。如果RPL的数值要比CPL大,就会执行特权级之间的返回操作。当执行返回到一个调用过程时,CPU会执行以下步骤:
- 检查保存的CS寄存器中RPL字段值,以确定在返回时特权级是否需要改变。
- 弹出并使用被调用过程堆栈上的值加载CS和EIP寄存器。在此过程中会对代码段描述符合代码段选择符的RPL进行特权级与类型检查。
- 如果RET指令包含一个参数个数操作数,并且返回操作会改变特权级 ,那么就在弹出栈中CS和EIP值之后把参数个数值加载到ESP寄存器中,以跳过(丢弃)被调用者栈上的参数。此时ESP寄存器指向原来保存的调用者堆栈的指针SS和ESP。
- 把保存的SS和ESP值加载到SS和ESP寄存器中,从而切换调用者的堆栈。而此时被调用者堆栈的SS和ESP值被抛弃。
- 如果RET指令包含一个参数个数操作数,则把参数个数值加到ESP寄存器值中,以跳过调用者栈上的参数。
- 检查段寄存器DS,ES,FS和GS的内容。如果其中有指向DPL小于新CPL的段(一致代码段除外),那么CPU就会用NULL选择符加载这个段寄存器。