内存管理 — 把栈、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。真正要紧的:
| 字段 | 作用 |
|---|---|
| type | 10=可读可执行(代码段)、2=可读可写(数据段) |
| DPL | 权限级别(0–3);代码段的 DPL 会成为 CPU 当前的 CPL |
| present | 0 则一用就 CPU 异常 |
| long_mode | 1 = 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 上岗。
- 内存管理 = 记录谁在用哪块地;开局情报就是 UEFI 内存地图;
- UEFI 留下栈/GDT/页表三样活物在「看似空闲」的区域,不搬走会被分配覆盖;
- 栈的实体是 RSP;迁栈就是改 RSP,栈向地址 0 生长,初值设在末尾;
- 64 位模式下分段只剩权限用途(DPL/CPL,0=内核 3=用户),base/limit 被忽略;
- CS 不能用 mov 改,retf 是换 CS 的正规门口;段寄存器隐藏区让换 GDT 不断链;
- 分页 = 线性地址→物理地址的逐页翻译,64 位强制开启,CR3 指向当前页表顶层;
- 教学系统用 2MiB 大页做身份映射(1GiB 页兼容性差),一套表管 64 GiB;
- 位图内存管理器:一页帧一比特,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/ret | 8.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 改 CS | 8.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」) |
-
出处:「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(搜「栈区域」))。 ↩ -
出处:「8.3 移动数据结构(osbook_day08b)」(
text/61-ch08-03-8-3-osbook-day08b.txt:16(搜「做一些准备」);text/61-ch08-03-8-3-osbook-day08b.txt:22(搜「栈区域」))。 ↩ -
出处:「8.4 移动栈区域」(
text/62-ch08-04-8-4.txt:9(搜「RSP是CPU的寄存器之一」);text/62-ch08-04-8-4.txt:17(搜「重写RSP的值」))。 ↩ -
出处:「8.4 移动栈区域」(
text/62-ch08-04-8-4.txt:9(搜「RSP是CPU的寄存器之一」);text/62-ch08-04-8-4.txt:17(搜「重写RSP的值」))。 ↩ -
出处:「8.4 移动栈区域」(
text/62-ch08-04-8-4.txt:18(搜「向地址0的方向增长」))。 ↩ -
出处:「8.4 移动栈区域」(
text/63-ch08-05-8-5.txt:4(搜「新的入口点」))。 ↩ -
出处:「8.5 设置分段」(
text/63-ch08-05-8-5.txt:17(搜「特权模式或内核模式」))。 ↩ -
出处:「8.5 设置分段」(
text/63-ch08-05-8-5.txt:69(搜「三个描述符」);text/63-ch08-05-8-5.txt:73(搜「空描述符」))。 ↩ -
出处:「8.5 设置分段」(
text/63-ch08-05-8-5.txt:94(搜「会被忽略」))。 ↩ -
出处:「8.5 设置分段」(
text/63-ch08-05-8-5.txt:184(搜「隐藏区域」))。 ↩ -
出处:「8.5 设置分段」(
text/63-ch08-05-8-5.txt:136(搜「lgdt指令」))。 ↩ -
出处:「8.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表」))。 ↩ -
出处:「8.6 设置分页」(
text/64-ch08-06-8-6.txt:74(搜「kPageDirectoryCount」);text/64-ch08-06-8-6.txt:79(搜「第7位」))。 ↩ -
出处:「8.6 设置分页」(
text/65-ch08-07-8-7-osbook-day08c.txt:14(搜「SetCR3」))。 ↩ -
出处:「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(搜「每个页帧使用一个位」))。 ↩ -
出处:「8.7 内存管理挑战(osbook_day08c)」(
text/65-ch08-07-8-7-osbook-day08c.txt:87(搜「128_GiB」))。 ↩ -
出处:「8.7 内存管理挑战(osbook_day08c)」(
text/65-ch08-07-8-7-osbook-day08c.txt:39(搜「可能会产生混淆」);text/65-ch08-07-8-7-osbook-day08c.txt:51(搜「用户定义字面量」))。 ↩ -
出处:「8.7 内存管理挑战(osbook_day08c)」(
text/65-ch08-07-8-7-osbook-day08c.txt:60(搜「MarkAllocated」))。 ↩ -
出处:「8.1 内存管理概述」(
text/60-ch08-01-8-1.txt:4(搜「经常需要或不需要内存」))。 ↩ -
出处:「8.5 设置分段」(
text/63-ch08-05-8-5.txt:94(搜「会被忽略」))。 ↩ -
出处:「8.6 设置分页」(
text/64-ch08-06-8-6.txt:74(搜「kPageDirectoryCount」);text/64-ch08-06-8-6.txt:79(搜「第7位」))。 ↩ -
出处:「8.5 设置分段」(
text/63-ch08-05-8-5.txt:94(搜「会被忽略」))。 ↩