文章随机晒最新文章关照最多的

jiayi Rss

80386段保护模式 Segment Protection

| Posted in Assembly |

18

这篇文章是jiayi学习 《intel 80386 programmer’s manual》 的段保护部分时做的笔记,其中翻译有一部分,自己理解的东西有一部分,交叉参考的 IA32 手册也有一部分。这是jiayi写过的最长的一篇文章,当然也是jiayi认为写起来最难的一篇,80386的保护模式的确比较繁琐……

本文提到的段寄存器 selector 断描述符 断描述符表 建议先在此弄明白 。

手册内容感觉比较乱,所以在每个主题开始之前,jiayi尽量用一句提纲挈领的话概括要点,接下来再参考手册描述细节。

段保护 Segment-Level Protection

段转换时如下 5 项保护工作被执行

  1. 类型检查 Type checking
  2. 边界限制检查 Limit checking
  3. 可被寻址区域的约束 Restriction of addressable domain
  4. 过程进入点的约束 Restriction of procedure entry points
  5. 指令集的约束 Restriction of instruction set

段是被保护的单元,段描述符中存储着保护参数。当一个段selector被载入段寄存器 或者段被访问时,CPU自动执行安全检查。段寄存器掌握当前可寻址段的保护参数。

1. 描述符存储着保护参数 Descriptors Store Protection Parameters

下图涂黑的部分展示了 段描述符中与保护有关的域
        

保护参数在描述符被创建时,由系统软件(system software)填入。通常,应用程序没有必要关心保护参数。

当程序将selector载入段寄存器,除了基地址被载入,保护信息也被装载进去。段寄存器在不可见部分,维护着用来存储 基地址、边界限制、类型、权限权限级别的 bits。因为如此,接下来的保护检查不会耗费多余的时钟周期。

数据段、可执行段的类型域的 各个bit 将进一步定义段的用途。

  • The writable bit in a data-segment descriptor specifies whether instructions can write into the segment.
  • The readable bit in an executable-segment descriptor specifies whether instructions are allowed to read from the segment (for example, to access constants that are stored with instructions). A readable, executable segment may be read in two ways:
    1. Via the CS register, by using a CS override prefix.
    2. By loading a selector of the descriptor into a data-segment register (DS, ES, FS,or GS).

处理器在两种情形下检查类型信息:

  1. 当一个描述符的selector被载入段寄存器,特定的段寄存器只能对应特定的描述符类型可以理解为段对描述符类型的限制。如:
    • The CS register can be loaded only with a selector of an executable segment.
    • Selectors of executable segments that are not readable cannot be loaded into data-segment registers.
    • Only selectors of writable data segments can be loaded into SS.
  2. 当一条指令直接或间接的应用一个段寄存器,这个段只能被指令在预先定义好的方式下应用可以理解为段对指令的限制。如:
    • No instruction may write into an executable segment.
    • No instruction may write into a data segment if the writable bit is not set.
    • No instruction may read an executable segment unless the readable bit is set.

 

1.1 类型检查 Type Checking

TYPE field有两项用途:
        1. 区分不同的描述符格式
        2. 表明一个段的用途
除了被应用程序广泛应用的 数据段和可执行段,80386CPU还有供操作系统门(gates)用的特殊段描述符。下表列出了 系统段(system segment) 和 门(gate) 用到的类型。值得注意的是,不是所有的描述符都定义段。gate描述符的特殊应用将在后面说明。

Table 6-1. System and Gate Descriptor Types

Code      Type of Segment or Gate

0       -reserved
1       Available 286 TSS
2       LDT
3       Busy 286 TSS
4       Call Gate
5       Task Gate
6       286 Interrupt Gate
7       286 Trap Gate
8       -reserved
9       Available 386 TSS
A       -reserved
B       Busy 386 TSS
C       386 Call Gate
D       -reserved
E       386 Interrupt Gate
F       386 Trap Gate

1.2 边界限制检查 Limit Checking

一句话,描述符 limit 域用来确定内存寻址的边界,有可能是上边界,也有可能是下边界。下面描述一下关于它的细节。
程序用边界限制域来防止越段寻址。处理器通过 G (granularity) bit来确定limit。对于数据段,还可能用到 E-bit (expansion-direction bit)、 B-bit (big bit) 。

a. 当 G bit 为0时,limit 就是描述符中 20-bits limit域的值(描述符数据结构请参照http://jiayii.com/showart/80386-Segment-Translation#figure5-3)。此时,limit 取值范围为00FFFFFH (2^(20) – 1 or 1 MB) b. 当 G 为1是时,处理器在limit 域后面添加 12-bits 的"1",此时limit的取值范围为 0FFFH (2^(12) – 1 or 4 KB)0FFFFFFFFH(2^(32) – 1 or 4 GB)

上面我们一直在说 limit,那limit到底是什么含义呢?其实很简单,段的大小(byte为单位)减去 1 就是limit的值啦。不过还有例外,向下增长的段(如stack段)不能这样算…在发生下面三种情况之一时,CPU将抛出普遍保护异常

  • Attempt to access a memory byte at an address > limit.
  • Attempt to access a memory word at an address >= limit.
  • Attempt to access a memory doubleword at an address >= (limit-2).

现在说说 向下增长的段 是如何解释limit的。对于向下增长的段,可寻址的范围limit + 164KB or 2^(32) – 1 (4 GB),到底是 64KB 还是 4GB取决于 B-bit。当limit为 0 时,向下增长的段有最大寻址范围。

描述符中对描述符表(descriptor table)进行寻址的limit 域,用于防止程序在 描述符表 中选择描述符时越界。描述符表的limit 确定表中最后一个描述符的最后一个byte。因为每个描述符有8 byte,所以值为 N * 8 – 1 的limit 所对应的描述符表,最多可容纳 N 个描述符。

1.3 权限级别 Privilege Levels

权限”的概念由 0 到 3 的数字来表示,其值称为“权限级别”,数字越小,级别越高,0 表示最高级别。下文提到的“权限级别高”与“数字小”是等价的。CPU通过下面三种权限标识来识别权限级别

  • Descriptors contain a field called the descriptor privilege level (DPL).
  • Selectors contain a field called the requestor’s privilege level (RPL). The RPL is intended to represent the privilege level of the procedure that originates a selector.
  • An internal processor register records the current privilege level (CPL). Normally the CPL is equal to the DPL of the segment that the processor is currently executing. CPL changes as control is transferred to segments with differing DPLs.

当一个过程试图访问其他段时,CPU用CPL与其他一个 甚至两个权限标识进行比较,由此得出此过程的权限。此比较发生在 段的selector被载入段寄存器的时候。 那么CPU如何进行比较呢?因为数据段的访问控制策略 与 可执行段的转移控制策略不同,所以这里针对 数据段可执行段 存在着两种机制。

2. 数据访问的约束 Restricting Access to Data

一句话,不能访问权限级别比自己高的数据段

为了能在内存中对操作数进行寻址,首先程序必须将描述符的selector载入到数据段寄存器(DS, ES, FS, GS, SS),然后CPU通过比较权限级别自动得到当前段的访问权限。 DPL RPL CPL 在比较中都被用到。

只有 ( 目标段的 DPL ) >= ( MAX( 当前段的 RPL, CPL) ) 时,指令才能载入目标段寄存器。换句话说,就是上面我们给出的“一句话”~

一个任务可寻址的范围随着 CPL 的变化而改变,当 CPL 为 0 (最高权限)时,所有数据段的数据都可以被它访问。如果 CPL 为 3 时,只有与它同级的数据段可以被访问。利用此特性,可以防止用户空间代码直接修改操作系统的数据结构。

        

2.1 操作代码段的数据 Manipulate Data in Code Segments

代码段来存储数据很少用到。用代码段存储常量是合法的,但是不能用操作数据段的方式往代码段写数据。下面给出想代码段写数据的方法:

  1. Load a data-segment register with a selector of a nonconforming, readable, executable segment.
  2. Load a data-segment register with a selector of a conforming, readable, executable segment.
  3. Use a CS override prefix to read a readable, executable segment whose selector is already loaded in the CS register.

访问代码段的数据同上 3 种方式。

3. 控制转移的限制 Restricting Control Transfers (既对可执行段的操作)

一句话,只能跳转到 同级或更高权限级别 的代码段
那个啥,再罗嗦一句,3、4节讨论的“跳转段”都是可执行段,或者干脆理解为代码段。有这个概念在里面,下面的东西看起来会轻松些~

80386中,控制转移 通过 JMP CALL RET INT IRET 等指令完成,当然还有 中断 和 异常。这里我们仅讨论 JMP CALL RET

NEAR型的 JMP CALL RET 仅在同一代码段内跳转,所以只需进行 limit 检查。CPU 会检查上述指令跳转的目的地址有没有越段,目的段的 limit 被缓存在 CS 寄存器中。

FAR型的 JMP CALL RET 将跳到其他代码段。JMP CALL 有两种方法跳到其他代码断

  1. The operand selects the descriptor of another executable segment.
  2. The operand selects a call gate descriptor. geta 类型的跳转会在后面介绍

其中 CPL DPL 被用来作权限检查。
一般情况下,当前代码段的 CPL 与 DPL相等,但是如果 conforming bit 被置 1,CPL 有可能 大于 DPL。如果现在不理解,继续往后看咯~
仅当下面其中一种情况满足时,CPU允许 JMP CALL 直接跳转到其他代码段

  • DPL of the target is equal to CPL.
  • The conforming bit of the target code-segment descriptor is set, and the DPL of the target is less than or equal to CPL.

恩,重要概念来啦…一个可执行段,如果它的描述符的 conforming bit 被置1 ,那么这个段被叫做“conforming segment。conforming bit 在可执行段描述符的 type 域低3位中,见上面的 figure6-1。那么 conforming segment 有什么特性呢?交叉参考 IA32 手册,如果目的代码段是 权限级别更高的 conforming segment,那么跳转过去之后 CPL 不变,继续在原来的 CPL 下执行;如果直接(没有使用 call gate )跳转到 权限级别不同的 noconforming segement ,那么CPU将会抛出 general-protection exception(#GP)。嘿,前面那句蓝字应该理解了吧~

下面再结合一张图演示了一个例子因为 IA32 手册英文说的很清楚,翻译过来反而生涩,所以直接copy啦…

  • Code segment C is a nonconforming code segment. A procedure in code segment A can call a procedure in code segment C (using segment selector C1) because they are at the same privilege level (CPL of code segment A is equal to the DPL of code segment C).
  • A procedure in code segment B cannot call a procedure in code segment C (using segment selector C2 or C1) because the two code segments are at different privilege levels.
  • code segment D is a conforming code segment. Therefore,calling procedures in both code segment A and B can access code segment D (using either segment selector D1 or D2, respectively), because they both have CPLs that are greater than or equal to the DPL of the conforming code segment. For conforming code segments, the DPL represents the numerically lowest privilege
    level that a calling procedure may be at to successfully make a call to the code segment.


手册上还有进一步说明,a. 指向 noconforming segment 的selector的 RPL 对权限检查有一定的约束,RPL 的值必须 <= ( the CPL of the calling procedure ) ,在上例中,selector C1、C2 的RPL 可以是 0、1、2,但不能为 3。 b. 指向 conforming segment 的selector 的 RPL 对权限检查没有影响。上例中,对于指向 Code Segment D 的 selector D1、D2 来说,尽管它们的RPL不等,但他们是等价的,因为 RPL 对访问 Segment D 没有影响。

又说了一通,最后对 权限级别检查的基本规则 再总结一下啦:对于 noconforming target segment,只能在相同权限级上跳转。对于 comforming target segment,必须向 权限级别相同或更高 的代码断跳转。

那么到底是怎样向 不同权限级别conforming segment 跳转的呢?这就需要借助 call-gate descriptors 啦,下面就介绍 call-gate descriptor。

4. 为过程载入点看门的“门描述符” Gate Descriptors Guard Procedure Entry Points

一句话,在不同权限级别代码断跳转时,Call gate 插足到 selector –> segment descriptor 之间,形成 selector –> Call gate –> segment descriptor

为了提供 向不同权限级别代码段 的跳转功能,CPU提供了一系列的特殊描述符:gate descriptors。gate descriptors 包含下面四种:

  • Call gates
  • Trap gates
  • Interrupt gates
  • Task gates

Task gates 用于任务切换,Trap 和 Interrupt gates 用于调用 异常处理程序和中断处理程序。这些超出本文讨论范围… 下面仅介绍和本文有关的 Call gates它的结构见下图6-5

call gate 可以存在于 GDT 或 LDT 中,但不能存在与 IDT 中。一个 call gate 有两种功能:

  1. To define an entry point of a procedure.
  2. To specify the privilege level of the entry point.

CALL JMP 这些指令理解 Call gate descriptor 和 理解 segment descriptor是一样的。 当硬件意识到 目的selector 指向一个 Call gate descriptor 时,指令的动作将会被 call gate descriptor 用已经定义好的方式扩展,这些已经定义好的方式就存储在 call gate descriptor 中。

Call gate descriptor 的 selector域和offset域 组成了过程的载入点,Call gate descriptor 保证过程载入点的正确性。段间跳转的目的指针不直接指向目的段,指针的selector 部分指向一个 Call gate descriptor,offset 部分没有用到。详情参见下面 figure-6-6

像图中所示一样,4种权限标识被用到:

  1. The CPL (current privilege level).
  2. The RPL (requestor’s privilege level) of the selector used to specify the call gate.
  3. The DPL of the gate descriptor.
  4. The DPL of the descriptor of the target executable segment.

Call gate 的 DPL 决定了什么权限级别可以使用这个 Call gate。Call gate 则控制跳向 更高权限级或相同权限级 的跳转(尽管同级跳转可以不必使用 Call gate)。下图展示了 CALL 和 JMP 在使用 Call gate 时的不同之处


4.1 堆栈切换 Stack Switching

为了维护系统的完整性,每个权限级别都对应一个堆栈段

当通过 Call gate 跳转到权限级别更高的 noconforming segment 时,处理器不仅会切换代码段,同时也将自动切换到权限更高的堆栈段。此切换是为了防止 权限级别更高的过程 因堆栈段空间太小而崩溃,也为了防止 低权限过程通过共享堆栈段来来骚扰高权限过程。

每个任务最多可以定义 4 个堆栈:一个为应用程序所用,权限级别为 3;剩下的 2、1、0 各分一个(如果只定义了2个权限级别[3和0],那么只能有两个堆栈)。每个堆栈都被分配到独立的内存段上,并可以用一个段选择器和偏移量的组合(stack pointer)来定位。

a. 当运行权限级别为 3 的代码时,权限级别 3 的 段选择器和偏移量 被存储在 SS和ESP 中;当发生 堆栈切换时,段选择器和偏移量被PUSH 到新的堆栈上。

b. 对比于权限级别 3 的堆栈段指针存储在 SS和ESP,2、1、0 的堆栈段指针则存储在当前运行任务TSS(Task State Segment) 中。TSS 结构如下图所示。这些被初始化的指针都是严格只读的,当这个堆栈被使用时,CPU不改变它们的值。(a) 当调用权限级别更高的过程时,堆栈指针只用来创建性的堆栈;(b) 当从过程返回时,这个堆栈被销毁;(c) 当这个过程再次被调用时,新的堆栈再次被堆栈指针创建。(TTS 并没有为 权限3 创建表项,因为 CPL 为 2、1、0 的过程不能跳转到 权限为3的过程)

操作系统负责为每个权限级别 创建堆栈和堆栈描述符,负责将初始好的堆栈指针载入 TTS 中。每个堆栈都必须是 可读可写 的,空间大小必须能容下以下数据:

  • The contents of the SS, ESP, CS, and EIP registers for the calling procedure.
  • The parameters and temporary variables required by the called procedure.
  • Interrupt gates

当通过 Call gate 在不同权限级别代码段跳转时,CPU切换堆栈的步骤如下:

  1. The new stack is checked to assure that it is large enough to hold the parameters and linkages; if it is not, a stack fault occurs with an error code of 0.
  2. The old value of the stack registers SS:ESP is pushed onto the new stack as two doublewords.
  3. The parameters are copied.
  4. A pointer to the instruction after the CALL instruction (the former value of CS:EIP) is pushed onto the new stack. The final value of SS:ESP points to this return pointer on the new stack.

上面步骤完后,新的堆栈看上去如 figure6-9 所示

4.2 从被调过程返回 Returning from a Called Procedure

一句话,只能向 同权限级或低权限级 的代码断返回。

RET 指令可以完成 同段间的near返回、far返回,不同段间的far返回。

对于 near 型的返回,CPU只检查 limit 域。前者 CALL 指令的下一条指令的偏移量 从被调过程的堆栈段 POP 出来,CPU确保这个偏移量不会越段。

对于 far 型的返回,RET 指令将前者 CALL 指令 PUSH 到堆栈的 ”OLD CS:EIP“ POP出来。正常情况下,这个 “OLD CS:EIP” 是合法的,但如果它们在被调过程中被修改,那么越段、越权也是有可能的,所以,CPU还是会对这个 POP 出来的 “OLD CS:EIP” 进行一番检查。CS 的selector 的 RPL 表征 前者主调(主动调用)过程的权限级别。返回步骤如下:

  1. The checks shown in Table 6-3 are made, and CS:EIP and SS:ESP are loaded with their former values that were saved on the stack.
  2. The old SS:ESP (from the top of the current stack) value is adjusted by the number of bytes indicated in the RET instruction. The resulting ESP value is not compared to the limit of the stack segment. If ESP is beyond the limit, that fact is not recognized until the next stack operation. (The SS:ESP value of the returning procedure is not preserved; normally, this value is the same as that contained in the TSS.)
  3. The contents of the DS, ES, FS, and GS segment registers are checked. If any of these registers refer to segments whose DPL is greater than the new CPL (excluding conforming code segments), the segment register is loaded with the null selector (INDEX = 0, TI = 0). The RET instruction itself does not signal exceptions in these cases; however, any subsequent memory reference that attempts to use a segment register that contains the null selector will cause a general protection exception. This prevents less privileged code from accessing more privileged segments using selectors left in the segment registers by the more privileged procedure.

5. 指针检验指令 Instructions for Pointer Validation

指针验证是定位程序错误的重要部分,也是维护权限级别之间的隔离所需要的,指针验证包括如下步骤:
        1. 检查指针的提供者是否被授权访问该段
        2. 检查段的类型与即将进行的操作是否相符
        3. 检查指针是否越界
80386处理器自动完成 2,3 的检查工作,程序要协助完成 1 的检查, 无权限指令(unprivileged instruction) ARPL 用于完成此任务。软件程序也可以明确的执行 2,3 检查,这样就不用等到 保护异常 抛出后才检查到违规操作,无权限指令 LAR, LSL, VERR,  VERW 用于完成此操作。

LAR (Load Access Rights) 指令

Opcode        Instruction      Clocks      Description

0F  02 /r     LAR r16,r/m16    pm=15/16    r16 := r/m16 masked by FF00
0F  02 /r     LAR r32,r32/m161 pm=15/16    r32 := r/m32 masked by 00FxFF00

NOTES:
        For all loads (regardless of source or destination sizing) only bits 16-0 are used. Other bits are
ignored.

从第二个操作(源操作数)数中读取访问权限,将其装载到第一个操作数(目的操作数)中 并 将ZF寄存器置 1。原操作数中含有一个 段选择器(segment selector),指向正在被访问的 段描述符(segment descriptor)。如果原操作数是内存寻址,则只有 16 bits 的数据被用到(因为一个 selector 是16 bits)。目的操作数是一通用寄存器。

作为载入程序的一部分,CPU要对访问进行检查。一旦被载入目的寄存器,软件就可以在访问权限方面进行更多的检查。

当原操作数为32 bits时,一个段描述符的访问权限包括 类型(type),DPL域,S, P, AVL, D/B,  G 标志,所有这些都在段描述符的第二个 doubleword(4 byte 到 7 byte)中。这个 doubleword 在载入目的寄存器前被 00FXFF00H 屏蔽掉(其中的 "x" 是一个未定义的byte)。当原操作数是16 bits时,访问权限波括 类型(type),DPL域。被装入目的寄存器前,doubleword 的两个低位字节(two lower-order bytes)被 FF00H 屏蔽。

这个指令在载入目的寄存器前进行如下检查:
        •  检查段的选择器不为 NULL
        •  检查 段选择器 指向的 段描述符 在GDT或LDT的边界范围内
        •  检查描述符的类型对此指令有效。所有代码段和数据段描述符可以被LAR指令访问。valid system segment 和 gate descriptor types在下面表中给出
        •  如果段不是一致(conforming)的代码段,还要检查被指定的段描述符的CPL可见(如果 CPL 和 段选择器的RPL 小于等于 段描述符的DPL)。

如果这个段描述符不能被访问,或者它的类型对LAR指令无效,ZF寄存器被清 0,而且没有数据被载入目的寄存器。
LAR指令只能用在保护模式下。

LSL (Load Segment Limit) 指令
与LAR指令类似
…………

描述符检验 Descriptor Validation

80386提供两个描述符检验指令:VERR VERW,用于确定指向段的selector在当前权限级别能不能被进行 读操作或写操作。

Opcode       Instruction   Clocks      Description

0F  00 /4    VERR r/m16    pm=10/11    Set ZF=1 if segment can be read,
                                       selector in r/m16
0F  00 /5    VERW r/m16    pm=15/16    Set ZF=1 if segment can be written,
                                       selector in r/m16

指针完整性与RPL Pointer Integrity and RPL

RPL特性可以防止 因指针不当引用而导致的 低权限级打乱高权限级代码或数据的操作。
UNIX系统的FREAD允许我们在用户级别引用指向文件系统程序和数据缓冲区的指针,正常情况下,我们不能直接修改文件系统维护的 file tables。如果没有一些检查指针可用性的标准,那么一个用户级别程序就可以直接引用指向 file tables 的指针,给系统带来混乱。
用RPL可以解决以上问题,RPL通常用于标明 生成这个selector的代码 的权限级别。80386CPU自动检查 被装载到段寄存器中的selector的RPL,以确定是否允许它访问。
被调用的过程只要确保所有传入的selectors有一个 RPL >= 调用者的CPL(随着控制的转换,CPL被CPU动态地记录在段寄存器的不可见部分,它的值等于当前段描述符的DPL)。这个动作确保selectors不会比它的提供者更受信任(即 随着一步一步的调用,selector的权限级别不会升高 Tip: Linux 的系统调用是靠软件中断切换到内核模式的)。如果违反以上规则,当selector被传入段寄存器的时候会导致 protection fault

调整RPL的指令为 ARPL (Adjust Requestor’s Privilege Level)

Opcode    Instruction          Clocks    Description

63 /r     ARPL r/m16,r16       pm=20/21  Adjust RPL of r/m16 to not
                                         less than RPL of r16
操作:
        IF RPL bits(0,1) of DEST < RPL bits(0,1) of SRC
        THEN
           ZF := 1;
           RPL bits(0,1) of DEST := RPL bits(0,1) of SRC;
        ELSE
           ZF := 0;
        FI;

Write a comment

You must be logged in to post a comment.