# 前言

本文是 "保护模式" 系列笔记的第一篇,主要介绍 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
2
3
4
5
6
7
struct SegMent
{
WORD Selector; // 段选择子 16位 可见
WORD Attributes; // 段属性 16位 不可见
DWORD Base; // 段起始地址 32位 不可见
DWORD Limit; // 段大小 32位 不可见
};

# 段寄存器的读写

段寄存器在的时候只能读取 16 位(即段选择子),但的时候会写入全部 96 位:

1
2
3
4
5
MOV AX, ES    ; 读(只读取 16 位段选择子)
MOV DS, AX ; 写(写入 96 位,包括不可见部分)

; 读写 LDTR:SLDT / LLDT
; 读写 TR:STR / LTR

除了 MOV 指令,还可以使用 LESLSSLDSLFSLGS 指令修改段寄存器。

注意:不存在 LCS 指令,因为 CS 不可直接写入。CS 的改变意味着 EIP 的改变,改变 CS 的同时必须修改 EIP,所以无法使用上述指令来修改 CS。

# 段寄存器的属性

# 探测 Attribute

例 1:以下代码可以成功编译并正常运行:

1
2
3
4
5
6
7
int var = 0;
__asm
{
mov ax, ss // 此处不能为 CS,CS 可读、可执行但不可写
mov ds, ax
mov dword ptr ds:[var], eax
}

例 2:以下代码可以成功编译,但运行时报错:

1
2
3
4
5
6
7
int var = 0;
__asm
{
mov ax, cs // SS 改成了 CS
mov ds, ax
mov dword ptr ds:[var], eax
}

这说明在写入段寄存器时, Attribute 会根据段描述符的内容被修改。

# 探测 Base

以下代码可以成功编译,但运行时报错:

1
2
3
4
5
6
7
8
9
10
int var = 1;
__asm
{
mov ax, fs
mov gs, ax
mov eax, gs:[0] // 不要使用 DS,否则编译不通过
mov dword ptr ds:[var], eax

// 等价于 mov edx, dword ptr ds:[0x7FFDF000]
}

这说明在写入段寄存器时, Base 会根据段描述符的内容被修改。

# 探测 Limit

以下代码可以成功编译,但运行时报错。这是因为 FS 段寄存器的 Limit0xFFF ,而输入的段偏移为 0x1000 ,超出了范围:

1
2
3
4
5
6
7
8
9
10
int var = 1;
__asm
{
mov ax, fs
mov gs, ax
mov eax, gs:[0x1000]
// 访问的地址相当于下面这行代码,但 DS 的 Limit 是 0xFFFFFFFF
// mov eax, dword ptr ds:[0x7FFDF000 + 0x1000]
mov dword ptr ds:[var], eax
}

# 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
2
3
4
5
6
char buffer[6];
__asm
{
// 高两个字节给 ES,低四个字节给 ECX
les ecx, fword ptr ds:[buffer]
}

注意:RPL 需要小于等于 DPL(数值上)。完整的权限检查规则(涉及 CPL、RPL、DPL 三者的关系)将在下一篇详细讲解。

实验思路:编译并运行以下代码,观察结果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
#include <stdio.h>

int main()
{
char buffer[6];
unsigned int data;
unsigned short es_original;
unsigned short es_now;

__asm
{
mov ax, es
mov es_original, ax
}

printf("es original = 0x%02X\n", es_original);

*(unsigned int *)&buffer[0] = 0x12345678;
*(unsigned short *)&buffer[4] = 0x1B;

__asm
{
// 高两个字节给 ES,低四个字节给 ECX
les ecx, fword ptr ds:[buffer]
mov data, ecx
mov bx, es
mov es_now, bx
}

printf("nothing\n"); // 这句话不会被打印,修改 ES 后缓冲区发生了异常

printf("es now = 0x%02X\n", es_now);
printf("data = 0x%08X\n", data);

return 0;
}

执行结果:


下一篇:保护模式(二)| 段权限检查与代码跨段跳转