第3讲 · 硬件视角的操作系统
2026 南京大学《操作系统原理》第三讲 · 课堂实录重构
| 主讲 | 蒋炎岩(Yanyan Jiang) |
| 视频 | https://www.bilibili.com/video/BV18fcozAEsy/ |
| 课程主页 | https://jyywiki.cn/OS/2026/ |
| 本讲讲义 | https://jyywiki.cn/OS/2026/lect3.md |
| 讲义 / 幻灯片作者 | 蒋炎岩,https://jyywiki.cn/ |
| 讲义 / 幻灯片许可 | CC BY-NC 4.0 |
| 图片来源 | 书中幻灯片插图为课程官方渲染图;演示类截图为视频画面 |
对应讲次:第3讲(南京大学 操作系统原理)
制作模型:deepseek-flash(coding-agent / pi;据 ~/.pi/agent/settings.json)
编者说明:本书由课堂录像的语音字幕重构而成,术语、章节骨架与参考链接已按课程官方讲义(
lect3.md)校准。讲师的口语风格、类比与案例被尽量保留;编辑性的补充一律以「编者说明」标出。
引子:为什么要从硬件看操作系统
(参考时间: 00:00)
绪论部分要讲两个视角。上一讲是应用视角:操作系统就是一组 API,一个程序通过 syscall 把控制权完全交给操作系统,就像做一次全身麻醉——醒来时请求已经完成,甚至可能「肾已经没了」。我们还看到,借助 coding agent 与 strace 这样的工具,任何应用程序对你来说都可以是透明的、可以打开的。
今天要讲的另一个视角是硬件视角。它更像是为「计算机系统基础」补上后段与 I/O 部分缺失的概念:一旦建立好这些概念,你就可以用大模型去写任何和硬件相关的代码。曾经「写硬件相关的代码」是做系统的人的核心技能,如今它似乎也不再值钱了。
编者说明:本讲定位为一次「科普式」的概念课,目的是让大家知道硬件里面有什么、能做什么,而不是掌握每一个寄存器细节。正如讲师反复强调的:你只需要有正确的概念,剩下的都可以交给 AI。
这不是一节「教你用工具」的课
本讲开头用了相当篇幅讲学习方法,因为在这个时代,学习方式本身已经发生了根本变化。课程网站上的示例代码点进去就让人不想下载,这是完全正常的——「人都是懒的」。但现在是 intelligence is cheap 的时代:只要你相信一件事能做成,并且能把它说清楚,你就能把它搞定。
有一个「改变世界的 prompt」:
我在做 X。如果你是这个领域的专家,你会怎么做?
你可以被动地学习——观察 coding agent 所做的「正确的事」;也可以主动地学习——让 Copilot 不停地追问、实时给出建议。两种方式都能帮你建立正确的概念。
📷 学习方法:你做对了吗?
回到原视频 (00:02:00)
一个现场演示:讲师把课程代码的链接丢给 AI,说「我的老师说给我这个链接」,AI first 用命令行取回 HTML,判断这是一个目录,然后用 wget -r 递归把所有内容(包括 bin 目录下的二进制)下载下来,还贴心地删掉 index.html,中途发现没有保留目录结构、又重做了一遍。你在这个过程里可以随时打断、追问「为什么」——这本身就是向专家学习。
📷 AI 用 wget -r 递归下载课程示例代码
回到原视频 (00:05:15)
讲师坦言自己也是新手:他也虚心请教 AI,让光标变大、让链接可以用一个组合键直接打开。他甚至开玩笑说,把这一页幻灯片讲完,这门操作系统课其实也就不用教了——因为你们有很好的教科书,自学时一边看一边问,其实不来上课也完全 OK。
编者说明:原文有大量关于「AI 时代学习效率存在十倍百倍差距」「1980/1990 年代遍地黄金」的感慨,这里保留其核心观点。这部分的完整价值不在于结论,而在于「遇到麻烦时正是提高自己的最好机会」这一心态。
一、复习:软件、硬件和操作系统
(参考时间: 00:09)
上一讲已经把整幅 big picture 画出来了。一个 .c 文件是一个状态机,每次执行一条语句;它最终变成一个 a.out,也是一个状态机,但每次执行一条指令;a.out 运行在一个叫操作系统的东西之上;操作系统本身里有很多对象(进程、文件……),a.out 和它的进程都是其中之一;再往下,操作系统运行在一个硬件之上,硬件同样是一个状态机。中间穿插着编译器与各种各样的知识——你的知识就这样串联起来了。
| 初始状态 | 状态迁移 | |
|---|---|---|
| 软件:C(SimpleC)、Python…… | stack = [main(argc, argv), globals] |
执行一条语句 |
| 硬件:x86、RISC-V、ARM…… | CPU Reset | 执行一条指令 / 响应中断 |
| 操作系统:使软件能运行在硬件上的程序 | —— | 系统调用 API(syscall/ecall/svc 指令),将控制权交给操作系统 |
📷 复习:软件、硬件和操作系统
回到原视频 (00:09:40)
二、硬件视角的操作系统
2.1 一句话:硬件根本不知道有没有操作系统
(参考时间: 00:11)
所谓硬件视角,其实就一句话:硬件根本不知道有没有操作系统,也不知道上面跑的是什么操作系统。它就是一台无情执行指令的机器,看见什么指令就执行什么指令。
你在「计算机系统基础」课上写 NEMU 模拟器时,用的就是这个模型:你从没有引入操作系统的概念,只是照着指令集的 specification,看一条 add、一条 move 会让状态怎样迁移。那些比较特殊的指令(int/int 0x80、out、in……)虽然被用来实现操作系统,或者要和系统外界交互,但硬件并不知道上面跑的是什么——你完全可以在它上面跑一个「不是操作系统」的程序。
📷 硬件视角的操作系统
回到原视频 (00:11:25)
抽象:隔离复杂性的根本手段
这个「不知道」的本质,是计算机系统里反复出现的一件事——抽象。「抽象」这个词本身就很抽象,但在计算机系统里它有明确含义:隔离上下两层。
- 中间的接口很简单:
syscall的 API 很少,却能支撑一整个应用生态; - 上面很复杂:文件描述符背后的「重定向」——为什么同一段
printf有时输出到屏幕、有时输出到文件,代码却完全一样? - 下面也很复杂:为了让一个像素点出现在屏幕上,操作系统要驱动显卡,而背后是一整套硬件与总线。
例如 write 带一个文件描述符(fd),这个 fd 指向操作系统里的某个对象(可能是文件、终端、管道……),正是它实现了「同一份代码,输出到不同地方」。
指令集体系结构(ISA)本身就是一层极典型的抽象:它只暴露最基础的功能,像 move、add、interrupt、out、in;至于底下的处理器有多复杂——现代处理器一个时钟周期可以执行不止一条指令,本质是一个动态的「指令集编译器」,做数据流分析后把无依赖的指令并行执行——上面完全不用知道。
编者拓展|ISA(Instruction Set Architecture,指令集体系结构):它是「硬件对软件」的约定——规定有哪些指令、有哪些寄存器、存储器怎么访问。同一份程序,只要 ISA 相同(都是 x86-64,或都是 RISC-V),就能在不同厂商、不同性能的 CPU 上运行。我们在《计算机系统基础》里写 NEMU,本质上就是在实现一个 ISA 的模拟器。
编者说明:讲师提到,一个抽象层设计得好,上面能生长、下面也能生长;设计得不好,改一个地方就变成整个项目的重构。他甚至认为,抽象层的设计可能是 Agentic AI 时代的重要能力。对低年级同学来说,当下要做的是广泛了解各种设计(例如 VS Code 这样的插件式系统「怎么构造」)。
2.2 计算机系统的状态机模型
(参考时间: 00:20)
再次给出那个说了无数遍的定义:硬件是一台无情执行指令的机器。
- 状态:内存、寄存器的数值;
- 初始状态:由系统设计者规定,有一个型号叫 CPU Reset,reset 完会到达一个约定好的状态(例如 PC 等于某个确定的值);
- 状态迁移:从 PC 取指令,译码,再按手册规定执行。
这个模型是准确的,但对实现操作系统来说还不够:如果硬件真的只有「取指—执行」,那么一条 jmp .(跳转到当前指令)就会让机器永远卡死,操作系统再也没机会运行。所以真实的硬件还提供了一些细节机制,正是这些机制让操作系统可以被实现出来。
📷 计算机系统的状态机模型
回到原视频 (00:20:20)
2.3 状态:内部状态与外部状态
(参考时间: 00:21)
计算机系统的状态分成两部分。
内部状态:寄存器、内存。
struct CPUState {
uint32_t regs[32], csrs[CSR_COUNT];
uint8_t *mem;
uint32_t mem_offset, mem_size;
};
外部状态:整个世界——计算机系统无法完全控制的客观存在。硬件是插在主板上的,主板位于物理世界里,所以任何瞬间都存在一个物理世界的状态。计算机通过以下方式访问它:
- 设备上的寄存器:通过 memory-mapped I/O 访问,这是计算机对外界产生影响的主要方式;
- Interrupt / Reset Line:一组物理连线。
编者拓展|MMIO(Memory-mapped I/O)与中断线:MMIO 把「设备的寄存器」映射到内存地址上——于是
gpio_set_value(...)这类代码看起来像在写内存,实际是在和设备说话。中断线(interrupt line)则是一根物理导线,设备通过拉低电平来「按铃」通知 CPU。二者合起来,就是计算机与物理世界交换信息的两条通道。
一个极好的例子是 GPIO(General Purpose Input/Output):
gpio_set_value(GPIO_23, 1); // 在 Linux 内核里,把 23 号引脚电平拉高
只要有一行代码把电平拉高或拉低,接上面包板与发光二极管,你就能真的看到它闪——这就是「计算机与物理世界交换电信号」。计算机世界里没有任何魔法。
打鸭子与一个像素的相机(编者整理):讲师讲了一个体感游戏的原理——那把枪其实就是一个只有一个像素的相机。扣下扳机时,屏幕依次输出一帧全白、一帧全黑;如果那一个像素先后看到「白—黑」,就判定命中;如果看到「黑—黑」,就判定未命中。这说明:计算机的状态里,确实包含物理世界的状态。
📷 计算机系统:状态
回到原视频 (00:21:50)
2.4 例子:GPIO
(参考时间: 00:24)
讲师的树莓派上有一排针脚(GPIO 引脚),你可以像「电流表测电」一样理解它们。更妙的是:你只要知道「GPIO」这个概念,剩下的都可以问 AI。讲师现场问 AI「我的计算机系统里有没有 GPIO 接口」,AI 自己就去用 gpioinfo 扫描:
📷 树莓派与一排 GPIO 针脚
回到原视频 (00:24:50)
📷 用 gpioinfo 扫描系统中的 GPIO 控制器
回到原视频 (00:26:30)
结果很有意思:你看到的这排针脚并不是系统里唯一的 GPIO 控制器,它只是留给你接额外设备的。系统内部还有大量 GPIO 控制器,管理 SPI 总线、按键、蓝牙、WiFi……。这也说明树莓派的可玩性很高:超声波传感器、摄像头、机械手,都可以用一行 C 代码写电平来驱动。
📷 例子:GPIO
回到原视频 (00:25:00)
2.5 初始状态:CPU Reset
(参考时间: 00:32)
死循环之所以不会把整台机器卡死,是因为还有一根线——中断线。一旦对应信号(或边沿)到来,CPU 会不管当前在执行什么,强行跳转到另一个位置。正因为有中断这个机制(时钟、按键、网卡收包……),操作系统才能建立起它的「霸主地位」:应用程序没有权限关闭中断。
你可以让 AI 写一段内联汇编,在你的用户态程序里试图关闭中断——运行到那条指令时,你会得到一个 segmentation fault。只有操作系统的代码(在 x86 上是 cli/sti,在 RISC-V 上对应 sstatus/csr 里的某一位)才有权限关中断。
编者拓展|特权级(privilege level)与内核态 / 用户态:CPU 把「能执行特权指令(关中断、改页表、访问设备)」和「不能」分成不同等级。x86 上叫 Ring 0 / Ring 3,RISC-V 上分为 M-mode / S-mode / U-mode。操作系统运行在高特权级(内核态),普通程序运行在低特权级(用户态)——这就是「应用程序没有权限关中断」的根本原因。反过来说,如果操作系统在关中断时出了 bug,它就可能真的死机。
📷 应用程序试图关闭中断,导致 segmentation fault
回到原视频 (00:30:10)
为什么老电脑要有 Reset 按钮
CPU Reset 是明确的行为:reset 后通常中断处于关闭状态——因为此时各种寄存器的值有些是不定义的(为了省电路),一旦 CPU 开始运行就直接和物理世界接上了,此时不宜有中断进来。
讲师顺势讲了个「代沟」故事:现在的笔记本电池焊死在机内,死机了要长按电源键足够久,主板上的电路会强制给 CPU 发一个 reset 信号;而在他的年代,台式机上都有一个 RESET 按钮。reset 按钮的行为就像你写数字电路时每个寄存器都有的那个 if (reset) ...——不管你运行到哪个状态,一按就全部复位。为什么那时候要配一个 reset 按钮?因为那时候的电脑总是死机:到处都是 bug、漏洞和病毒。历史总是惊人地相似——就像今天各种「不合规、不可靠」的新产品(例如被戏称为「小龙虾」的 OpenClaw),终会被一点点补成非常好用的东西。
📷 计算机系统:初始状态
回到原视频 (00:32:30)
实际的 CPU Reset:x86、ARM、RISC-V
过去为了建立这些概念,讲师整个大四都在读 Intel 手册(几千页),要「大海捞针」地从海量信息里筛选出哪些重要、哪些不重要。今天不必了——直接问 AI。
不过讲师提醒:AI 的回答未必都对。他现场演示时就说「我感觉刚才那个回答有点幻觉,有些应该是不对的;如果你把正确的手册丢进去,它应该能给出正确回答」。他读书的收获,是建立了一套关于「处理器能干什么、操作系统怎么用处理器」的正确概念——相当于经历了一次漫长的「预训练」,如今处于「后训练/微调」阶段。
📷 实际的 CPU Reset
回到原视频 (00:36:50)
为了把「CPU Reset 后的初始状态」真正展示出来,讲师用 AI 写了三个架构的最小代码:一段能直接从 CPU Reset 开始执行、并打印出指令序列与寄存器变化的程序。在 RISC-V(QEMU 实现)上,你可以亲眼看到 reset 后一堆寄存器的初值、看到自己写的第一条 move 指令,以及 mstatus(0xA00000000)这类 CSR 的值是什么意思——通过追问 AI,每个寄存器的含义、为什么这样设计,都可以讲得清清楚楚。
📷 CPU Reset 演示:RISC-V 最小代码与寄存器初值
回到原视频 (00:41:30)
📷 ARM 最小代码在 CPU Reset 后的寄存器状态
回到原视频 (00:44:30)
编者说明:讲师特别说明,这段演示代码中的一条
jump是他自己加的——因为模拟器会把连续指令打包执行,加上跳转后才能在「结束」之前观察到寄存器状态。这段最小程序在演示里的地位,就相当于一段自己写的固件(firmware)。
2.6 状态迁移:执行指令、响应中断、I/O
(参考时间: 00:45)
绝大部分时候,状态迁移就是执行一条指令(in 从物理世界读,out 把电平送到物理世界,其余在处理器内部)。但这个模型有很大的扩展性:
- 多个处理器:每个 CPU 有自己独立的寄存器,共享一份内存。因为「每次选一个 CPU 执行一条指令」,而可选的 CPU 不止一个、每个 CPU 还可能响应中断,所以每一次状态迁移都是不确定的:同一段并发程序运行很多次,结果可能各不相同。这既是这套机制强大的地方,也是它复杂性的根源。
- 响应中断:可以概括成
if (intr) goto vec;。 - 输入输出:与「计算机系统外」交换数据。
stateDiagram-v2
[*] --> S0: CPU Reset
S0 --> S0: 取指/译码/执行(内部状态变化)
S0 --> S0: in/out(与物理世界交换电信号)
S0 --> S1: 中断到来(强行跳转 vec)
S1 --> S0: 中断返回
📷 计算机系统:状态迁移
回到原视频 (00:45:20)
2.7 小结:操作系统就是一个普通程序
(参考时间: 00:50)
硬件提供的是底层机制:多个处理器的资源、确定的 reset 状态、中断、I/O……它不需要知道有没有操作系统。
操作系统只是一个普通的(二进制)程序:
- 它接管了中断、I/O 等资源,应用程序不能直接访问;
- 它把应用程序「放」到 CPU 上运行一会;
- 中断(系统调用)发生后,操作系统又开始执行。
一个精辟的说法是:操作系统启动、初始化完毕之后,它就变成了一个「中断处理程序」。它虽然驻留在内存里,但平时并不在 CPU 上执行——只有当中断到来、或程序执行到系统调用时,它的代码才开始运行。于是它成了一位「服务的提供者」,并创建出我们看到的应用程序世界。
📷 小结:硬件视角的操作系统
回到原视频 (00:50:00)
三、固件:硬件和操作系统之间的桥梁
3.1 刚才遗漏的一个细节
(参考时间: 00:51)
CPU 是无情执行指令的机器,CPU Reset 的行为是 well-defined 的:从 Mem[PC] 取指令执行。于是有一个推论:如果 reset 后取出的第一条指令不合法,整个系统不就坏掉了吗?
而内存条(DRAM)是易失性的,掉电数据全丢;冷启动上电后 CPU reset,内存里是什么根本无从谈起。
编者拓展|易失性 vs 非易失性存储:DRAM(内存条)靠电容存电荷,断电即失,是易失(volatile)的;ROM / 闪存 / 磁盘断电仍保存数据,是非易失(non-volatile)的。所以刚上电时内存里是「一片空白」,CPU Reset 指向的地方必须是非易失存储器(固件)。所以 reset 后 PC 指向的那个位置,必须有合法的代码。问题是:这段代码是谁写的?
📷 刚才遗漏的一个细节
回到原视频 (00:52:00)
答案:系统厂商的代码。CPU 厂商在手册里规定好了 reset 后的行为;而生产主板/整机的厂商,可以把一个只读存储器(早期是 ROM,现在一般是闪存)以内存映射的方式映射到 CPU Reset 后的那个地址。
编者拓展|ROM / PROM / EEPROM / 闪存:都是非易失的存储芯片。ROM 出厂即写死,不能改;PROM 可烧写一次;EEPROM 可电擦写但较慢;今天的固件一般存在闪存(Flash)里,可以反复更新——这也正是 CIH 病毒能「改写固件」的前提。这段代码一「出生」就拥有机器的完整控制权,它的名字叫 Firmware(固件)。
编者拓展|Firmware(固件):被厂商固化在计算机里的代码,是 CPU Reset 之后执行的第一段程序。它比操作系统更底层——「一旦它有 bug,系统就成砖」。它的职责是初始化硬件、再把磁盘上的操作系统加载进内存并跳转过去。
3.2 Firmware 是什么
(参考时间: 00:54)
所谓「固件」,就是厂商固定在计算机系统里的代码。因为它是 reset 后直接执行的,比写在 SD 卡上的操作系统更底层、更重要——一旦它有 bug,系统就成砖了。CPU Reset 后真的什么也没有:内存里没有操作系统,操作系统还躺在磁盘上。固件要负责把计算机系统完整初始化,再把磁盘上的操作系统加载到内存,最后跳转过去执行。好消息是它几乎总是从 CPU Reset 开始启动,环境相对可控。
Firmware 的功能:
- 完成硬件扫描、初始化和配置(这些配置要生效可能需要重启计算机);
- 不严格地说,加载操作系统。
编者说明:QEMU 可以绕过 Firmware 直接加载操作系统(见 QEMU 手册
-kernel等选项)。
📷 Firmware
回到原视频 (00:54:40)
3.3 配置计算机系统
开机时按一个特殊的组合键(Del、F2、F1、ESC、F10、F12……品牌差异很大,到现在都没统一),就会进入固件的配置菜单,得到一个直接「看到」系统硬件的机会。你可以关闭某些 CPU 核心、开关硬件虚拟化、配置接口功能、甚至调整风扇转速、供电电压、内存时序……游戏主板会画得很 fancy,但做的事情和文本界面没有本质区别。
📷 Firmware 功能:配置计算机系统
回到原视频 (00:58:00)
3.4 固件就是一段代码
(参考时间: 00:59)
Firmware 就是一个小「操作系统」:CPU Reset 后初始化硬件,对接操作系统的 Boot Loader。它可以是一段普通的、程序员可以写的代码。
Legacy BIOS(Basic I/O System)
在 IBM PC 时代,所有设备/BIOS 中断都有 specification。16 位 DOS 时代,BIOS 常驻内存,提供 I/O 等功能。因为内存只有 640K,操作系统(如 DOS)加上应用程序已经很紧,再做图形库也不划算。于是 DOS 时代 BIOS 会一直驻留内存,程序用一条 int 指令(本质是跳转)查中断向量表,跳转到 BIOS 预设的地址——比如 int 0x10 打印字符、绘制图形、读写磁盘、读取鼠标位置。
编者拓展|中断向量表(interrupt vector table):一张放在内存固定位置的「跳转表」,第 n 项写着「第 n 号中断该跳到哪」。
int 0x10(BIOS 显示服务)这类调用,就是「取表里第 16 项 → 跳过去」。今天的系统调用(syscall)和它一脉相承——只不过入口由操作系统而非固件提供。
这套约定成就了百花齐放的「兼容机」时代,也让 AMI 和 Phoenix BIOS 这些厂商活到了今天。
UEFI(Unified Extensible Firmware Interface)
今天的硬件复杂得多:指纹锁需要在加载操作系统前就驱动起来,以验证身份;U 盘可能是移动固态硬盘、可能带指纹;还能从网络启动(PXE),而市面上的网卡形形色色——内置的、USB 扩展坞上的、PCIe 万兆的。于是固件需要大量设备驱动。UEFI 提供了一个编程框架:它有一个 FAT 分区,可以以文件形式存放驱动程序、引导加载器;在此基础上可以实现各种驱动,最终仍然只做一件事——把操作系统加载起来。
编者拓展|UEFI(统一可扩展固件接口):取代 Legacy BIOS 的现代固件标准。它把固件做成一个小型的编程框架——有自己的文件系统(FAT)、能加载驱动(
.efi文件)、支持从网络启动(PXE)。今天装系统时看到的图形化启动菜单、Secure Boot,都属于 UEFI。编者说明:讲师顺带讲了「磁盘」这个术语改不过来的例子——现在机器大多已是 SSD(闪存盘),已经没有「磁」了,但我们仍然叫它磁盘(legacy vice,遗留术语)。他还提到很多设备交互已经从固件移交给操作系统直接完成,但指纹锁这类「加载操作系统前就要用」的设备,仍由固件驱动。
📷 Firmware:就是一段代码
回到原视频 (00:59:40)
四、梦回 1998:可写的固件与 CIH 病毒
4.1 固件更新与写保护
(参考时间: 01:07)
固件通常是只读的——它接管了 CPU Reset,store 指令本不该有任何效果。但固件也需要更新:如果来了一个新硬件(比如新的磁盘控制器挂在固件不支持的新总线上),不更新固件就可能连磁盘都看不到、操作系统都起不来。
于是,Intel 430TX(Pentium,俗称 586)芯片组允许写入(更新)PROM。这样厂商就能以发布补丁(甚至寄一张软盘)的方式更新固件,就像今天的 Windows 更新转圈圈——只不过那时还没有普及互联网。
为了防止 bug 损坏固件,平时写保护是打开的,需要向 BIOS 写入一个特定序列才打开写保护、进入编辑状态。这个序列就写在手册里……
编者说明:Firmware 本质就是一段「往 I/O 端口写数据」的代码,所以写保护平时必须打开。否则一个程序的 bug 就可能把固件覆盖掉:内存被 corrupt 不要紧,按 reset 从头再来即可;但第一条指令一旦被改成
jmp .,计算机系统就真的成砖了——除非在物理意义上把芯片拆下来换一片。
📷 梦回 1998
回到原视频 (01:07:10)
4.2 CIH 病毒
(参考时间: 01:11)
1998 年,一位和同学们差不多大的年轻人设计了一个病毒——CIH,也叫切尔诺贝利病毒,因为它每年 4 月 26 日发作。他的动机非常简单:想让一家在广告上号称「百分之百防病毒」的软件公司丢脸。但它成了世界历史上影响最大的病毒之一。
它特别的地方在于——它可以破坏硬件。因为它的 payload 会改写固件,改写之后电脑就真的成砖了,CPU Reset 后执行的指令已经被完全 corrupt,只能拆主板、换 EEPROM。当年作者被捕,未被定罪(时年 23 岁)。
📷 CIH 病毒
回到原视频 (01:11:45)
今年讲师做了一件事:把 CIH 的汇编代码让 AI 翻译成了 C,于是它一下子变得非常好读。病毒之所以叫病毒,是因为它像流感、新冠一样能感染、传播、潜伏、再发作:
- 感染阶段:通过劫持中断描述符表获得 Ring 0 特权,先驻留在系统里而不发作。它把代码里的某一块(比如 A)抹掉,改成跳转到「A+」(行为与 A 相同,但额外做自己的事)再跳回去——系统运行起来好像没变化,只是稍微变慢一点,因为偶尔会扫描有没有磁盘、把病毒传播出去。
- 发作阶段:判断是不是 4 月 26 日(作者的生日),然后执行擦除 BIOS 的操作——向指定硬件 I/O 端口写入特定序列解锁,再直接覆盖 BIOS 数据。
flowchart TD
A["病毒代码"] --> B["劫持中断描述符表, 获得 Ring 0"]
B --> C["抹掉一块代码 A, 改为跳转到 A+"]
C --> D["A+ = 原逻辑 + 病毒逻辑, 然后跳回"]
D --> E{"是 4 月 26 日?"}
E -->|否| F["继续潜伏 / 感染磁盘上的程序"]
E -->|是| G["向 I/O 端口写序列解锁写保护"]
G --> H["覆盖 BIOS 数据 → 主板成砖"]
F --> D
📷 CIH 病毒汇编源码(cih-1.4.asm)
回到原视频 (01:14:50)
编者说明:讲师坦言,在那个时候能把这样的病毒调试正确是相当了不起的成就;他也借此提醒——历史上的年轻人和今天玩 AI 的年轻人一样,内心都想「干点这样的事」,但更希望大家做「正义的白帽子」,让世界变得更好。
4.3 如何预防病毒破坏固件
(参考时间: 01:19)
只允许写入信任的固件更新——数字签名机制。
固件更新由固件自身(而非操作系统)完成:把要更新的固件写到固件分区,下次启动时固件验证它是否经过正确的数字签名。这个过程是操作系统上的任何程序都无法破坏的:你即使获得整个计算机的控制权(rootkit),只要固件自己才有权限打开写保护,系统就极难攻破。这背后是公钥加密(Diffie-Hellman/RSA)与今天的 SSL/TLS(HTTPS)。
编者拓展|数字签名、公钥加密与 rootkit:公钥加密用一对密钥——私钥自己留着、公钥公开;用私钥签名、任何人用公钥验证,就能确认「这段数据确实由持有私钥的人签发」。固件用它来拒绝一切未经授权的刷写(这就是 Secure Boot 的基础)。rootkit 指能拿到最高权限、并长期潜伏在系统里的恶意软件——即便如此,它也改不了由数字签名保护的固件。
编者说明:参考资料还提到 Intel Boot Guard 私钥泄露事件。
现在的电脑病毒:
- 炫技搞破坏的意义越来越小:更安全的操作系统、App Store 机制(软件包经过审查)、云端备份(讲师说他曾不小心
rm -rf了 iCloud,也找回来了)。 - 获取权限的意义越来越大:偷取隐私、LLM Token、移动支付……
讲师由此推测,OpenClaw(「小龙虾」)这样的平台总有一天会出一次重大的安全事故,逼迫大家反思怎么做对安全。AI 时代的安全攻击很像诈骗:通过骗取信任,把你的 token、钱转出来。他希望大家研究安全问题,但做正义的白帽子。
📷 如何预防病毒破坏固件?
回到原视频 (01:19:30)
五、从固件到操作系统
5.1 回到 43 年前
(参考时间: 01:23)
IBM PC / PC-DOS 2.0(1983) 确立了固件加载操作系统的经典方式。它与 CPU Reset 有个约定:x86 上 CS:IP 等于一个确定的值(如 0xFFFF0),固件(也叫 BIOS)从这里开始执行,然后按顺序扫描系统里所有的存储设备。这解释了一个童年疑问——为什么有 A 盘、B 盘、C 盘:因为软盘(A、B)排在硬盘(C)之前,可启动软盘会优先启动。
编者拓展|
CS:IP(程序计数器):IP(Instruction Pointer)就是 PC;CS(Code Segment)是代码段寄存器。x86 用「段 + 偏移」CS:IP共同表示下一条指令的地址(物理地址 = CS × 16 + IP)。这是 16 位实模式的历史遗产,也是理解「为什么 8086 能寻址 1MB」的关键。
那个时代一切都是那么简单:一个存储设备就是一个字节数组,逻辑上按 512 字节组织。固件管的是:如果磁盘的第一个 512 字节的尾部是 0x55 和 0xAA,就把这 512 字节加载到内存 0x7C00,并设置 CS:IP = 0x7C00,然后跳转过去执行。
编者拓展|MBR 与引导扇区:磁盘第一个 512 字节叫引导扇区,其中前 446 字节是主引导记录(MBR, Master Boot Record)(一小段程序),接着 64 字节是分区表,最后 2 字节是固定「魔数」
0x55 0xAA——固件正是靠这两个字节判断「这块盘可不可以启动」。
0x55 = 0b01010101
0xAA = 0b10101010
(当然签名可以选任何数字,它只是约定。)这 512 字节里有一个 boot loader(启动引导器),它再把一个 ELF 可执行文件加载到内存执行。
📷 回到 43 年前
回到原视频 (01:23:30)
构造一个可启动镜像
既然约定如此简单,我们就能亲手造一个启动盘——只用一条命令,构造一个以 EB FE(一条经典的 jmp . 死循环)开头、后面跟 508 个零、最后两个字节是 0x55 0xAA 的字节序列:
$ (printf "\xeb\xfe"; cat /dev/zero | head -c 508; printf "\x55\xaa") > a.img
$ qemu-system-i386 -hda a.img
用 QEMU 运行它,你会看到 Booting from Hard Disk...,并且 CS:EIP 正好是 0x7C00。如果把 0x55AA 改掉,同样的镜像就不再被识别——QEMU 会依次尝试 CD-ROM、网络启动等,最后报出 No bootable device。
📷 QEMU 启动 EB FE 镜像:CS:EIP = 0x7C00
回到原视频 (01:28:05)
📷 改掉 0x55AA 后 QEMU 报出 "No bootable device"
回到原视频 (01:28:45)
早期的固件,行为就是这么简单、容易理解——所以我们真的可以调试它。
5.2 如果固件也是代码:调试加载过程
(参考时间: 01:29)
一个自然而然的问题:既然固件也是代码,那我们是不是能看到「把 512 字节从磁盘加载到内存」的过程?CPU Reset 时 0x7C00 处应该还什么都没有,所以理论上应该能看到固件里有一段代码负责这个搬运。
能想到的事情就一定有人能做到。想找到「到底是哪一条指令加载了磁盘数据」,用 GDB 的一个功能就够了——Watch Point:
编者拓展|Watch point(监视点):GDB 的一种断点——当某个内存地址被读/写时就停下来(普通断点只在「执行到某行代码」时停)。它特别适合追查「到底是谁改了这个变量 / 写了这块内存」,比如这里找「哪条指令把启动扇区搬到了
0x7C00」。
- 在
0x7C00这个位置的内存上设一个 watch point; - 或在
0x7C00设一个 breakpoint,当指令跳转过去执行时就知道。
再让 AI 写一句「每次停下来时打印 0x7C00 附近的内存」,用 init.gdb 配合 hook,就能在调试专属任务时把 GDB 扩展得更强大。这曾是我们系统程序员最重要的护城河——知道什么事情可以做、知道怎么找系统里的问题。你会发现:固件的执行与操作系统的执行、应用程序的执行,没有本质区别——因为 Everything is a state machine。
📷 如果固件也是代码
回到原视频 (01:30:00)
演示里用的 make debug 会同时启动 QEMU 和 GDB(因为是 arm64,需要用 gdb-multiarch 去调试 x86 代码),执行 GDB 脚本。程序刚停下时,0x7C00 附近全是零;单步执行后,你会看到它两个字节、两个字节地被加载到正确位置。此时 CS = 0xF000,说明固件代码正在运行。
📷 用 GDB watch point 调试固件加载 0x7C00 的过程
回到原视频 (01:34:20)
5.3 固件与系统程序员的第一个接口
一个 512 字节的 MBR,去掉末尾的 55 AA(2 字节)和分区表(64 字节),剩下 446 字节可写。这就是系统程序员与固件的第一个接口:写 446 字节的 16 位代码。
以 Grub 为例,它是一个多阶段的引导器:
- Stage 1:扫描磁盘,找到附近的 ELF 文件头,加载到内存(根据文件系统类型,可能需要 Stage 1.5);
- Stage 2:这个 ELF 文件就是 Grub,弹出我们熟悉的选择系统窗口;
- Stage 3:加载 Linux Kernel。
📷 Firmware 和系统程序员的第一个接口
回到原视频 (01:26:55)
编者说明:早期课程会把这段引导代码逐行讲清楚,今天只建立概念——重点仍是「计算机系统里真的没有任何魔法」。
5.4 RISC-V 与 OpenSBI
(参考时间: 01:23 & 收尾)
现代系统不再用 1983 年那套方式,而是用像 OpenSBI 这样的开源固件。OpenSBI 是 RISC-V 的 Supervisor Binary Interface(SBI) 实现,管理 RISC-V 平台的启动过程,为操作系统提供硬件抽象层,简化系统启动与硬件管理,广泛用于嵌入式系统和开发板。
编者拓展|SBI 与 OpenSBI:RISC-V 把运行模式分成 M/S/U 三级,SBI(Supervisor Binary Interface)是 M-mode 固件提供给 S-mode 操作系统的一组标准调用(类似操作系统给应用的
syscall)。OpenSBI 就是这套接口的开源实现——它扮演的角色,正相当于 x86 上的 BIOS/UEFI。
- 项目地址:https://github.com/riscv-software-src/opensbi
- 以前的年代要靠「古法阅读」并反复试错,Agent 还常常不听指令;今天只要提问就行——「OpenSBI 的入口位于什么地方?如何组织……?」在大型系统上,self-attention 与「编程直觉」强得可怕。
📷 RISC-V: 固件与操作系统引导(OpenSBI)
回到原视频 (01:35:20)
总结
- 硬件不知道有没有操作系统:它是一台无情执行指令的状态机,见什么指令执行什么指令;它提供中断、I/O 等机制,正是这些机制让操作系统得以实现。
- 操作系统就是一个普通的二进制程序:它接管中断与 I/O,把应用程序放到 CPU 上运行一会,中断(系统调用)后自己又开始执行——启动完成后,它本质上是一个中断处理程序。
- 抽象是隔离复杂性的根本手段:应用与操作系统之间有
syscall,操作系统与硬件之间有指令集,都是「上复杂、下复杂、中间接口简单」。 - 固件(Firmware) 是 CPU Reset 后执行的、厂商固化在系统里的代码,负责初始化硬件并加载操作系统;它本身就是一段普通代码,因此可调试、也可被攻击。
- CIH 病毒 之所以能破坏硬件,是因为当时固件可写;今天的数字签名把固件更新变成只有固件自己才能开写的操作,使其极难攻破。
- 调试固件 与调试普通程序没有本质区别:
Everything is a state machine。用 QEMU + GDB 的 watch point,你能亲眼看到固件把启动扇区加载到0x7C00的过程。
附:官方参考与延伸阅读
本讲的讲义与幻灯片见课程主页,以下链接按书中出现顺序整理:
阅读材料:大家可以在 AI 的帮助下,了解你感兴趣的体系结构的执行模型(CPU Reset 行为、中断处理、特权级等);了解 BIOS/UEFI 的工作原理;尝试编译 OpenSBI 并理解固件的编译链接过程。
版权说明:本讲讲义与幻灯片作者为蒋炎岩,采用 Creative Commons BY-NC 4.0 许可,来源于 https://jyywiki.cn/OS/2026/。视频画面与字幕版权归原作者所有。本书为课堂内容的学习性重构。