主要参考资料有:《4.5万字透视FAT32》、《FAT文件系统原理》及网上各种资料。
所谓文件系统,就是对数据进行管理的一种方法,其下的所有的数据都要按照它规定的格式和规则进行存取和修改。而FAT32文件系统简单的说就是以FAT表来对数据进行管理,用32位长度的值来表示位置偏移的一种文件系统。它还有两个大哥,FAT12和FAT16,这里不做说明。
不考虑操作系统和MBR的限制,FAT32文件系统最大支持128T的大小(2^28*4K*128,即最多有2的28次方个簇,每个簇为128个扇区,每个扇区的大小为4KB)
MBR:
凡是介绍FAT32文件系统(就算是其他文件系统也会提到的)的资料中都会提到MBR,而事实上MBR根本就不是FAT32文件系统的一部分。MBR是用来表示硬盘的信息的一种方式(还有一种是GPT),每个硬盘都有且只有一个MBR,并且MBR一定是位于硬盘的最开始位置,即第0扇区。
而文件系统是基于分区的。一个硬盘可以有多个分区,也就是说,一个硬盘可以同时存在多个不同的文件系统,但一个分区只能有一种文件系统。
MBR与文件系统之间的关系,就是MBR中有一个分区信息表,里面记录了每个分区的总体信息,如分区的开始位置,分区的大小,分区的文件系统等,我们就是通过这些信息来找到分区的位置,从而获得文件系统的具体信息的。
MBR的结构如下:
typedef unsigned char uchar
typedef unsigned short int ushort
typedef unsigned int uint
typedef struct _SECTOR_AND_CYLINDER
{
#ifdef __LITTLE_ENDIAN__
ushort sylinder :10; //开始柱面,最大值为1023
ushort sector :6; //开始扇区,用的是0-5位
#endif
}SEC_AND_CYL;
typedef struct DISK_PARTITION_TABLE
{
uchar boot_indicator; //引导指示符
uchar start_head; //开始磁头
SEC_AND_CYL start_offset; //开始扇区和开始柱面
uchar system_id; //系统ID fat32:0x01;ntfs:0x07
uchar end_head; //结束磁头
SEC_AND_CYL end_offset; //结束扇区和结束柱面
uint relative_sectors; //相对扇区数,从磁盘的开始到该分区的开始的位移量,以扇区来计算
uint total_sectors; //总扇区数,该分区中的扇区总数
}DPT;
typedef struct _MASTER_BOOT_RECORD
{
uchar boot_code[446];
DPT dpt[4];
uchar boot_sign[2]; //0x55,0xAA
}MBR;
FAT32文件系统:
FAT32文件系统主要包括三块内容,DBR区、FAT表区和数据区,它们之间的位置如下:
DBR区:
DBR区包含两部分内容,DBR扇区和若干个逻辑扇区大小的保留扇区。总的大小一般为32个扇区(有些第三方工具分出来的分区里保留扇区可能不为32;像36或者63都是有可能的)。
DBR扇区:
即DOS引导记录,大小为512个字节。它一定是位于文件系统的最开始,即第0扇区(这是对于文件系统而言的,对于硬盘而言它的位置记录于MBR中的DPT中,程序也是更加这个值来找到它的)。它记录了诸如逻辑扇区大小,簇大小,FAT表的位置和大小等文件系统的具体参数(这部分数据又统称为BPB和扩展BPB)以及厂商代码、跳转指令(程序根据这个指令找到引导信息所在的位置并跳到该位置)和引导信息(引导信息只有当该分区被标记为活动分区时才有效)等其他信息,它的最后两个字节必须为0XAA55,否则认为里面的数据无效。它的结构体如下:
typedef struct BIOS_PARAMETER_BLOCK
{
ushort sector_size; //Bytes per sector,扇区字节数,合法值有512、1024、2048和4096
uchar clus_size; //sector per clus,每簇扇区数,值必须是2的幂次,最大128
ushort reserved_sector; //fat表前的保留扇区,一般为32
uchar fat_number; //fat表副本数,一般为2
ushort root_entries; //根目录项数,fat32为0
ushort small_sector; //小扇区数,fat32为0
uchar media_descriptor; //媒体描述符,0xf8:硬盘;0xf0:软盘
ushort fat_size1; //sectors per FAT,FAT的扇区数,fat32为0
ushort track_size; //secotrs per track,每道扇区数
ushort head_number; //磁头数
uint hidden_sector; //引导扇区前的扇区数
uint large_sector; //总的扇区数
uint fat_size32; //sectors per FAT, FAT表所占的扇区数,只用于fat32
EXT_FLAG ext_flag; //扩展标记
ushort fs_version; //文件系统版本
uint root_clus_number; //根目录簇号,只用于fat32,一般为2
ushort fs_info_sector_number; //文件系统信息扇区号,FSINFO结构体所在的扇区号,一般为1,只用于fat32
ushort back_boot_sector_number; //备份引导扇区的位置,一般为6
uint reserved[3];
}BPB;
typedef struct EXT_BIOS_PARAMETER_BLOCK
{
uchar drive_number; //物理驱动器号,硬盘为0x80
uchar reserved; //保留
uchar ext_boot_signature; //扩展引导标签,本字段必须要有能被win2000所识别的值0x28或0x29
uint volume_serial_number; //分区序号,格式化时产生一个随机值,有助于区分磁盘
uchar volume_label[11]; //本字段只能使用一次,被用来保存卷标号,常见为"NO NAME"
uchar system_id[8]; //fat32下为“FAT32”
}EXT_BPB;
typedef struct DOS_BOOT_RECORD
{
uchar jump_code[3];
uchar factory_code[8];
BPB bpb; //sizeof==53
EXT_BPB eBpb; //sizeof==26
uchar boot_code[420];
uchar boot_sign[2];
}DBR;
保留扇区:
保留扇区为紧接着DBR扇区之后的几个扇区,总扇区个数等于BPB.reserved_sector - 1(因为DBR扇区也算在reserved_sector里面)。保留扇区的作用主要是扩展和备用。如一般情况下第一个扇区作为FSINFO扇区,第六个扇区作为DBR的备份,当然并不是一定要用这两个扇区不可,可以通过修改BPB中对应的记录来选用其他的扇区作为FSINFO扇区扇区或者备份扇区。
还有就是DBR中的引导信息只有420个字节,不够用的情况下会把保留扇区中的某个(或某几个)扇区作为扩展。
这里顺带提一下FSINFO扇区,这个扇区是用来记录文件系统内空闲簇的数量以及下一个可用簇信息(比如:如果mount挂载fat32分区时加上usefree参数,就会从fsinfo扇区中读取数据,而非遍历硬盘),注意这里面的信息不要求十分准确,只需要一个大概即可,结构体如下:
struct
{
uint fsinfo_symbol; //值为0x41615252,用来表示该扇区为FSINFO扇区
uchar reserved[480]; //以后扩展使用,当前fat不访问该字段
uint used_symbol; //值为0x61417272,更具体地表明该扇区已被使用
uint last_clusters; //最新的剩余簇数量,0xffffffff表明未知,该值不要求十分准确,但需保证其值<=磁盘所有的簇数
uint search_entry; //告诉驱动程序从哪开始寻找剩余簇,通常设定为驱动程序最后分配出去的簇号,如果值为0xffffffff,表示从簇2开始查找
uchar reserved2[12]; // 当前fat不访问该字段
uint end_sign; //0xaa550000
}FSINFO;
FAT区:
FAT区内存放的是FAT表,对于FAT文件系统来说FAT表至关重要,因此在FAT表1后面还有一份FAT表2,二者的内容完全一样,FAT2作为备份,当FAT1出现错误或损坏时对其进行恢复。
FAT区紧跟在保留扇区之后,大小与分区的大小有关,BPB中有记录FAT表的个数以及每个FAT表的大小。
FAT表其实就是一张表,它的每一项大小为32个字节,按照顺序,从第二项开始分别对应着数据区里的每一个簇(第0项和第1项保留给系统使用)。FAT表内项里的内容表示下一个数据存放的簇号,如果已经是最后一簇了则项里的内容为0XFFFFFFFF。项里面的数据表示意义如图:
在读取数据时,只要找到文件的第一个簇,然后根据FAT表中对应项里的值依次读取下一个簇里面的数据直到0XFFFFFFFF为止,就可以得到所有需要的数据了。因此,FAT是链式结构的。
还有一点需要注意,虽然项里面是用32个字节来表示下一个簇的,但事实上它的高4个字节系统保留了不做处理,因此实际使用时最多只支持2的28次方的簇,这也是一开始计算FAT32最大支持容量是用的2^28而不是2^32的原因。
数据区:
数据区紧跟着FAT区后面,DBR中没有记录数据区的开始位置,但可以根据FAT表的大小和DBR的位置等信息计算出数据区的开始位置。从数据区开始,数据存取的最小单位为簇,而在之前包括DBR区和FAT区数据的最小存取单位都为扇区。也就是说,假设一个簇的大小为4K,一个文件的实际大小为1K,那么它也会占用数据区里一个簇的空间。
数据区可以分成两个部分:根目录区和实际数据区。
根目录区:
根目录区一般位于数据区的最开始的位置,对于FAT32文件系统来说,这个不是一定的(而对于FAT12和FAT16是一定的),它可以存在于数据区的任何位置,DBR中有记录它的位置。但由于根目录区是在文件系统创建时就被创建好的,因此一般还是在数据区的开头,即第2簇。
根目录区存放分区根目录下的文件(文件夹)的信息,即FDT表。它的作用是用来记录文件的位置。当我们需要找到某个文件的具体路径时总是从根目录开始一层一层的往下找,直到最终找到该文件的位置。
在FAT32文件系统中,根目录其实和普通目录没太大的区别,因此原来在FAT12和FAT16中对根目录的一些限制都没有了。FAT32中唯一的几点不同应该就是:1、它是在文件系统创建时就被创建的;2、它里面没有当前目录和父目录这两个特殊的目录项;
FDT表:
File Directory Table,由多个32个字节的目录项组成。作用是记录当前文件夹下的各个文件(文件夹)的信息,比如:文件名、属性、创建时间、存放位置等(注意当前两个字)。
在FAT32文件系统,其实文件夹也是一种文件,是文件在创建时就会占空间(FAT32中就是簇),文件夹也一样。普通文件占的簇中存放的是真实的数据,而文件夹占的簇里面,存放的就是FDT表数据。(为了方便,下面就用文件来表示文件和文件夹)
FDT表中除了每个文件或子目录对应的目录项外,还有两个特殊的目录项,分为为父目录目录项(它的文件名为“..”)和当前目录目录项(它的文件名为“.”)。根目录除外,根目录没有父目录目录项。
需要打开文件时,就是从根目录开始逐级的找目录项,直到找到文件对应的目录项,从目录项中得知文件开始的簇号,从该簇开始读取数据,然后进到FAT表依次找到下一个簇,继续读取,直到遇到0XFFFFFFFF为止(定位FAT表时需要注意:数据区的第一个簇对应的簇号是2)。如,现在需要打开data文件夹下的a.cpp文件,首先从根目录区中找到data文件夹对应的目录项,从该目录项中找到data文件夹所在的簇号;然后进到该簇里面,获取a.cpp的目录项,从而得知a.cpp的开始簇号;最后打开a.cpp的第一个簇。
FAT32的目录项分两种,长目录项和短目录项:
短目录项是必须的,里面记录了一个文件所有必要的信息,一个短目录项对应一个文件。
而长目录项是非必须的,里面只记录了文件名和用于找到对应短目录项的校验值;要想知道长目录项对应文件的信息,就必须要找到它所对应的短目录项。一个文件对应的长目录项可能有多个。因为一个长目录项只有32个字节,只能记录13个字符,当文件名大于13个字符时就要拆分成多个长目录项进行存放。这些对应同一个文件的长目录项依次从1开始编号(注意:其他都是从0开始的计数的,这里是从1开始的,原因下面会提到),最后一个长目录项的编号还要与上0x40。且存放时以倒序形式存放,即最后一个目录项位于最开始的位置。
长目录项一定有一个与它对应的短目录项存在,且在硬盘的存放位置位于长目录项的后面。还有一点,对于上层来说,如果同时存在长短目录项,文件系统对外是屏蔽短目录项的,但需要定位文件时还是用的短目录项。
长短目录项的结构体如下:
struct
{
ushort day :5; //1-31
ushort month :4; //1-12
ushort year :7; //0-127,相对1980年的年数
}FDT_DATE;
struct
{
ushort two_sec :5; //0-29,每2秒加1,表示0-58秒
ushort min :6; //0-59
ushort hour :5; //0-23
}FDT_TIME;
//fat32短目录项结构
struct
{
uchar file_name[8]; //文件名,不足用空格(0x20)填充
uchar ext_name[3]; //扩展名(后缀名),不足用空格(0x20)填充
uchar attribute; //属性:0:读写;1只读;10隐藏;100系统;1000卷标;10000子目录;100000归档,0x0f表示是长目录结构;(除0xf外其他为二进制值)
uchar reserved; //系统保留
uchar milli_time; //创建时间的10毫秒位
FDT_TIME create_time; //文件创建时间
FDT_DATE create_date; //文件创建日期
FDT_DATE last_visit_date; //文件最后访问日期
ushort high_clus; //文件起始簇号的高16位
FDT_TIME change_time; //文件的最近修改时间
FDT_DATE change_date; //文件的最近修改日期
ushort low_clus; //文件起始簇号的低16位
uint file_length; //文件的长度
}SHORT_FDT32;
//fat32长目录项结构,文件名不足时用0xff填充
struct
{
uchar num; //目录项编号,从1开始
ushort large_name1[5]; //文件名的第1-5个字符
uchar attribute; //必须为0x0F
uchar reservedl; //系统保留,为0
uchar check_value; //校验值(根据短文件名计算得出)
ushort large_name2[6]; //文件名的第6-11个字符
ushort start_clus; //文件起始簇号,目前常置0
ushort large_name[2]; //文件名的第12-13个字符
}LONG_FDT32;
关于短目录项:
短目录项的文件名的第一个字符不能为0x05、0xE5、0x20、0x00(0x05和0xE5用于表示该目录项已被删除,0x00表示目录项为空)其他字符必须要大于0x20,且小于0x80,除此之外,文件名扩展中不得包含如下字符0x22(”), 0x2A(*), 0x2B(+), 0x2C(,), 0x2E(.), 0x2F(/), 0x3A(:), 0x3B(;), 0x3C(<), 0x3D(=), 0x3E,(>) 0x3F(?), 0x5B([), 0x5C(/), 0x5D(]), and 0x7C(});;
短目录项的文件名称区分文件名和扩展名,两者分开存放。若文件名称不满足8.3规则,不足的用空格(0x20)填充。如果文件名大于8个字节,或者扩展名大于3个字节,需要对名称进行截断。为防止出现重名的问题,截取时,最开始是取文件名的前6个字符,然后后两位从"~1"开始编号,很多资料上说只能到“~5”,但微软的文档中没有提到这个限制,实际测试中也发现能到"~9"(是在linux下测试的),不知道是不是跟操作系统有关。从测试结果来看,貌似最后一位必须是数字才行。如果两位已经不够,则只保留文件名的前2个字符,其他几位一定的算法进行变换。微软的文档中提到后面的编号可以从“~1”一直到“~99999”,同时也提到数字不建议太大,可以采用其他的方式对文件名进行变更,但一直没找到相关变更的规则说明,不清楚有什么限制没。
短目录项中还有一位系统保留位,有资料说这个值与文件名称的大小写有关,但我测试时发现它一直为0;不清楚取值时有没有什么要求。
如果是子目录的短目录项,文件大小为0。这点需要注意下。
还有一点,测试时发现文件在命名时是全小写的,但短目录项中记录的全是大写,全是大写的话应该是为了兼容FAT12,目前还不清楚是否一定要大写。
关于长目录项:
长目录项里面记录的文件名与短目录项的不同,它是不区分文件名和扩展名的,包括用于区分文件名和扩展名的“.”也会被记录(从目前的测试结果来看是这样的,而且也没找到相关的资料说是两者分开的,有机会的话还要进一步验证一下)。而且长目录项中用的是unicode编码,用两个字节来表示一个字符。
长目录项的文件名的第一个字符同样不能为0x05、0xE5和0x00。0x05和0xE5都是表示目录项已被删除,0x00表示目录项为空。其他字符的限制要远少于短目录项的限制(因此如果转换成短目录项时可能会出现非法的文件名,这时要用“_”来替代)。
长目录项文件名不足时用0xff进行填充,这与短目录项有所不同,需要注意一下。
长目录项中除了文件名外,还有一个校验值,这个值用来找到对应的短目录项的,是用短目录项的文件名计算得到的。计算的公式如下:
uchar CheckValue(char shortname[])
{
int i,j = 0;
<span style="white-space:pre"> </span>uchar chknum = 0;
for (i = 11; i >0; i--)
{
chknum=((chknum & 1) ? 0x80 :0) + (chknum>>1) + shortname[j++];
}
return chknum;
}
函数返回的chknum就是所需的校验值,shortname就是对应的短目录项文件名,一共11位,包括扩展名。