说起来 FAT12 文件系统的历史相当久远,早在 DOS 系统的时代就使用 FAT12 作为文件系统使用,一直沿用至今仍会在软盘的结构上使用 FAT12 格式。
当软盘以 FAT12 格式组织格式化后将会以如下标准设定:
80 个磁道;18 个扇区;512 字节。标准 FAT12 软盘空间:2 * 80 * 18 * 512 = 1474560B = 1440KB = 1.44MB
因此一个标准的 1.44MB 大小的 FAT12 格式软盘共有 2 * 80 * 18 = 2880 个扇区。这 2880 个扇区被分为 5 个部分,如下:

MBR (Main Boot Record) 主引导记录占用大小为 1 个扇区,即 512 B。在这个扇区里记录了整个文件系统的组织结构信息和引导程序两部分内容。
| 标识 | 偏移量 | 类型 | 大小 | 默认值 | 描述 |
|---|---|---|---|---|---|
| BS_JmpBoot | 0 | db | 3 | - | 跳转指令 |
| BS_OEMName | 3 | db | 8 | MSWIN4.1 | OEM字符串,必须为 8 个字符,不足会以空格填充 |
| BPB_BytePerSec | 11 | dw | 2 | 0x200 | 每个扇区字节数 |
| BPB_SecPerClus | 12 | db | 1 | 1 | 每簇占用的扇区数 |
| BPB_RsvdSecCnt | 14 | dw | 2 | 1 | Boot占用的扇区数 |
| BPB_NumFATs | 16 | db | 1 | 2 | FAT表的数量 |
| BPB_RootEntCnt | 17 | dw | 2 | 0xE0 | 根目录可容纳的目录项数 |
| BPB_TotSec16 | 19 | dw | 2 | 0xB40 | 逻辑扇区总数 |
| BPB_Media | 21 | db | 1 | 0xF0 | 媒体描述符 |
| BPB_FATSz16 | 22 | dw | 2 | 9 | 每个FAT占用扇区数 |
| BPB_SecPerTrk | 24 | dw | 2 | 0x12 | 每个磁道扇区数 |
| BPB_NumHeads | 26 | dw | 2 | 2 | 磁头数 |
| BPB_HiddSec | 28 | dd | 4 | 0 | 隐藏扇区数 |
| BPB_TotSec32 | 32 | dd | 4 | 0 | 若BPB_TotSec16是0,则在这里记录扇区总数 |
| BS_DrvNum | 36 | db | 1 | 0 | 中断 13(int 13h)的驱动器号 |
| BS_Reserved1 | 37 | db | 1 | 0 | 未使用 |
| BS_Bootsig | 38 | db | 1 | 0x29 | 扩展引导标志 |
| BS_VolID | 39 | dd | 4 | 0 | 卷序列号 |
| BS_VolLab | 43 | db | 11 | - | 卷标,必须为11个字符,不足会以空格填充 |
| BS_FileSysType | 54 | db | 8 | FAT12 | 文件系统类型,必须是8个字符,不足以空格填充 |
| BOOT_Code | 62 | db | 448 | 0x00 | 引导代码,由偏移0字节(BS_JmpBoot)跳转过来 |
| END | 510 | db | 2 | 0x55, 0xAA | 系统引导标识,引导扇区结束标识 |
BPB_NumFATs: 描述在存储介质中 FAT 表的数量。此处虽然规定最小设置的值为 1,但是为了能够起到恢复文件的作用,一般建议设置为 2,即表示拥有两份 FAT 表。
BPB_Media: 描述存储介质类型,对于不可移动的存储介质,标准值为 0xF8,对于可移动的存储介质,常用值为 0xF0
。该字段的合法值有 0xF0、0xF8、0xF9、0xFA、0xFB、0xFC、0xFD、0xFE、0xFF。此处写入的值也必须向 FAT 的第 0 项的最后一个字节写入同样的值。
本文采用两个 FAT 表格式的 FAT12 文件系统,由于 FAT2 是用于数据恢复作用,因此 FAT2 的内容与 FAT1 表的内容完全相同,即是拷贝了 FAT1 一份。
FAT12 文件系统以簇 (Cluster) 为单位分配数据区(管理扇区),每个簇大小为 BPB_NumFATs * BPB_RootEntCnt 个字节。
FAT 表中的表项位宽与 FAT 类型有关,FAT12 文件系统的表项位宽为 12bit,FAT16 的表项位宽为 16bit,而 FAT32 的表项位宽为 32bit。FAT 表中的表项与数据区的簇是一一对应的关系,即一个表项对应数据区的一个簇大小的内存单元。
FAT 表项的取值如下:
| FAT 项 | 可取值 | 描述 |
|---|---|---|
| 0 | BPB_Media | 磁盘标识字,低字节需与 BPB_Media 数值保持一致 |
| 1 | FFFh | 表示第一个簇已占用 |
| 2 ~ N | 000h | 可用簇 |
| 002h~FEFh | 已用簇 | |
| FF0h~FF6h | 保留簇 | |
| FF7h | 坏簇 | |
| FF8h~FFFh | 文件的最后一个簇 |
[注]:FAT[0] 和 FAT[1] 始终不作为数据区的索引使用。
根目录区只保存目录项 (BootEntry) 信息,数据区的不仅可以保存目录项信息,也可以保存文件数据。目录项是由一个 32B 组成的结构体,目录项本身可以表示一个目录,也可以表示一个文件,其中记录着名字、长度 以及数据起始簇号等信息。其完整结构如下:
| 名称 | 偏移 | 长度 | 描述 |
|---|---|---|---|
| DIR_Name | 0x00 | 11 | 文件名 8B,扩展名 3B |
| DIR_Attr | 0x0B | 1 | 文件属性 |
| 保留 | 0x0C | 10 | 保留位 |
| DIR_WrtTime | 0x16 | 2 | 最后一次写入时间 |
| DIR_WrtDate | 0x18 | 2 | 最后一次写入日期 |
| DIR_FstCtus | 0x1A | 2 | 起始簇号 |
| DIR_FileSize | 0x1C | 4 | 文件大小 |
其中 DIR_FstClus 字段描述的是文件在磁盘中存放的具体位置,由于 FAT[0] 和 FAT[1] 已明确其作用不能用于数据区的簇索引,因此这里的值不能取 0 或 1,有效值将从 2 开始。
根目录占用扇区数的计算方法为: (BPB_RootEntCnt * 32 + BPB_BytesPerSec - 1) / BPB_BytesPerSec = (224 * 32 + 512 - 1) / 512 = 14,根目录区的扇区起始号为 MBR + FAT[0] + FAT[1] = 1+ 9 + 9 = 19,数据区的扇区起始号为 Root + Sizeof( Root ) = 19 + 14 = 33。
听了上面这么多的概念性东西,总觉得讲的很虚,让我们用实际的例子来认识这个 FAT12 结构。
需要用到的工具:WinImagne
打开 WinImage 软件,选择 文件 > 新建,选择 1.44MB 大小。


需要事先准备好需要放置进去的文件,这里笔者准备了两个。一个名为 imboot.txt,其中可以看到只有简单的一句话,这段内容长度只有 32 KB,在文件系统中会占据一个簇的大小。
另一个名为 BPB.txt 的文件,这里的内容是摘抄一篇有关 FAT 文件系统中 BPB 介绍的内容,该文件总长度为 2488 KB,按照 FAT12 中规定的每簇中仅包含 1 个扇区,即每个簇为 512 KB,因此按照道理该文件将会在文件系统中为其分配 5 个簇来存储,即 5 * 512 = 2560KB。这里读者需要自己做实验的话,随便填充文件内容,只要超过 1024KB 即可,目的是为了查看 FAT 表项的索引簇号的原理,因此需要必须超过一个簇大小的文件。

依次选择 镜像 > 加入,找到提前准备好的文件,选择即可。

可以看到我们准备好的文件已经放置进来。

依次选择 文件 > 另存为,这里的格式选择 vfd 或者 ima 格式均可。(其它格式笔者并未尝试,可以自行选择尝试,记得留言告诉笔者哦❤️)

这里笔者选择的使用 VSCode 软件,当然还有很多其它文本查看工具可以以 Hex 格式阅读文本,根据个人习惯选择即可。
首先简单看一下这里的内容,第一个扇区就不用看了,第一个扇区主要记录文件系统的结构信息以及引导程序,这里直接定位到了第二个扇区的起始位置 200h,虽然暂时还不明白这串内容是什么,但至少我们已经发现了,第一个扇区的最后两个字节为 0x55、0xAA,这起码说明我们的文件格式是没问题的。

根据 FAT12 文件系统的结构,MBR、FAT[1]、FAT[2] 分别占用 1 个扇区、9 个扇区、9 个扇区,即根目录的起始扇区号应该为 1 + 9 + 9 = 19。十六进制的地址为 19 * 512 = 2600h。OK,我们直接定位到文件的 2600h 的位置。

其实你已经从文件右侧的 ASCII 码解码器显示的文本中看到了刚才我们放入的两个文件名。由于 Inter 采用的大端存储,即低字节存储在地位,高位字节存储在高位,因此这里看到的字母是顺序的。
我们已经知道了根目录是由一个个目录项组成的,而在 FAT12 结构中,一个目录项长度为 32 bit,在这里刚好占两行。这里笔者以 C 语言结构体的形式来解读这里的根目录项内容。
将根目录项视为一个 C 语言的结构体:
struct RootEntry {
char DIR_Name[11]; // 前 8B 为文件名,后 3B 为扩展名
char DIR_Attr[1]; // 文件属性
char DIR_Save[10]; // 未使用,保留
char DIR_WrtTime[2]; // 最后一次写入时间
char DIR_WrtDate[2]; // 最后一次写入日期
char DIR_FstClus[2]; // 起始簇号
char DIR_FileSize[4]; // 文件大小
};
先来看第一个目录项:
struct RootEntry Entry_01 = {
.DIR_Name = {0x49, 0x4D, 0x42, 0x4F, 0x4F, 0x54, 0x20, 0x20, 0x54, 0x58, 0x54}, // IMBOOT TXT
.DIR_Attr = 0x00,
.DIR_Save = {0x18, 0x2A, 0x92, 0x7B, 0x0F, 0x55, 0x00, 0x00, 0x00, 0x00},
.DIR_WrtTime = {0xC3, 0x7B}, // 0x7BC3
.DIR_WrtDate = {0x0F, 0x55}, // 0x550F
.DIR_FstClus = {0x07, 0x00}, // 0x0007
.DIR_FileSize = {0x20, 0x00, 0x00, 0x00} // 0x00000020 = 32
};
从这里我们可以看出这个目录项是对 imboot.txt 文件的描述,文件属性为 0x00 表示普通文件,可任意读写。DIR_FstClus 的值为 0x0007,表示该文件在数据区的起始簇号为 7,DIR_FileSize 的值为 0x20,表示该文件大小为 32 B。
再看第二个目录项:
struct RootEntry Entry_02 = {
.DIR_Name = {0x42, 0x50, 0x42, 0x20, 0x20, 0x20, 0x20, 0x20, 0x54, 0x58, 0x54}, // BPB TXT
.DIR_Attr = 0x00,
.DIR_Save = {0x10, 0x1B, 0x9D, 0x7B, 0x0F, 0x55, 0x00, 0x00, 0x00, 0x00},
.DIR_WrtTime = {0x7A, 0x7C}, // 0x7C7A
.DIR_WrtDate = {0x0F, 0x55}, // 0x550F
.DIR_FstClus = {0x02, 0x00}, // 0x0002
.DIR_FileSize = {0xB8, 0x09, 0x00, 0x00} // 0x000009B8 = 2488
};
从文件名可以看出,该目录项是对 BPB.txt 文件,同样属性是普通文件。DIR_FstClus 的值为 0x0002,表示该文件在数据区的起始簇号为 2,DIR_FileSize 的值为 0x09B8,表示该文件大小为 2488 B,可以对比 上图 中文件大小,是一样的。
当我们找到一个目录项后,最重要关心的是该文件保存在数据区的起始簇号,根据这里的簇索引便可以在数据区找到该文件对应的第一个簇,紧接着再去查 FAT[1] 表中的对应表项(例,文件起始簇号为 20,需要查看 FAT 表中的第 20 项),根据表项内容知道需要继续找下一个簇还是已经找完所有簇到了文件结束位置。具体流程大概如下图这样:

在 FAT12 中 FAT 表的每个表项长度为 12 bit,加上大端存储的缘故,这里阅读起来并不会那么直观。笔者来解读一下这里应该如何阅读。

上图正是以文中的 FAT 表为例,可以理解为一个 FAT 表项是由一个字节和另一个字节的一半拼接而成,上图仅是为了容易理解画的示意图。而实际上上图中的这几个字节内容从内存中完全拿出来并排序后会变成这样:0x00_40_03_FF_FF_F0,然后再以 12 bit 断之后就会得到这样的效果:0x004_003_FFF_FF0,这样正好会与 FAT 表中的数据对应起来,FAT[0] 为 0xFF0 表示可移动存储介质,FAT[1] 为 0xFFF 表示第 1 个簇已经被占用。
根据上文分析,imboot.txt 文件在数据区的起始簇号为 7,那么首先需要去数据区找到第 7 号簇,根据上文分析,我们知道数据区的起始扇区号为 33,那么数据区的起始簇的地址为 33 * 512 = 4200h,这个地址对于数据区来讲是第一个簇,但根目录项中的起始簇有效值是从 2 开始,即根目录中的 2 号簇对应的即为数据区的起始簇,这个内容在上文也提到过,为了避免翻来翻去,笔者将上文截图贴在这里。

那么这样计算的话,第 7 簇就应该在第 2 簇的基础上再加上 5 个簇的大小,即得到 imboot.txt 在数据区存储位置,(33 + 5) * 512 = 4C00h,让我们直接定位到文件的 4C00h 的位置。

虽然这里我们很直观的看到,该文件的所有内容都被保存在这里,但对于整套检索流程并没有结束,在查阅完第 7 号簇后,应立马去 FAT 表查看第 7 个表项,这里我们只用看 FAT1 表即可,定位到 FAT1 表的位置 200h。

通过上文我们已经会查看 FAT 表项了,这里的 FAT[7] 的值为 FFFh,表示该簇为最后一个簇,到这里将不会继续索引下去,文件内容结束。
已经分析过 imboot.txt 文件,再来看 BPB.txt 将会非常的快了。根据上文分析,BPB.txt 文件在数据区的起始簇号为 2,也就是数据区的第一个簇单元,直接定位到数据区的第一个簇位置 4200h。

每个簇大小为 512 B,当读完 2 号簇后,将会去 FAT 表中查询第 2 号表项。

FAT[2] 的值为 003h,那么这时候表示下一个簇号为 3 号簇,再读完数区的第 3 簇后又会回来查询 FAT[3] 表项,接下来的 4、5 簇以及 FAT[4]、FAT[5] 与 3 相同,FAT[5] 中的值为 006h,在读完数据区的 6 号簇后,来查询 FAT[6],发现 FAT[6] 的值为 FFFh,表示文件结束。
到这里,相信各位读者已经对 FAT12 文件系统有了清楚的认识,本来是要在接下来介绍使用 FAT12 文件系统实现 Boot 加载 Loader 程序到内存中的内容,但忽然看了一下本文已经超出 8500 字了,阅读到这里显然大家已经累了,那么请休息一会,然后请继续接着看笔者的下一篇文章《使用 FAT12 文件系统实现简单的 Boot 加载 Loader 到内存》
[1] FAT12/16/32 Media Boot Record: https://docs.microsoft.com/en-us/azure/rtos/filex/chapter3#fat121632-media-boot-record
[2] FAT Filesystem: http://elm-chan.org/docs/fat_e.html
[3] exFAT file system specification: https://docs.microsoft.com/en-us/windows/win32/fileio/exfat-specification
觉得这篇文章对你有帮助的话,就留下一个赞吧^v^*
请尊重作者,转载还请注明出处!感谢配合~
[作者]: Imagine Miracle
[版权]: 本作品采用「署名-非商业性使用-相同方式共享 4.0 国际」许可协议进行许可。
[本文链接]: https://blog.csdn.net/qq_36393978/article/details/126305288