上一章切到了保护模式,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 位保护模式下,按顺序操作:
- 建好页表,把 PML4 地址写入
CR3(页表根寄存器) - 开启 PAE(Physical Address Extension,物理地址扩展,让 32 位系统支持超过 4GB 的物理地址,长模式必须开启):
CR4.PAE=1 - 设置 EFER.LME=1(通过 MSR 寄存器(Model Specific Register,CPU 内部的特殊寄存器,用 rdmsr/wrmsr 读写)开启长模式使能位)
- 开启分页:
CR0.PG=1,CPU 正式进入长模式兼容态 - 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 写的内核加载进来,让后续开发回到人类能看懂的语言。