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 挂悬念。
- FAT = 簇号链表:NextCluster 沿链走,≥0x0FFFFFF8 收尾;一簇装不下的文件全靠它;
- 执行文件 = 读进内存 + 调入口,与引导加载器启动内核同构;
- 无头二进制只有机器语言,正经程序要 ELF(静态变量、字符串的位置都要交代);
- 应用没标准库就手写 strcmp/atol;-ffreestanding + --image-base 0 是裸机应用的标准构建姿势;
- ELF 魔数 "\x7f" "ELF" 相邻拼接——写成一个字符串会吃进 0x7fE 的解析陷阱;
- 关中断 + hlt = 睡死;漏 sti 这类「最后一步」的 bug,症状是整个系统冻结;
- 同 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」) |
-
出处:「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」))。 ↩ -
出处:「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」))。 ↩ -
出处:「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」))。 ↩ -
出处:「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」))。 ↩ -
出处:「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(搜「相对短跳转」))。 ↩ -
出处:「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就是计算结果」))。 ↩ -
出处:「18.3 C++中的计算器(osbook_day18c)」(
text/117-ch18-03-18-3-c-osbook-day18c.txt:83(搜「没有#include」))。 ↩ -
出处:「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」))。 ↩ -
出处:「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(搜「合并」))。 ↩ -
出处:「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」))。 ↩ -
出处:「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」))。 ↩ -
出处:「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」))。 ↩ -
出处:「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」))。 ↩ -
出处:「18.3 C++中的计算器(osbook_day18c)」(
text/117-ch18-03-18-3-c-osbook-day18c.txt:83(搜「没有#include」))。 ↩ -
出处:「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(搜「合并」))。 ↩