前言
在上一篇文章中,我们通过实验证明了段寄存器拥有96位的结构。 那么,这80位隐藏缓存中的数据(Base, Limit, Attribute)究竟是从哪里来的?
答案是:GDT(全局描述符表) 。
GDT可以理解巨大的数组,数组中的每一个元素(8字节)就是一个段描述符。而我们手中的段寄存器(如 CS, DS)里存的16位数据,即为数组的索引,我们称之为段选择子 。
本节我们将继续分析这两个结构,并研究它们是如何配合CPU完成权限的检查(Ring0/Ring3 隔离)。
一、 段描述符 (Segment Descriptor)
段描述符是GDT表中的基本单元,每个描述符占用8个字节(64 位)。它详细定义了一个内存段的基址、大小以及最重要的权限属性。
1. 为什么结构看起来是碎片化的?
观察下图,你会发现Base(基址)和Limit(界限) 并不是连续存放的,而是被拆得四分五裂:

- Base (32位) :被拆分为三段 (Base 31-24, Base 23-16, Base 15-0)。
- Limit (20位) :被拆分为两段 (Limit 19-16, Limit 15-0)。
设计原理:向后兼容性。这种设计并非随意为之,而是Intel架构演进的历史产物:
- 80286时代:处理器是16位的,地址线只有24位。当时的描述符虽然也是64位,但高位有大量保留位。
- 80386时代:处理器升级为32位,地址线扩展到32位。为了让基于286编写的操作系统能在新CPU上直接运行,Intel必须保持描述符的低位结构不变。
- 结果:硬件设计者只能利用原先未使用的高位保留字段,将 Base扩充的高8位和Limit扩充的高4 位填充进去。这种架构扩展策略虽然导致了视觉上的结构碎片化,但在硬件层面实现了完美的兼容。
2. 核心属性详解 (Attributes)
在 64 位数据中,除了基址和界限,剩下的属性位决定了内存的身份和权限。
| 字段 | 名称 | 关键作用 |
|---|---|---|
| P | Present | 存在位。 P=1:段在内存中,可正常访问。 P=0:段无效。访问此类段会触发NP (Segment Not Present) 异常,操作系统通常利用此机制实现内存换页。 |
| DPL | Descriptor Privilege Level | 特权级(门锁) 。 范围 0-3。0代表最高权限 (Ring 0内核) ,3代表最低权限 (Ring 3用户)。这是CPU护机制的核心门锁。 |
| S | System | 描述符类型。 S=1:数据段/代码段 (我们最常接触的)。 S=0:系统段 (如TSS、调用门、中断门)。 |
| Type | Type | 具体类型 (取决于S位)。 若 S=1:决定该段是可读写 (Data) 还是可执行 (Code) ,以及是否被访问过 (Accessed)。 |
| G | Granularity | 粒度位。 G=0:Limit单位是字节,最大段长1MB。 G=1:Limit 单位是4KB,最大段长4GB (Limit×4KB+0xFFFLimit \times 4KB + 0xFFFLimit×4KB+0xFFF)。 |
| D/B | Default Operation Size | 位宽位。 1 = 32位段 (使用 EIP/ESP);0 = 16位段 (使用IP/SP)。 |
二、 段选择子 (Segment Selector)
段选择子就是我们在汇编指令中操作的那个16位数值(例如MOV DS, AX 中的 AX)。在上一节中我们已经详细分析过它的结构,这里只做核心回顾。
它主要包含三个部分:
- Index (索引) :GDT数组的下标。CPU 通过
GDTR.Base + (Index * 8)定位描述符。 - TI (表指示位) :决定查GDT (TI=0) 还是LDT (TI=1)。
- RPL (请求特权级) :它代表了你是以什么权限身份去请求访问这个段的。
三、 权限检查机制
在保护模式下,能不能把一个选择子加载到段寄存器(如DS, ES),不仅看段是否存在,更要看权限是否匹配。
这是理解Windows内核保护的关键,我们需要理解三个概念:
- CPL (Current Privilege Level) :当前特权级。即当前代码运行在什么环(CS寄存器的低位)。
- DPL (Descriptor Privilege Level) :描述符特权级。目标内存段要求的最低权限。
- RPL (Requested Privilege Level) :请求特权级。选择子里的权限。
数据段访问规则 (DS/ES/FS/GS)
CPU硬件执行的检查公式如下:
MAX(CPL, RPL) ≤ DPL
规则解析:
- 在x86架构中,数值越小代表权限越高(0为最高,3为最低)。
- 上述公式表示:当前运行权限 (CPL) 和 选择子中的请求权限 (RPL) ,在数值上都必须小于等于目标段的DPL。
- 也就是说访问者的权限级别必须高于或等于目标段要求的权限级别。
举例说明:
- 失败:用户态代码 (CPL=3) 试图访问内核数据段 (DPL=0)。MAX(3, RPL) > 0,检查失败,触发0xC0000005异常。
- 成功:内核态代码 (CPL=0) 访问用户数据段 (DPL=3)。MAX(0, RPL) ≤ 3,检查通过。
四、权限验证
为了验证权限检查机制,我们使用VC++ 6.0内联汇编,通过LES指令加载段描述符。LES 指令会加载ES段寄存器,并同时触发 CPU的硬件权限检查。
我们将尝试加载Ring 3选择子0x1B(Index=3, TI=0, RPL=3),先使用windbg查看如下:

- Pl (DPL): 3
- RPL (0x1B的低2位): 3
- CPL: 3 (调试器上下文,当前运行在Ring 3)
- 检查公式:MAX(3, 3) <= 3成立!所以放行。
- 代码实战:
int main()
{
char buf[6] = {
0x78, 0x56, 0x34, 0x12,
0x1B, 0x00
};
unsigned short es_val_before = 0;
unsigned short es_val_after = 0;
__asm
{
mov ax,es
mov es_val_before,ax
// 执行加载,尝试改为0x1B
// les: 将buf高2字节加载到ES,低4字节加载到 EAX
les eax,fword ptr [buf]
mov ax, es
mov es_val_after, ax
}
printf("Load Success! Privilege check passed.\n");
printf("Before LES: 0x%04X\n", es_val_before);
printf("After LES: 0x%04X\n", es_val_after);
system("pause");
return 0;
}
执行成功,结果如下:

五、总结
保护模式的权限检查,就是CPU硬件在段寄存器加载时强制执行的一道身份验证,确保只有高权限的程序才能访问高权限的内存,从而把内核和应用程序隔离开来。

被折叠的 条评论
为什么被折叠?



