# 前言

上一篇我们学习了段寄存器和 GDT 的基本结构。本篇将深入探讨段权限检查机制以及代码跨段跳转的原理。

# 段权限检查

# CPU 分级

平时我们称应用程序为 "3 环",系统程序为 "0 环"—— 这里所说的 "环" 与 CPU 有关,与操作系统无关。

# 进程特权级别

# CPL(Current Privilege Level,当前特权级)

描述:段寄存器 CS 的最低两个比特位称为当前特权级,表示当前任务(或正在执行的代码)所处的特权级别。

注意:段选择子 SS 和 CS 的最低两个比特位始终相同。

:当 CS = 0x001B 时:

  • 0x001B = 0b0000000000011011
  • 最低两位 = 0b11 = 3
  • 因此:当前进程处于 3 环

# RPL(Requested Privilege Level,请求特权级)

描述:RPL 存储在段选择子中。它是一个请求的特权级,表示当程序访问一个特定段时,希望使用的特权级。

要点:

  1. RPL 是段选择子结构中的一部分
  2. RPL 是针对段选择子而言的,每个段的选择子都有自己的 RPL
  3. RPL 表示用什么权限去访问一个段

:以下两段代码指向同一个段描述符,但 RPL 不同。

1
2
MOV AX, 0008    ; 0000000000001 0 00(RPL = 0)
MOV DS, AX
1
2
MOV AX, 000B    ; 0000000000001 0 11(RPL = 3)
MOV DS, AX

# DPL(Descriptor Privilege Level,描述符特权级)

描述:DPL 是由操作系统或程序员设置的,表示段本身的特权级别。DPL 的值越,段的特权级越,访问要求越严格

例 1:内核代码段的 DPL = 0

假设有一个内核代码段,其段描述符中的 DPL 设置为 0,这意味着该段只能被特权级为 0 的代码访问(即只能被内核代码访问)。

1
2
3
4
5
segment_code_descriptor:
base: 0x00000000
limit: 0xFFFFF
type: 0xA (代码段)
DPL: 0 ; 内核级,只有内核代码可以访问

如果 CPL 为 0,则可以访问该段;如果 CPL 为 3(用户模式),则无法访问,系统会触发保护异常(#GP 错误)。

例 2:用户数据段的 DPL = 3

假设有一个用户数据段,其段描述符中的 DPL 设置为 3,表示任何特权级的代码都可以访问该段。

1
2
3
4
5
segment_data_descriptor:
base: 0x10000000
limit: 0xFFFFF
type: 0x2 (数据段)
DPL: 3 ; 用户模式

由于 DPL = 3,无论 CPL 为 0 还是 3,均可以访问该段(因为 CPL ≤ DPL 的条件始终满足)。

# 代码段与数据段权限检查

数据段的权限检查遵循以下规则:

  • max(CPL, RPL) ≤ DPL:允许访问

即 CPL 和 RPL 都必须小于等于 DPL,访问才被允许。

思考:既然已经有 CPL(当前特权级)了,为什么还需要 RPL(请求特权级)?

答案:我们本可以用 " 读写 "的权限去打开一个文件,但为了避免出错,有些时候我们会使用" 只读 " 的权限去打开。RPL 的作用就是允许高特权级代码主动降低自己的访问权限。

# 系统段权限检查

系统段描述符(如 TSS 或 LDT)的权限检查更为严格,通常只有更高特权级(如内核模式)的任务才能访问,以保护内核资源。

# 代码跨段跳转

# 段间跳转

描述:段间跳转(Inter-segment Jump)指的是在程序执行过程中,通过改变 CS 和 EIP 来从一个代码段跳转到另一个代码段的过程。

段间跳转分为两种情况

  1. 目标段是一致代码段
  2. 目标段是非一致代码段

只改变 EIP 的指令

1
2
3
4
JMP
CALL
JCC
RET

能够同时修改 CS 与 EIP 的指令

1
2
3
4
5
JMP FAR
CALL FAR
RETF
INT
IRETD

# 执行流程

JMP 0x20:0x0004183D 为例:

1) 段选择子拆分

1
2
3
4
5
segment_selector: 0x20
binary: 0000000000100 0 00
RPL: 00 ; 请求特权级为 0
TI: 0 ; 查 GDT 表
Index: 4 ; 段描述符索引为 4

2) 查表得到段描述符

四种情况可以跳转:

  1. 代码段
  2. 调用门
  3. TSS 任务段
  4. 任务门

3) 权限检查

  • 一致代码段:要求 CPL ≥ DPL (数值上)
  • 非一致代码段:要求 CPL == DPL 并且 RPL ≤ DPL

4) 加载段描述符

通过权限检查后,CPU 会将段描述符加载到 CS 段寄存器中。

5) 代码执行

CPU 将 CS.Base + Offset 的值写入 EIP,然后执行 CS:EIP 处的代码,段间跳转到此结束。

# 总结

  • 一致代码段(共享段)

    1. 特权级高的程序不允许访问特权级低的代码:核心态不允许跳转到用户态的一致代码段
    2. 特权级的程序可以访问特权级高的代码,但特权级不会改变:用户态还是用户态
  • 非一致代码段(普通代码段)

    1. 只允许同级访问
    2. 绝对禁止不同级别的访问:核心态不是用户态,用户态也不是核心态

注意:直接对代码段进行 JMPCALL 操作,无论目标是一致代码段还是非一致代码段,CPL 都不会发生改变。如果要提升 CPL 的权限,只能通过调用门

# 实验:JMP FAR 指令

一、复制一个非一致代码段描述符,写入 GDT 表空白处。

复制前:

复制后:

二、在 OD 中执行 JMP FAR 命令。

执行前,CS = 001B

执行后,CS = 004B

三、将段描述符的 DPL 设置为 0。

四、再次执行 JMP FAR 命令。

执行前:

执行后,程序进入了异常处理,说明执行失败:

失败原因:目标代码段是一个非一致代码段(DPL=0),而程序的 CPL=3,权限检查未通过。非一致代码段要求 CPL == DPL 时才允许跳转,这样设计的目的是防止 3 环程序跳转到系统段执行。

五、将段描述符 Type 域的 C 位改为 1,令其成为一致代码段。

六、再次执行 JMP FAR 命令,可以成功执行。

# 总结

  1. 为了对数据进行保护,普通代码段禁止用户态的代码 / 数据和内核态的代码 / 数据相互访问
  2. 如果选择一致代码段,低级别的程序就可以在不提升 CPL 权限等级的情况下进行访问,并且不会破坏内核态的数据。
  3. 如果想访问非一致代码段,必须通过 " 调用门 " 等方式提升 CPL 权限。

上一篇:保护模式(一)| 段机制基础
下一篇:保护模式(三)| 调用门