编程语言研究书库

内存管理 — 把栈、GDT、页表从 UEFI 手里接管过来

内存管理 — 把栈、GDT、页表从 UEFI 手里接管过来

这一章讲什么: 上一章有了内存地图,本章做两件真正「当家作主」的事: ① 搬家——UEFI 退场后,栈、GDT、页表还住在 UEFI 名下的内存里, 内存分配一开工就会覆盖它们,所以先把这三样迁到 OS 自己的区域; ② 记账——用一张位图实现 BitmapMemoryManager,内核从此有了 Allocate()/Free()。 搬家的过程中会顺手把分段、分页这两件 x86 的祖传机制摆正位置。

1. 先看现象:一次有惊无险的乔迁

主线例子,本章结束时 KernelMain 的开头长这样:

  KernelMain()(汇编写的新入口)
    │  mov rsp, kernel_main_stack 的末尾   ← 栈搬家:换个 RSP 就搬完
    │  call KernelMainNewStack
    ▼
  KernelMainNewStack()
    SetupSegments()     ← 在 OS 区域重建 GDT:空描述符 + 代码段 + 数据段,lgdt 注册
    SetDSAll() / SetCSSS()  ← 段寄存器 DS/SS/CS 指向新 GDT(retf 才能改 CS)
    SetupIdentityPageTable() ← 自建四级页表,全用 2MiB 大页做身份映射
    SetCR3(pml4)        ← CR3 一写,CPU 立刻改用我们的页表
    memory_manager->MarkAllocated(...)  ← 按内存地图把「已占用」的页帧在位图里标 1
    SetMemoryRange(...)  ← 可用范围划好,BitmapMemoryManager 上岗

  图说:从此内核的每个字节都站在自己的地基上。

2. 顶层全景

  8.1–8.2  为什么管内存 + 内存地图作为 KernelMain 的新参数传进内核
  8.3–8.4  搬家第一单:栈(RSP 语义、call/ret 如何用它、栈向地址 0 生长)
  8.5      搬家第二单:GDT(段描述符字段逐个讲;64 位模式分段只剩权限用途)
  8.6      搬家第三单:页表(四级分页、身份映射、2MiB 大页、CR3)
  8.7      记账:BitmapMemoryManager(位图、页帧、Allocate/Free/MarkAllocated)

  图说:三样数据结构为什么要搬——IsAvailable() 视 UEFI 区域为空地,
  不搬走 = 分配内存时把它们当空地覆盖,CPU 故障。

3. 核心原理

3.1 为什么要管内存

应用启动、文件打开都需要临时内存,数量事先不知道,所以必须有个函数分配和释放内存, 并且记住哪些区域在用、哪些空闲——不管就等着两个应用踩同一块地、数据损坏1。 开局的情报就是第 2 章那张内存地图,本章把它作为参数正式传给 KernelMain()2。

3.2 搬家之前:三样活物住在危险区

IsAvailable() 认为三种类型(EFI 的 LoaderCode/Data、BootServicesCode/Data 等之外的特定类型) 算空闲——问题恰恰在这:UEFI 启动时创建的 64 位运行环境,栈、GDT、页表,就放在 kEfiBootServicesData 区域里,这些区域被判定为「未使用」3。未来的分配请求会覆盖它们, 「导致 CPU 故障」4。所以要搬。

3.3 栈:RSP 一改,搬完收工

栈是 CPU 运行程序时用的内存区域:call 指令把返回地址压栈(RSP 减 8、写入 RIP、跳转), ret 逆操作(读出、RSP 加 8、跳回);C++ 局部变量也放栈上。栈的实体就是 RSP 寄存器5。 所以迁栈 = 重写 RSP;注意栈向地址 0 生长,所以 RSP 初值要设在区域末尾6。 新入口用汇编写:设好 RSP 再调用 C++ 的 KernelMainNewStack()—— 新栈上没有返回地址,万一返回就死循环兜底7。

3.4 分段:64 位模式里,它只剩「权限开关」

x86-64 里 CPU 有 0–3 共四级操作权限:0 = 特权/内核模式,3 = 用户模式; 分段在 64 位模式下不再划分内存,实际用途就是设权限——哪些指令(特权指令)只能在 0 级执行, 配合分页还能圈出「只有 0 级能访问」的内存8。设置分段的表叫 GDT(全局描述符表): 8 字节描述符的数组。本书建三个:第 0 个全 0(空描述符,规矩如此)、gdt[1] 代码段、 gdt[2] 数据段9。

段描述符的字段里,64 位模式下 base(起始地址)和 limit(大小)都被忽略—— 「我们还是以某种方式为它们设置了值」,纯属礼仪10。真正要紧的:

字段作用
type10=可读可执行(代码段)、2=可读可写(数据段)
DPL权限级别(0–3);代码段的 DPL 会成为 CPU 当前的 CPL
present0 则一用就 CPU 异常
long_mode1 = 64 位代码段

注册 GDT 用 lgdt 指令(汇编写),吃一个 10 字节内存区(2 字节 limit + 8 字节地址), 存进 CPU 的 80 位 GDTR 寄存器11。 然后刷新段寄存器:DS/SS 可以 mov 改,CS 不行,得用 retf(far return)—— 把「新 CS + 返回地址」压栈,retf 一并弹出,CS 就此换成 1<<3(指向 gdt[1])12。 这里有个暗桩被作者点破:gdtr 换过之后、CS 更新之前,旧 CS 可能指向新表之外的描述符—— 能不出事,全靠段寄存器有个程序员摸不到的隐藏区:mov 段寄存器时 CPU 会把描述符抄进隐藏区, 之后一直用抄本,直到下次更新13。

3.5 分页:四级表 + 身份映射 + CR3

分页 = 以页为单位做地址翻译:CPU 读写内存前查页表,把程序用的线性地址翻成 物理地址(物理地址不只指内存,连到地址总线上的设备都靠它访问)14。 64 位模式强制分页,不能关15,所以页表也必须搬到 OS 区域。

x86-64 是四级结构:PML4 表 → PDP 表 → 页面目录 → 页表,每层 512 项; 命名很混乱(「页面目录」原指两层时的顶层),Intel 手册管整体叫「分层分页结构」16。 本章只做身份映射(linear = physical,一一对应),用尽可能大的页: 1GiB 页有的 CPU 不支持,所以用 2MiB 页——页目录里每个元素的第 7 位置 1 就是大页标志17。 kPageDirectoryCount = 64,一张页面目录管 1 GiB,所以这套表最多身份映射 64 GiB18。 最后 SetCR3(新 PML4 的物理地址):CR3 一写,CPU 立即改用新表翻译地址19。

两个伏笔在此埋下:页表「逐页设翻译」的能力第 19 章用来给应用造独立地址空间, 「页帧(物理) vs 页面(线性)」的区别到那时才有意义——现在两者相等20。

3.6 位图内存管理器

搬完家,终于能记账。BitmapMemoryManager 以 4 KiB 页帧为单位, 管理的是物理地址的使用情况(「页帧」指物理侧,「页面」指线性侧)21。 核心数据结构是位图:一个页帧一个比特,1=占用、0=空闲,帧号和比特位置一一对应22。 几个设计决定值得记住:

  • 上限 128 GiB(kMaxPhysicalMemoryBytes),位图数组就是 128GiB/4KiB 个比特—— 定长数组,不要动态内存(内存管理器自己不能依赖内存管理);
  • FrameID 包成类:纯整数容易和「页大小」之类混淆,封装一下类型自带语义23;
  • 初始化顺序:先假设全空,遍历内存地图把 IsAvailable()=false 的区段 MarkAllocated() 标 1,再 SetMemoryRange() 圈出可用范围,Allocate()/Free() 才能上岗24。

顺手学了个 C++11 小糖:4_KiB 这种用户定义字面量,本质是 operator""_KiB(4) 返回 409625。

4. 作者的判断与证据

判断:64 位模式下,分段是「规定了但没人真用」的机制。 书里原话:「在广泛使用的现代操作系统中, 并没有使用分段功能。原因不明」——然后照规矩把 base/limit 填了值26。这种「机制还在、用途已死」 的诚实记录,是理解 x86 历史包袱的最佳教材。

判断:大页优先。 身份映射不需要细粒度,「通过采用尽可能大的页面来减小需要创建的数据结构」; 2MiB 而非 1GiB 是出于兼容性27。这个「用对齐和大粒度换简单」的取舍,在第 19、27 章会反复出现。

证据: 本章没有截图式证据,真正的验收在下一章——内存管理器上岗后,第 9 章立刻用它 new 出图层系统, 崩了就是搬家没搬干净。

5. 边界与局限

  • 位图是教学级实现:定长 128 GiB 上限、线性扫描分配,没有空闲链表、没有按 CPU 分桶; 真内核用伙伴系统等更精巧的结构。
  • 身份映射意味着线性地址 = 物理地址,应用的地址隔离要等第 19 章的分层分页结构重写。
  • CS 的 retf 切换是 x86 特有仪式;别的架构(ARM)切换权限的门口完全不同。
  • 搬家的隐患被点破而未深挖:段寄存器隐藏区救了场,但这类「规范没明说、硬件兜底」 的知识,只能靠读手册和他人的踩坑记录积累。

6. 可带走的

本章主线例子,一行写完: 新汇编入口换 RSP → 重建 GDT 并 lgdt → DS/SS/CS 换新(retf)→ 自建四级页表(2MiB 身份映射)→ SetCR3 → 位图按内存地图标占用 → BitmapMemoryManager 上岗。

  1. 内存管理 = 记录谁在用哪块地;开局情报就是 UEFI 内存地图;
  2. UEFI 留下栈/GDT/页表三样活物在「看似空闲」的区域,不搬走会被分配覆盖;
  3. 栈的实体是 RSP;迁栈就是改 RSP,栈向地址 0 生长,初值设在末尾;
  4. 64 位模式下分段只剩权限用途(DPL/CPL,0=内核 3=用户),base/limit 被忽略;
  5. CS 不能用 mov 改,retf 是换 CS 的正规门口;段寄存器隐藏区让换 GDT 不断链;
  6. 分页 = 线性地址→物理地址的逐页翻译,64 位强制开启,CR3 指向当前页表顶层;
  7. 教学系统用 2MiB 大页做身份映射(1GiB 页兼容性差),一套表管 64 GiB;
  8. 位图内存管理器:一页帧一比特,1 占用 0 空闲;定长、无动态内存,自己不能依赖自己。

7. 原文地图

主题原书章原文位置
内存管理的必要性8.1 内存管理概述text/60-ch08-01-8-1.txt:4(搜「经常需要或不需要内存」)
内存地图传参进内核8.1 内存管理概述text/60-ch08-01-8-1.txt:20(搜「传递给KernelMain」)
三个数据结构要搬8.3 移动数据结构(osbook_day08b)text/61-ch08-03-8-3-osbook-day08b.txt:16(搜「做一些准备」) · text/61-ch08-03-8-3-osbook-day08b.txt:22(搜「栈区域」)
栈与 RSP、call/ret8.4 移动栈区域text/62-ch08-04-8-4.txt:9(搜「RSP是CPU的寄存器之一」) · text/62-ch08-04-8-4.txt:17(搜「重写RSP的值」)
栈向地址 0 生长8.4 移动栈区域text/62-ch08-04-8-4.txt:18(搜「向地址0的方向增长」)
新汇编入口8.4 移动栈区域text/63-ch08-05-8-5.txt:4(搜「新的入口点」)
权限级别 0/3 与分段用途8.5 设置分段text/63-ch08-05-8-5.txt:17(搜「特权模式或内核模式」)
GDT 三描述符与空描述符8.5 设置分段text/63-ch08-05-8-5.txt:69(搜「三个描述符」) · text/63-ch08-05-8-5.txt:73(搜「空描述符」)
base/limit 被忽略8.5 设置分段text/63-ch08-05-8-5.txt:94(搜「会被忽略」)
段寄存器隐藏区8.5 设置分段text/63-ch08-05-8-5.txt:184(搜「隐藏区域」)
lgdt 与 10 字节内存区8.5 设置分段text/63-ch08-05-8-5.txt:136(搜「lgdt指令」)
retf 改 CS8.5 设置分段text/64-ch08-06-8-6.txt:4(搜「retf指令」) · text/64-ch08-06-8-6.txt:17(搜「1<<3」)
分页 = 地址转换、强制开启8.6 设置分页text/64-ch08-06-8-6.txt:21(搜「以页为单位管理」) · text/64-ch08-06-8-6.txt:23(搜「无法禁用」)
身份映射与四级结构8.6 设置分页text/64-ch08-06-8-6.txt:38(搜「身份映射」) · text/64-ch08-06-8-6.txt:45(搜「PML4表」)
2MiB 大页与 64 GiB 上限8.6 设置分页text/64-ch08-06-8-6.txt:74(搜「kPageDirectoryCount」) · text/64-ch08-06-8-6.txt:79(搜「第7位」)
SetCR3 切换页表8.6 设置分页text/65-ch08-07-8-7-osbook-day08c.txt:14(搜「SetCR3」)
页帧 vs 页面8.7 内存管理挑战(osbook_day08c)text/65-ch08-07-8-7-osbook-day08c.txt:27(搜「页帧代表物理地址」)
位图设计8.7 内存管理挑战(osbook_day08c)text/65-ch08-07-8-7-osbook-day08c.txt:31(搜「每个页帧使用一个位」)
128 GiB 上限与定长数组8.7 内存管理挑战(osbook_day08c)text/65-ch08-07-8-7-osbook-day08c.txt:87(搜「128_GiB」)
FrameID 封装与用户定义字面量8.7 内存管理挑战(osbook_day08c)text/65-ch08-07-8-7-osbook-day08c.txt:39(搜「可能会产生混淆」) · text/65-ch08-07-8-7-osbook-day08c.txt:51(搜「用户定义字面量」)
MarkAllocated 初始化8.7 内存管理挑战(osbook_day08c)text/65-ch08-07-8-7-osbook-day08c.txt:60(搜「MarkAllocated」)

Footnotes

  1. 出处:「8.1 内存管理概述」(text/60-ch08-01-8-1.txt:4(搜「经常需要或不需要内存」))。 ↩

  2. 出处:「8.1 内存管理概述」(text/60-ch08-01-8-1.txt:20(搜「传递给KernelMain」))。 ↩

  3. 出处:「8.3 移动数据结构(osbook_day08b)」(text/61-ch08-03-8-3-osbook-day08b.txt:16(搜「做一些准备」); text/61-ch08-03-8-3-osbook-day08b.txt:22(搜「栈区域」))。 ↩

  4. 出处:「8.3 移动数据结构(osbook_day08b)」(text/61-ch08-03-8-3-osbook-day08b.txt:16(搜「做一些准备」); text/61-ch08-03-8-3-osbook-day08b.txt:22(搜「栈区域」))。 ↩

  5. 出处:「8.4 移动栈区域」(text/62-ch08-04-8-4.txt:9(搜「RSP是CPU的寄存器之一」); text/62-ch08-04-8-4.txt:17(搜「重写RSP的值」))。 ↩

  6. 出处:「8.4 移动栈区域」(text/62-ch08-04-8-4.txt:9(搜「RSP是CPU的寄存器之一」); text/62-ch08-04-8-4.txt:17(搜「重写RSP的值」))。 ↩

  7. 出处:「8.4 移动栈区域」(text/62-ch08-04-8-4.txt:18(搜「向地址0的方向增长」))。 ↩

  8. 出处:「8.4 移动栈区域」(text/63-ch08-05-8-5.txt:4(搜「新的入口点」))。 ↩

  9. 出处:「8.5 设置分段」(text/63-ch08-05-8-5.txt:17(搜「特权模式或内核模式」))。 ↩

  10. 出处:「8.5 设置分段」(text/63-ch08-05-8-5.txt:69(搜「三个描述符」); text/63-ch08-05-8-5.txt:73(搜「空描述符」))。 ↩

  11. 出处:「8.5 设置分段」(text/63-ch08-05-8-5.txt:94(搜「会被忽略」))。 ↩

  12. 出处:「8.5 设置分段」(text/63-ch08-05-8-5.txt:184(搜「隐藏区域」))。 ↩

  13. 出处:「8.5 设置分段」(text/63-ch08-05-8-5.txt:136(搜「lgdt指令」))。 ↩

  14. 出处:「8.5 设置分段」(text/64-ch08-06-8-6.txt:4(搜「retf指令」); text/64-ch08-06-8-6.txt:17(搜「1<<3」))。 ↩

  15. 出处:「8.6 设置分页」(text/64-ch08-06-8-6.txt:21(搜「以页为单位管理」); text/64-ch08-06-8-6.txt:23(搜「无法禁用」))。 ↩

  16. 出处:「8.6 设置分页」(text/64-ch08-06-8-6.txt:38(搜「身份映射」); text/64-ch08-06-8-6.txt:45(搜「PML4表」))。 ↩

  17. 出处:「8.6 设置分页」(text/64-ch08-06-8-6.txt:74(搜「kPageDirectoryCount」); text/64-ch08-06-8-6.txt:79(搜「第7位」))。 ↩

  18. 出处:「8.6 设置分页」(text/65-ch08-07-8-7-osbook-day08c.txt:14(搜「SetCR3」))。 ↩

  19. 出处:「8.7 内存管理挑战(osbook_day08c)」(text/65-ch08-07-8-7-osbook-day08c.txt:27(搜「页帧代表物理地址」))。 ↩

  20. 出处:「8.7 内存管理挑战(osbook_day08c)」(text/65-ch08-07-8-7-osbook-day08c.txt:31(搜「每个页帧使用一个位」))。 ↩

  21. 出处:「8.7 内存管理挑战(osbook_day08c)」(text/65-ch08-07-8-7-osbook-day08c.txt:87(搜「128_GiB」))。 ↩

  22. 出处:「8.7 内存管理挑战(osbook_day08c)」(text/65-ch08-07-8-7-osbook-day08c.txt:39(搜「可能会产生混淆」); text/65-ch08-07-8-7-osbook-day08c.txt:51(搜「用户定义字面量」))。 ↩

  23. 出处:「8.7 内存管理挑战(osbook_day08c)」(text/65-ch08-07-8-7-osbook-day08c.txt:60(搜「MarkAllocated」))。 ↩

  24. 出处:「8.1 内存管理概述」(text/60-ch08-01-8-1.txt:4(搜「经常需要或不需要内存」))。 ↩

  25. 出处:「8.5 设置分段」(text/63-ch08-05-8-5.txt:94(搜「会被忽略」))。 ↩

  26. 出处:「8.6 设置分页」(text/64-ch08-06-8-6.txt:74(搜「kPageDirectoryCount」); text/64-ch08-06-8-6.txt:79(搜「第7位」))。 ↩

  27. 出处:「8.5 设置分段」(text/63-ch08-05-8-5.txt:94(搜「会被忽略」))。 ↩

On this page