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。

段描述符 8 字节结构 段描述符 8 字节结构(64 位) Limit[15:0] Base[15:0] Base[23:16] Type/S/DPL/P Limit/AVL/L/D/B/G Base[31:24] 字节 0-1 字节 2-3 字节 4 字节 5 字节 6 字节 7 字段说明: • Base[31:0]:32 位基地址,决定段起始线性地址 • Limit[19:0]:20 位段界限,结合 G 位计算段大小 • Type:4 位段类型(代码/数据/系统) • DPL:2 位特权级(0=最高,3=最低) • G:粒度位(0=字节,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 执行内存访问指令时,分段单元自动完成以下转换步骤:

  1. 提取段选择子:从指令指定的段寄存器(如 DS)中取出 16 位段选择子。
  2. 选择描述符表:根据 TI 位选择 GDT(TI=0)或 LDT(TI=1)。
  3. 索引描述符:将 Index 乘以 8,加上 GDT/LDT 的基地址(存储在 GDTR/LDTR 寄存器中),找到对应的段描述符。
  4. 权限检查:比较当前 CPL(当前特权级)、RPL(请求特权级)和 DPL(描述符特权级),确保访问合法。
  5. 提取基地址:从段描述符中取出 32 位 Base 字段。
  6. 计算线性地址:线性地址 = 段基地址 + 偏移量(指令中给出的逻辑地址偏移)。
  7. 界限检查:验证偏移量是否在段界限内,若超出则触发 #GP(通用保护异常)。
逻辑地址到线性地址转换流程 逻辑地址 → 线性地址 转换流程 逻辑地址 段选择子 : 偏移量 段选择子 Index | TI | RPL 选择描述符表 TI=0 → GDT | TI=1 → LDT 段描述符 Base[31:0] | Limit | DPL | Type 线性地址 Base + 偏移量 💡 转换公式:线性地址 = 段描述符.Base + 逻辑地址.偏移量

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 任务私有,两者共同构成分段描述符表体系。