跳转至

FAT 与第一个应用 — 顺着簇链读完一个文件,再把它跑起来

这一章讲什么: 上一章的 ls 只会点名,读不了大于一个簇的文件。 本章补上文件系统的最后一张表——文件分配表(FAT):簇号的链表, 顺着它走,cat 能吐任意大的文件。然后做那件「正是 OS」的事: 把磁盘上的可执行文件读进来,跳过去执行——先是 3 字节的汇编程序, 再是 C++ 写的逆波兰计算器,最后是链接了标准库、Linux/MikanOS 通吃的真应用。

1. 先看现象:簇链、冻结的终端、和算错的计算器

主线例子 A,BOOTX64.EFI 的簇链:

  FAT 是 uint32 数组:fat[簇号] = 下一个簇号
  disk.img 实测:5→6→7→…→15→16→(0x0FFFFFFF = 链尾)
    → 引导加载器 12 个簇 × 1KiB ≈ 12KiB,与实际 11.8KiB 吻合
  cat 命令 = FindFile(名字 → 起始簇) + 逐簇打印 + cluster = fat[cluster] 直到链尾

主线例子 B,rpn 计算器:

  $ rpn 2 3 +        ← 终端把 "2 3 +" 拆成 argv
  结果 = 5?          ← 裸写 strcmp/atol 的版本:对
  链接标准库的重写版:打印 0 ← bug!原因:退出码根本没被正确取回(下一章揭晓)

2. 顶层全景

  18.1 FAT:簇链语义(≥0x0FFFFFF8 = 链尾);NextCluster();FindFile();cat 命令
  18.2 第一个应用:onlyhlt(3 字节,nasm -f bin)→ ExecuteFile():读文件、当函数调用
       冻结 bug:漏写 sti,关中断执行 hlt → 任务切换中断永远不来
  18.3 C++ 应用:无头二进制表达力不够 → ELF;rpn(逆波兰式);
       无标准库,手写 strcmp/atol;-ffreestanding;--image-base 0;ELF 魔数分流;
       MakeArgVector() 造 argv
  18.4 标准库:OS 和应用同 CPU,直接链接 Newlib/libc++(-lc -lc++ -lc++abi);
       但结果变 0,悬念挂到第 19 章

  图说:文件系统读文件的终点,是执行文件的起点——「应用时代」开幕。

3. 核心原理

3.1 FAT:簇号的链表

FAT(文件分配表)记录每个文件占着哪些簇:fat[簇号] = 下一簇号, 链尾是 ≥0x0FFFFFF8 的值(实际卷里 8 和 F 混用,故定义 kEndOfClusterchain 兜底)1。 文件系统通常备两份相同的 FAT(冗余),推荐校验一致性,本书省略2。 有了 NextCluster(),FindFile() 就能处理跨任意多簇的文件和目录: 从目录起始簇开始,逐簇遍历目录项数组,名字匹配就返回那条目录项3。 比较文件名要先把用户输入的 cafe.txt 规范成 8+3 大写格式再比(逐字节,空格补齐)4。 cat 命令 = FindFile + 从起始簇逐簇 Print + 沿链推进5。 注意 cat 出来的 MEMMAP 行尾有个怪符号——那是 UEFI 写文件自动加的 CR(0x0d),不是 bug6。

3.2 执行一个文件:读进来,调过去

终端找不到内置命令时,就 FindFile 同名文件,读进 file_buf, 然后把缓冲区首地址当函数入口调用——和引导加载器启动内核同构7。 第一个应用 onlyhlt 只有 3 字节(f4 = hlt;eb fd = 相对跳转 -3,跳回 hlt), nasm -f bin 生成无头二进制:纯机器语言,没有头、没有静态变量的位置信息—— 表达力只够这种玩具8。

本章最生动的 bug:跑起来整个系统冻结,不只终端—— 原因是 TaskTerminal() 取完消息后忘了 sti,关着中断跳进 hlt; 任务切换定时器的中断永远进不来,所有任务全部停摆。 「如果终端被冻结,则执行成功;如果没有被冻结,则执行失败」—— 预期中的冻结之外,还多抓到一个真 bug9。

3.3 C++ 应用:rpn 与它的脚手架

正经程序需要表达「静态变量占多少字节、字符串放哪」——回到 ELF(第 3、4 章的内核格式)。 样例应用 rpn:逆波兰式计算器,2 3 + = 栈推 2、3,遇 + 弹两个相加,结果当退出码返回10。 它没有标准库可用(MikanOS 还没给应用准备 newlib_support),所以手写 strcmp/atol, 连 #include 都没有11。 构建选项有讲究:-ffreestanding(MikanOS 还不满足托管环境的条件:多线程、标准 I/O、文件系统); --image-base 0(入口点地址按文件放在 0 处计算,加载时平移,第 19 章细说)12。 ExecuteFile 升级:先 memcmp 前 4 字节是不是 "\x7f" "ELF"(两个字符串字面量相邻自动拼接, 写成 "\x7fELF" 会把 0x7fE 当成一个十六进制数——一个可爱的 C 语言注脚)13; 是 ELF 就按 ELF 头取入口点、用 MakeArgVector 把 "2 3 +" 拆成 argv 再调用14。 这个 rpn 在 Linux 上同样能编译运行(a.out 2 3 +;echo $?)—— 同一份源码、两种系统,作者「地衣感动」15。

3.4 链接标准库:同 CPU 的红利

手写 strcmp 让作者「感到有些失败」。转机:OS 和应用跑在同一种 CPU 上, 应用直接链接给内核用的 Newlib/libc++ 即可——加 -lc -lc++ -lc++abi, rpn 重写为带 #include 的普通 C++16。 但跑起来答案从 5 变成 0:退出码的通路有毛病,调查留给下一章—— 「应用是怎么被放进内存、怎么拿到 argv、怎么把结果还回来的」,每一条都将在第 19 章被翻出来修。

4. 作者的判断与证据

判断:应用的入口用「函数调用」而不是新机制。 ExecuteFile 与引导加载器启动内核同构 (读文件、找入口、调用)——刻意复用已懂的机制,把「新东西」压到最小。

判断:标准库能链接就先链接,别重复造轮子——「我不想在标准库里创建自己的函数」。

证据: 簇链与文件实际大小吻合(12 KiB vs 11.8 KiB);onlyhlt 冻结 = 执行成功的反常验证; rpn 在 Linux 和 MikanOS 上同行为;以及结尾处一个明写的「不对劲」(答案变 0)—— 留悬念的方式本身就是教学: bug 是下一章的入口。

5. 边界与局限

  • 两份 FAT 不校验、长名不支持、子目录 FindFile 只支持一层(directory_cluster 参数留了口);
  • cat 逐字节打印,大文件很慢,且无分页(第 30 章的 more 才补);
  • 应用与内核同特权级:onlyhlt 能冻住整个系统就是证据——应用隔离(第 20 章)之前, 所有应用本质都是「内核的一部分」;
  • argv 机制是手搓的:MakeArgVector 按空格切,没有引号转义。

6. 可带走的

本章主线例子,一行写完: fat[簇号]=下一簇 → cat 沿链读文件 → onlyhlt 当函数调用(冻结成功 + sti bug)→ ELF 版 rpn(argv + 手写 strcmp + --image-base 0)→ 链接标准库 → 答案变 0 挂悬念。

  1. FAT = 簇号链表:NextCluster 沿链走,≥0x0FFFFFF8 收尾;一簇装不下的文件全靠它;
  2. 执行文件 = 读进内存 + 调入口,与引导加载器启动内核同构;
  3. 无头二进制只有机器语言,正经程序要 ELF(静态变量、字符串的位置都要交代);
  4. 应用没标准库就手写 strcmp/atol;-ffreestanding + --image-base 0 是裸机应用的标准构建姿势;
  5. ELF 魔数 "\x7f" "ELF" 相邻拼接——写成一个字符串会吃进 0x7fE 的解析陷阱;
  6. 关中断 + hlt = 睡死;漏 sti 这类「最后一步」的 bug,症状是整个系统冻结;
  7. 同 CPU 的 OS 与应用共享同一套 Newlib/libc++,链接即用。

7. 原文地图

主题 原书章 原文位置
簇链语义与示例 18.1 文件分配表(osbook_day18a) text/115-ch18-01-18-1-osbook-day18a.txt:7(搜「簇链」) · text/115-ch18-01-18-1-osbook-day18a.txt:35(搜「12KiB」)
两份 FAT 与 kEndOfClusterchain 18.1 文件分配表(osbook_day18a) text/115-ch18-01-18-1-osbook-day18a.txt:38(搜「两个相同」) · text/115-ch18-01-18-1-osbook-day18a.txt:53(搜「0x0FFFFFF8」)
FindFile 与 8+3 名字比较 18.1 文件分配表(osbook_day18a) text/115-ch18-01-18-1-osbook-day18a.txt:59(搜「FindFile」) · text/115-ch18-01-18-1-osbook-day18a.txt:75(搜「NameIsEqual」)
cat 与 CR 注脚 18.1 文件分配表(osbook_day18a) text/115-ch18-01-18-1-osbook-day18a.txt:88(搜「cat命令」) · text/116-ch18-02-18-2-osbook-day18b.txt:13(搜「CR」)
onlyhlt 与相对短跳转 18.2 第一个应用(osbook_day18b) text/116-ch18-02-18-2-osbook-day18b.txt:27(搜「onlyhlt」) · text/116-ch18-02-18-2-osbook-day18b.txt:58(搜「相对短跳转」)
ExecuteFile 与冻结 bug 18.2 第一个应用(osbook_day18b) text/116-ch18-02-18-2-osbook-day18b.txt:71(搜「ExecuteFile」) · text/116-ch18-02-18-2-osbook-day18b.txt:82(搜「sti并没有被执行」)
逆波兰式算法 18.3 C++中的计算器(osbook_day18c) text/117-ch18-03-18-3-c-osbook-day18c.txt:49(搜「不使用括号」) · text/117-ch18-03-18-3-c-osbook-day18c.txt:62(搜「14就是计算结果」)
手写 strcmp/atol、无 include 18.3 C++中的计算器(osbook_day18c) text/117-ch18-03-18-3-c-osbook-day18c.txt:83(搜「没有#include」)
-ffreestanding 与 --image-base 0 18.3 C++中的计算器(osbook_day18c) text/117-ch18-03-18-3-c-osbook-day18c.txt:98(搜「独立环境」) · text/117-ch18-03-18-3-c-osbook-day18c.txt:109(搜「image-base」)
ELF 魔数与 argv 18.3 C++中的计算器(osbook_day18c) text/117-ch18-03-18-3-c-osbook-day18c.txt:120(搜「memcmp」) · text/117-ch18-03-18-3-c-osbook-day18c.txt:125(搜「合并」)
链接标准库与变 0 的悬念 18.4 标准库(osbook_day18d) text/118-ch18-04-18-4-osbook-day18d.txt:20(搜「-lc」) · text/118-ch18-04-18-4-osbook-day18d.txt:34(搜「显示为0」)

  1. 出处:「18.1 文件分配表(osbook_day18a)」(text/115-ch18-01-18-1-osbook-day18a.txt:7(搜「簇链」); text/115-ch18-01-18-1-osbook-day18a.txt:35(搜「12KiB」))。 ↩

  2. 出处:「18.1 文件分配表(osbook_day18a)」(text/115-ch18-01-18-1-osbook-day18a.txt:38(搜「两个相同」); text/115-ch18-01-18-1-osbook-day18a.txt:53(搜「0x0FFFFFF8」))。 ↩

  3. 出处:「18.1 文件分配表(osbook_day18a)」(text/115-ch18-01-18-1-osbook-day18a.txt:59(搜「FindFile」); text/115-ch18-01-18-1-osbook-day18a.txt:75(搜「NameIsEqual」))。 ↩

  4. 出处:「18.1 文件分配表(osbook_day18a)」(text/115-ch18-01-18-1-osbook-day18a.txt:88(搜「cat命令」); text/116-ch18-02-18-2-osbook-day18b.txt:13(搜「CR」))。 ↩

  5. 出处:「18.2 第一个应用(osbook_day18b)」(text/116-ch18-02-18-2-osbook-day18b.txt:27(搜「onlyhlt」); text/116-ch18-02-18-2-osbook-day18b.txt:58(搜「相对短跳转」))。 ↩

  6. 出处:「18.2 第一个应用(osbook_day18b)」(text/116-ch18-02-18-2-osbook-day18b.txt:71(搜「ExecuteFile」); text/116-ch18-02-18-2-osbook-day18b.txt:82(搜「sti并没有被执行」))。 ↩

  7. 出处:「18.3 C++中的计算器(osbook_day18c)」(text/117-ch18-03-18-3-c-osbook-day18c.txt:49(搜「不使用括号」); text/117-ch18-03-18-3-c-osbook-day18c.txt:62(搜「14就是计算结果」))。 ↩

  8. 出处:「18.3 C++中的计算器(osbook_day18c)」(text/117-ch18-03-18-3-c-osbook-day18c.txt:83(搜「没有#include」))。 ↩

  9. 出处:「18.3 C++中的计算器(osbook_day18c)」(text/117-ch18-03-18-3-c-osbook-day18c.txt:98(搜「独立环境」); text/117-ch18-03-18-3-c-osbook-day18c.txt:109(搜「image-base」))。 ↩

  10. 出处:「18.3 C++中的计算器(osbook_day18c)」(text/117-ch18-03-18-3-c-osbook-day18c.txt:120(搜「memcmp」); text/117-ch18-03-18-3-c-osbook-day18c.txt:125(搜「合并」))。 ↩

  11. 出处:「18.4 标准库(osbook_day18d)」(text/118-ch18-04-18-4-osbook-day18d.txt:20(搜「-lc」); text/118-ch18-04-18-4-osbook-day18d.txt:34(搜「显示为0」))。 ↩

  12. 出处:「18.1 文件分配表(osbook_day18a)」(text/115-ch18-01-18-1-osbook-day18a.txt:88(搜「cat命令」); text/116-ch18-02-18-2-osbook-day18b.txt:13(搜「CR」))。 ↩

  13. 出处:「18.1 文件分配表(osbook_day18a)」(text/115-ch18-01-18-1-osbook-day18a.txt:7(搜「簇链」); text/115-ch18-01-18-1-osbook-day18a.txt:35(搜「12KiB」))。 ↩

  14. 出处:「18.1 文件分配表(osbook_day18a)」(text/115-ch18-01-18-1-osbook-day18a.txt:38(搜「两个相同」); text/115-ch18-01-18-1-osbook-day18a.txt:53(搜「0x0FFFFFF8」))。 ↩

  15. 出处:「18.3 C++中的计算器(osbook_day18c)」(text/117-ch18-03-18-3-c-osbook-day18c.txt:83(搜「没有#include」))。 ↩

  16. 出处:「18.3 C++中的计算器(osbook_day18c)」(text/117-ch18-03-18-3-c-osbook-day18c.txt:120(搜「memcmp」); text/117-ch18-03-18-3-c-osbook-day18c.txt:125(搜「合并」))。 ↩