上一章我们让机器打出了第一行字。但那时候 CPU 还处于一种"出厂状态"——实模式,最多只能用 1MB 内存,没有任何权限隔离,任何代码想写哪块内存就写哪块。
这对一个操作系统来说是不可接受的。
所以这一章要做一件事:把 CPU 从实模式切换到保护模式(Protected Mode,32 位工作模式,支持内存保护和权限隔离)。
为什么叫"保护"模式
名字来源很直接——这个模式下,内存是被保护的。
实模式里,程序可以随意读写任何内存地址,一个 bug 就能把整个系统搞崩。保护模式引入了两个机制:
- 内存段的权限描述:每块内存都有自己的访问权限,越界访问直接触发异常
- 特权级别(Ring):内核跑在 Ring 0,用户程序跑在 Ring 3,用户程序没法直接碰内核内存
现代操作系统全都建立在保护模式之上。
切换的关键:GDT
切到保护模式之前,必须先准备好 GDT(Global Descriptor Table,全局描述符表,内存里的一张表,描述每个内存段的地址、大小和权限)。
可以把 GDT 理解成一个"内存段登记簿"。CPU 进入保护模式后,所有内存访问都要查这张表,看看你有没有权限。
GDT 最少需要三项:
| 索引 | 用途 |
|---|---|
| 0 | 空描述符(CPU 规定必须全零,不能用) |
| 1 | 代码段(可执行,Ring 0) |
| 2 | 数据段(可读写,Ring 0) |
GDT 写好之后,用 lgdt 指令告诉 CPU 表在哪里,CPU 才知道去哪查。
切换步骤
整个切换过程就四步,缺一不可:
- 关中断(
cli)—— 切换过程不能被打断,否则 CPU 状态会乱 - 加载 GDT(
lgdt)—— 告诉 CPU 描述符表在哪 - 设置 CR0 寄存器(
CR0.PE=1)—— CR0(控制寄存器0,控制CPU工作模式的关键寄存器)第0位置1,CPU 正式进入保护模式 - far jump 刷新流水线 —— 跳转的同时加载新的代码段选择子,清空 CPU 的指令预取缓存
最后这一步很容易漏掉。CPU 有指令流水线,切换模式后如果不强制刷新,可能还在用旧模式的指令译码方式,导致莫名其妙的错误。
切换之后:BIOS 失效了
进入保护模式,有一个代价:之前能用的 BIOS 中断全部失效。
上一章用 int 0x10 打印字符,在保护模式里不能用了。因为 BIOS 是实模式的东西,保护模式下的中断机制完全不同。
替代方案是串口输出(Serial Port,通过 COM1 端口直接发送字符,I/O 地址 0x3F8)。往串口发字符,配合 QEMU 的 -serial mon:stdio 参数,终端里就能看到输出。
这也是嵌入式和内核开发里最常用的调试手段——图形界面还没有的时候,串口是你唯一的眼睛。
跑起来
编译运行:
nasm -f bin boot.asm -o boot.bin
qemu-system-x86_64 -drive format=raw,file=boot.bin -nographic -serial mon:stdio
输出:
Protected mode ON!
CPU 现在跑在 32 位保护模式下,可以访问 4GB 内存,有了内存保护和权限机制。
这一步之后,才算真正有了写操作系统的基础。
下一步
保护模式是 32 位的,但现代 64 位系统需要进一步切换到长模式(Long Mode)。下一章就做这件事。