目录
全局描述符表(Global Descriptor Table,GDT)是 x86 架构保护模式下用于定义内存段(如代码段、数据段、堆栈段等)属性和访问权限的核心数据结构。GDT 的执行过程涉及 CPU 启动时从实模式切换到保护模式、加载 GDT 寄存器(GDTR)、以及后续通过段选择子(Segment Selector)访问段描述符进行内存访问控制。下面将详细描述 GDT 的完整执行过程。
一、背景知识
在 x86 架构中,CPU 有多种运行模式:
- 实模式(Real Mode):16 位地址空间,无内存保护,段地址直接左移 4 位 + 偏移。
- 保护模式(Protected Mode):32/64 位地址空间,支持虚拟内存、分页、特权级、段保护等机制。
GDT 仅在保护模式下使用。
二、GDT 的结构
GDT 是一个由 段描述符(Segment Descriptor) 组成的数组。每个段描述符占 8 字节(64 位),描述一个内存段的以下信息:
- 段基地址(Base Address):段在物理内存中的起始地址。
- 段界限(Segment Limit):段的大小(以字节或页为单位)。
- 访问权限(Access Rights):
- 类型(代码/数据/系统段)
- 可读/可写/可执行
- 特权级(DPL:Descriptor Privilege Level,0~3)
- 存在位(Present)
- 其他标志:如粒度(Granularity)、默认操作数大小(D/B 位)等。
三、GDT 执行过程详解
步骤 1:在实模式下准备 GDT
在操作系统引导阶段(如 bootloader 或内核初始化早期),CPU 处于实模式。此时需在内存中预先定义好 GDT 表。
示例(伪代码/汇编风格):
gdt_start:
dq 0x0000000000000000 ; 空描述符(必须存在,索引 0)
gdt_code:
dw 0xFFFF ; 段界限低 16 位
dw 0x0000 ; 基地址低 16 位
db 0x00 ; 基地址中间 8 位
db 10011010b ; 访问字节:代码段,可读,DPL=0,存在
db 11001111b ; 高 4 位界限 + 标志(G=1, D/B=1, L=0, AVL=0)
db 0x00 ; 基地址高 8 位
gdt_
dw 0xFFFF
dw 0x0000
db 0x00
db 10010010b ; 数据段,可写,DPL=0,存在
db 11001111b
db 0x00
gdt_end:
gdt_descriptor:
dw gdt_end - gdt_start - 1 ; GDT 界限(字节数 - 1)
dd gdt_start ; GDT 基地址(32 位)
注意:GDT 的第一个描述符(索引 0)必须为空(null descriptor),用于无效段选择子。
步骤 2:加载 GDTR 寄存器
CPU 使用 GDTR(Global Descriptor Table Register) 来定位 GDT。GDTR 是一个 48 位寄存器,包含:
- 16 位界限(Limit):GDT 的字节长度 - 1。
- 32 位基地址(Base):GDT 在物理内存中的起始地址。
使用 LGDT(Load GDT Register) 指令加载:
lgdt [gdt_descriptor]
此时 GDT 已被 CPU 知晓,但尚未生效,因为 CPU 仍在实模式。
步骤 3:启用保护模式
通过设置 CR0 控制寄存器 的 PE(Protection Enable)位(第 0 位) 来开启保护模式:
mov eax, cr0
or eax, 1 ; 设置 PE 位
mov cr0, eax
执行完此指令后,CPU 进入保护模式,但当前段寄存器(CS、DS 等)仍包含实模式下的值,这些值现在被解释为段选择子(Segment Selector)。
⚠️ 此时必须立即执行远跳转(far jump),以刷新 CS 寄存器,使其指向 GDT 中的有效代码段描述符。
步骤 4:远跳转刷新 CS 段寄存器
执行一个远跳转(far jump),目标地址包含新的段选择子和偏移:
jmp 0x08:protected_mode_start
0x08是段选择子(Selector):- 二进制:
0000 1000 - RPL(请求特权级) = 00(最低特权)
- TI(Table Indicator) = 0(表示使用 GDT,而非 LDT)
- Index = 0001(即 GDT 中第 1 个描述符,即代码段)
- 二进制:
CPU 执行此跳转时:
- 从 GDTR 获取 GDT 基地址。
- 用选择子的 Index(1)乘以 8,得到偏移
0x08。 - 从
GDT_base + 0x08读取 8 字节的代码段描述符。 - 验证描述符有效性(Present=1,类型=代码段,DPL ≥ CPL 等)。
- 将描述符中的基地址、界限、属性加载到 CS 的隐藏部分(不可见的段描述符缓存)。
- 跳转到目标偏移地址(
protected_mode_start),使用新的段属性执行代码。
此后,所有通过 CS 访问的指令都基于新的代码段描述符。
步骤 5:加载其他段寄存器(DS, ES, SS 等)
类似地,需用 MOV 指令加载其他段寄存器,使其指向 GDT 中的数据段或堆栈段:
mov ax, 0x10 ; 数据段选择子(Index=2)
mov ds, ax
mov es, ax
mov ss, ax
注意:SS(堆栈段)必须是可写的数据段,且通常与 DS 相同。
每次加载段寄存器时,CPU 都会:
- 解析段选择子(Index, TI, RPL)
- 从 GDT(或 LDT)中读取对应描述符
- 验证权限(如 DPL ≥ CPL,RPL ≤ DPL 等)
- 将描述符内容缓存到段寄存器的隐藏部分
步骤 6:后续内存访问的段检查
在保护模式下,每次内存访问(如 mov eax, [ebx])都隐含使用某个段寄存器(默认 DS,或显式指定如 mov eax, [es:edi])。
CPU 执行时:
- 从段寄存器获取缓存的段描述符(基地址、界限、属性)。
- 检查偏移是否在段界限内(若超出则触发 #GP 异常)。
- 检查访问权限(如代码段不可写,数据段不可执行)。
- 若启用分页,则线性地址 = 段基地址 + 偏移,再经分页转换为物理地址。
四、关键机制说明
1. 段选择子(Segment Selector)格式
| 15 … 3 | 2 | 1–0 |
|---|---|---|
| Index | TI | RPL |
- Index:GDT/LDT 中描述符的索引(乘以 8 得偏移)。
- TI:0 = GDT,1 = LDT。
- RPL:请求特权级(0~3),用于权限检查。
2. 描述符缓存(Hidden Register)
为避免每次内存访问都查 GDT,CPU 将描述符内容缓存在段寄存器的“隐藏部分”。只有在加载段寄存器时才访问 GDT。
3. 特权级检查(CPL, DPL, RPL)
- CPL(Current Privilege Level):当前代码段的 DPL(在 CS 中)。
- DPL(Descriptor Privilege Level):段描述符中的特权级。
- RPL(Requested Privilege Level):段选择子中的特权位。
访问规则(以数据段为例):
max(CPL, RPL) ≤ DPL
否则触发 #GP(General Protection Fault)
五、总结:GDT 执行流程图
实模式启动
↓
在内存中定义 GDT(含空描述符、代码段、数据段等)
↓
使用 LGDT 加载 GDTR(GDT 基址 + 界限)
↓
设置 CR0.PE = 1,进入保护模式
↓
执行 far jump 到新代码段(刷新 CS)
↓
加载 DS/ES/SS 等段寄存器(指向 GDT 中数据段)
↓
CPU 使用段描述符进行内存访问控制(基址+界限+权限)
↓
正常运行保护模式下的操作系统
六、注意事项
- 在 64 位长模式(Long Mode)下,段机制被大幅简化:CS/DS/ES/SS 的基地址强制为 0,界限忽略(除 FS/GS 外),GDT 仍需存在(用于定义 FS/GS 基址及 TSS)。
- GDT 本身必须位于物理内存中,且在保护模式启用后不能随意移动(除非更新 GDTR 并刷新所有段寄存器)。
- 操作系统通常在内核初始化早期设置 GDT,并可能在后续切换到更复杂的 GDT(如支持多核、TSS 等)。
通过以上过程,GDT 成为 x86 保护模式内存管理和特权控制的基石。
1万+

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



