目录
一、核心机制:x86 的两级地址转换
在 x86 32 位保护模式下,CPU 访问内存时,地址需经历两个阶段的转换:
逻辑地址 →(GDT 分段)→ 线性地址 →(页表分页)→ 物理地址
这是理解现代操作系统内存管理的基石。我们逐层拆解。
第一阶段:逻辑地址 → 线性地址(通过 GDT 分段)
1. 什么是逻辑地址?
- 由两部分组成:
- 段选择子(Segment Selector):存于段寄存器(如 DS=0x10)
- 偏移(Offset):程序直接使用的地址(如
[0x12345678])
- 例如:
mov eax, [0x12345678],若 DS=0x10,则逻辑地址 =(0x10, 0x12345678)
2. GDT 如何参与转换?
- CPU 读取 GDTR 寄存器,获得 GDT 基地址。
- 从段选择子中提取 Index(索引),计算描述符位置:
GDT_base + Index × 8 - 读取 8 字节的 段描述符,获取:
- 段基地址(Base)
- 段界限(Limit)
- 访问权限(DPL、R/W/X、存在位等)
3. 生成线性地址
- 若权限检查通过且偏移 ≤ Limit,则:
线性地址 = 段基地址 + 偏移
✅ 此时,程序的“段内偏移”被映射到全局统一的线性地址空间。
现代操作系统采用 平坦内存模型,具体做法是:
1. 为所有常用段设置相同的描述符:
- 基地址(Base) = 0
- 段界限(Limit) = 0xFFFFFFFF(或 48 位虚拟地址空间上限)
- 粒度(Granularity) = 4KB 或字节级
- 权限和类型按需设置(如代码段可执行、数据段可读写)
例如,在 Linux 内核初始化时,会设置如下段描述符(简化):
| 段寄存器 | 段选择子 | 段基地址 | 限长 | 用途 |
|---|---|---|---|---|
| CS | 0x10 | 0 | 0xFFFFF | 内核代码段 |
| DS/ES/SS | 0x18 | 0 | 0xFFFFF | 内核数据段 |
| CS (user) | 0x23 | 0 | 0xFFFFF | 用户代码段 |
| DS (user) | 0x2B | 0 | 0xFFFFF | 用户数据段 |
在 x86-64 的 64 位模式(long mode) 下,除 FS 和 GS 外,所有段的基地址被强制视为 0,即使 GDT 中设置了非零基地址也无效。这是硬件行为。
结果:逻辑地址 ≡ 线性地址
由于段基地址为 0,地址转换公式变为:
线性地址 = 0 + 偏移 = 偏移
因此,程序中使用的指针(如 0x7fff12345678)直接就是线性地址(即虚拟地址),无需程序员或编译器关心段选择子。
4.为什么这样做?
- 简化内存模型:程序员和编译器无需处理复杂的段地址,统一使用平坦的虚拟地址空间。
- 兼容性:x86 架构要求保护模式下必须使用段机制,但可通过配置使其“透明”。
- 性能:避免段机制带来的额外地址计算开销(虽然现代 CPU 对此优化得很好)。
- 统一虚拟内存管理:将所有地址空间管理交给分页机制,便于实现 demand paging、共享内存、内存保护等高级功能。
第二阶段:线性地址 → 物理地址(通过页表分页)
前提:CR0.PG = 1(分页已启用)
1. 线性地址的结构(32 位)
将 32 位线性地址划分为:
- 页目录索引(10 位)
- 页表索引(10 位)
- 页内偏移(12 位)
2. 页表查找过程
- CR3 寄存器 指向当前进程的页目录(物理地址)。
- 两级查找:
- 页目录项 → 页表物理地址
- 页表项 → 物理页帧地址(4KB 块)
- 同时检查页的 存在位(P)、读写位(R/W)、用户/特权位(U/S)
3. 生成物理地址
- 物理地址 = 页帧地址 + 页内偏移
✅ 此阶段实现了虚拟内存:线性地址连续,但物理页可分散在内存任意位置。
二、关键对比:启用分页 vs. 仅用分段
| 场景 | 地址转换路径 | 线性地址是否等于物理地址? | 内存管理能力 |
|---|---|---|---|
| 启用分页(现代 OS) | 逻辑 → 线性 → 物理 | ❌ 不等 | 支持虚拟内存、进程隔离、按需调页、内存保护 |
| 仅用分段(CR0.PG=0) | 逻辑 → 线性(=物理) | ✅ 相等 | 无虚拟内存,物理地址直接暴露 |
三、核心问题:为什么“仅用分段”会导致严重内存分配问题?
1. 程序视角:地址必须“逻辑连续”
- 程序员假设地址空间是平坦连续的:
int a; // 假设在 0x1000 char buf[4096]; // 紧跟在 0x2000 - 这些偏移在段内是线性递增的,称为 “逻辑连续”。
2. 仅分段时:逻辑连续 → 物理连续
- 若 GDT 中段基地址 =
0x100000,则:0x1000→ 物理0x1010000x2000→ 物理0x102000
- 程序占用的物理内存必须是一整块连续区域。
3. 但现实:物理内存高度碎片化
- 多任务系统中,内存布局如:
[内核][程序A][空闲12MB][程序B][空闲10MB][程序C] - 即使总空闲 > 程序所需(如 22MB > 20MB),但没有一块连续的 20MB → 无法加载程序!
这就是 外部碎片(External Fragmentation) —— 内存总量够,但无法分配。
4. 动态扩展几乎不可能
- 程序运行中调用
malloc(5MB),需要扩展堆。 - 分段要求:新增内存必须紧邻当前段末尾。
- 如果后面已被占用 → 扩展失败,即使系统还有大量空闲内存。
❌ “逻辑连续”被强制映射到“物理连续”,而物理内存既不连续,也无法动态扩展。”
四、GDT 与分页的真实关系:分工与演进
1. GDT 的作用(即使在分页系统中)
- 定义代码/数据段的 特权级(DPL) → 控制 CPL(当前特权级)
- 支持 TSS(任务状态段) → 中断时切换内核栈
- 在 32 位中,为 FS/GS 段设置非零基址 → 实现线程局部存储(TLS)
2. 现代操作系统的标准做法
- GDT 中普通段的 Base = 0,Limit = 4GB
→线性地址 = 偏移,分段“退化”为恒等映射。 - 启用分页(CR0.PG = 1),由页表承担全部内存管理:
- 虚拟内存(地址空间 > 物理内存)
- 进程隔离(独立页目录)
- 内存保护(每页独立权限)
- 共享内存(多进程指向同一物理页)
3. 64 位模式下的演进
- 分段地址转换被废弃:CS/DS/ES/SS 的 Base 强制为 0。
- GDT 仅用于:
- 设置 64 位代码段(L=1)
- 定义 TSS
- 兼容性保留
- 分页成为唯一地址转换机制(4 级页表)
五、总结:
| 常见问题 | 核心答案 |
|---|---|
| 1. 内存分页与 GDT 的关系? | GDT 属于分段机制,负责段属性与特权控制;分页负责虚拟地址到物理地址的映射。现代 OS 同时启用两者,但将分段“退化”为恒等映射(Base=0),由分页实现真正的虚拟内存管理。 |
| 2. 仅用分段是否导致内存分配问题? | 是的。因为线性地址 = 物理地址,程序的逻辑连续性强制要求物理内存连续,而实际物理内存因碎片化往往无法满足,导致分配失败、无法多任务、无法动态扩展。 |
| 3. “逻辑连续映射到物理连续,但物理不连续”是什么意思? | 程序假设地址连续(如数组、代码顺序存放),分段机制将其映射到物理内存的连续区域。但现实中物理内存被分割成碎片,没有足够大的连续块,也无法在非连续位置扩展段,造成严重资源浪费和功能限制。 |
地址转换流程
逻辑 → 线性 → 物理揭示了 x86 内存管理的两层抽象:
- 分段(GDT) 提供段属性与特权控制;
- 分页(页表) 提供虚拟内存与物理映射。
而仅使用分段的问题在于:它强制将程序的逻辑连续性映射到物理连续内存,但现实中的物理内存因多任务和碎片化,既不连续,也无法动态扩展。
分页机制通过“虚拟连续、物理分散”的设计,彻底解决了这一根本矛盾,成为现代操作系统内存管理的基石。
这正是为什么从 Linux、Windows 到 macOS,所有现代操作系统都必须启用分页,并将分段机制“架空”的根本原因。
、分段、分页与内存分配问题&spm=1001.2101.3001.5002&articleId=153514670&d=1&t=3&u=08417f590a3e4376aa23bbf91048f247)
1万+

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



