手写FAT文件系统:Rust Hypervisor Firmware的FAT12/16/32目录遍历实现原理
【免费下载链接】rust-hypervisor-firmware 项目地址: https://gitcode.com/gh_mirrors/ru/rust-hypervisor-firmware
你有没有想过,一台虚拟机在没有 BIOS、没有 UEFI 的情况下,是怎么从磁盘里找到并启动 Linux 内核的?答案就藏在一个不起眼却至关重要的模块里——FAT文件系统。Rust Hypervisor Firmware(rust-hypervisor-firmware)是一个用 Rust 编写的极简虚拟化固件,它不依赖 TianoCore/edk2 这类复杂固件,而是自己手写了一套支持 FAT12/16/32 目录遍历与文件读取的代码,从磁盘镜像中直接加载引导程序。这篇文章将带你深入 src/fat.rs,逐行拆解这套目录遍历的实现原理,让新手也能看懂"手写 FAT 文件系统"这件事到底是怎么做到的。
为什么虚拟机固件需要自己实现 FAT 文件系统?📦
现代 UEFI 固件动辄上百万行代码,而 rust-hypervisor-firmware 的目标恰恰相反——用最少的代码完成"从磁盘引导内核"这一件事。它的启动链路是这样的:
- 通过 virtio-blk 协议读取磁盘扇区(src/block.rs)
- 解析 GPT 分区表,找到 EFI 系统分区(src/part.rs)
- 用自研 FAT 文件系统遍历目录、读取内核文件(src/fat.rs)
- 按 Boot Loader Specification 解析引导配置,加载 bzImage(src/loader.rs)
整条链路在 src/main.rs 的 boot_from_device 函数中一气呵成。可以说,FAT 文件系统实现是这个固件的"心脏",没有它,固件连内核文件都读不到。
FAT 文件系统结构速览:从引导扇区到数据区 🗂️
要理解目录遍历,先要理解 FAT 磁盘的物理布局。一张 FAT 格式的磁盘从上到下依次是:
| 区域 | 说明 |
|---|---|
| 保留区(Reserved) | 第一个扇区是引导扇区(Boot Sector),存放 BPB 参数 |
| FAT 表区 | 记录每个簇(Cluster)的下一跳地址,构成"簇链" |
| 根目录区 | FAT12/16 的根目录固定在这里;FAT32 的根目录只是一个普通目录 |
| 数据区 | 真正的文件内容,按簇(Cluster)为单位分配 |
固件在 src/fat.rs 的 init() 函数中读取引导扇区,把关键字段解析出来:每扇区字节数、每簇扇区数、FAT 表个数、FAT 表大小、根目录项数等。注意这些字段都是通过 #[repr(C, packed)] 结构体直接映射到扇区字节上的,这正是 Rust 做底层开发的优雅之处——结构体即协议。
FAT12、FAT16、FAT32 的判定方法
不同 FAT 类型的主要区别是 FAT 表项宽度不同,而判定依据是数据区的簇数量:
- 簇数 < 0xFF5(4085)→ FAT12
- 簇数 < 0xFFF5(65525)→ FAT16
- 否则 → FAT32
self.fat_type = if self.clusters < FAT12_MAX {
FatType::FAT12
} else if self.clusters < FAT16_MAX {
FatType::FAT16
} else {
FatType::FAT32
};
一个文件在磁盘上占用若干簇,这些簇通过 FAT 表串联成一条"簇链"。读文件的过程,本质上就是沿着簇链逐个簇地读下去。这是 FAT 系列文件系统最核心的思想。
FAT 目录项(Directory Entry)的底层结构 📄
目录在 FAT 里不是一个特殊对象,它就是一个特殊的文件,其内容是一连串 32 字节的目录项。每个 32 字节的短文件名目录项(Short Name Entry)结构如下:
| 偏移 | 长度 | 字段 | 说明 |
|---|---|---|---|
| 0 | 11 | name | 8.3 短文件名(8 字节名 + 3 字节扩展名) |
| 11 | 1 | flags | 属性:0x10 表示目录,0x0F 表示 LFN 长文件名项 |
| 20 | 2 | cluster_high | 起始簇号高 16 位(FAT32 使用) |
| 26 | 2 | cluster_low | 起始簇号低 16 位 |
| 28 | 4 | size | 文件大小(字节) |
在 src/fat.rs 的 FatDirectory 结构体中,这些字段被一一映射。目录项的名称、起始簇号、大小这三个信息,就是遍历目录的全部"弹药"。
8.3 短文件名与 LFN 长文件名解析
传统 DOS 只支持 8.3 短文件名,但现代磁盘上到处都是长文件名。FAT 的解决方案是:在真正的短文件名目录项之前,插入一个或多个 LFN(Long File Name)目录项,属性标记为 0x0F,每项携带 13 个 UTF-16 字符,多个 LFN 项按序号逆序排列。
固件在 src/fat.rs 定义了 FatLongNameEntry,并在 next_entry() 中按序号把字符拼接回 long_entry 数组,最后通过 ucs2_to_ascii 转成 ASCII(src/fat.rs)。这就是为什么固件既能读 BOOTX64.EFI 这样的短名,也能读 longfilenametest 这样的长名。
目录遍历核心算法:next_entry 一步步怎么走 🚶
现在到了本文的重头戏——目录遍历。固件把"读一个目录项"封装成了 Directory::next_entry()(src/fat.rs),它的处理逻辑相当清晰:
- 调用
read_next读取当前扇区(512 字节,正好 16 个目录项) - 逐项检查,遇到三种特殊标记:
name[0] == 0x00:目录结束标记,返回EndOfFilename[0] == 0xE5:目录项已被删除,跳过flags == 0x0F:LFN 长文件名项,拼接到long_entry后继续
- 遇到正常目录项,组合出
DirectoryEntry(文件名、类型、簇号、大小)返回
跨簇遍历:目录太大放不下怎么办?
一个目录也可能占用多个簇。当当前簇的扇区读完后,read_next(src/fat.rs)会调用 next_cluster 沿着 FAT 表跳到下一个簇继续读:
if self.sector >= self.filesystem.sectors_per_cluster {
match self.filesystem.next_cluster(self.cluster.unwrap()) {
Ok(new_cluster) => {
self.cluster = Some(new_cluster);
self.sector = 0;
self.offset = 0;
}
...
}
}
配合 seek(0) 重置遍历位置,目录可以反复扫描,这正是后面路径解析的基础。测试用例 test_fat_large_directory(src/fat.rs)专门验证了跨簇大目录的遍历能力。
FAT 表簇链遍历:FAT12/16/32 三种读取方式 ⛓️
簇链是 FAT 的灵魂,而 next_cluster()(src/fat.rs)用三个分支优雅地处理了三种 FAT 类型,这也是手写 FAT 文件系统中最考验细节的地方:
- FAT12:每 1.5 字节存一个表项,实现时先按
cluster + cluster / 2定位偏移,再根据簇号奇偶性取低 12 位或高 12 位,还要处理偏移恰好落在扇区末尾的跨界情况(读取下一个扇区的第一个字节)。 - FAT16:每 2 字节一个表项,直接按小端序读出即可,表项值 ≥ 0xFFF8 表示文件结束。
- FAT32:每 4 字节一个表项,但只取低 28 位(
& 0x0FFF_FFFF),高 4 位保留。
文件读取器 File::read(src/fat.rs)每次读满一个簇的扇区后,就调用 next_cluster 跳转到下一个簇,直到表项值达到结束标记。值得称赞的是,它还精细地处理了最后一个扇区只有部分有效数据的情况,返回的实际字节数不会超出文件大小。
从根目录到任意路径:open 路径解析实战 🔍
有了逐项遍历的能力,路径解析就水到渠成了。Filesystem::open(src/fat.rs)从根目录出发,open_from(src/fat.rs)把路径按 / 或 \ 切分成逐级目录名:
- 在当前位置逐项扫描目录,用
compare_name做大小写不敏感的短名匹配(src/fat.rs) - 匹配到目录且路径未结束 → 进入该子目录继续循环
- 匹配到文件 → 返回
File节点
测试用例 test_fat_open 中,固件成功解析了 \EFI\BOOT\BOOTX64.EFI 并定位到簇号 166、大小 92789 字节的文件,验证了整条路径解析链路的正确性。这套实现同时兼容正反斜杠,对 Windows 风格路径也游刃有余。
完整启动链路:固件如何用 FAT 加载内核 🚀
理解了目录遍历,再回头看完整启动流程就一目了然了。boot_from_device 找到 EFI 分区后创建 Filesystem 并调用 init(),随后交给 loader::load_default_entry:
- 打开
/loader/loader.conf,读取default字段(src/loader.rs) - 用 glob 模式匹配
/loader/entries/下的.conf文件(src/loader.rs) - 解析
linux、initrd、options三行配置 - 通过
fs.open()打开内核和 initrd,加载进内存后跳转执行
如果 BLS 引导失败,固件会优雅降级:直接打开 \EFI\BOOT\BOOTX64.EFI,用内置的 PE32+ 加载器启动 shim + GRUB2。无论哪条路,都离不开FAT 目录遍历这块基石。
总结 📝
从引导扇区解析到目录项读取,从 FAT 表簇链追踪到长文件名拼接,rust-hypervisor-firmware 用不到一千行代码,就在 no_std 的裸机环境下完整实现了 FAT12/16/32 文件系统的读取能力。对于想学习文件系统原理、Rust 底层编程或虚拟化固件开发的读者来说,src/fat.rs 就是一份不可多得的"活教材"——它没有层层抽象,把 FAT 最本质的数据结构直接映射到内存里,清晰、完整、可运行。如果你也想体验"手写 FAT 文件系统"的快感,不妨 clone 下来跑一遍它的单元测试,亲手感受从扇区字节到完整文件路径的奇妙旅程。
【免费下载链接】rust-hypervisor-firmware 项目地址: https://gitcode.com/gh_mirrors/ru/rust-hypervisor-firmware
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考



