Rust OS 入门:从按下电源键到内核接管
按下电源键的那一秒,CPU 读到的第一条指令到底是什么?在 RAM 空空如也、操作系统还没影子的情况下,电脑是怎么一步步把控制权交给你的 Rust 内核的?
本文是《Writing an OS in Rust》(https://os.phil-opp.com/)入门系列的前置铺垫,回答”按下电源键到 Kernel 接管电脑”之间发生的事。它正好接上上一篇《Rust OS 入门:从 main 到 _start》里没展开的启动链路。
📚 基本概念速读
| 名称 | 定义 | 省流 |
|---|---|---|
| 固件(firmware) | 比操作系统更早运行的软件,存在主板 Flash 上,断电不消失 | 启动第一棒 |
| BIOS / UEFI | 最常见的固件,负责自检、初始化硬件、找启动盘 | 找盘、点火 |
| POST | Power-On Self Test,开机自检 | 体检 |
| Boot Sector | 磁盘第一个扇区,BIOS 把它加载进内存并执行 | 512 字节的入口 |
| Bootloader | 负责找 Kernel、加载 Kernel、配置 CPU 的程序 | BIOS 与 Kernel 的交接员 |
| 实模式 / 保护模式 / 长模式 | CPU 的 16 位 / 32 位 / 64 位运行模式 | 越新越强 |
| 寄存器(register) | CPU 内部极快的小块存储 | CPU 的便签 |
| RIP | 指令指针,指向下一条要执行的指令 | 程序计数器 |
| RSP | 栈指针,指向当前栈顶 | 栈顶在哪 |
| Memory Map | 描述内存地址空间中哪些区域可用的表 | 内存地图 |
| bootimage | 把 Bootloader 和你的 Kernel 打包成可启动镜像的工具 | 一键出镜像 |
🗺️ 先建立一张”启动地图”
不急着逐句抠细节,先记住整条链路:
graph TD
A["按下电源键"] --> B["CPU Reset"]
B --> C["固件 BIOS / UEFI<br/>(存在主板 Flash 上)"]
C --> D["POST 自检 + 初始化硬件"]
D --> E["找可引导的存储介质"]
E --> F["读取磁盘第一个扇区<br/>(512 字节 Boot Sector)"]
F --> G["第一阶段 Bootloader"]
G --> H["第二阶段 Bootloader"]
H --> I["配置 CPU:16 位 → 32 位 → 64 位"]
I --> J["把 Kernel 加载到 RAM"]
J --> K["跳转到 Kernel 入口,移交控制权"]
K --> L["Kernel 接管电脑"]
整篇文章就是在解释这张图里的每一个箭头。下面按顺序拆开。
🔌 CPU 刚通电时,什么都不知道
想象一台刚通电的电脑:RAM 里空空如也,没有操作系统,没有程序。那么 CPU 要去哪里找第一条指令?
答案是固件(firmware)——一个比操作系统更早运行的软件。
平时我们写个 Rust 程序
cargo run,背后已经有操作系统、进程、虚拟内存、文件系统、硬盘驱动层层伺候着。但问题是:
操作系统还没启动的时候,谁来负责启动操作系统?
这时候不能指望 Linux / Windows,于是主板上的固件出场了。最常见的就是 BIOS 和 UEFI。
固件存在哪里
固件不在 RAM 里,而是存储在主板上的一块非易失性 Flash 存储器中:
1 | 主板 |
所以断电之后:RAM 内容消失,BIOS / UEFI 的内容还在。这就是为什么你关机以后 BIOS 并不会”消失”。
CPU 怎么找到固件
这是 x86 CPU 的设计规定:上电后 CPU 进入一种特殊的初始状态,从一个预定义的地址开始取指令并执行。这个地址就是固件代码所在的位置。
这里引出了启动流程中第一个关键概念——指令指针(Instruction Pointer,IP / RIP):
“CPU 下一条要执行哪条指令?”
1 | 内存: |
CPU 不断重复”读指令 → 执行 → IP 移到下一条”,启动的本质就是这条执行链,而链条最开头的指令来自固件。
🧬 BIOS 上电后干什么
BIOS 的工作可以拆成四步:
1 | BIOS |
前几步合称 POST(Power-On Self Test,开机自检)——你开机时看到的厂商 Logo、听到的提示音、内存检查,都属于这个阶段。
去哪里找操作系统
老式 BIOS 的方法非常朴素:去磁盘的最开始找。磁盘第一个区域叫启动扇区(Boot Sector):
1 | 磁盘 |
BIOS 把这个扇区的内容加载进内存,然后 CPU 开始执行里面的代码。
📦 为什么处处强调”512 字节”
传统 BIOS 启动时,读取的是磁盘的第一个扇区(sector),而传统磁盘扇区大小是 512 bytes。所以 Bootloader 的起点只有 512 字节的舞台:
1 | 磁盘第一个扇区 |
512 字节写一个像样的 Bootloader 都很勉强,更别说写操作系统了。于是才有了两阶段设计。
🪜 两阶段 Bootloader:接力棒
第一阶段 Bootloader(First Stage Bootloader)只有几百字节,它的全部任务就是:找到第二阶段 Bootloader,把它加载到 RAM,然后跳过去执行。
graph TD
A["BIOS"] -->|"读取第一扇区,加载进 RAM"| B["第一阶段 Bootloader<br/>(512 字节)"]
B -->|"找到并加载"| C["第二阶段 Bootloader"]
C -->|"找到 Kernel,加载到 RAM"| D["Kernel"]
D -->|"接管电脑"| E["操作系统"]
为什么不直接让 BIOS 加载 Kernel?因为 BIOS 太古老、功能太简单——它只负责”我找到一个启动扇区了,这里面的程序你自己玩吧”,剩下的事全部交给 Bootloader。
第二阶段 Bootloader 干的事才是重头戏:
- 找 Kernel:Kernel 在磁盘/SSD 上,Bootloader 要知道它在哪。
- 把 Kernel 放进 RAM:CPU
不能直接执行硬盘上的普通文件,必须先读到内存里。比如把
kernel.bin加载到0x100000,再让 CPU 跳过去执行。 - 配置 CPU:把 CPU 从启动时的 16 位状态配置成 64 位状态。
- 收集机器信息传给 Kernel:内存多大、哪些区域可用。
🕰️ CPU 的模式变迁:16 位 → 32 位 → 64 位
这是启动流程中最容易懵的部分:电脑明明是 64 位 CPU,为什么启动时非得从 16 位开始?
答案是一个著名的历史包袱:x86 为了兼容几十年前的软件,保留了古老的启动机制。
最早的 x86 CPU(8086,1981 年的 IBM PC 用的就是它)是 16 位处理器。后来 Intel 一路升级到 80286、80386……直到现代 x86-64,但 Intel 没有把老东西扔掉,而是尽量兼容旧软件。所以今天的 CPU 上电后,仍然按照 1980 年代的规则启动。
“模式”可以粗略理解为:CPU 当前按哪种规则解释指令、访问内存。你需要知道的三个模式:
| 模式 | 位数 | 说明 |
|---|---|---|
| 实模式(Real Mode) | 16 位 | CPU 上电后的默认状态,启动的起点 |
| 保护模式(Protected Mode) | 32 位 | 80386 引入,有内存保护、特权级 |
| 长模式(Long Mode) | 64 位 | 现在的 x86-64 运行模式,你的 Rust Kernel 在这里跑 |
所以启动过程是:
graph LR
A["16-bit Real Mode<br/>(上电默认)"] --> B["32-bit Protected Mode"]
B --> C["开启 PAE / 配置页表 / 设置 EFER"]
C --> D["64-bit Long Mode"]
D --> E["Rust Kernel"]
为什么不能从 16 位直接跳到 64 位? 因为 x86 的历史设计就是这样,中间必须经过一系列”意图并不明显的步骤”——比如往某个寄存器写入魔术数字:
1 | mov eax, cr0 |
看到
CR0、CR3、CR4、EFER、PAE、页表、GDT
这些词先别慌。你现在不需要全部理解,只需要知道:
Bootloader 通过修改 CPU 的特殊寄存器和内存管理结构,把 CPU 从”兼容老系统的状态”配置成现代 64 位状态。
这正是教程作者推荐 bootimage、而不是让你手写 Bootloader
的原因。
🧮 寄存器速览:RIP 与 RSP
寄存器(register)就是 CPU 内部非常非常快的一小块存储空间:
1 | mov rax, 42 ; 把 42 放进 RAX |
其中有两个和启动流程强相关的寄存器。
RIP:指令指针
“CPU 下一条要执行的指令在哪?”
1 | 内存 |
注意三条指令的长度各不相同:mov rax, 1
占 5 字节,add rax, 2 占 3 字节,hlt 只有 1
字节。
x86 是变长指令集(variable-length instruction set):指令长度不固定,短则 1 字节(如
hlt),长则可达十几字节,具体长度取决于操作数、寻址方式等编码细节(上图只是示意图,实际编码长度以汇编器输出为准)。x86 的 CPU 取完一条指令后,要靠解码算出这条指令到底占多长,才能把 RIP 正确推进到下一条指令的起点。这跟 RISC-V、ARM 等定长指令集(fixed-length instruction set)不同——后者每条指令固定占 4 字节,取指按固定步长推进即可。
理解了这一点,再看”RIP 为什么能从 0x1000 精确跳到
0x1005“就不会困惑了。
RIP 指向 0x1000,CPU 就取这条指令执行,然后 RIP 移到
0x1005,如此往复。启动的最后一步——“跳转到 Kernel
入口”——本质就是:
RIP = Kernel 的入口地址
CPU 从此不再执行 Bootloader,而开始执行 Kernel。
RSP:栈指针
RSP(Stack Pointer)指向当前栈顶地址,它直接回应上一篇遗留的问题:“创建栈的底层操作是什么?申请内存空间?还是只是给寄存器存一个地址?”
答案是:两者都有,但概念上是分开的。
1 | 先准备一块 RAM: |
然后告诉 CPU 把这块内存当栈用:
1 | mov rsp, 0x801000 |
分配栈空间 ≠ 设置 RSP。前者是准备内存,后者只是告诉 CPU”把这块内存当栈用”。Bootloader 把 CPU 切到 64 位之后,在跳进 Kernel 之前,就负责把 RSP 设置好。
🗺️ 内存映射:Kernel 需要知道机器长什么样
假设电脑有 16GB RAM,你可能会天真地认为地址空间里全是可用的内存。实际上不是——里面混杂着可用 RAM、BIOS/firmware 区域、MMIO(硬件映射)、保留区域等。
Kernel 必须知道:哪些地址对应可用 RAM,哪些区域不能碰。这个信息就记录在内存映射(Memory Map)里,由 BIOS 提供、Bootloader 整理后传给 Kernel。
为什么 Kernel 自己不去问 BIOS?因为 BIOS 只是启动阶段的固件接口,现代操作系统并不是一直依赖它。可以这样理解整个交接关系:
Bootloader 是 BIOS 和 Kernel 之间的”交接员”——BIOS 提供硬件信息 → Bootloader 整理 → 传给 Kernel → Kernel 接管。
🔗 bootimage:把启动细节藏起来
理解了上面的复杂度,就知道为什么教程作者说”我们不会讲解如何编写自己的引导程序”——他在保护你,不让你现在就被 BIOS + 汇编 + x86 启动细节折磨。
bootimage 工具帮你生成:
1 | Bootloader |
使用方式:
1 | cargo bootimage |
得到 bootimage.bin 后,用 QEMU 模拟一台电脑启动它:
1 | qemu-system-x86_64 -drive format=raw,file=bootimage.bin |
🏗️ 学习 OS 应有的”层次感”
现在千万不要一头扎进
CR0、CR3、EFER、GDT、IDT、页表、PAE
的细节里,否则非常容易崩。先建立这张地图:
1 | ① 硬件 |
你正在看的 Rust OS 教程,恰好处于 ③ Bootloader → ④ Kernel 这个交界处。
最后送一个类比——把电脑启动想象成一家公司换老板:
1 | BIOS: "公司开门了,我检查一下水电、员工、设备。" |
所以 BIOS → Bootloader → Kernel 不是三个独立的程序,而是电脑启动控制权逐级交接的过程。
✅ 总结
按下电源键后,CPU 从主板固件开始取指令,BIOS 自检并找到启动扇区,Bootloader 接力加载 Kernel 并把 CPU 从 16 位一路配置到 64 位,最后跳转到 Kernel 入口——控制权逐级交接,直到内核接管电脑。
真正值得花时间打通的下一条链是:
1 | BIOS |
这条链的后半段——_start、栈、寄存器、no_std、#[no_mangle]、链接器脚本——正是上一篇《Rust OS 入门:从 main 到
_start》的内容。两篇连起来读,启动的完整图景就通了。
Happy Hacking! 🎉