从0开始的操作系统(3)
硬件视角的操作系统:计算机启动时到底发生了什么?——从 CPU Reset、Firmware 到 Bootloader
往期回顾
《指针合集》《c语言基础》《数据结构》《机器学习导论》《前端基础》
声明
这部分内容主要是笔者根据NJU的蒋炎岩老师的OS课程整理而成,大家如果感兴趣的话可以上课程的网站看看.
课程网站
https://jyywiki.cn
目录
- 硬件视角的操作系统:计算机启动时到底发生了什么?——从 CPU Reset、Firmware 到 Bootloader
-
- 一、前言:这次我们站在“硬件”这边看操作系统
- 二、Everything is a state machine
- 三、硬件不知道有没有操作系统
- 四、抽象:隔离复杂性的根本手段
- 五、硬件状态:不仅有寄存器和内存,还有整个外部世界
- 六、CPU Reset:计算机启动的第一步
- 七、Firmware:硬件和操作系统之间的第一座桥
- 八、BIOS 和 UEFI:两代固件体系
- 九、启动扇区:512 字节里的第一个操作系统接口
- 十、Bootloader:把操作系统真正加载起来
- 十一、一个最小启动扇区:`eb fe` 为什么会死循环?
- 十二、从 Firmware 到 OS:控制权是一步步交接的
- 十三、操作系统本质上也是一个普通二进制程序
- 十四、操作系统启动后,它变成了什么?
- 十五、中断:让操作系统重新拿回控制权
- 十六、固件安全:为什么 Firmware 也会成为攻击目标?
- 十七、RISC-V 世界里的 Firmware:OpenSBI
- 十八、从硬件视角重新理解系统调用
- 十九、总结
- 二十、关系
- 二十一、 重点总结
- 二十二、结语:计算机世界没有魔法,只有一层层交接的控制权
一、前言:这次我们站在“硬件”这边看操作系统
前两篇文章我们分别从两个角度看了操作系统:
第一讲:为什么需要操作系统?
第二讲:应用程序如何使用操作系统?
因此我们可以得到以下两个结论
1.操作系统是复杂度逼出来的抽象层。
2.应用程序通过系统调用使用操作系统提供的抽象。
到了这一讲,我们继续往下挖一层。
那就是:
从硬件的角度看,操作系统到底是什么?
那我们为何要干这个事情呢?
因为我们平时总觉得操作系统很神秘,好像它天生就在那里:
开机之后,Windows、Linux、macOS 就出现了;
然后程序能运行,文件能打开,鼠标键盘能用,网络也能连。
但如果从硬件视角看,事情其实非常“冷酷”:
硬件根本不知道有没有操作系统。
CPU 不会想:
我要启动 Windows 了。
我要加载 Linux 了。
我要运行用户程序了。
CPU 只会做一件事:
从某个地址取指令,然后执行。
再取下一条,再执行。
所以这篇文章的主线就是给大家讲明白:
计算机硬件是一台无情执行指令的状态机;操作系统本质上也是一个被加载起来执行的二进制程序;而 Firmware 则是硬件和操作系统之间的第一座桥。
二、Everything is a state machine
前面我们一直在强调一个视角:
Everything is a state machine.
这句话是理解系统的钥匙。
2.1 软件是状态机
一个 C 程序运行时,它的状态包括:
栈帧
全局变量
局部变量
堆区数据
当前执行到哪一条语句
执行一条语句,就是一次状态迁移:
当前程序状态
│ 执行一条语句
▼
新的程序状态
2.2 硬件也是状态机
硬件也一样。
一台处理器的状态大致包括:
寄存器
内存
程序计数器 PC
控制状态寄存器
外设状态
它的状态迁移规则很简单:
┌──────────────┐
│ 当前硬件状态 │
│ 寄存器 + 内存 │
└──────┬───────┘
│ 从 PC 取指令
▼
┌──────────────┐
│ 执行这条指令 │
└──────┬───────┘
│ 更新状态
▼
┌──────────────┐
│ 新的硬件状态 │
└──────┬───────┘
│
└── 继续取下一条指令
所以从硬件角度看,计算机没有什么魔法。
它只是:
一条指令
一条指令
一条指令
……
一直执行下去。
三、硬件不知道有没有操作系统
这是这一讲最重要的观点之一:
硬件根本不知道有没有操作系统。
CPU 不会区分:
这条指令来自操作系统
这条指令来自应用程序
这条指令来自 Bootloader
这条指令来自 Firmware
在 CPU 看来,它们都是指令。
CPU 的思维非常简单:
见什么指令,执行什么指令。
它并不会天然理解:
- 什么是进程
- 什么是文件
- 什么是用户态
- 什么是内核态
- 什么是操作系统
而这些概念,都是上层系统设计出来的抽象。
四、抽象:隔离复杂性的根本手段
既然硬件只会无情执行指令,那为什么我们还能写 C 程序、跑 Python、打开浏览器、使用文件系统?
答案是:
靠抽象。
抽象的作用就是:
下层只负责提供能力
上层不需要知道下层全部细节
比如:
┌──────────────────────┐
│ 应用程序 │
│ 使用 OS API │
└─────────▲────────────┘
│ syscall
┌─────────┴────────────┐
│ 操作系统 │
│ 管理进程 / 内存 / 文件 │
└─────────▲────────────┘
│ 指令 / 中断 / I/O
┌─────────┴────────────┐
│ 硬件 │
│ CPU / 内存 / 设备 │
└──────────────────────┘
这就是计算机系统的层次结构。
每一层都不用彻底理解另一层的全部细节,只要接口设计得好,系统就能继续往上搭。
五、硬件状态:不仅有寄存器和内存,还有整个外部世界
如果我们把计算机看成状态机,那么首先要问:
这个状态机的状态包括什么?
最容易想到的是:
寄存器
内存
PC
但这还不够。
因为计算机不是关在真空里的。它还要和外界交互:
- 键盘输入
- 鼠标移动
- 显示器输出
- 磁盘读写
- 网卡收包
- GPIO 电平变化
- 中断信号
- Reset 信号
所以硬件状态可以粗略分成两类:
┌──────────────────────────┐
│ CPU / 内存 │
│ 寄存器、PC、RAM │
└───────────▲──────────────┘
│ memory-mapped I/O / 指令
┌───────────┴──────────────┐
│ 外部设备 │
│ 磁盘、网卡、键盘、GPIO │
└──────────────────────────┘
这里要注意一个东西:
memory-mapped I/O
意思是:
一些设备寄存器会被映射到内存地址空间中。
于是 CPU 访问某些“内存地址”时,实际上是在读写设备寄存器。
比如嵌入式系统里常见的 GPIO:
gpio_set_value(GPIO_23, 1);
从上层看,这像是在调用函数。
从底层看,本质上可能就是把某个设备寄存器的某一位设置为 1。
所以,计算机和外部世界发生关系,很多时候就是通过这种方式完成的。
六、CPU Reset:计算机启动的第一步
那我们继续追问:
计算机一上电,CPU 从哪里开始执行?
答案是:
从 CPU Reset 规定的初始状态开始。
也就是说,CPU Reset 不是“随便开始”,而是体系结构规定好的行为。
Reset 之后,处理器会进入一个明确的初始状态
按下电源 / Reset
│
▼
CPU 进入 Reset 状态
│
▼
PC 被设置到指定位置
│
▼
从该地址取第一条指令
│
▼
开始执行
这就引出了一个非常自然的问题:
CPU Reset 后取到的第一条指令是谁放在那里的?
毕竟,刚上电时,内存里还没有操作系统。
操作系统甚至都还没被加载。
那么第一段代码从哪里来?
答案是:
Firmware
七、Firmware:硬件和操作系统之间的第一座桥
Firmware 中文一般叫:
固件
它本质上就是厂商预先放在机器里的代码。
你可以把它理解成:
计算机出生时就自带的一小段程序。
这段程序非常特殊,因为 CPU Reset 后最先执行的就是它。
7.1 Firmware 做什么?
Firmware 主要负责:
扫描硬件
初始化硬件
配置设备
提供基本输入输出能力
寻找可启动设备
加载 Bootloader 或操作系统
换句话说,它要先把机器“点亮”。
整个过程就是:
CPU Reset
│
▼
执行 Firmware
│
├── 初始化 CPU / 内存 / 设备
├── 扫描磁盘 / USB / 网络启动项
├── 找到 Bootloader
└── 把控制权交给 Bootloader / OS
所以 Firmware 是什么?
它不是硬件,也不是完整意义上的操作系统。
它更像是:
硬件和操作系统之间的启动桥梁
八、BIOS 和 UEFI:两代固件体系
在 PC 世界里,最常见的两类 Firmware 是:
Legacy BIOS
UEFI
8.1 Legacy BIOS
BIOS 全称是:
Basic Input/Output System
在早期 PC 时代,BIOS 提供了很多基础能力:
- 硬件初始化
- 中断服务
- 基本 I/O
- 从磁盘加载启动扇区
经典 BIOS 启动流程大致是:
CPU Reset
│
▼
BIOS 执行
│
▼
扫描可启动磁盘
│
▼
读取第一个扇区到 0x7c00
│
▼
检查末尾是否为 0x55AA
│
▼
跳转到 0x7c00 执行
如果各位的数感比较好,可能这里发现了这两个数字:
0x7c00
0x55AA
0x7c00是传统 BIOS 把启动扇区加载到内存的位置0x55AA是启动扇区末尾的 Magic Number
而他们的差值刚好是512字节,因为那时候内存的最大值刚好是512.
也就是说,如果一个 512 字节的扇区最后两个字节是:
55 aa
BIOS 就会把它当成可启动扇区。
8.2 UEFI
UEFI 是更现代的固件接口。
相比传统 BIOS,UEFI 提供了更丰富的启动服务、运行时服务、驱动支持和启动管理能力。
它不再简单依赖“读取第一个扇区然后跳转执行”的模型,而是有更完整的 Boot Manager。
简化来看:
CPU Reset
│
▼
UEFI Firmware
│
├── 初始化硬件
├── 读取 NVRAM 中的启动项
├── 找到 EFI System Partition
├── 加载 .efi 启动程序
└── 启动 OS Loader / Kernel
所以可以这样理解:
BIOS 更像是“传统手工启动流程”
UEFI 更像是“现代化的启动平台”
九、启动扇区:512 字节里的第一个操作系统接口
传统 BIOS 会把磁盘第一个扇区加载到内存 0x7c00,然后执行它。
这个扇区通常是:
MBR:Master Boot Record
一个典型 MBR 大小是 512 字节,大致结构如下:
┌────────────────────────────┐
│ Boot Code │ 446 字节
├────────────────────────────┤
│ Partition Table │ 64 字节
├────────────────────────────┤
│ Boot Signature │ 2 字节:55 AA
└────────────────────────────┘
也就是说,真正能写启动代码的空间很小。
传统 MBR 里,Boot Code 部分只有大约 446 字节。
这么点空间显然放不下一个完整操作系统。
所以它通常只做一件事:
加载更大的 Bootloader。
这就是为什么后来会有分阶段启动。
十、Bootloader:把操作系统真正加载起来
Bootloader 的作用可以概括成一句话:
把操作系统内核加载到内存,并把控制权交给它。
以 GRUB 为例,它的启动常常可以理解成多阶段:
Stage 1:
非常小,位于 MBR 或启动扇区中
Stage 1.5:
提供更多磁盘 / 文件系统识别能力
Stage 2:
完整的 GRUB,显示启动菜单,加载内核
这一层设计非常自然:
512 字节太小
│
▼
先加载一个更大的程序
│
▼
再由更大的程序加载操作系统
这也是计算机系统里很常见的思路:
用一个简单程序加载一个更复杂的程序。
当然各位可能对这个事情感到有点不可思议,这里推荐各位可以去看看b站up主漫士沉思录的这个视频当作拓展
十一、一个最小启动扇区:eb fe 为什么会死循环?
课程里有一个非常经典的小实验:
(printf "\xeb\xfe"; cat /dev/zero | head -c 508; printf "\x55\xaa") > a.img
这条命令生成了一个 512 字节的镜像。
拆开看:
eb fe 两个字节的机器指令
508 个 0 填充
55 aa 启动扇区标志
其中:
eb fe
是一条短跳转指令,大意是:
跳回自己
也就是:
jmp .
所以它会让 CPU 永远卡在原地:
执行 eb fe
│
▼
跳回自己
│
▼
再次执行 eb fe
│
▼
继续跳回自己
它让我们亲眼看到:
只要启动扇区合法,BIOS 就会把控制权交给它;至于它做什么,CPU 并不关心。
你写一个死循环,它就执行死循环。
你写一个 Bootloader,它就执行 Bootloader。
你写一个玩具 OS,它就开始启动你的玩具 OS。
计算机世界没有魔法,只有指令。
十二、从 Firmware 到 OS:控制权是一步步交接的
现在我们可以把启动过程串起来。
传统 BIOS 模式下,简化流程是:
CPU Reset
│
▼
执行 BIOS Firmware
│
▼
BIOS 初始化硬件
│
▼
读取磁盘第一个扇区到 0x7c00
│
▼
检查 55 AA
│
▼
执行 MBR / Bootloader Stage 1
│
▼
加载更大的 Bootloader
│
▼
Bootloader 加载 Kernel
│
▼
Kernel 接管中断、I/O、内存管理
│
▼
操作系统开始运行
从这张图我们可以理解:
操作系统不是一开始就存在的,它是被前面的代码一步步加载起来的。
十三、操作系统本质上也是一个普通二进制程序
现在再回到那个问题:
从硬件视角看,操作系统是什么?
答案其实有点“降维打击”:
操作系统也是一个普通的二进制程序。
只不过它很特殊:
它被 Firmware / Bootloader 加载
它运行在更高权限级
它接管中断
它管理 I/O
它控制内存
它调度应用程序
但从硬件角度看,它仍然是:
一段指令
一些数据
从某个入口开始执行
和普通程序没有本质上的神秘差异。
区别在于:
普通应用程序:
被操作系统加载
操作系统内核:
被 Firmware / Bootloader 加载
所以,OS 不是魔法。
它只是一个更早被加载、更有权限、负责管理其他程序的程序。
十四、操作系统启动后,它变成了什么?
操作系统启动后,最关键的一件事是:
接管中断、异常和 I/O。
也就是说,当应用程序执行时,CPU 大部分时间可能都在跑应用程序。
但一旦发生:
系统调用
时钟中断
设备中断
异常
缺页
控制权就会回到操作系统。
所以可以形象地说:
操作系统启动后,很大程度上就变成了一个中断处理程序。
平时它把 CPU 交给应用程序跑。
一旦有事,它又重新接管。
这就像一个管理者:
平时让员工干活
有事件发生时介入处理
处理完再把工作交回去
十五、中断:让操作系统重新拿回控制权
如果 CPU 只是一直无情执行当前程序的指令,会发生什么?
比如一个程序写了:
jmp .
那它就会永远卡住。
如果没有额外机制,操作系统就再也拿不回 CPU。
所以硬件必须提供一种机制,让外部事件可以打断当前执行流。
这就是:
Interrupt,中断
中断可以来自:
时钟
键盘
磁盘
网卡
外设
最典型的是时钟中断。
时钟中断让操作系统可以定期重新获得控制权:
应用程序运行一会
│
▼
时钟中断到来
│
▼
CPU 跳转到中断处理程序
│
▼
操作系统决定是否切换进程
这就是操作系统实现多任务的基础之一。
十六、固件安全:为什么 Firmware 也会成为攻击目标?
既然 Firmware 是 CPU Reset 后最先执行的代码,那么它的地位就非常敏感。
如果 Firmware 被篡改,会发生什么?
CPU Reset
│
▼
执行被篡改的 Firmware
│
▼
攻击代码比操作系统更早获得控制权
这非常危险。
因为这意味着攻击者可能在操作系统启动之前就已经控制了机器。
16.1 CIH 病毒的启示
历史上有一些病毒并不只是破坏文件,而是试图破坏 Firmware。
其中一个经典例子是 CIH 病毒。
它曾经试图覆盖部分系统 Firmware,使机器无法正常启动。
这类攻击在当年影响很大,因为一旦 Firmware 损坏,普通用户往往无法通过重装系统解决。
这说明:
系统安全不是从操作系统开始的,而是从 Firmware 就已经开始了。
16.2 现代固件为什么要签名?
为防止随意写入恶意固件,现代系统通常会引入数字签名机制。
基本思想是:
厂商发布 Firmware 更新
│
▼
使用私钥签名
│
▼
设备使用公钥验证签名
│
▼
验证通过才允许更新
这背后就是现代计算机启动安全的基础逻辑之一。
十七、RISC-V 世界里的 Firmware:OpenSBI
在 RISC-V 平台上,经常会看到一个名字:
OpenSBI
SBI 的全称是:
Supervisor Binary Interface
它可以理解成:
RISC-V 平台上,Firmware 和操作系统之间的一层标准接口。
在 RISC-V 的特权级模型中,通常会有:
┌──────────────────────┐
│ User Program │ U-mode
└─────────▲────────────┘
│ syscall
┌─────────┴────────────┐
│ Operating System │ S-mode
└─────────▲────────────┘
│ SBI call
┌─────────┴────────────┐
│ OpenSBI / Firmware │ M-mode
└─────────▲────────────┘
│
┌─────────┴────────────┐
│ Hardware │
└──────────────────────┘
这个结构非常漂亮。
它说明不同平台上虽然细节不同,但核心思想类似:
下层隐藏硬件差异,上层通过接口获得服务。
这仍然是抽象的力量。
十八、从硬件视角重新理解系统调用
上一讲我们从应用程序视角看 syscall:
应用程序通过 syscall 请求操作系统服务。
这一讲从硬件视角看,syscall 更像是一条特殊指令。
例如不同架构上会有类似机制:
x86:syscall / int
RISC-V:ecall
ARM:svc
它们的共同作用是:
让 CPU 从当前执行流切换到更高权限的操作系统处理逻辑。
所以 syscall 不是普通函数调用。
普通函数调用仍然在同一个权限世界里。
syscall 则跨过了用户态和内核态的边界。
这也是为什么它是操作系统中最核心的接口之一。
十九、总结
这一讲可以用一张图串起来:
CPU Reset
│
▼
Firmware
│
├── 初始化硬件
├── 配置设备
└── 寻找启动设备
│
▼
Bootloader
│
├── 加载内核
└── 传递启动参数
│
▼
Operating System Kernel
│
├── 接管中断
├── 管理内存
├── 管理设备
├── 调度进程
└── 提供 syscall API
│
▼
User Programs
│
├── 普通计算
└── syscall 请求 OS 服务
再从“状态机”的角度看:
硬件:
从 Reset 开始,无情执行指令
Firmware:
最早执行的厂商代码,初始化硬件并加载下一阶段
Bootloader:
加载操作系统内核
操作系统:
接管中断和 I/O,把应用程序放到 CPU 上运行
应用程序:
执行普通计算,通过 syscall 请求系统服务
二十、关系
现在我们可以把前三讲合起来看。
20.1 第一讲:为什么需要 OS?
硬件越来越复杂
程序越来越复杂
用户越来越多
资源需要共享
所以需要操作系统这个抽象层
20.2 第二讲:应用如何使用 OS?
应用程序 = 普通计算 + OS API
程序通过 syscall 请求操作系统服务
20.3 第三讲:硬件如何承载 OS?
硬件是无情执行指令的状态机
CPU Reset 后执行 Firmware
Firmware 加载 Bootloader / OS
OS 接管中断和 I/O
二十一、 重点总结
21.1 硬件不知道有没有操作系统
CPU 只会执行指令。
操作系统只是被执行的程序之一,只是权限更高、职责更特殊。
21.2 CPU Reset 是启动的起点
计算机启动不是从操作系统开始,而是从 CPU Reset 后的第一条指令开始。
21.3 Firmware 是硬件和 OS 之间的桥
Firmware 负责初始化硬件,并把后续控制权交给 Bootloader 或操作系统。
21.4 Bootloader 负责加载操作系统
传统 BIOS 模式下,MBR 的 512 字节空间太小,所以通常需要多阶段加载。
21.5 中断让 OS 能重新拿回 CPU
如果没有中断,一个死循环程序可能永远霸占 CPU。
时钟中断让操作系统可以实现调度。
21.6 OS 本质上也是二进制程序
它不是魔法,只是一个更早被加载、更有权限、负责管理资源的程序。
二十二、结语:计算机世界没有魔法,只有一层层交接的控制权
很多人第一次学操作系统,会觉得它很神秘。
但从硬件视角看,事情反而变简单了:
CPU 不知道 OS 是什么。
CPU 只知道从 PC 取指令执行。
于是问题就变成:
第一条指令在哪里?
谁把它放在那里?
它又如何加载下一段代码?
下一段代码又如何加载操作系统?
操作系统如何接管中断和 I/O?
这一串问题回答完,操作系统启动的轮廓也就出来了。
最终我们看到的是一个非常朴素的过程:
CPU Reset
→ Firmware
→ Bootloader
→ Kernel
→ User Program
每一层都只是把控制权交给下一层。
所以,操作系统不是从天而降的神秘存在。
它只是计算机启动链条中的一个关键阶段。
说到底:
计算机世界没有魔法。所谓操作系统,不过是一段被正确加载、获得足够权限、接管中断和设备,并负责管理其他程序的二进制代码。
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐
所有评论(0)