前言
上一节中,我们讲解了段选择子、段描述符以及GDT/LDT的核心结构。
当我们将16位的选择子加载到段寄存器时(例如 MOV DS, AX),CPU为了避免每次都去内存查表,会将描述符中的信息(Base, Limit, Attribute)加载到段寄存器的不可见部分。
实际上,段寄存器并非仅有可见的16位,而是拥有96位的完整结构。其中包括16位选择子和80位高速缓存。本节我们将结合WinDbg静态分析与编写代码进行实验,来验证这96位结构的存在。
一、 段寄存器的96位结构
在保护模式下,ES、CS、SS、DS、FS、GS 这些寄存器看起来只有16位(可见部分),但CPU内部为这些寄存器都配备了一个80位的影子寄存器(Shadow Register / Cache)。
完整结构如下:
| 组成部分 | 位宽 | 来源 | 说明 |
|---|---|---|---|
| Selector | 16位 | 可见 | 我们指令中操作的部分 (如 0x23) |
| Attribute | 16位 | 隐藏 | 属性(可读写、特权级等,有效位为12位) |
| Base | 32位 | 隐藏 | 段的起始地址 |
| Limit | 32位 | 隐藏 | 段的大小 |
填充机制:
当我们执行 MOV DS, AX 时,CPU会拿着AX中的Selector去GDT表中找到对应的64位描述符,拆解出Attribute, Base, Limit,填入隐藏部分。
此后,CPU访问数据时,不再查表,而是直接读取这96位缓存。
二、静态分析:WinDbg手动拆解描述符
为了研究这96位中到底填充什么,我们需要手动拆解一个描述符。
实验对象:Windows XP下以ds=0x23为例进行拆解。

1. 寻找描述符
- 0x23的二进制:0000 0000 0010 0011。
- Index = 100b = 4。
- TI = 0 (查找 GDT)。
- RPL = 3 (用户态)。
我们需要从GDT表中获取第四项数据。
2. 读取 GDT (WinDbg)

从图中可以看出,我们得到64位的描述符数据:High=00cff300, Low=0000ffff。
3. 手动拆解
我们按照Intel格式将其拆解还原为寄存器缓存中的格式。
A. Base (基址) - 32位
- 拼接High的高8位、低8位和Low的高16位:
Base = 00 00 0000 = 0x00000000
B. Limit (界限) - 32位
拼接High的低4位(Limit High) 和Low的低16位:
-
Raw Limit = F FFFF = 0xFFFFF
-
G位 (Granularity,粒度):High Dword的Bit 23是1 (00cf… 中的 c = 1100)。
- G=1代表单位是 4KB。
- 实际Limit = 0xFFFFF * 0x1000 + 0xFFF = 0xFFFFFFFF (4GB)
C. Attribute (属性) - 12位有效位
-
Byte 5: F3 (1111 0011) -> P=1, DPL=3, S=1, Type=Read/Write/Accessed.
-
Byte 6 (High 4 bits): C (1100) -> G=1, D/B=1
-
拼接结果:0x0CF3(高4位补零对齐,但有效控制位就是12位)
-
使用windbg命令查看并验证,如下所示:

总结:当 0x23加载入DS时,DS寄存器变成了: Selector=0x23, Base=0, Limit=4GB, Attr=0x0CF3 (可读写)
三、动态探测:代码实战
实验 1:探测 Attribute (属性位)
我的windows xp环境中CS的选择子是0x1B,它的属性是只读/可执行。如果我们把CS的选择子强行赋给DS,此时DS的属性会更新为只读。如果尝试写数据,程序会崩溃。
#include "stdafx.h"
int main(int argc, char* argv[])
{
int num = 100;
__asm
{
mov ax,cs
mov ds,ax
mov dword ptr ds:[num],0x1000
}
printf("num = %d\n",num);
return 0;
}
结果:程序运行至写操作时触发了0xC0000005异常。

证明:段寄存器确实缓存了Attribute属性,且该属性决定了内存访问权限。
实验 2:探测 Base (基址)
原理:在windows xp中,FS寄存器用于指向TEB,其Base通常是 0x7FFDF000(非0)。此时我们将FS赋给ES,那么ES的Base也会变成0x7FFDF000(0x7FFDF000因系统环境的不同会有所改变)。
int main(int argc, char* argv[])
{
int val_es = 0;
int val_linear = 0;
__asm
{
mov ax, fs
mov es, ax
mov eax, es:[0]
mov val_es, eax
mov eax,0x7FFDF000
mov ecx, ds:[ebx]
mov val_linear,ecx
}
printf("ES:[0]: %X\n", val_es);
printf("线性地址0x7FFDF000读取值: %X\n", val_linear);
if(val_es == val_linear)
{
printf("探测成功!证明ES的隐藏Base: 0x7FFDF000\n");
}
return 0;
}
结果:两个读出的值相同。
证明:段寄存器隐藏部分确实缓存了Base地址参与寻址计算。
实验 3:探测 Limit (界限)
原理:FS段不仅Base特殊,Limit也很小(通常是4KB,即 0xFFF)。超过这个范围的访问程序会崩溃。
int main(int argc, char* argv[])
{
__asm
{
mov ax,fs
mov es,ax
mov eax,es:[0xFFC]
mov eax,es:[0x1000]
}
return 0;
}
结果:读取0xFFC正常,读取0x1000崩溃

四、总结
通过WinDbg和代码的验证,我们得出以下结论:
- 96位是真实的:段寄存器远比汇编指令中看到的16位复杂。
- 缓存即真理:CPU运行时,真正起作用的是隐藏部分(Base/Limit/Attr)。可见的Selector仅作为索引,用于在GDT中定位并加载对应的段描述符信息。
2060

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



