一、为什么会有保护模式
8086实模式中,内存寻址如下:
逻辑地址 = 段寄存器(16 位) : 偏移(16 位)
物理地址 = 段值 << 4 + 偏移
在这种模式下,只能访问1MB的内存。而且实模式中没有权限检查,没有隔离,内存任意读写,这就导致内存写错系统直接崩溃。
后来80386引入保护模式 (Protected Mode):
- 地址空间扩展到4GB
- 引入权限级别(Ring0~Ring3),用于区别内核态/用户态
- 引入段机制 + 分页机制,能够对内存访问进行安全检查
在保护模式中,段寄存器不再直接参与“地址 = 段值 << 4 + 偏移”,而是变成了:
- 段寄存器中存放的是段选择子 (Selector),一个16位编号
- 通过GDT/LDT获取段描述符 (Descriptor)
- 从描述符中得到:Base(段基址)、Limit(段界限)Attributes(属性中包含段类型、特权级别等信息)
- 计算出线性地址:线性地址 = Base + 偏移
- 最终通过线性地址映射到物理内存中
二、保护模式下有哪些段寄存器?
x86里一共有 8 个段相关寄存器:
-
CS:代码段寄存器(指令取址用)
-
DS:数据段寄存器(普通数据访问默认用)
-
SS:栈段寄存器(push/pop、call/ret 等用)
-
ES / FS / GS:额外的数据段,主要给“字符串指令”或“特殊用途”用
-
LDTR:局部描述符表寄存器(指向LDT)
-
TR:任务寄存器(指向当前TSS)
三、段选择子(Selector)
段寄存器可见的部分只有16位,这16位就是段选择子:

-
Index(高 13 位):段描述符在 GDT/LDT 中的下标
-
TI(Table Indicator,第 2 位):
- 0:在 GDT 里找
- 1:在 LDT 里找
-
RPL(Requested Privilege Level,低 2 位):请求特权级,一般是0或3
当前我们使用windows xp进行实验,使用ollydbg打开notepad.exe:

其中DS/ES/SS都是0x23,我们把它拆成二进制看一下:
0x23 = 0010 0011b
Index = 高 13 位 = 0000000000100b = 4
TI = 第 2 位 = 0 → 在GDT中查
RPL = 低 2 位 = 11b = 3 → Ring 3(用户态)
所以0x23的含义为在GDT中取第4项描述符,这个段是以Ring 3权限访问的。
四、段描述符(Descriptor)
段描述符就是真正定义这个段的结构,在GDT或者LDT中可以查看。段描述符的基本布局如下:

- Base(基址,总共32位)
- 低16位:Base[15:0]
- 中8位:Base[23:16]
- 高8位:Base[31:24]
- Limit(段界限,总共20位)
- 低 16 位:Limit[15:0]
- 高 4 位:Limit[19:16]
- 属性(Attribute)
- Type:代码段/数据段/系统段,是否可读写、是否可执行等
- S:描述符类型(系统段/代码数据段)
- DPL:描述符特权级(0~3)
- P:存在位(Present)
- G:粒度位(Granularity)
- D/B:默认操作数大小或者栈指针大小(32/16 位)
- AVL 等少量辅助位
对于Limit只有20位,看似只能描述1MB(20位)空间。但实际上需要配合G位来确定这个段的大小:
- G = 0:按字节粒度
段大小 = Limit + 1 字节 - G = 1:按 4KB 粒度
段大小 = (Limit << 12) | 0xFFF
所以在G=1时,段能够描述4GB空间,线性地址范围:
0x00000000 ~ 0xFFFFFFFF
这就是所谓的平坦段,整个0~4GB被看成一个大段,配合分页机制来做真正的隔离。
五、GDT 与 GDTR
1. GDT:全局描述符表
- GDT 是一个数组:每项 8 字节,每项一个描述符
- 基地址 + 限长存放在一个特殊寄存器 GDTR 中
- 所有 TI=0 的段选择子,都是在GDT中查描述符
在 Windows XP 下,系统启动后会把 GDT 放在内核空间某个地址上。
2. GDTR 的结构与读取
GDTR 是一个48位寄存器:
- 低 16 位:GDT限长(字节数 - 1)
- 高 32 位:GDT基地址
在代码里不能直接用 mov 读 GDTR,需要专门的指令:
-
SGDT m:把 GDTR 的 48 位值存到内存操作数m中- 权限要求:Ring 0/3 都可以使用(XP 下用户态也可以用)
-
LGDT m:从内存中加载 GDTR- 权限要求:只允许 Ring 0
使用C语言进行读取,代码如下:
#include "stdafx.h"
#pragma pack(push, 1)
typedef struct _GDTR_BUF
{
unsigned short limit;
unsigned int base;
} GDTR_BUF;
#pragma pack(pop)
int main()
{
GDTR_BUF gdtr = { 0 };
unsigned char buff[6];
__asm {
sgdt buff
}
gdtr.limit = *(unsigned short*)&buff[0];
gdtr.base = *(unsigned int*)&buff[2];
printf("GDT Limit = 0x%04X\n", gdtr.limit);
printf("GDT Base = 0x%08X\n", gdtr.base);
return 0;
}
使用windbg读取如下:
0: kd> r gdtr, gdtl
gdtr=8003f000 gdtl=000003f
六、LDT 与 LDTR
GDT 是全局的,对整个系统(或者每个 CPU)唯一;
LDT(Local Descriptor Table)是局部的,原本设计是给每个任务/进程有自己的私有段。
- LDT 的基址 + 限长存放在 LDTR 寄存器里
- LDTR 本身的值来自 GDT 中的一个“系统段描述符”(LDT Descriptor)
- 当段选择子的 TI = 1 时,就去 LDT 找对应的段描述符
在 Windows XP 中,普通应用基本不会使用LDT;因为windows使用了平坦模式。所有进程都共用 0~4GB 的线性空间,依靠页表来做进程隔离,不需要给每个进程单独搞一个LDT私有段表。
2904

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



