前言:为了搞定OS的Lab2实验——实现一个FAT12镜像查看工具,还是耗费了较多的精力的,以此文记录下收获

准备工作

工具:为了方便较为直观呈现效果,采用vscode + Hex Editor插件

镜像文件:a.img 1.44MB

镜像结构:

ls.png

FAT12文件系统

FAT

FAT(File Allocation Table)文件配置表。用于记录文件所在位置的表格。

在DOS v1.0时代就已引入,是最基本的文件系统之一。

FAT家族包括:FAT12、FAT16、FAT32、ExFAT、VFAT

FAT12

12位地址,故最大容量为16MB,软盘文件系统使用。

标准以FAT12组织的软盘包括以下设定:

  • 两磁头
  • 每磁头80个磁道
  • 每磁道18扇区
  • 每扇区512字节

故FAT12的标准软盘大小为2 * 80 * 18 * 512 = 1.44MB,故一个标准1.44MB的FAT12软盘共有2 * 80 * 18 = 2880扇区

文件系统为了方便组织与管理,会将磁盘分为若干层次:

  • 扇区:磁盘上最小数据单元
  • 簇:一个或多个扇区,是数据存储的基本单位
  • 分区:不同功能区

上述2880扇区被分为如下5个不同的分区

FAT12.png

由于根目录分区的大小不确定,数据区大小也不能够加以确定。

MBR区

引导扇区,占用一个扇区大小空间。

先前的学习中,我们了解到上电后Bios自检,会检测第0磁头、第0磁道、第1扇区是否以0x550xaa作结,以此判断该扇区是否为引导扇区。

从第11个字节开始的25个字节构成一个较为特殊的结构BPB(Bios Parameter Block)

用一个结构体描述BPB的结构就可以表示为:

typedef struct BPB {
u16 BPB_BytsPerSec; //每扇区字节数
u8 BPB_SecPerClus; //每簇扇区数
u16 BPB_RsvdSecCnt; //Boot记录占用的扇区数
u8 BPB_NumFATs; //FAT表个数
u16 BPB_RootEntCnt; //根目录最大文件数
u16 BPB_TotSec16; //扇区总数
u8 BPB_Media; //介质描述符
u16 BPB_FATSz16; //每个FAT表所占扇区数
u16 BPB_SecPerTrk; //每磁道扇区数(Sector/track)
u16 BPB_NumHeads; //磁头数(面数)
u32 BPB_HiddSec; //隐藏扇区数
u32 BPB_TotSec32; //如果BPB_ToSec16为0,该值为扇区数
} BPB; //25字节

可以看出BPB对于接下来许多操作都具有决定性作用,比如:

BPB_BytsPerSec * BPB_SecPerClus,这就是一个簇的字节大小

BPB_RsvdSecCnt + BPB_FATSz16 * BPB_NumFATs,这就是MBR区以及FAT表区所占扇区数,所对应扇区也是根目录区开始扇区号

查看以下1.44MBa.img的第一个扇区
BPB.png

  • 扇区以0x55 0xAA结尾
  • 扇区第1135字节为00 02 01 01 00 02 E0 00 40 0B F0 09 00 12 00 02 00 00 00 00 00 00 00 00 00

对应于BPB即有

BPB {
u16 BPB_BytsPerSec = 0x0200 = 512; //每扇区字节数512
u8 BPB_SecPerClus = 0x01 = 1; //每簇扇区数1
u16 BPB_RsvdSecCnt = 0x0001 = 1; //Boot记录占用的扇区数1
u8 BPB_NumFATs = 0x02 = 2; //FAT表个数2
u16 BPB_RootEntCnt = 0x00E0 = 224; //根目录最大文件数224
u16 BPB_TotSec16 = 0x0B40 = 2880; //扇区总数2880
u8 BPB_Media = 0XF0; //介质描述符
u16 BPB_FATSz16 = 0x0009 = 9; //每个FAT表所占扇区数9
u16 BPB_SecPerTrk = 0x0012 = 18; //每磁道扇区数(Sector/track)18
u16 BPB_NumHeads = 0x0002 = 2; //磁头数(面数)2
u32 BPB_HiddSec = 0x00000000 = 0; //隐藏扇区数0
u32 BPB_TotSec32 = 0x00000000 = 0; //如果BPB_ToSec16为0,该值为扇区数
};

符合先前

标准以FAT12组织的软盘包括以下设定:

- 两磁头
- 每磁头80个磁道(2880 / 2 / 18)
- 每磁道18扇区
- 每扇区512字节

同时扇区划分,扇区总数,每个簇所对应扇区数,FAT表个数以及每个FAT表所占扇区数等数据均吻合。

FAT表

FAT1FAT2互为备份,所以理论上两张表是一样的。

FAT表被划分为紧密排列的若干表项,每个表项与数据区中一个簇对应,表项序号同时与簇号一一对应。

FAT12每个表项由12位组成,故先前说其是12位地址,最大容量为16MB

需要注意的是,由于1.44MB的软盘上FAT12前三个字节为固定的0xF00xFF0xFF,用于表示这是一个FAT12文件系统,所以3字节,24位,2表项被占据,对应的簇0 簇1就没有存在意义,故数据区起始不是簇0,而是簇2

FAT表项的值的含义:

  • 通常情况下代表文件下一簇号
  • >= 0xFF8,该簇已经是文件最后一个簇
  • = 0xFF7,表示一个坏簇

根目录区

根目录区由根目录下的目录项/文件构成,一个目录项占据32字节

类似的,每个目录项都有固定的结构,可用如下结构体进行模拟:

typedef struct RootEntry {
char DIR_Name[11]; //长度名+扩展名
u8 DIR_Attr; //文件属性
char reserved[10]; //保留位
u16 DIR_WrtTime; //最后一次写入时间
u16 DIR_WrtDate; //最后一次写入日期
u16 DIR_FstClus; //开始簇号
u32 DIR_FileSize; //文件大小
} RootEntry; //32字节

通过查询根目录区即可得到文件/目录开始簇号,也就可以去相应簇号读取。那么引入FAT表进行定位意义何在?大文件!
上述方式只适用于文件大小小于一个簇的,鉴于文件的零散分布,就需要FAT表指示文件的下一部分所在簇号,方便加以定位,将散乱的文件整合起来。

由此,归结起来,FAT12访问文件的基本操作为:

  1. 首先通过根目录文件查找文件名,确定是哪一个条目,接着在条目中访问DIR_FstClus对应的开始簇号
  2. 当一个簇号访问完后,通过FAT表项查询下一簇号,决定是结束还是继续访问下一簇号,重复第二条

由上面区域划分可知,根目录区始于19扇区,即19 * 512 = 2600h

先前又可知根目录区最大文件数为224,故根目录区所占扇区大小为(32 * BPB_RootEntCnt + BPB_BytsPerSec - 1) / BPB_BytsPerSec = (32 * 224 + 512 - 1) / 512 = 14
加上BPB_BytsPerSec-1保证此公式在根目录区无法填满整个扇区时仍然能够成立

数据区

数据存放区域,包括文件以及根目录下目录对应的文件信息,该信息同根目录区,使用Entry对目录加以存储。

由于根目录区占据14扇区,加上先前MBR以及FAT占据的19扇区,计算得出数据区起始于33扇区,即33 * 512 = 4200h

实例

下面就实际的通过FAT12对文件进行查找

回顾步骤:

  1. 首先通过根目录文件查找文件名,确定是哪一个条目,接着在条目中访问DIR_FstClus对应的开始簇号
  2. 当一个簇号访问完后,通过FAT表项查询下一簇号,决定是结束还是继续访问下一簇号,重复第二条

流程.png

大致流程也就如上图所示

再看一下目录结构

ls.png

分别以一个目录和文件举例

查看HOUSE目录

step1:查看根目录目录项

定位到2600h

RootEntry.png

HOUSE目录具有以下结构

RootEntry HOUSE {
char DIR_Name[11] = "HOUSE "; //长度名+扩展名
u8 DIR_Attr = 0x10; //文件属性
char reserved[10]; //保留位
u16 DIR_WrtTime; //最后一次写入时间
u16 DIR_WrtDate; //最后一次写入日期
u16 DIR_FstClus = 0x0004 = 4; //开始簇号
u32 DIR_FileSize = 0x00000000 = 0; //文件大小
};

判断是文件还是目录,FAT12下文件属性常见如下:

  • 0x01:只读文件
  • 0x02:隐藏文件
  • 0x04:系统文件
  • 0x08:卷标
  • 0x10:子目录
  • 0x20:存档文件

根据DIR_Attr = 0x10也能判断出HOUSE为子目录类型

HOUSE子目录下还有ROOM子目录,去数据区HOUSE开始簇号4查看

簇号4应该是定位到(4 - 2) * 512 + 4200h = 0400h + 4200h = 4600h

HOUSE.png

忽略...子目录,ROOM子目录具有以下结构:

RootEntry ROOM {
char DIR_Name[11] = "ROOM "; //长度名+扩展名
u8 DIR_Attr = 0x10; //文件属性
char reserved[10]; //保留位
u16 DIR_WrtTime; //最后一次写入时间
u16 DIR_WrtDate; //最后一次写入日期
u16 DIR_FstClus = 0x0006 = 6; //开始簇号
u32 DIR_FileSize = 0x00000000 = 0; //文件大小
};

去对应的簇号6(4A00h)

ROOM.png

查看到ROOM目录下的信息

step2:查询FAT表

HOUSE起始簇内容全部访问完了,紧接着我们就要去访问与簇号对应的FAT表了,FAT表对应表项为簇号4

FAT1表起始于1扇区,即200h

FAT表.png

前三个字节为固定的0xF00xFF0xFF,符合FAT12

如何读取12位FAT表项取值?

可不是照着顺序F0F FFF这样,这就涉及到存储的方式了。以下是几张较为直观的示意图:

FAT表构成.png

FAT表1.png

FAT12结构.png

以我的看法,由于是按字节(8位)进行存取的,12位的尴尬取值,就不得不由一个完整的字节与另一字节的一半拼接起来。

FAT(0)FAT(1)为例,依据低地址在大端,高地址在小端的顺序,构成的三个字节应该为0xFF-FF-F0,读取12字节划分应该为0xFFF-FF0FAT(0)的高位Fbyte2的低位,FAT(1)的低位Fbyte2的高位F,真是奇怪的读取方式。

16位存储关联两字节后获取这12位的FAT表项也就得分奇偶进行区分了。粗略有以下写法

if(奇数表项号){
//去掉value低4位
value >> 4;//FF-FF >> 4 = 0F-FF
}else {
//去掉value高4位
value << 4;//FF-F0 << 4 = FF-00
value >> 4;//FF-00 >> 4 = 0F-F0
}

所以簇号4对应FAT表项值为0xFFF>0xFF8,表明已是文件的最后一个簇(HOUSE作为目录,对应的首簇能放512 / 32 = 16entry)

查看/NJU/SOFTWARE/SE1.TXT

之所以选择该文件一是目录结构有点复杂,而是文件大小超过512字节,也就是1簇,便于理解FAT表

step1:查看根目录目录项

RootEntry.png

大致与上文一样,定位NJU目录开始簇号为0x0005 = 5,定位至4800h

NJU表项.png

定位SOFTWARE目录开始簇号为0x0008 = 8,定位至4E00h

SOFTWARE表项.png

定位SE1.TXT开始簇号为0x000B = 11,定位至5400h

SE1.png

step2:查询FAT表

FAT表.png

查询表项FAT(11),为0x00C = 12,接着继续去簇12读取接下来内容,查询表项FAT(12),为0x00D = 13,接着去簇13读取下一段内容,查询表项FAT(13) = 0x00E = 14,去簇14读取剩余内容,查询表项FAT(14) = 0xFFF,读取完毕。

所以簇14为最后一个簇,定位到5A00h,确实如此。

SE1.2.png

至此已完成了FAT12文件系统的了解,镜像查看工具的实现依据上面所说文件读取过程进行模拟即可。

参考文章:

【实现操作系统 02】FAT12 文件系统(摆脱术语用实际例子介绍)

linux之 引导扇区(一)