6.3 段级保护(Segment-Level Protection)

段保机护机制有以下五个方面:

1、 类型检查(Type Checking)

2、 界限检查(Limit Checking)

3、 寻址范围约束(Restriction of addressable domain)

4、 子程序入口点约束(Restriction of procedure entry points)

5、 指令集约束(Restriction of instruction set)

段是保护的单元,段描述符用来存储保护机制参数。当把一个选择子加载进段描述符时和每次段访问时CPU自动执行保护检查。段选择寄存器保存着当前可寻址段的保护机制参数。

6.3.1s 描述符存储保护机制参数(Descriptors Store Protection Parameters)

图6-1高亮显示了段描述符中与保护相关的字段。

在描述符创造时,系统软件同时把保护参数入在描述符中。一般来说,应用程序员不用管保护参数的。

当程序把一个选择子装入段寄存器时,处理器不仅加载段的基址部分,而且也把保护参数装入段寄存器。每个段寄存器有一个不可见的部分用来存放基址、界限、类型、和特权级。所以对于以后的保护检查,处理器不必浪费多于的时钟周期去从内存中加载这些信息。

6.3.1.1 类型检查(Type Checking)

描述符的类型字段有2个作用:

1、 它用来区分不同格式的描述符。

2、 它暗示了描述符的用处。

除了被应用程序广范使用的数据段和可执行段描述符外,80386还有特殊的描述符,用来描述和操作系统相关的段(Segment)和门(Gate)。图6-1列出了所有类型的系统段和门。注意,不是所有的描述符都定义了一个段。门描述符有不同的用法,下一章将讲述。

数据段和可执行段的类型字段包括了以下这些位,用来定义一个段的用途(参看图6-1):

  • 在一个数据段描述符中,可写位指出了指令是否有权向这个段写入数据。

  • 在一个可执行段描述符中,可读位指出了指令是否有权从这个段中读出数据(例如,用来访问与指令一起储存的常量数据)。可读的可执行段可以在以下两种方式下读取:

1、 通过使用CS前缀,来访问CS 寄存器指定的段。

2、 把描述符加载到数据段寄存器(DS,ES, FS, GS)。

类型检查可以检测出某些程序错误,比如,当程序员访问一个不是为某种目的设定的段时。处理器在以下两种情况下检查类型:

1、 当把一个描述符加载到一个段寄存器时。有些段寄存器只能加载某些类型的描述符,比如:

  • CS 寄存器只能加载一个可执行段的描述符。

  • 只有可执行的数据段描述符才能加载入SS段寄存器。

2、 当一条指令访问一个段时(显式的或者隐式的)。一些段只能通过一些特定的方式才能使用,例如:

  • 可执行段不允许任何指令写入数据。

  • 如果一个数据段的可写位没有置位,任何指令不可向其写入数据。

  • 如果一个可执行段的可读位没有置位,任何指令不可从该段读取数据。

表6-1,系统段描述符和门描述符

6.3.1.2 界限检查(Limit Checking)

一个描述符的界限字段是用来防止程序在访问一个段时超出段的范围的。处理器根据描述符的G位(granularity bit)来解析界限字段的。对于数据段,处理器在解析界限字段时还要根据E位(expansion-direction bit)和 B位(big bit)(参看表6-2)。

当G=0 时,界限字段的值即是描述符中20位的 limit-field。这时,界限可能从0~0FFFFF (2^20 – 1 或者说 1 M)。当G=1时,处理器将会自动的在描述符的 limit-field 低位加12位0。这样,实际的界限值可以从0FFFH(2^12 – 1 或者说4K)到0FFFFFFFFH(2 ^ 32 – 1 或者说4G)。

除了向下延伸的段外,界限值总比段的大小少1(字节表示)。当以下任一情况发生时,处理器引发异常:

  • 试图访问一个 地址 > 界限 的字节。

  • 试图访问一个 地址 >= 界限 的字。

  • 试图访问一个 地址 >= (界限 – 2 ) 的字双字。

对于向下延伸的数据段,界限做用相同,但是被处理器以不同的方式来解析。这个时候,有效地址则从 limit + 1 到 64K 或者 2^32 – 1(4G)(由B位决定)。向下延伸的段当界限设为0时,有最大的段长。

向下延伸的特性允许把堆栈拷贝到一个更大的段,而不改变内部段指针,来增大一个堆栈的大小。

描述符表的界限字段用来防止程序寻址超出一个描述符表。界限用来确定描述符表的最后一个描述符的最后一个字节。因为一个描述符是8字节长,界限字段的值为

N * 8 – 1 ,对于一个包含N个描述符的描述符表。

界限字段可以查测到类似下标出界和非法指针运算等程序错误。这些错误一当发生时就可以被发现,所以确定这种错误是很简单的。如果没有界限检查,这些的错误会使一个模块受到破坏,这种错误只有当下一次受损的模块不正常工作时才会被发现,而且发现也是比效困难的。

6.3.1.3 特权级

处理器通过赋给一个重要的对象以一个 0 ~ 3 的数字来实现特权级。这个数字被称为特权级。0代表最高特权级,3代表最低特权级。以下的对象包含了特权级:

  • 描述符包含了一个叫做描述符特权级(DPL)的字段。

  • 选择子包含了一个叫做请求特权级(RPL)的字段。RPL 代表着指向子程序的选择子。

  • 处理器的一个内部寄存器记录了一个叫做当前特权级(CPL)的字段。一般来说,CPL 和当前正在执行的代码段的DPL是相同的。当控制在不同特权级的段间转移时,CPL发生变化。

当某个段的一个子程序要访问一个段时,处理器会自动的把一个要访问某个段的子程序的特权级和CPL或者更多的特权级相比。这种比较是在当一个描述符被加载到一个段寄存器时执行的。比较的标准在访问数据时和控制转移时是分别不同的。所以,就有了以下两种不同的查测:

图6-2显示了不同特权级环的解析方式。中心是用来放最关键的软件的,一般来说是操作系统内核。外面是用来放次关键的应用软件段的。

4个特权级全用并不是必要的。已存在的软件如果是主两级特权级设计的,也可以很好的被80386支持的。一个只用一级特权级的系统应该使用特权级0;一个使用两级特权级的系统应该使用特权级0和特权级3。

6.3.2 访问数据约束(Restricting Access to Data)

为了寻址一个操作数,80386必须把一个选择子加载到一个段寄存器(DS, ES, FS, GS, SS)里。处理器自动执行访问特权级的检查。检查是在当选择子被加载入段寄存器时执行的。图6-3显示了,在这种检测下的3种不同的特权级。

1、 CPL(当前特权级(current privilege level))

2、 用来指定目标段的选择子的RPL(请求特权级(requestor’s privilege level))。

3、 目标段的DPL(描述符特权级)

一条指令只有当一个目标段的DPL在数值上大于或等于CPL和选择子的RPL中的最大值时,才可以加载目标段的选择子到一个段寄存器里。也就是说,一个子程只能访问它同级的或比它特权级低的数据。

当一个任务的CPL改变时,它的可寻址范围也会改变。当CPL是0时,任何特权级的数据段都是可寻址的。当CPL是1时,只有特权级从1 ~ 3 的数据段才是可寻址的,当CPL是3时,只有特权级是3 的数据段才是可寻址的。80386的这个性质可以保护操作系的内部表不被应用程序所读取或更改。

6.3.2.1 在代码段中访问数据(Accessing Data in Code Segments)

更少见点的情况可能是在一个代码段内存储数据。代码段可以存储常量。任何指令不能向一个代码段写入数据。以下是可以在代码段内访问数据的方法:

1、 用一个非一致性的、可读的、可执行的选择子加载一个数据段寄存器。

2、 用一个一致性的、可读的、可执行的选择子加载一个数据段寄存器。

3、 用CS前缀来读取一个可读的,可执行的代码段(该段当前已被CS寄存器所指向)。

在访问数据时,和正常的数据访问规则适用于第1种情况。情况2总是合法的,一致性段的特权级总是和当前特权级(CPL)相同,无论该段的DPL是多少。第三种情况也是合法的,因为目标段的DPL就是代码段的DPL,根据定义,也就是CPL。

6.3.3 控制转移约束(Restricting Control Transfers)

在80386中,控制转移是通过指令 JMP, CALL, RET, INT, 和 IRET 当然还有异常和中断机制。异常和中断是特殊的情况,在第9章中讲述。这一章讲述JMP,CALL,和RET 指令。

JMP, CALL, RET 指令的“NEAR”(近)形式,只在当前的代码段内发生转移,所以安全检查只涉及到界限检查。处理器保证JMP, CALL, RET 指令不会超出当前执行的代码段的限长。这个限长被存储在CS段寄存器的不可见部分。所以这种检查不会带来额外的时钟周期。

而JMP, CALL, RET 的“FAR”(远)形式则会转移到不同的段内,所以,处理器将执行特权级检查。JMP和CALL有两种方法转移到另一个段:

1、 操作数选择一个另一个可执行段的描述符。

2、操作数选择了一个调用门描述符。这种门形式的转移将在下一节介绍调用门时讲述。

图6-4显示了,两种不同特权级之间的控制转移(没有使用调用门时):

1、 CPL(当前特权级(Current privilege level))。

2、 目标段的描述符的DPL。

一般来说,CPL和处理器正在执行的段的DPL是相同的。但是,当正在执行的段的一致性位(conforming bit)置位时,CPL也可能比DPL要大。处理器把当前特权级(CPL)缓存在CS段寄存器里,这个值也可能和当前代码段的描述符特权级不同的。

只有当以下条件致少满足一个时,处理器才允许JMP或CALL直接转移到另一个段:

  • 目标段的DPL和CPL相同时。

  • 目标代码段的一致性位(conforming bit)设置时,而且目标代码段的DPL与CPL相等或者目标代码段的DPL比CPL小。

一致性位设置的段被称为一致性段。一致性段的机制允许不同特权级共享子程序,而且在执行其中的子程序时使用自已的特权级,而不是使用一致性段的段描述符特权级。一个例子就是数学库子程序和一些异常处理子程序。当控制转称到一致性段时,CPL不会改变。这就是唯一的CPL不等于当前可执行代码段的情况。

许多代码都是非一致性的(non-conforming)。上述的基本特权级检查意思是,对于非一致性段,不通过门描述符可以转移到一个相同特权级的可执行段。但是,有时我们也需要从低特权级向高特权级(数值上比较小的)转移。这种需要就可以能过调用门(call-gate)来实现。调用门在下一节中介绍。JMP 指令不可能通过任何方法转移到DPL与CPL不同的非一致性段中去。

6.3.4 门描述符保证子程序入口点(Gate Descriptors Guard Procedure Entry Points)

为了提供转移到不同特权级段中的机制,80386使用了门描述符。有4种门描述符:

  • 调用门(Call Gates)

  • 陷阱门(Trap Gates)

  • 中断门(Interrupt Gates)

  • 任务门(Task Gates)

这一章只讲述调用门。任务门是用来任务切换的,所以在第7章中介绍。第9章说明了陷阱门和中断门,用来处理异常和中断。图6-5显示了调用门的格式。一个调用门可以在GDT中,也可以在LDT中,但不能在IDT中。

一个调用门主要有2个作用:

1、 定义一个子程序的入口。

2、 定交了入口的特权级。

调用门描述符被CALL和JMP指令使用,方法和普通的代码段描述符相同。当硬件识别了目标选择子是指向一个调用门时,操作过程将由这个调用门来决定。

门中的选择子和偏移量将用来形成一个子程序入口点的指针。调用门保证了控制转移到一个段内的合法入口点,而不是转移到一个子程序的中间,或者更糟糕的是转移到一条指令的中间。作为操作数的远指针不再象平常那样指向一个段中的某个偏移了,而是选择子部分指向了一个门,偏移部分没有使用。图6-6显示了这种寻址方式。

如图6-7所示,4种不同的特权级将用来做安全检测:

1、 CPL

2、 用来指向调用门的选择子的RPL

3、 门描述符的DPL

4、 可执行目标段的DPL

调用门描述符的DPL决定了什么样的特权级可以使用这个门。一个段可能有多个子程序,这些子程序被设计给不同特权级程序来使用。例如,操作系统可能有几个子程序,这些服务被设计来给应用程序使用,但其它的子程序可能只被设计成给系统软件使用。

门描述符可以用来向高特权级(数值上更小的)或同特权级控制转移(这样实际上没有必要)。只有CALL指令才能用门描述符向高特权级转移。JMP只能使用门描述符向同级特权级转移或向一个一致性段转移。

对于JMP 指令,向一个非一致性代码段转移时,以下两点必须要同时满足,否则处理器引发异常:

MAX(CPL,RPL) <= Gate DPL

Target Segment DPL = CPL

对于CALL指令(或者对于向一致性代码段转移的JMP)以下两点必须要同时满足,否则处理器引发异常:

MAX(CPL, RPL) &lt;= Gate DPL
Target Segment DPL &lt;= CPL

6.3.4.1 堆栈切换(Stack Switching)

如果调用门中指示的目标代码段和当前特权级(CPL)不同,段间跳转发生了。

为了保证系统的完整性,每一个特权级使用了一个相互独立的堆栈。这样,可以保证高特权级有足够的堆栈空间使用。没有它们时,如果调用者不提供足够的堆栈空间,那么一个受信任的子程序就无法正常工作。处理器通过任务状态段(task state segment)(请看图6-8)来寻址这些堆栈。每一个任务有一个单独的TSS, 所以允许任务拥有自已的堆栈。系统软件的责任是创建TSS还要把它们的堆栈指针设置好。TSS最初的堆栈指针是只读的。处理器绝对不会在执行程序时更改它们。

当一个调用门用来改变特权级时,处理器使用TSS中的堆栈指针来建立一个新的堆栈。处理器用目标代码段的DPL来索引TSS中的堆栈指针,PL0,PL1 或PL2。

新堆栈的DPL必须和新的CPL相等,如果不是,处理器引发堆栈异常。为每一个特权级创建堆栈和堆栈段描述符是系统软件的责任。每一个堆栈必须包含必要的空间来容纳旧的SS:ESP,返回地址(CS:EIP)和所有的参数,局部变量等。

内部调用时,传给子过程序参数放在堆栈上。为了使特权转移相对被调用者来说透明,处理器把参数拷贝到新堆栈上。调用门的 count 字段说明了有多少个双字参数需要从调用者堆栈上拷贝到新的堆栈上。如果 count 字段为0,则不用拷贝任何参数。

在特权级转移调用过程中,处理器执行以下堆栈相关的操作:

1、 处理器检测新的堆栈是否有足够的空间容纳各参数和返回链。如果不能,则引发一个错误码为0 的堆栈错误异常。

2、 旧的堆栈寄存器 SS:ESP 各以双字的形式压入新的堆栈中。

3、 拷贝参数。

4、 一个在CALL指令后的指令指针(旧的CS:EIP)被压入新堆栈。最后SS:ESP指针将指向新堆栈中的这个返回值。

图6-9显示了在成功调用后的堆栈内容。

TSS 段没有特权级3的堆栈指针保存区,因为特权级3不能被任何一个别的特权级

调用。

被别的特权级调用的子程序,如果需要比31个双字还要多的参数的话,就必须使用保存的SS:ESP链来访问第31个以后的参数了。

通过调用门的子程序调用并不检测拷贝到新栈的参数的值。被调用程序有责任对参数的有效性检测。后面的小节将讨论如何使用 ARPL, VERR, VERW, LSL, 和LAR 指令来检测指针的有效性。

6.3.4.2 从子过程中返回(Returning from a Procedure)

NEAR 形式的 RET 指令只是在当前段内做控制转移,所以只做界限检测。跟在CALL指令后的OFFSET 部分将从堆栈中弹出。处理器保证OFFSET 部分不会超过当前可执行段的界限。

FAR 形式的RET指令把先前通过FAR CALL 指令而压入栈的返回指针从栈中弹出。一般情况下,该值应该是有效的,因为它和先前的CALL和INT对应。但是,处理器还是会执行特权检测,以防止当前执行的子程更改这个指针或者子程序没有对堆栈做正确的维护。 从堆栈中弹出的CS寄存器中的RPL字段指示了调用者的特权级。

段间返回指令可能会改变特权级,但是只能向更低的特权级返回。当一条RET指令遇到一个保存的CS寄存器,而且它的RPL字段比CPL(当前特权级)要数值上更大时,段间返回便发生了。那样的返回执行以下的步骤:

1、 图6-3显示的检测被执行,用以前存储在堆栈中的旧值加载CS:EIP和SS:ESP寄存器。

2、 RET 指令指示要对旧的SS:ESP所做的调整。结果的ESP将不会和堆栈段的界限做检查。如果ESP超出了界限,直到下一次的堆栈操作才会被识别到。(做返回动作的子过程的SS:ESP部分是不会被保存的,一般说来,这个值和TSS中保存的相同)

3、 DS, ES, FS,GS 寄存器的内容将被检查。如果有任何一个寄存器指向了一个比当前特权级高的段(当然除了一致性段),该寄存器将被加载一个NULL选择子(INDEX=0,TI=0)。RET指令本身并不会产生任保异常。任何使用空选择子的以后的内存访问将引发一个通用保护异常(general protection exception)。这样可以防止低特权级程序通过使用高特权级留在堆栈里的选择子来访问高特权级的段。

6.3.5 一些指令是为操作系统保留的(Some Instructions are Reserved for Operationg System)

一些会对保护机制产生影响的指令和会对系统性能产生影响的指令只能由受信任的

代码执行。80386 有两种这样的指令:

1、 特权指令——这些指令用于系统控制

2、 敏感指令——这些是用于I/O或和I/O相关的指令。

6.3.5.1 特权指令(Privileged Instructions)

影响系统数据结构的指令只能在特权级0下执行。如果处理器在当前特权级大于0的情况下遇到这样的指令,将产生一个通用保护异常。这些指令包括:

6.3.5.2 敏感指令(Sensitive Instructions)

当当前特权级不是0时,和I/O相关的指令需要一定的约束条件才能执行。I/O执行机制将在第8章(输入、输出)讲述。

6.3.6 指针有效性检测(Instruction for Pointer Validation)

指针有效性检测是检测程序错误的一个重要手段。指针检测还是保持不同特权级间的独立性的必需。指针检测包括以下几个步骤:

1、 检查指针的提供者有权访问段。

2、 检测段的类型是否可以以指针指示的方式使用。

3、 检查指针没有越界。

虽然80386在指令执行时会自动执行2和3检测,但软件必须要协助来做第1类检测。 非特权指令的作用就是体现在这方面的。软件也可以自已做2和3类检测,以避免处理器产生异常。非特权指令 LAR, LSL, VERR, VERW 就是用于执行这样的操作的。

LAR(Load Access Rights)用来检测一个特权级的指针针访问了合适特权级和正确类型的段。LAR 有一个操作数——想检察属性的段描述符的选择子。描述符必须要可被访问(CPL和选择子的RPL)。如果描述符可被访问,LAR将得到描述符的第二个双字部分,用值0xFxFF00H来掩它,存储到一个32位的目的地寄存器里,设置ZERO 标志(X指的是存储的这4位没有定义)。一旦加载了,便可以对这些访问特权进行测试。

如果RPL或CPL比DPL要大,或者选择子超出了描述符表的界限,则没有访问权限被返回,ZERO位置0。一致性段可以被任意特权级访问。

LSL(Load Segment Limit)允许软件测试一个描述符。如果在当前特权级下可访问该描述符的话,LSL加载32位的。字节计数的、 从界限片段字段计算出来的没有整合的界限,G-位到指定的32位寄存器。只有数据段,代码段,任务状态段和局部描述符表才可以做这些操作;门描述符是不可访问的(表6-4详细列出了哪种类型的可以,哪种类型的不行)。怎么解析界限和段的类型相关,比如,向下增长的数据段对于界限的解析和代码段对界限的解析是不同的。对于LAR和LSL,如果操作执行了,ZERO位被置位,否则ZF位清除。

6.3.6.1 描述符的有效性(Descriptor Validation)

80386 有两条指令,VERR 和VERW, 用来测试当前特权级是否有权对一个段的读写。当不可访问时,两条指令都不会产生异常。

VERR(Verify for Reading)检查一个段的可读性,当在当前特权级可读时把ZF置1。VERR做以下检测:

1、 指向描述符的选择子在LDT或GDT的界限之内。

2、 它指向一个代码或数据段描述符。

3、 在一合适的特权级下段可读

对于数据段和非一致性代码段的检查是,DPL必须要同时在数值上大于或等于CPL

选择子RPL。一致性段不做特权检查。

VERW(Verify for Writing)和VERR一样做相似的检测,不过是对于写。和VERR指令一样,当在当前特权级下可写时,VERW置ZF位为1。指令还会检查描述符在描述符表界限内,是一个段描述符,可写,DPL在数值上同时比CPL和选择子的RPL大或相等。代码段永远不可写,不管是一致性还是非一致性的。

6.3.6.2 指针完不整性和RPL

请求特权级(RPL)特性可以防止一个低特权级的不正确的使用指针而破坏高特权级的数据或代码一个很常见的例子是,文件系统子程序,FREAD(file_id, n_bytes, buffer_ptr)。这个假设的子程序从一个文件读取数据,然后把数据放入缓冲区,不管缓冲区内是任何数据,都将被覆盖。一般情况下,FREAD对于用户程序是可见的。在没有指针检测的标准下,一个用户程序可能提供一个文件表的指针而非一个缓冲区的指针,以至使FREAD子程序使文件表受到损坏。

使用RPL可以避免这种问题。RPL字段允许赋一个特权级给选择子。这个特权级属性一般指出了产生选择子的代码的特权级。当选择子被加载时,80386会自动检查RPL是否允许访问。

为了更好地利用好处理器对RPL的检测,被调用的子过程只用确保传给它的选择子至少在数值上比CPL大就行了。这样就可以使选择子被赋予比它们的提供者更低的特权级。如果一个选择子用来访问一个调用者都不能访问的段时,也就是说RPL在数值上比DPL更大,当加载该选择子时便会产生保护异常。

ARPL(Adjust Requestor’s Privilege Level)调整一个选择子的RPL字段或一个寄存器的RPL字段,使RPL变大。后者一般都是从一个保存在堆栈上的CS寄存器的映象加载的。如果请求特权级被调整,ZF位置1,否则置0。