上一章切到了保护模式,32 位,最多 4GB 内存。

听起来挺多,但现代系统动不动就几十 GB 内存,4GB 根本不够用。所以还得再往前走一步:进入长模式(Long Mode,x86_64 的 64 位工作模式,支持 128TB 虚拟地址空间)。

现在跑在你电脑上的 Linux、macOS、Windows,全都在长模式里。


长模式比保护模式多了什么

最直接的:寻址空间从 4GB 扩展到理论上的 16EB(16 × 10¹⁸ 字节),实际当前 CPU 支持 128TB,够用很久了。

但更重要的变化是强制引入了分页(Paging,把物理内存切成固定大小的页,通过页表做虚拟地址到物理地址的映射)。

保护模式下分页是可选的,长模式下分页是必须开启的。没有页表,CPU 根本不让你进长模式。


先建页表

切换之前,必须在内存里建好页表(Page Table,记录虚拟地址到物理地址映射关系的数据结构)。

x86_64 用四级页表,一个虚拟地址的翻译路径是:

虚拟地址 → PML4 → PDPT → PD → PT → 物理地址

每级都是一张表,每张表 512 项,每项 8 字节。

启动阶段不需要映射所有内存,先映射前 2MB 就够了——把四张表放在物理地址 0x1000 开始的位置,让虚拟地址和物理地址一一对应(恒等映射,虚拟地址和物理地址相同,是启动阶段最简单的映射策略)。


切换步骤

在 32 位保护模式下,按顺序操作:

  1. 建好页表,把 PML4 地址写入 CR3(页表根寄存器)
  2. 开启 PAE(Physical Address Extension,物理地址扩展,让 32 位系统支持超过 4GB 的物理地址,长模式必须开启):CR4.PAE=1
  3. 设置 EFER.LME=1(通过 MSR 寄存器(Model Specific Register,CPU 内部的特殊寄存器,用 rdmsr/wrmsr 读写)开启长模式使能位)
  4. 开启分页CR0.PG=1,CPU 正式进入长模式兼容态
  5. far jump 跳入 64 位代码段,彻底进入 64 位模式

顺序不能乱。尤其是第 3 步必须在开启分页之前,不然 CPU 会拒绝。


GDT 要更新

保护模式的 GDT 是 32 位的,进长模式要新增一个 64 位代码段描述符,关键是把 L 位(第 21 位)设为 1,告诉 CPU 这是 64 位段。

far jump 的时候用这个新描述符的选择子(Selector,GDT 表的索引,用来告诉 CPU 去哪个描述符取段信息),CPU 才会切到真正的 64 位执行环境。


一个值得加的检测

进长模式之前,可以先用 CPUID 指令(CPU 自我介绍指令,可以查询 CPU 支持的特性)确认一下当前 CPU 是否支持长模式:

mov eax, 0x80000001
cpuid
test edx, (1 << 29)   ; LM 位
jz no_long_mode        ; 不支持就优雅退出

这不是必须的,但养成检测硬件能力的习惯,写内核的时候很有用。


跑起来

nasm -f bin boot.asm -o boot.bin
qemu-system-x86_64 -drive format=raw,file=boot.bin -nographic -serial mon:stdio

输出:

Long mode (64-bit) ON!

CPU 现在跑在 64 位长模式下了。


这时候手里有什么

到这里,手里已经有了:

  • ✅ 64 位执行环境
  • ✅ 前 2MB 内存可用(恒等映射)
  • ✅ 串口可以打印调试信息

但代码还全是汇编,做任何稍微复杂的事都很痛苦。下一章要做的,就是把一个 C 写的内核加载进来,让后续开发回到人类能看懂的语言。

源码在这里:github.com/tongpengfei/learn-with-ai