保护模式(二)| 段权限检查与代码跨段跳转
# 前言
上一篇我们学习了段寄存器和 GDT 的基本结构。本篇将深入探讨段权限检查机制以及代码跨段跳转的原理。
# 段权限检查
# CPU 分级
平时我们称应用程序为 "3 环",系统程序为 "0 环"—— 这里所说的 "环" 与 CPU 有关,与操作系统无关。
# 进程特权级别
# CPL(Current Privilege Level,当前特权级)
描述:段寄存器 CS 的最低两个比特位称为当前特权级,表示当前任务(或正在执行的代码)所处的特权级别。
注意:段选择子 SS 和 CS 的最低两个比特位始终相同。
例:当 CS = 0x001B 时:
0x001B=0b0000000000011011- 最低两位 =
0b11= 3 - 因此:当前进程处于 3 环
# RPL(Requested Privilege Level,请求特权级)
描述:RPL 存储在段选择子中。它是一个请求的特权级,表示当程序访问一个特定段时,希望使用的特权级。
要点:
- RPL 是段选择子结构中的一部分
- RPL 是针对段选择子而言的,每个段的选择子都有自己的 RPL
- RPL 表示用什么权限去访问一个段
例:以下两段代码指向同一个段描述符,但 RPL 不同。
1 | MOV AX, 0008 ; 0000000000001 0 00(RPL = 0) |
1 | MOV AX, 000B ; 0000000000001 0 11(RPL = 3) |
# DPL(Descriptor Privilege Level,描述符特权级)
描述:DPL 是由操作系统或程序员设置的,表示段本身的特权级别。DPL 的值越小,段的特权级越高,访问要求越严格。
例 1:内核代码段的 DPL = 0
假设有一个内核代码段,其段描述符中的 DPL 设置为 0,这意味着该段只能被特权级为 0 的代码访问(即只能被内核代码访问)。
1 | segment_code_descriptor: |
如果 CPL 为 0,则可以访问该段;如果 CPL 为 3(用户模式),则无法访问,系统会触发保护异常(#GP 错误)。
例 2:用户数据段的 DPL = 3
假设有一个用户数据段,其段描述符中的 DPL 设置为 3,表示任何特权级的代码都可以访问该段。
1 | segment_data_descriptor: |
由于 DPL = 3,无论 CPL 为 0 还是 3,均可以访问该段(因为 CPL ≤ DPL 的条件始终满足)。
# 代码段与数据段权限检查
数据段的权限检查遵循以下规则:
- max(CPL, RPL) ≤ DPL:允许访问
即 CPL 和 RPL 都必须小于等于 DPL,访问才被允许。
思考:既然已经有 CPL(当前特权级)了,为什么还需要 RPL(请求特权级)?
答案:我们本可以用 " 读写 "的权限去打开一个文件,但为了避免出错,有些时候我们会使用" 只读 " 的权限去打开。RPL 的作用就是允许高特权级代码主动降低自己的访问权限。
# 系统段权限检查
系统段描述符(如 TSS 或 LDT)的权限检查更为严格,通常只有更高特权级(如内核模式)的任务才能访问,以保护内核资源。
# 代码跨段跳转
# 段间跳转
描述:段间跳转(Inter-segment Jump)指的是在程序执行过程中,通过改变 CS 和 EIP 来从一个代码段跳转到另一个代码段的过程。
段间跳转分为两种情况:
- 目标段是一致代码段
- 目标段是非一致代码段
只改变 EIP 的指令:
1 | JMP |
能够同时修改 CS 与 EIP 的指令:
1 | JMP FAR |
# 执行流程
以 JMP 0x20:0x0004183D 为例:
1) 段选择子拆分
1 | segment_selector: 0x20 |
2) 查表得到段描述符
四种情况可以跳转:
- 代码段
- 调用门
- TSS 任务段
- 任务门
3) 权限检查
- 一致代码段:要求
CPL ≥ DPL(数值上) - 非一致代码段:要求
CPL == DPL并且RPL ≤ DPL
4) 加载段描述符
通过权限检查后,CPU 会将段描述符加载到 CS 段寄存器中。
5) 代码执行
CPU 将 CS.Base + Offset 的值写入 EIP,然后执行 CS:EIP 处的代码,段间跳转到此结束。
# 总结
-
一致代码段(共享段)
- 特权级高的程序不允许访问特权级低的代码:核心态不允许跳转到用户态的一致代码段
- 特权级低的程序可以访问特权级高的代码,但特权级不会改变:用户态还是用户态
-
非一致代码段(普通代码段)
- 只允许同级访问
- 绝对禁止不同级别的访问:核心态不是用户态,用户态也不是核心态
注意:直接对代码段进行 JMP 或 CALL 操作,无论目标是一致代码段还是非一致代码段,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 命令,可以成功执行。
# 总结
- 为了对数据进行保护,普通代码段禁止用户态的代码 / 数据和内核态的代码 / 数据相互访问。
- 如果选择一致代码段,低级别的程序就可以在不提升 CPL 权限等级的情况下进行访问,并且不会破坏内核态的数据。
- 如果想访问非一致代码段,必须通过 " 调用门 " 等方式提升 CPL 权限。
上一篇:保护模式(一)| 段机制基础
下一篇:保护模式(三)| 调用门