4. 分段机制:段描述符、段选择子、逻辑地址到线性地址的转换
分段是 x86 架构中从逻辑地址到线性地址转换的核心机制。在保护模式下,CPU 不再直接使用物理地址,而是通过分段单元将程序员可见的逻辑地址(由段选择子 + 偏移量组成)转换为线性地址。这一过程依赖于两个关键数据结构:段描述符和段选择子。
💡 核心概念:逻辑地址是程序员在代码中直接使用的地址形式(例如
mov eax, [ds:0x1000]),而线性地址是经过分段单元转换后、供分页单元(如果启用)或直接访问物理内存的中间地址。
4.1 段描述符:段的“身份证”
段描述符是一个 8 字节(64 位)的数据结构,用于描述一个内存段的基地址、大小、访问权限和类型等信息。所有段描述符集中存放在两张全局表中:全局描述符表(GDT)和局部描述符表(LDT)。
📋 段描述符字段详解(8 字节布局)
| 位域范围 | 字段名称 | 说明 |
|---|---|---|
| 0–15 | Limit[15:0] | 段界限低 16 位 |
| 16–31 | Base[15:0] | 基地址低 16 位 |
| 32–39 | Base[23:16] | 基地址中 8 位 |
| 40–43 | Type | 段类型(代码段/数据段/系统段) |
| 44 | S | 描述符类型(0=系统段,1=代码/数据段) |
| 45–46 | DPL | 描述符特权级(0~3) |
| 47 | P | 段存在位(1=在内存中) |
| 48–51 | Limit[19:16] | 段界限高 4 位 |
| 52 | AVL | 软件可用位 |
| 53 | L | 64 位代码段标志(IA-32e 模式) |
| 54 | D/B | 默认操作大小(0=16 位,1=32 位) |
| 55 | G | 粒度位(0=字节,1=4KB) |
| 56–63 | Base[31:24] | 基地址高 8 位 |
段描述符的 Base 字段(32 位)定义了段的起始线性地址,Limit 字段(20 位)结合 G 粒度位定义了段的大小。当 G=0 时,段大小 = Limit + 1(字节);当 G=1 时,段大小 = (Limit + 1) × 4KB。
4.2 段选择子:索引段的“钥匙”
段选择子是一个 16 位的寄存器值,用于在 GDT 或 LDT 中索引对应的段描述符。它由三部分组成:索引值、TI 位和RPL。
🔑 段选择子结构(16 位)
| 位域 | 名称 | 说明 |
|---|---|---|
| 15–3 | Index | 13 位索引,指向 GDT/LDT 中的第 Index 个描述符 |
| 2 | TI | 表指示位:0=GDT,1=LDT |
| 1–0 | RPL | 请求特权级(0~3),用于权限检查 |
段选择子通常由编译器或操作系统自动设置,并加载到段寄存器(CS、DS、ES、FS、GS、SS)中。例如,mov ds, ax 指令会将 AX 中的段选择子加载到 DS 寄存器,CPU 随后使用该选择子查找对应的段描述符。
⚠️ 注意:段选择子的 Index 值必须乘以 8(因为每个段描述符占 8 字节)才能得到在 GDT/LDT 中的字节偏移。例如,Index=2 对应 GDT 中偏移 16 字节处的描述符。Index=0 是空选择子,不允许用于访问内存。
4.3 逻辑地址到线性地址的转换流程
当 CPU 执行内存访问指令时,分段单元自动完成以下转换步骤:
- 提取段选择子:从指令指定的段寄存器(如 DS)中取出 16 位段选择子。
- 选择描述符表:根据 TI 位选择 GDT(TI=0)或 LDT(TI=1)。
- 索引描述符:将 Index 乘以 8,加上 GDT/LDT 的基地址(存储在 GDTR/LDTR 寄存器中),找到对应的段描述符。
- 权限检查:比较当前 CPL(当前特权级)、RPL(请求特权级)和 DPL(描述符特权级),确保访问合法。
- 提取基地址:从段描述符中取出 32 位 Base 字段。
- 计算线性地址:线性地址 = 段基地址 + 偏移量(指令中给出的逻辑地址偏移)。
- 界限检查:验证偏移量是否在段界限内,若超出则触发 #GP(通用保护异常)。
4.4 段描述符表:GDT 与 LDT
系统中有两张主要的段描述符表:
- GDT(全局描述符表):全局唯一,包含所有任务共享的段描述符(如内核代码段、数据段)。由 GDTR 寄存器指向,最大可容纳 8192 个描述符(因为 Index 为 13 位)。
- LDT(局部描述符表):每个任务可拥有独立的 LDT,用于存放任务私有的段描述符。由 LDTR 寄存器指向,最大同样可容纳 8192 个描述符。
📊 GDT 与 LDT 对比
| 特性 | GDT | LDT |
|---|---|---|
| 作用域 | 全局(所有任务共享) | 局部(每个任务独立) |
| 寄存器 | GDTR(48 位:32 位基地址 + 16 位界限) | LDTR(16 位选择子 + 缓存) |
| TI 位值 | 0 | 1 |
| 典型用途 | 内核段、TSS、LDT 描述符 | 用户进程私有段 |
| 最大描述符数 | 8192(Index 13 位) | 8192(Index 13 位) |
4.5 权限检查:CPL、RPL 与 DPL
分段机制通过三级特权级实现保护:
- CPL(当前特权级):当前执行代码段的特权级,存储在 CS 寄存器的低 2 位。
- RPL(请求特权级):段选择子中指定的特权级,表示访问请求的来源特权级。
- DPL(描述符特权级):段描述符中定义的特权级,表示访问该段所需的最低特权级。
访问数据段时,CPU 要求:max(CPL, RPL) ≤ DPL。即当前代码和请求者的最高特权级必须不高于(数值上小于等于)描述符的特权级。访问代码段时,规则更为严格,通常要求 CPL = DPL。
💡 实际应用:在 Linux 中,内核使用 DPL=0 的段(最高特权级),用户态使用 DPL=3 的段(最低特权级)。通过分段权限检查,用户程序无法直接访问内核段,从而实现基本的用户态/内核态隔离。
4.6 分段机制的现代意义
在 x86-64 长模式下,分段机制被大幅简化:段基地址被强制为 0,段界限被忽略,逻辑地址直接等于线性地址。但理解分段机制对于以下场景仍然至关重要:
- 操作系统启动:实模式到保护模式的切换必须正确设置 GDT。
- 虚拟化技术:VMM 需要模拟客户机的分段状态。
- 调试与逆向:分析保护模式下的内存访问异常需要理解分段规则。
- 嵌入式系统:部分 RTOS 仍使用分段进行内存保护。
📌 本章要点
- 段描述符是 8 字节结构,包含基地址、界限、类型和特权级信息。
- 段选择子通过 Index 索引 GDT/LDT 中的描述符,TI 位选择表,RPL 标识请求特权级。
- 逻辑地址 = 段选择子 : 偏移量;线性地址 = 段基地址 + 偏移量。
- 权限检查规则:max(CPL, RPL) ≤ DPL 才能访问数据段。
- GDT 全局共享,LDT 任务私有,两者共同构成分段描述符表体系。