上一章我们让机器打出了第一行字。但那时候 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 才知道去哪查。


切换步骤

整个切换过程就四步,缺一不可:

  1. 关中断cli)—— 切换过程不能被打断,否则 CPU 状态会乱
  2. 加载 GDTlgdt)—— 告诉 CPU 描述符表在哪
  3. 设置 CR0 寄存器CR0.PE=1)—— CR0(控制寄存器0,控制CPU工作模式的关键寄存器)第0位置1,CPU 正式进入保护模式
  4. 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)。下一章就做这件事。

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