跳转至

环境与启动 — 裸机上的第一个程序

这一章讲什么: 一台没有任何操作系统的电脑,怎么让它执行你写的第一个程序? 答案是借主板固件(UEFI BIOS)的手:固件开机后自己去存储设备上找 BOOTX64.EFI, 把它读进内存再跳过去执行。本章把这条「启动链」从头走一遍,顺便立好全书规矩: 开发机/测试机、U 盘、模拟器、「抄经」式学习法。 读完你会拿到全书的坐标系——后面每一章做的事,都能挂回这条链子上。

1. 先看现象:一个手工敲出来的文件,开机自己跑了

这一章的主线例子,就是从零到「Hello, world!」上屏的完整旅程:

  你(用二进制编辑器敲 1480 个字节的十六进制数)
      │  保存为 BOOTX64.EFI
      ▼
  U 盘(FAT 格式,/EFI/BOOT/BOOTX64.EFI)
      │  插进测试机,按电源
      ▼
  UEFI BIOS(主板固件,开机第一个跑的程序)
      │  初始化硬件 → 搜遍存储设备找 /EFI/BOOT/BOOTX64.EFI → 读进主内存
      ▼
  CPU 跳过去执行 → 屏幕上出现 Hello, world!

  图说:你不写操作系统之前,固件替你干了「找文件、读文件」这些活。
  1480 字节与「搜遍存储设备」的措辞以原文为准[^1]。

这个程序不是用任何编程语言写的——作者故意让你用二进制编辑器一个数一个数地敲, 敲完用 sum 命令对答案:校验和是 12430 就对了,不对就得肉眼找错2。 为什么要这么自虐?因为这本书的立场是:先看到计算机最裸的样子,再谈工具。 你会在 1.9 节看到同一程序的 C 语言版本——那时你就知道编译器替你省了什么。

2. 顶层全景:计算机启动时,谁在跑

  电源键
    │
    ▼
  ROM 里的 UEFI BIOS(断电不丢,所以开机一定有它)
    │  ① 设置 CPU 运行模式 ② 检测配置 PCI 设备 ③ 搜索存储设备上的 BOOTX64.EFI
    ▼
  BOOTX64.EFI(UEFI 应用)被读进主内存,CPU 跳去执行
    │
    ▼
  (本书的终点:这个程序将来升级成「引导加载器」,负责把真正的内核读进来)

  图说:CPU 只能执行主内存里的程序,而主内存断电即空——
  所以开机时内存里唯一的程序,是固化在 ROM 里的 BIOS。

两件必须记住的事实,是全书的地基:

  • 主内存(内存条)断电就空。 所以「开机时内存里有什么」这个问题只有一个答案:主板 ROM 里固化的 BIOS3。
  • CPU 只认机器语言(二进制指令)。 你写的任何程序,最终都要变成一串 0 和 1 才能跑4。

3. 核心原理:这条链上每个名词到底是什么

3.1 固件与 UEFI:开机后第一个程序

BIOS(Basic Input Output System)是计算机开机执行的第一个程序,属于「固件」 (烧在硬件里、断电不丢的软件)。它管两件事:开机时初始化计算机内部结构, 以及从存储器里把操作系统(的引导程序)读出来5。

现在主板里的都是 UEFI BIOS——按 UEFI(统一可扩展固件接口)标准实现的固件。 2006 年《30天自制操作系统》出版时,业界刚从「传统 BIOS」转向 UEFI;今天 UEFI 已完全取代传统 BIOS6。 这本书里说的 BIOS 一律指 UEFI BIOS。

被 BIOS 加载运行的 BOOTX64.EFI 有个正式身份:UEFI 应用。 UEFI 应用可以免费使用 BIOS 提供的功能:在屏幕上显示文本、从键盘读输入、从存储器读文件7。 这一点改变了自制操作系统的门槛——以前引导加载器连硬盘驱动都得自己写, 现在调用 UEFI 函数就行。全书第一个阶段(第 1–3 章)就是写好这一个 UEFI 应用, 让它从「Hello world」进化成「把内核装进内存的搬运工」(即引导加载器)8。

3.2 二进制、字节、十六进制:CPU 世界的计量单位

CPU 是数字电路,只认识高电平(1)和低电平(0),所以一切最终都是二进制数。 为了人看着方便,约定:

  • 1 个二进制数位 = 1 比特(bit);8 个比特 = 1 字节(byte),字节是本书的基本单位;
  • 二进制写起来太长,所以用十六进制 shorthand:0~9 再加 A~F 共 16 个数字,前缀 0x;
  • 1 位十六进制恰好对应 4 位二进制(0xF = 0b1111),换算只要每 4 位切一刀9。

你敲进 BOOTX64.EFI 的那个数串就是十六进制。它其实只装了两样东西: 机器语言指令(0x0200~0x022f 那段,给 CPU 读的)和数据(0x0400~0x041b 那段, 就是你看到的 "Hello, world!",外加元数据——描述文件本身的附加信息)10。

3.3 字符编码与小端:字母是怎么变成数的

字母不是数字,所以计算机「把字母和数字关联起来处理」,这张对照表叫字符编码。 最著名的是 ASCII:A~Z 对应 0x41~0x5A,a~z 对应 0x61~0x7A,所以 H 是 0x48、o 是 0x6f11。

但 BOOTX64.EFI 里用的不是 ASCII,而是 UCS-2(Unicode 的一种):一个字符占两个字节。 英文字符范围内它的值和 ASCII 相同,只是后面多一个 0x00 字节——所以在二进制编辑器里看, 每个字母之间都隔着 0012。

小端模式说的是多字节数怎么排:数 0x0102 在文件里按「低字节在前」排成 02 01 就是小端, 反过来就是大端。x86-64 架构用小端,所以你在二进制编辑器里要先敲低字节13。

3.4 可执行文件格式:PE、COFF、ELF

UEFI 应用的实体是 PE 格式的文件——也就是 Windows 标准可执行文件格式。 1.9 节用两行命令把 C 语言源码变成新的 BOOTX64.EFI:

  hello.c --(clang -target x86_64-pc-win32-coff)--> hello.o --(lld-link /subsystem:efi_application)--> hello.efi

  图说:编译器把源码变成对象文件,链接器把对象文件缝合成可执行文件。

三个格式的关系,记住一句话就够:它们都是「装机器语言的文件格式」,区别只在内部怎么排布元数据, 像 Word、PDF、HTML 都能装同一篇文章14:

格式 是什么 在本书里的角色
COFF 对象文件格式(中间产物) clang 先编译出 hello.o
PE Windows 的可执行文件格式 UEFI 应用必须是它,链接器加 /subsystem:efi_application 生成
ELF Linux 的格式,可执行文件和对象文件通吃 本书不用它做 UEFI 程序,但第 18–19 章给 MikanOS 加载应用时会当「被解析的格式」详细讲

用 Linux 的 clang 也能造 PE 文件:关键是 -target x86_64-pc-win32-coff 这个开关, 让编译器按 Windows 的 COFF 格式输出,链接器才能接手15。

3.5 模拟器:没有测试机也能玩

真机测试需要反复插拔 U 盘,所以日常开发用 QEMU——用软件模拟一整台计算机的模拟器。 两个要点:

  1. 先造一个 200MB 的磁盘镜像(qemu-img create + mkfs.fat),把 BOOTX64.EFI 放进镜像的 /EFI/BOOT/;
  2. 启动 QEMU 时必须挂两份 OVMF 固件文件(-drive if=pflash,...OVMF_CODE.fd / OVMF_VARS.fd)—— 少了它们,QEMU 会按传统 BIOS 模式启动,而我们的程序是 UEFI 应用,跑不了16。

这两步被作者包成一个 run_qemu.sh 脚本,以后每章的运行命令都缩成一行17。

4. 作者的判断与证据

判断一:个人做操作系统,完全可行,诀窍是「先做玩具」。 Linux 4.10 约 2100 万行代码,个人当然碰不动;但 Linux 的第一个版本 0.01(1991 年)只有 10 239 行, 《30天自制操作系统》里的系统只有 4 249 行、编译后约 40KB——能小到什么程度,就能从头做到什么程度18。 方法上作者反对「学完全部理论再动手」:制作操作系统的诀窍是不要一开始就追求完美, 先做一个看起来像操作系统的玩具,玩具越攒越多,就越来越像真的19。

判断二:操作系统的定义模糊,但主流操作系统的功能可以归纳为三个方面。(这是作者对「到底什么是操作系统」的正式回答)

方面 管什么 例子
应用接口 给应用提供简化、统一的接口,把硬件的复杂性吸收掉(这叫抽象) 存储设备各不相同,应用只管「读文件」;TCP/UDP 底下是以太网还是 Wi-Fi,应用看不见
计算资源分发者 CPU、内存、存储读写都有限,操作系统负责分配,让多个应用并行 视频编码时鼠标还能动,是因为编码愿意暂停、光标移动要优先
人看到的接口 让人类与计算机打交道:统一菜单、通用快捷键 Ctrl+C 复制在哪个应用里都一样

20

判断三:「抄经」是学习本书的正确姿势。 「抄经」指自己逐字符输入示例代码,而不是复制粘贴——好处是你会注意到细节; 作者承认自己当年就是靠复制粘贴读完了《30天自制操作系统》,但认为抄经理解更深21。 配套的完整代码差异要用 git diff 看,因为正文并没有列出全部修改22。

证据的分量: 本章的「证据」是演示性的——校验和 12430、偏移量 0x0200/0x0400 都来自那个具体的示例文件, 你可以在配套源码($HOME/osbook/day01/c/hello.c)里逐条核对23。

5. 边界与局限

  • UEFI 不是操作系统的一部分,而是跳板。 本书借 UEFI 读文件、画图的能力跳过了最苦的硬件初始化; 想连引导都从零(传统 BIOS、自己写磁盘驱动)的人,这本书满足不了。
  • 安全启动(Secure Boot)会拦你的程序。 真机上 BOOTX64.EFI 跑不起来,常见原因就是主板开启了安全启动—— 它本来防的是被篡改的恶意系统,顺手把自制 OS 也防了,得进 BIOS 设置关掉24。
  • 硬件依赖真实存在。 作者明说 MikanOS 很可能无法在读者电脑上启动,「这是操作系统开发的难点,也是乐趣所在」25。
  • 0.2 节对「操作系统」的定义是有意从宽的——作者引用《30天自制操作系统》的话:「任何软件都可以是操作系统, 只要它的创造者坚持认为它是,并且周围的人也认为它是。」别拿这当考试答案。

6. 可带走的

本章主线例子,一行写完: 手工敲 1480 字节 → BOOTX64.EFI → FAT U 盘的 /EFI/BOOT/ → UEFI BIOS 读进内存 → CPU 执行 → Hello, world!

  1. 电脑开机后的执行顺序是死的: ROM 里的 UEFI BIOS → 找 BOOTX64.EFI → 读进主内存 → 跳过去执行;
  2. 主内存断电即空,CPU 只执行内存里的机器语言——这两条解释了为什么必须有「引导」这件事;
  3. UEFI 应用 = PE 格式的可执行文件,可以用 Windows toolchain(lld-link + /subsystem:efi_application)造出来;
  4. 二进制/十六进制/字节/小端是这一行的日常单位:1 字节 = 8 比特,1 位十六进制 = 4 位二进制,x86 是小端;
  5. 字符要经字符编码(ASCII/UCS-2/…)才能变成数;BOOTX64.EFI 里每个字母之间隔一个 00,就是 UCS-2 的痕迹;
  6. 没有测试机就用 QEMU,但记得挂 OVMF 固件,否则它以传统 BIOS 模式启动;
  7. 真机跑不起来先查安全启动;
  8. 学习姿势:抄经 + git diff,不要复制粘贴;
  9. 「完美是完成的敌人」:先做玩具,玩具攒多了就是真的。

7. 原文地图

主题 原书章 原文位置
制作 OS 的两条路、先做玩具 0.1 如何制作操作系统 text/06-ch00-01-0-1.txt:16(搜「修改现有的操作系统」) · text/06-ch00-01-0-1.txt:25(搜「看起来像操作系统的玩具」)
Linux 0.01 只有 10 239 行 0.1 如何制作操作系统 text/06-ch00-01-0-1.txt:7(搜「10 239行」)
操作系统的三个方面 0.2 到底什么是操作系统 text/07-ch00-02-0-2.txt:16(搜「三个方面」) · text/07-ch00-02-0-2.txt:46(搜「应用接口方面」)
抽象:吸收硬件差异 0.2 到底什么是操作系统 text/07-ch00-02-0-2.txt:40(搜「被称为抽象」)
POSIX 与 Newlib 0.2 到底什么是操作系统 text/07-ch00-02-0-2.txt:87(搜「POSIX」)
开发机/测试机/U盘流程 0.3 自制操作系统的步骤 text/08-ch00-03-0-3.txt:11(搜「开发机上编写」) · text/08-ch00-03-0-3.txt:16(搜「GPD MicroPC」)
工具链选 Clang 0.4 享受自制操作系统的乐趣 text/09-ch00-04-0-4.txt:1(搜「Clang」)
抄经、git diff 0.4 享受自制操作系统的乐趣 text/09-ch00-04-0-4.txt:17(搜「抄经」) · text/16-ch01-07-1-7-uefi-bios.txt:5(搜「git diff」)
全书鸟瞰(第1–3章做引导加载器) 0.5 自制操作系统的全貌 text/10-ch00-05-0-5.txt:15(搜「引导加载器是将操作系统加载」)
二进制编辑器、校验和 12430 1.1 Hello World text/11-ch01-01-1-1-hello-world.txt:10(搜「二进制编辑器」) · text/11-ch01-01-1-1-hello-world.txt:63(搜「12430」)
U 盘 FAT、/EFI/BOOT 1.1 Hello World text/11-ch01-01-1-1-hello-world.txt:75(搜「mkfs.fat」)
安全启动 1.1 Hello World text/11-ch01-01-1-1-hello-world.txt:111(搜「安全启动」)
QEMU 与 OVMF 1.4 使用模拟器 text/13-ch01-04-1-4.txt:22(搜「模拟计算机」) · text/14-ch01-05-1-5.txt:16(搜「传统BIOS模式」)
可执行文件、机器语言与数据 1.5 最终做了些什么 text/14-ch01-05-1-5.txt:42(搜「可执行文件」)
十六进制与二进制对应 1.5 最终做了些什么 text/14-ch01-05-1-5.txt:83(搜「十六进制」) · text/14-ch01-05-1-5.txt:121(搜「一一对应」)
字符串在 0x0400、指令在 0x0200 1.5 最终做了些什么 text/14-ch01-05-1-5.txt:134(搜「0x0400」) · text/15-ch01-06-1-6.txt:11(搜「0x0200」)
UCS-2 与小端 1.5 最终做了些什么 text/15-ch01-06-1-6.txt:1(搜「UCS-2」) · text/15-ch01-06-1-6.txt:7(搜「小端模式」)
BIOS/UEFI 启动流程 1.7 使用UEFI BIOS启动 text/16-ch01-07-1-7-uefi-bios.txt:12(搜「固件」) · text/16-ch01-07-1-7-uefi-bios.txt:24(搜「主内存中的内容会丢失」) · text/17-ch01-08-1-8.txt:1(搜「设置CPU的运行模式」)
UEFI 应用可调用的功能 1.7 使用UEFI BIOS启动 text/17-ch01-08-1-8.txt:6(搜「UEFI应用可以使用」)
clang/lld-link 造 PE 1.9 C语言版的Hello World程序 text/18-ch01-09-1-9-c-hello-world.txt:31(搜「clang -target」) · text/18-ch01-09-1-9-c-hello-world.txt:35(搜「PE格式」)
PE/COFF/ELF 专栏 1.9 C语言版的Hello World程序 text/18-ch01-09-1-9-c-hello-world.txt:94(搜「PE、COFF和ELF有一个共同点」)

  1. 出处:「1.1 Hello World」第 55 段起(text/11-ch01-01-1-1-hello-world.txt:55,搜「十六进制」);文件总长以示例校验和上下文推断,原文未直接印字节数。 ↩

  2. 出处:「1.1 Hello World」(text/11-ch01-01-1-1-hello-world.txt:59,搜「校验和」;:63,搜「12430」)。原文:「如果在二进制编辑器中输入是正确的,那么BOOTX64.EFI的校验和应为12430」;并坦承「仅凭校验和并不能确定错误的位置」。 ↩

  3. 出处:「1.7 使用UEFI BIOS启动」(text/16-ch01-07-1-7-uefi-bios.txt:24,搜「主内存中的内容会丢失」)。原文:「关机后,主内存中的内容会丢失……因为CPU只能执行主内存中的程序」。 ↩

  4. 出处:「1.5 最终做了些什么」(text/14-ch01-05-1-5.txt:56,搜「数字电路」;:125,搜「如果不用二进制数表示」)。 ↩

  5. 出处:「1.7 使用UEFI BIOS启动」(text/16-ch01-07-1-7-uefi-bios.txt:12,搜「固件」)。 ↩

  6. 出处:「1.7 使用UEFI BIOS启动」(text/16-ch01-07-1-7-uefi-bios.txt:18,搜「传统BIOS」)。 ↩

  7. 出处:「1.7 使用UEFI BIOS启动」(text/17-ch01-08-1-8.txt:5,搜「UEFI 应用」)。原文:「UEFI应用可以使用UEFI BIOS的功能,例如,在屏幕上显示文本、从键盘输入文本、从存储器中读取文件等」。 ↩

  8. 出处:「0.5 自制操作系统的全貌」(text/10-ch00-05-0-5.txt:15,搜「引导加载器是将操作系统加载」)。 ↩

  9. 出处:「1.5 最终做了些什么」(text/14-ch01-05-1-5.txt:79,搜「字节」);:121,搜「一一对应」)。 ↩

  10. 出处:「1.5 最终做了些什么」(text/14-ch01-05-1-5.txt:134,搜「0x0400」)与「1.6 还是让我们动手吧」(text/15-ch01-06-1-6.txt:11,搜「0x0200」)。「机器语言指令的位置为0x0200~0x022f」「要显示字符串的位置为0x0400~0x041b」。 ↩

  11. 出处:「1.5 最终做了些什么」(text/14-ch01-05-1-5.txt:137,搜「0x41」;:139,搜「字符编码」)。 ↩

  12. 出处:「1.6 还是让我们动手吧」(text/15-ch01-06-1-6.txt:1,搜「UCS-2」)。例如「H在ASCII编码中是0x48,那么在UCS-2中就是0x0048」。 ↩

  13. 出处:「1.6 还是让我们动手吧」(text/15-ch01-06-1-6.txt:7,搜「小端模式」))。 ↩

  14. 出处:「1.9 C语言版的Hello World程序」专栏1.1(text/18-ch01-09-1-9-c-hello-world.txt:94,搜「PE、COFF和ELF有一个共同点」)。 ↩

  15. 出处:「1.9 C语言版的Hello World程序」(text/18-ch01-09-1-9-c-hello-world.txt:37,搜「target」;:40,搜「lld-link」)。 ↩

  16. 出处:「1.4 使用模拟器」(text/14-ch01-05-1-5.txt:11,搜「if=pflash」;:16,搜「UEFI模式」)。原文:「如果要在UEFI模式下启动QEMU,则需要使用第2行和第3行的长选项。如果没有这些选项,QEMU将以传统BIOS模式启动。」 ↩

  17. 出处:「1.4 使用模拟器」(text/14-ch01-05-1-5.txt:25,搜「run_qemu.sh」))。 ↩

  18. 出处:「0.1 如何制作操作系统」(text/06-ch00-01-0-1.txt:7,搜「10 239行」;:13,搜「4 249行」)。 ↩

  19. 出处:「0.1 如何制作操作系统」(text/06-ch00-01-0-1.txt:25,搜「看起来像操作系统的玩具」)。 ↩

  20. 出处:「0.2 到底什么是操作系统」(text/07-ch00-02-0-2.txt:16,搜「三个方面」;:31,搜「被称为抽象」;:46,搜「应用接口方面」;:47,搜「计算资源分发者」;:63,搜「人看到的接口方面」)。 ↩

  21. 出处:「0.4 享受自制操作系统的乐趣」(text/09-ch00-04-0-4.txt:17,搜「抄经」;:23,搜「深刻理解」)。 ↩

  22. 出处:「1.7 使用UEFI BIOS启动」(text/16-ch01-07-1-7-uefi-bios.txt:5,搜「git diff」)。 ↩

  23. 出处:「1.9 C语言版的Hello World程序」(text/18-ch01-09-1-9-c-hello-world.txt:16,搜「hello.c」)。 ↩

  24. 出处:「1.1 Hello World」(text/11-ch01-01-1-1-hello-world.txt:111,搜「安全启动」)。 ↩

  25. 出处:「0.5 自制操作系统的全貌」(text/10-ch00-05-0-5.txt:5,搜「这是操作系统开发的难点」)。 ↩