保护模式(一)| 段机制基础
# 前言
本文是 "保护模式" 系列笔记的第一篇,主要介绍 x86 架构下保护模式的基本概念和段机制基础。
参考资料:Intel 白皮书第三卷
# Windows 工作模式
实模式:早期的 DOS 和 Windows 版本工作在此模式下,允许直接访问硬件,但只能寻址 1MB 的内存(因为地址空间为 20 位)。
保护模式:Windows 95 和更高版本大多运行在保护模式下,能够使用 x86 架构的保护特性,如多任务、内存保护和更大的内存寻址,最大可访问 4GB 内存空间,是现代操作系统的基本运行模式。
虚拟 8086 模式:是保护模式下的一种特殊运行模式,允许在一个受保护的环境中运行 16 位的 DOS 应用程序。每个 DOS 程序运行在独立的虚拟 8086 环境中,不会干扰其他程序。
# 段寄存器
在 x86 架构中,当读写某一个地址时:
1 | mov dword ptr ds:[0x123456], eax |
真正读写的地址是:
1 | ds.base + 0x123456 |
其中, ds 是段寄存器。在 x86 架构中,段寄存器共有 8 个:
| 段寄存器 | 含义 |
|---|---|
| CS (Code Segment) | 代码段寄存器 |
| DS (Data Segment) | 数据段寄存器 |
| SS (Stack Segment) | 栈段寄存器 |
| ES (Extra Segment) | 附加段寄存器 |
| FS (Additional Segment) | 附加段寄存器 |
| GS (Additional Segment) | 附加段寄存器 |
| LDTR (Local Descriptor Table Register) | 局部描述符表寄存器 |
| TR (Task Register) | 任务寄存器 |
# 段寄存器的结构
段寄存器共有 96 位,结构如图:
使用结构体表示:
1 | struct SegMent |
# 段寄存器的读写
段寄存器在读的时候只能读取 16 位(即段选择子),但写的时候会写入全部 96 位:
1 | MOV AX, ES ; 读(只读取 16 位段选择子) |
除了 MOV 指令,还可以使用 LES 、 LSS 、 LDS 、 LFS 、 LGS 指令修改段寄存器。
注意:不存在 LCS 指令,因为 CS 不可直接写入。CS 的改变意味着 EIP 的改变,改变 CS 的同时必须修改 EIP,所以无法使用上述指令来修改 CS。
# 段寄存器的属性
# 探测 Attribute
例 1:以下代码可以成功编译并正常运行:
1 | int var = 0; |
例 2:以下代码可以成功编译,但运行时报错:
1 | int var = 0; |
这说明在写入段寄存器时, Attribute 会根据段描述符的内容被修改。
# 探测 Base
以下代码可以成功编译,但运行时报错:
1 | int var = 1; |
这说明在写入段寄存器时, Base 会根据段描述符的内容被修改。
# 探测 Limit
以下代码可以成功编译,但运行时报错。这是因为 FS 段寄存器的 Limit 为 0xFFF ,而输入的段偏移为 0x1000 ,超出了范围:
1 | int var = 1; |
# GDT(全局描述符表)
当 CPU 执行类似 MOV DS, AX 指令时,会根据 AX 的值来决定查找 GDT 还是 LDT(Local Descriptor Table,局部描述符表),包括查找的位置以及查出多少数据等。
工具:WinDbg
gdtr 寄存器用于存储 GDT 的位置:
gdtl 寄存器用于存储 GDT 的大小:
GDT 具体数据:
如果想要看懂这些数据,需要先学习段描述符和段选择子。
# 段描述符
GDT 由若干个段描述符组成,每个段描述符占 8 个字节。
# 段描述符的结构
段描述符高位在前,低位在后。
例如 GDT 的第二项 00cf9b00'0000ffff :其中 00cf9b00 对应结构图的高四字节(上面一行), 0000ffff 对应结构图的低四字节(下面一行)。
# 段描述符的属性
| 属性位 | 含义 |
|---|---|
| Base | 段基址。共 32 位,由三个部分共同组成。 |
| G | 粒度位。表示段限长的计算单位。 G=0: Limit 单位为字节,最大值为 0x000FFFFF G=1: Limit 单位为 4KB,最大值为 0xFFFFFFFF |
| D/B | 默认操作大小。 对于 CS 段:D=1 采用 32 位寻址方式;D=0 采用 16 位寻址方式 对于 SS 段:D=1 隐式堆栈访问指令(如 PUSH、POP、CALL)使用 32 位堆栈指针 ESP;D=0 使用 16 位堆栈指针 SP 对于向上扩展的数据段:D=1 段上限为 4GB;D=0 段上限为 64KB |
| L | 表示该段是否为 64 位代码段,仅在 IA-32e 模式(64 位)下有效。 L=1:64 位代码段;L=0:32 位代码段 |
| AVL | 可供系统软件使用的标志位,用于操作系统管理。 |
| DPL | 段描述符特权级别,表示段的访问权限。 DPL=0:特权级(内核态) DPL=3:用户级(用户态) 若指令访问的段描述符 DPL=0,但程序的 CPL=3,那么该指令无法成功执行。 注意:在 Windows 中,DPL 只会出现两种情况:0 或 3。 |
| P | 有效位。P=1:段描述符有效;P=0:段描述符无效。 |
| S | 段描述符类型。S=0:系统段描述符;S=1:代码段或数据段描述符。 |
| TYPE | 类型字段,具体含义取决于 S 位,详见下方说明。 |
TYPE 字段详解:
当 S=0(系统段)时:
当 S=1(代码段 / 数据段)时:
各标志位含义:
- A=0:该段未被访问过;A=1:该段已被访问过
- W=1:可写;W=0:不可写
- E=0:向上扩展,有效范围为
Base ~ Base+Limit;E=1:向下扩展 - R=1:可读;R=0:不可读
- C=1:一致代码段(允许低特权级别访问);C=0:非一致代码段(只能从相同特权级别访问)
| Limit | 段的长度。共 20 位,由两个部分共同组成。 |
# 段选择子
| 字段 | 含义 |
|---|---|
| RPL | 请求特权级别(Requested Privilege Level) |
| TI | TI=0:查 GDT TI=1:查 LDT |
| Index | 索引值。该值乘以 8 再加上 GDT 或 LDT 的基地址,就是要加载的段描述符的位置。 |
# 实验:加载段描述符到段寄存器
目标:为 buffer 赋值,并成功执行以下代码。
1 | char buffer[6]; |
注意:RPL 需要小于等于 DPL(数值上)。完整的权限检查规则(涉及 CPL、RPL、DPL 三者的关系)将在下一篇详细讲解。
实验思路:编译并运行以下代码,观察结果。
1 |
|
执行结果: