FAT文件系统详解


前言

微软FAT文件系统详解


1 术语和定义

  • byte
    A string of binary digits operated upon as a unit.
  • bad (defective) sector
    A sector whose contents cannot be read or one that cannot be written.
  • file
    A named stream of bytes representing a collection of information.
  • sector
    A unit of data that can be accessed independently of other units on the media
  • cluster
    A unit of allocation comprising a set of logically contiguous sectors. Each cluster within the volume is referred to by a cluster number “N”. All allocation for a file must be an integral multiple of a cluster.
  • partition
    An extent of sectors within a volume.
  • volume
    A logically contiguous sector address space as specified in the relevant standard for recording.
  • Numerical Notation
    Numbers in decimal notation are represented by decimal digits, namely 0 to 9.
    Numbers in hexadecimal notation are represented as a sequence of one or more hexadecimal digits namely 0 to 9 and A to F, prefixed by “0x”.
    Zero represents a single bit with the value 0.
  • Arithmetic Notation
    The notation ip(x) shall mean the integer part of x.
    The notation ceil(x) shall mean the minimum integer that is greater than x.
    The notation rem(x,y) shall mean the remainder of the integer division of x by y.

2 卷结构

FAT文件系由四个区域组成
0-保留区
1-FAT区
2-根目录区(FAT32中没有)
3-文件和目录数据区
请添加图片描述
所有数据由小端字节序存储

3 启动Sector和BPB

BPB(BIOS Parameter Block)位于卷的保留区的第一个Sector内。该Sector有时也被叫做“Boost sector”或者“第0 sector”。同时,该sector是卷的第一个扇区。

所有FAT卷必须在启动扇区内有一个BPB(BIOS参数块)。多年来,BPB的发展如下:

  • 对于FAT32,BPB与FAT16/FAT12的BPB不同,从偏移量36开始

BPB(在FAT卷的启动扇区内的)必须总是有所有的BPB fields,无论是对FAT12/FAT16或是FAT32 PBP type。这确保了FAT卷的最大兼容性。

名字以 BPB_ 开头的字段都是BPB的一部分。所有以 BS_ 开头的字段都是启动扇区的一部分,不是BPB真正的一部分

本节将进一步描述构成BPB的字段。

3.1 FAT12, FAT16, FAT32的通用BPB结构

#pragma pack(push, 1)
struct common_BPB_structure 
{
    uint8_t  BS_jmpBoot[3];
    uint8_t  BS_OEMName[8];
    uint16_t BPB_BytsPerSec;
    uint8_t  BPB_SecPerClus;
    uint16_t BPB_RsvdSecCnt;
    uint8_t  BPB_NumFATs;
    uint16_t BPB_RootEntCnt;
    uint16_t BPB_TotSec16;
    uint8_t  BPB_Media;
    uint16_t BPB_FATSz16;
    uint16_t BPB_SecPerTrk;
    uint16_t BPB_NumHeads;
    uint32_t BPB_HiddSec;
    uint32_t BPB_TotSec32;
};
#pragma pack(pop)

sizeof(struct common_BPB_structure) == 36
字段描述
BS_jmpBoot跳转到启动代码的指令
该字段有两种格式:
- [0]=0xE9, [1]=0x??, [2]=0x90
- [0]=0xE9, [1]=0x??, [2]=0x??
这形成了一个三字节的Intel x86无条件分支(跳转)指令,跳转到操作系统引导代码的开头。启动代码通常占据卷的0扇区(除了BPB的部分),且紧随BPB,也有可能占据其它扇区。这两种形式都是可以接受的。JmpBoot[0]=0xEB是更常用的格式。
BS_OEMNameOEM标识。通常,这表明是什么系统格式化了卷
BPB_BytsPerSec每个扇区的字节数。只可能是512,1024,2048,4096
BPB_SecPerClus每个分配单元的扇区数。必须为2的指数倍。合法的值为:1,2,4,8,16,32,64,128
BPB_RsvdSecCnt从卷的第一个扇区开始,卷的保留区域中保留扇区的个数。 此字段用于将数据区的起始位置与分区/介质起始位置的簇大小的整数倍对齐。该值>0。此字段通常用于将数据区(簇#2)的开始与所需的对齐单位(通常是簇大小)对齐。
BPB_NumFATsfile allocation tables(FATs)在卷中的数量。建议有2个,当然1也能接受
BPB_RootEntCnt对于FAT12/16卷,此字段包含根目录中32字节目录条目的计数。对于FAT12和FAT16卷,此值应始终指定一个计数,当该计数乘以32时,会得到BPB_BytsPerSec的偶数倍。为了获得最大的兼容性,FAT16卷应使用值512。
BPB_TotSec16卷所有的扇区数。老标准,只有16位。如果该字段为0,BPB_TotSec32必不能为0。
BPB_Media合法值: 0xF0(可移动介质), 0xF8(可不移动介质的标准值), 0xF9, 0xFA, 0xFB, 0xFC, 0xFD, 0xFE, 0xFF
BPB_FATSz16FAT12/FAT16中为一个FAT占据的扇区数。FAT32中,必为0,BPB_FATSz32包含了FAT的扇区数
BPB_SecPerTrk中断0x13的每条轨道的扇区数。此字段仅适用于具有几何形状(体积由多个磁头和柱面分解为磁道)且在中断0x13上可见的介质。
BPB_NumHeads中断0x13的磁头数。如前所述,该字段与BPB_SecPerTrk相关。
BPB_HiddSec包含此FAT卷的分区之前的隐藏扇区计数。此字段通常仅与中断0x13上可见的媒体相关。 在未分区的介质上,此字段必须始终为零。试图利用此字段对齐数据区的起始位置是不正确的。
BPB_TotSec32卷的所有扇区数,包含所有4个区域的所有扇区数。若为0则BPB_TotSec16必不为0。对于FAT12/FAT16卷,如果BPB_TotSec16为0(扇区大于>0x10000),则代表扇区数。对于FAT32卷,该字段必不为0。
+-----sector0------+
| BPB   |   boot    |
+--------------------+

下面是BPB/boot扇区 FAT12和FAT16于FAT32的不同点。
NOTE: 建议将分区的第一个扇区对齐到所需的对齐单元。

3.2 FAT12和FAT16的扩展BPB结构

如果卷为FAT12或16,则剩余部分的BPB结构必须如下所述:

#pragma pack(push, 1)
struct extended_BPB_fat12_16
{
    uint8_t  BS_DrvNum;
    uint8_t  BS_Reserved1;
    uint8_t  BS_BootSig;
    uint32_t BS_VolID;
    uint8_t  BS_VolLab[11];
    char BS_FilSysType[8];
    uint8_t placeholder[448];
    uint8_t Signature_word[2];
    uint8_t dummy[0];
};
#pragma pack(pop)
BS_DrvNum中断0x13驱动号。将值设为0x80或0x00
BS_Reserved1值为0
BS_BootSig扩展启动签名。如果下面的两个字段非0则设为0x29。
这是签名字节,预示随后的三个字段在启动分区中存在
BS_VolID卷序列号。该字段和BS_BolLab一起支持在移动媒介上的卷追踪。这些值允许FAT文件系统驱动检测错误的硬盘插入移动驱动器。
BS_VolLab卷标签。该字段匹配记录在根目录下的11字节卷标签NOTE:当在跟目录内的卷标签名发生变更或创建时,FAT文件系统驱动必须确保它更新这个字段
BS_FilSysType字符串“FAT12 ","FAT16 ", "FAT " 之一
此字符串仅供参考,不决定FAT的类型
-448个字节的0x00
Signature_word2个字节,{ [0] = 0x55, [1] = 0xAA }
-如果BPB_BytsPerSec > 512,则余下的所有都设为0

3.3 FAT32卷的扩展BPB结构

FAT32卷的扩展BPB结构必须如下:

#pragma pack(push, 1)
struct extended_BPB_fat32
{
    uint32_t BPB_FATSz32;
    struct {
        uint8_t id : 4;
        uint8_t r1 : 3;
        uint8_t mi : 1;
        uint8_t r2 : 8;
    } BPB_ExtFlags;
    uint16_t BPB_FSVer;
    uint32_t BPB_RootClus;
    uint16_t BPB_FSInfo;
    uint16_t BPB_BkBoostSec;
    uint8_t  BPB_Reserved[12];
    uint8_t  BS_DrvNum;
    uint8_t  BS_Reserved1;
    uint8_t  BS_BootSig;
    uint32_t BS_VolID;
    uint8_t  BS_VolLab[11];
    char BS_FilSysType[8];
    uint8_t  placeholder[420];
    uint8_t Signature_word[2];
    uint8_t dummy[0];
};
#pragma pack(pop)
BPB_FATSz32该字段是FAT32 的 一个FAT所包含的扇区 32位计数
此时BPB_FATSz16必须为0
BPB_ExtFlags0-3位:基于0的活动FAT数,仅在禁用镜像时有效
4-6位:保留
7位 0-意味着FAT在运行时镜像到所有FAT中 1-仅有一个FAT是活动的,那就是0-3位所指的那个
8-15位:保留
BPB_FSVer高字节是主修订号,低字节是次修订号。这个FAT32卷的版本号,必须设为0x0
BPB_RootClus这设为根目录的第一个族的族号。这个值应当是2或之后第一个可用的族(不等是bad的)
BPB_FSInfoFSINFO结构体在保留区中的扇区号。在随后的备份启动扇区中有一个FSINFO结构体的拷贝,但是只有被该字段所指的拷贝才保持更新(换句话说,主和备份启动扇区都指向同一个FSINFO扇区)
BPB_BkBoostSec设为0或6。如果非零,则表示启动记录副本卷保留区域中的扇区号。
BPB_Reserved12个字节的0
BS_DrvNum中断0x13驱动号。将值设为0x80或0x00
BS_Reserved1值为0
BS_BootSig扩展启动签名。如果下面的两个字段非零则设为0x29
BS_VolID卷序列号。该字段和BS_BolLab一起支持在移动媒介上的卷追踪。这些值允许FAT文件系统驱动检测错误的硬盘插入移动驱动器。
这个ID应当由日期和时间简单生成一个32位的数
BS_VolLab卷标签。该字段匹配记录在根目录下的11字节卷标签.NOTE:当在跟目录内的卷标签名发生变更或创建时,FAT文件系统驱动必须确保它更新这个字段。当没有卷标签时该字段设为字符串"NO NAME "
BS_FilSysType设为字符串“FAT32 ”
此字符串仅供参考,不决定FAT的类型
-420个字节的0x00
Signature_word2个字节,{ [0] = 0x55, [1] = 0xAA }
-如果BPB_BytsPerSec > 512,则余下的所有都设为0

3.4 初始化FAT卷

FAT类型的抉择依赖族号。

  • 软盘格式化为FAT12。FAT12卷<=4MB
  • 对于512字节扇区大小的媒介:如果卷<=512MB,格式化为FAT16,否则格式化为FAT32。可以覆盖默认的FAT类型选择。

单位是512字节(一个扇区的大小)

struct DSKSZTOSECPERCLUS {
  DWORD DiskSize;
  BYTE  SecPerClusVal;
};
DSKSZTOSECPERCLUS DskTableFAT16 [] = {
	{8400, 0}, /* disks up to 4.1 MB, the 0 value for SecPerClusVal trips an error */
	{32680, 2}, /* disks up to 16 MB, 1k cluster */
	{262144, 4}, /* disks up to 128 MB, 2k cluster */
	{524288, 8}, /* disks up to 256 MB, 4k cluster */
	{1048576, 16}, /* disks up to 512 MB, 8k cluster */
	/* The entries after this point are not used unless FAT16 is forced */
	{2097152, 32}, /* disks up to 1 GB, 16k cluster */
	{4194304, 64}, /* disks up to 2 GB, 32k cluster */
	{0xFFFFFFFF, 0} /*any disk greater than 2GB, 0 value for SecPerClusVal trips an error */

};
DSKSZTOSECPERCLUS DskTableFAT32 [] = {
	{66600, 0}, /* disks up to 32.5 MB, the 0 value for SecPerClusVal trips an error */
	{532480, 1}, /* disks up to 260 MB, .5k cluster */
	{16777216, 8}, /* disks up to 8 GB, 4k cluster */
	{33554432, 16}, /* disks up to 16 GB, 8k cluster */
	{67108864, 32}, /* disks up to 32 GB, 16k cluster */
	{0xFFFFFFFF, 64}/* disks greater than 32GB, 32k cluster */
};

给定硬盘大小和FAT类型(16/32),上面决定了BPB_SecPerClus值

下面计算 FAT拿走了 多少扇区 用以使能设置BPB_FATSz16或BPB_FATSz32字段。此时假定BPB_RootEntCnt,BPB_RsvdSecCnt和BPB_NumFATs已经正确的设置。同时也假定DskSize是卷大小,同时被BPB_TotSec32或BPB_ToSec16反射

RootDirSectors = ((BPB_RootEntCnt * 32) + (BPB_BytsPerSec – 1)) / BPB_BytsPerSec; (=32计算2-区的大小)
TmpVal1 = DskSize – (BPB_ResvdSecCnt + RootDirSectors); 
TmpVal2 = (256 * BPB_SecPerClus) + BPB_NumFATs; (256*8+2 = 2050)
If(FATType == FAT32)
	TmpVal2 = TmpVal2 / 2; (1025)
FATSz = (TMPVal1 + (TmpVal2 – 1)) / TmpVal2;  (  (?+1024)/1025 )
// 计算1-区的大小
If(FATType == FAT32) {
	BPB_FATSz16 = 0;
	BPB_FATSz32 = FATSz;
} else {
	BPB_FATSz16 = LOWORD(FATSz);
	/* there is no BPB_FATSz32 in a FAT16 BPB */
}

NOTE:上述计算并不完美。它偶尔将FATSz计算值高达2扇区,这对FAT太大了,偶尔高达8扇区对FAT32太大了。但它从不会将FATSz算小。因为以浪费几个扇区为代价,拥有太大的FATSz是可以的,所以这种计算出奇地简单,在某些情况下甚至可以安全地弥补它的不足。
FAT中未使用的扇区必须设置为0x0。

3.5 当挂载卷时FAT类型的决议

FAT类型单独的由卷中族的数量决定(CountOfClusters)
下述步骤描述了族数量的计算

  1. 首先,决定被2-根目录区占用的扇区的数量:
    RootDirSectors = ((BPB_RootEntCnt * 32) + (BPB_BytsPerSec – 1)) / BPB_BytsPerSec
    注意到:FAT32卷,BPB_RootEntCnt为0,因此在FAT32卷上,RootDirSectors总是为0
  2. 决定3-数据区的扇区数
    if (BPB_FATSz16 != 0)  FATSz = BPB_FATZs16;
    else FATSz = BPB_FATSz32;
    if (BPB_TotSec16 != 0)  TotSec = BPB_TotSec16;
    else TotSec = PB_TotSec32;
    
    DataSec = TotSec - (BPB_ResvdSecCnt + (BPB_NumFATs*FATSz) + RootDirSectors);
    数据区大小 = 总大小 - (保留区大小 + FAT区大小 + 根目录区大小)
    
3. 最后,决定族数量
	```
	CountofClusters = DataSec / BPB_SecPerClus;

下面方法决定FAT类型

FAT12FAT16FAT32
<4085<65525其它数量
  • 一个FAT12卷不能包含大于4084个族
  • 一个FAT16卷不能包含小于4085个族或大于65524个 族

3.6 备份BPB结构

sector #0包含了BPB。在FAT32格式卷上,sector #6必须包含BPB的备份。
sector #0 和#6 的 BPB_BkBoostSec字段的值都为6
当sector #0损坏时,卷修复工具从sector #6中获取BPB

NOTE:从PBP_BkBootSec扇区启动的就是一个完整的boot record。微软FAT32 启动扇区通常有三个扇区的长度。从BPB_BkBootSec扇区启动就有所有这三个扇区的拷贝。FSInfo扇区的拷贝也在这儿,虽然该备份启动扇区的BPB_FSInfo字段的值和 sector#0 BPB相同

4 FAT

File Allocation Table(FAT)中每一个有效的入口代表一个族的状态,族来自包含根目录区、文件和目录区的族集合中。每个入口的大小如下:

  • 对于FAT12卷,每个FAT入口长12位
  • 对于FAT16卷,每个FAT入口长16位
  • 对于FAT32卷,每个FAT入口长32位

一个FAT(文件分配表)可能比实际需要的要大。FAT中额外的入口(位于FAT的最后)必须被设为0。FAT定义了一个文件的“族”的单向链表,从而按簇号映射卷的数据区域。卷中的第一个数据族就是 cluster#2。
NOTE:一个FAT 目录或文件是一个简单的常规文件,目录类型有特殊属性标记。目录项,是一系列的32字节的 目录条目。

FAT条目值必须如下表描述:

FAT12FAT16FAT32评论
0x0000x00000x0000000cluster is free
0x002~MAX0x0002~MAX0x0000002 ~MAX族被分配。条目值是 该族的下一个族的族号 。MAX是最大有效族号
(MAT+1) ~-xFF6(MAX+1)~0xFFF6(MAX+1)~0xFFFFFF6保留并且不能使用
0xFF70xFFF70xFFFFFF7指示坏族
0xFF8~0xFFE0xFFF8~0xFFFE0xFFFFFF8~0xFFFFFFE保留并不能使用。可以解释为文件中的分配的族和最后一个族(表示文件结束条件)。

|0xFFF |0xFFFF |0xFFFFFFFF | 族被分配且是文件的最后一个族|
NOTE:FAT32条目的高四位是保留的。当修改任意FAT32的FAT条目时,所有FAT的实现必须保留高四位的最近值,除了在卷初始化(也称为格式化)期间(整个FAT表必须设置为0)。
NOTE:No FAT32 volume should ever be configured containing cluster numbers available for
allocation >= 0xFFFFFF7.
NOTE: FAT12卷的FAT的大小被限制在6K个扇区。FA16是128K。FAT32则没有限制

4.1 决议族的FAT条目

给定任意有效的族号N,该节表述用于为该族号决议 FAT条目的算法。

FAT16 和 FAT32
第一个FAT的FAT条目决议如下:

if (BPB_FATSz16 != 0
  FATSz = BPB_FATSz16
else
  FATSz = BPB_FATSz32

if (FATType == FAT16)
  FATOffset = N*2
else if (FATType == FAT32)
  FATOffset = N*4

ThisFATSecNum = BPB_ResvdSecCnt + (FATOffset / BPB_BytsPerSec);
ThisFATEntOffset = REM(FATOffset / BPB_BytsPerSec);

ThisFATSecNum: 扇区号,在第一个FAT中,包含族N 目录项的FAT扇区的号码
在其他FAT中计算扇区号:

SectorNumber = (FatNumber * FATSz) + ThisFATSecNum
FatNumber:如果是第二个FAT则是2,如果是第三个扇区则为3,and so on.

FAT条目的内容可有扇区的内容中抽取(一旦该扇区被读取):

if (FATType == FAT16)
  *((WORD *) &SecBuff[ThisFATEntOffset]) = FAT16ClusEntryVal;
else {
  FAT32ClusEntryVal = FAT32ClusEntryVal & 0x0FFFFFFF;
  *((DWORD *) &SecBuff[ThisFATEntOffset]) =  (*((DWORD *) &SecBuff[ThisFATEntOffset])) & 0xF0000000;
  *((DWORD *) &SecBuff[ThisFATEntOffset]) = (*((DWORD *) &SecBuff[ThisFATEntOffset])) | FAT32ClusEntryVal;
}
  

NOTE: typedef uint16_t WORD; typedef uint32_t DWORD;
NOTE: FAT16/FAT32 FAT条目不能跨越扇区边界

FAT16 和 FAT12
每条FAT12的FAT目录项大小为1.5字节(12比特)。第一个FAT的FAT条目决定如下:

if (FATType == FAT12)
	FATOffset = N + (N / 2);
/* Multiply by 1.5 without using floating point, the divide by 2 rounds DOWN */
ThisFATSecNum = BPB_ResvdSecCnt + (FATOffset / BPB_BytsPerSec);
ThisFATEntOffset = REM(FATOffset / BPB_BytsPerSec);
//The logic below checks for the entry spanning a sector boundary and comments on an
//approach to deal with this case:
If(ThisFATEntOffset == (BPB_BytsPerSec – 1)) {
	/* This cluster access spans a sector boundary in the FAT */
	/* There are a number of strategies to handling this. The */
	/* easiest is to always load FAT sectors into memory */
	/* in pairs if the volume is FAT12 (if you want to load */
	/* FAT sector N, you also load FAT sector N+1 immediately */
	/* following it in memory unless sector N is the last FAT */
	/* sector). It is assumed that this is the strategy used here */
	/* which makes this if test for a sector boundary span */
	/* unnecessary. */
}

ThisFATSecNums 是 第一个FAT中 FAT扇区包含 族N的扇区号
计算其它FAT中的扇区号:
SectorNumber = (FatNumber * FATSz) + ThisFATSecNum
where, FatNumber will be 2 for the second FAT, 3 for the third FAT, and so on.

FAT12ClusEntryVal = *((WORD *) &SecBuff[ThisFATEntOffset]);
If(N & 0x0001)
 	FAT12ClusEntryVal = FAT12ClusEntryVal >> 4; /* Cluster number is ODD */
Else
	FAT12ClusEntryVal = FAT12ClusEntryVal & 0x0FFF; /* Cluster number is EVEN */

NOTE:对于号码为偶数的族,上述逻辑正确地获得了描述FAT条目的低12位。对于奇数簇,逻辑正确地获得高12位。

修改内存中的FAT条目实例,事项下述逻辑:

If(N & 0x0001) {
	FAT12ClusEntryVal = FAT12ClusEntryVal << 4; /* Cluster number is ODD */
	*((WORD *) &SecBuff[ThisFATEntOffset]) = (*((WORD *) &SecBuff[ThisFATEntOffset])) & 0x000F;
} Else {
	FAT12ClusEntryVal = FAT12ClusEntryVal & 0x0FFF; /* Cluster number is EVEN */
	*((WORD *) &SecBuff[ThisFATEntOffset]) = (*((WORD *) &SecBuff[ThisFATEntOffset])) & 0xF000;
}
*((WORD *) &SecBuff[ThisFATEntOffset]) = (*((WORD *) &SecBuff[ThisFATEntOffset])) | FAT12ClusEntryVal;

4.2 保留FAT目录项(条目)

FAT的前两条目是保留项。
第一个保留项( FAT[0] ), 它的低8位是BPB_Media字节值,其它位都为1。如BPB_Media=0xF8,则:

  • 对于FAT12, FAT[0] = 0x0FF8
  • 对于FAT16, FAT[0] = 0xFFF8
  • 对于FAT32, FAT[0] = 0x0FFFFFF8

第二天保留条目( FAT[1] ),由格式化程序设置为EOC值。

  • 在FAT12卷上,此条目在格式之后不会被修改,因此始终包含EOC标记
  • 对于FAT16/32,在微软windows系统上的启动程序可能会将FAT[1]的高两位用于 dirty卷标志(其它位总是设为1)
    • For FAT16:
      ClnShutBitMask = 0x8000;
      HrdErrBitMask = 0x4000;
    • FAT32:
      ClnShutBitMask = 0x08000000;
      HrdErrBitMask = 0x04000000;
    • Bit ClnShutBitMask :
      如果为1,卷是”clean“的。卷可以挂载访问。如果为0,卷是”dirty“的,意味着FAT文件系统驱动不能正确卸载该卷(在之前的挂载操作进行中)。应扫描卷内容,查看是否损坏了文件系统元数据
    • Bit HrdErrBitMask:
      dsf 如果为1,没有遇到读写错误。如果为0,则在最后一次挂载时,文件系统驱动遇到了一个硬盘IO错误,这指示了存在坏的扇区。此时建议用硬盘修复工具扫描该卷,它会进行分析找出坏的扇区。

4.3 free space 决议

文件系统驱动程序必须扫描所有的FAT目录项,来构造一系列的free/available族。一个free族由 对应的FAT条目值记录:

  • FAT12: 0x000
  • FAT16: 0x0000
  • FAT32: 0x0000000 (FAT32的目录项长32位,高四位为保留项和被忽略的,因此实际使用28位)

请注意,空闲族列表不存储在卷上。在FAT32卷上,BPB_FSInfo扇区可能包含卷上可用簇的有效计数

4.4 其他需要注意的事项

  • FAT 的最后一个扇区不一定是 FAT 的全部部分。FAT在最后一个FAT扇区中的簇号处停止,该簇号对应于簇号条目(CountOfClusters+1),并且该条目不一定位于最后一个FAT扇区的末尾。
    FAT实现不得对(CountOfClusters+1)FAT条目后最后一个FAT扇区的内容做出任何假设。初始化(格式化)卷时,实现必须将最后一个FAT条目后的所有字节清零。
  • 为每个FAT保留的扇区数(字段 BPB_FATSz16或BPB_FATSz32的值)可能比实际需要的要大(包含主整个FAT)。因此,每个FAT后面都有未使用的FAT扇区。每个实现都必须使用CountOfClusters确定FAT中最后一个有效扇区的值(FAT中的最后一个合法扇区是包含编号为CountOfClusters+1的FAT条目的扇区)。
    在卷初始化/格式化期间,必须将最后一个有效扇区(定义为包含最后一个簇的FAT条目的扇区)之外为FAT保留的所有扇区设置为0x0。

5 文件系统信息(FSInfo)结构

文件系统信息(FSInfo)结构结构帮助优化驱动器实现,通过包含卷上的可用族和第一个可使用的族号。
NOTE: 结构体中的信息必须考虑为咨询值,文件系统驱动实现必须在挂载时验证该值。文件系统驱动实现不被要求确保结构体中的信息保持一致性(虽然建议这么做)。

FSInfo结构仅会出现在FAT32卷中。在卷初始化(格式化)时,结构体必须持久性的在硬盘上。结构体必须位于 sector#1——紧随包含BPB的扇区。

该结构体的备份位于 sector#7

#pragma pack(push, 1)
struct FSInfo 
{
  uint32_t FSI_LeadSig;
  uint8_t  FSI_Reserved1[480];
  uint32_t FSI_StrucSig;
  uint32_t FSI_Free_Count;
  uint32_t FSI_Nxt_Free;
  uint8_t  FSI_Reserved2[12];
  uint32_t FSI_TrailSig;
};
#pragma pack(pop)

FSInfo结构体表:

字段偏移量大小描述
FSI_LeadSig04值为=x41615252,leading signature用于验证FSInfo在扇区中的开头
FSI_Reserved14480保留,必须为0
FSI_StrucSig4844值为0x61417272。验证FSInfo结构完整性的附加签名。
FSI_Free_Count4884卷上包含最新的已知可用族的数量
值0xFFFFFFFF知名可用数量时未知的,该字段的内容必须在挂载时验证(然后在内存中由驱动维护)
建议该字段在卷卸载时包含可用族的准确值
FSI_Nxt_Free4924包含卷中第一个可使用的族号
FSI_Reserved249612保留,必须为0
FSI_TrailSig5084值为0xAA550000, 尾部签名用于验证包含FSInfo结构扇区数据的准确性

6 目录结构体

一个FAT目录一种特殊的文件类型。目录像是一种为其他文件和子目录服务的容器。目录内容(数据)是一串32字节的目录项(条目)。

字段偏移量大小描述
DIR_Name01111个字节的“短”文件名
DIR_Attr111合法的文件属性类型:
ATTR_READ_ONLY 0x01
ATTR_HIDDEN 0x02
ATTR_SYSTEM 0x04
ATTR_VOLUME_ID 0x08
ATTR_DIRECTORY 0x10
ATTR_ARCHIVE 0x20
ATTR_LONG_NAME定义如下:(ATTR_READ_ONLY
DIR_NTRes121保留,必须为0
DIR_CrtTimeTenth131文件创建时间的组成部分。计数十分之一秒。有效范围为:0<=DIR_CrtTimeTenth<=199
DIR_CrtTime142创建时间,颗粒度为2s
DIR_CrtDate162创建时间
DIR_LstAccDate182最后访问日期。最后访问被定义为读或写操作,在该条目表示的文件/目录上。该字段必须在文件修改(写操作)时必须被更新,日期值必须等于DIR_WrtDate
DIR_FstClusHI202被该目录项描述的文件/目录所在的第一个数据族号的高两字节
仅卷FAT32有效。在FAT12/16中必须为0
DIR_WrtTime222最后修改(写)时间。必须与DIR_CrtTime相等,在文件被创建时
DIR_WrtDate242最后修改日期,必须与DIR_CrtDate想等,在文件被创建时
DIR_FstClusLO262被该目录项描述的文件/目录所在的第一个数据族号的低两字节
DIR_FileSize28432位,被该目录项描述的文件/目录实际大小

6.1 文件/目录 名称(字段DIR_Name)

DIR_Name字段包含11个字符(短)名称,它逻辑上包含两个部分

  • 8字节名称的主要部分
  • 3字节扩展
    上面的两个用空格(0x020)填补空余
  1. 隐式“.”字符用于分离主要和扩展部分,但是“.”从不存储在DIR_Name字段中
  2. DIR_Name[0] = 0xE5暗示目录项是可用的。然而在KANJI字符集中,0xE5是一个有效值,所以在KANJI字符集中用0x05代替0xE5
  3. DIR_NAME[0] = 0x00 代表目录项是空闲的,同时在它之后的目录项目也都是空闲的
  4. DIR_NAME[0] 目录项不能为0x00(也就是说,名称不能以空格开头)
  5. 目录中的目录项不能重名

命名限制:

  • 小写字符不允许
  • 非法值:
    • < 0x20的值(除了DIR_NAME[0]=0x05)
    • 0x22, 0x2A, 0x2B, 0x2C, 0x2E, 0x2F, 0x3A, 0x3B, 0x3C, 0x3D, 0x3E, 0x3F, 0x5B, 0x5C, 0x5D, and 0x7C

eg:

| 用户输入的文件/目录名|存储在字段DIR_Name中的内容 |
|--|--|
foo.bar          "FOO     BAR"
FOO.BAR          "FOO     BAR"
Foo.Bar          "FOO     BAR"
foo              "FOO        "
foo.             "FOO        "
PICKLE.A         "PICKLE  A  "
prettybg.big     "PRETTYBGBIG"
.big             "        BIG"

6.2 文件/目录属性

属性值描述
ATTR_READ_ONLY (0x01)文件不能被修改——所有的求改请求将会返回适当的错误码
ATTR_HIDDEN (0x02)“隐藏文件”默认不显示
ATTR_SYSTEM (0x04)系统组成文件。默认隐藏
ATTR_VOLUME_ID (0x08)对应的目录项卷标。DIR_FstClusHI 和 DIR_FstClusLO 必须总为0,因为该目录项不需要族
仅有根目录能够包含一个拥有该属性的目录项,子目录不允许包含。长文件名条目除外。
ATTR_DIRECTORY (0x10)子目录。DIR_FileSize为0,即使族已经被分配用于目录
ATTR_ARCHIVE (0x20)当文件被创建、重命名或修改时必须设置该值,该值的存在意味着该文件的属性被修改
备份工具能够利用这个信息来备份文件

6.3 日期/时间

下面是可选项:

  • DIR_CrtTime
  • DIR_CrtTimeTenth
  • DIR_CrtDate
  • DIR_LstAccDate

如果驱动支持上述值则应当为0,如果上述值为0则应当忽略

下述必须被更新:

  • DIR_WrtTime
  • DIR_WrtDate

数据格式
日期格式

6.4 文件/目录大小

最大文件大小是0xFFFFFFFF字节,任意分配给文件的族大小必须<=0x100000000字节。在该大小族的最后一个字节不能是文件的一部分

最大有效目录大小是2^21字节

6.5 目录创建

当一个新的目录创建时,文件系统驱动必须确保以下几项:

  • ATTR_DIRECTORY位必须在DIR_Attr字段被设置
  • DIR_FileSize必须设为0
  • 至少分配一个族——DIR_FstClusLO和DIR_FstClusHI字段必须指向第一个被分配的族号
  • 如果只分配了一个族,对应的文件分配表条目必须指示EOF条件
  • 分配的族必须被初始化为0
  • 除了根目录,每个目录必须在目录项的开头包含如下两个条目:
    • 第一个目录条目的目录名必须为.
      这个.指向当前目录。上述关于DIR_Attr和DIR_FileSize字段的规则同样试用。因为.目录项指向当前目录,DIR_FstClusLO and DIR_FstClusHI fields的内容必须和当前目录一样。所有的日期和时间字段必须设为和当前目录的一样。
    • 第二个目录项名字必须是..
      ..目录项指向当前目录的父目录。上述关于DIR_Attr和DIR_FileSize字段的规则同样试用。因为..目录项指向当前目录的父目录,DIR_FstClusLO and DIR_FstClusHI fields的内容必须和当前目录的父目录一样。如果父目录为根目录DIR_FstClusLO and DIR_FstClusHI contents must be set to 0,所有日期和时间字段必须设置为与包含目录的值相同的值。

6.6 根目录

根目录是一个特殊的容器文件,它在卷初始化(格式化)时创建,因此在格式化后的卷上永存。
在FAT32, FAT16卷中,根目录必须紧随在最后一个文件分配表后。根目录的第一个扇区的位置计算如下:
FirstRootDirSecNum = BPB_ResvdSecCnt + (BPB_NumFATs * BPB_FATSz16)
FAT12和FAT16卷上根目录的大小的计算用到了BPB_RootEntCnt 字段的内容。

在格式化的FAT32卷上,根目录的大小可以是可变的。根目录第一个族的位置存储在BPB_RootClus字段。
只有根目录能够包含含有DIR_Attr为ATTR_VOLUME_ID的一个目录项。
根目录没有名字,也没有任何与日期/时间戳相关的信息。最后,根目录不包含点或dotdot条目(?是这样吗)。

6.7 文件分配

文件的第一个族的族号记录在目录中对应的目录项内。对于0大小的文件,目录项中将其第一个族的族号设为0.

文件分配表的目录项对应的为文件分配的族,要么指明这个单独/最后一个分配的族,要么包含下一个分配族的族号。

分配的族的第一个扇区的决议
3.5说明了f RootDirSectors, and FATSz的计算
数据区(cluster#2)的开始的第一个扇区由下方法计算:
FirstDataSector = BPB_ResvdSecCnt + (BPB_NumFATs * FATSz) + RootDirSectors

给定任意有效数据族号N,该族的第一个扇区的扇区号计算:
FirstSectorofCluster = ((N – 2) * BPB_SecPerClus) + FirstDataSector;

7 长文件名实现(可选的)

在DIR_Name字段中文件名/目录名只有11个字节,这个被称为“短名”,对应的目录条目称为 短名目录条目。

NOTE: 长文件名可被所有FAT格式支持

长文件名存储在一组额外的目录条目(长名目录条目)中。这组额外的目录条目(也称为长名称目录条目)必须紧接在相应的短名称目录条目之前,因此与短名称目录条目的物理上是连续的。

NOTE:长名称目录条目的顺序以相反的顺序存储(首先存储集合中的最后一个条目,然后是条目n-1,接着是条目n-2,以此类推,直到条目1

一个长名目录条目结构如下:

offsetsize
LDIR_Ord91该条目的顺序
LDIR_Name1110包含构成长名称一部分的字符1到5
LDIR_Attr111属性——必须设为 ATTR_LONG_NAME,定义如下:
ATTR_LONG_NAME = (ATTR_READ_ONLY | ATTR_HIDDEN | ATTR_SYSTEM | ATTR_VOLUME_ID)
NOTE: 决议一个目录项是否为长名目录项的掩码定义如下:
#define ATTR_LONG_NAME_MASK =
(ATTR_READ_ONLY |
ATTR_HIDDEN |
ATTR_SYSTEM |
ATTR_VOLUME_ID |
ATTR_DIRECTORY |
ATTR_ARCHIVE)
LDIR_Type121必须为0
LDIR_Chksum131长名称目录条目集末尾关联的短名称目录条目中的名称校验和。
LDIR_Name21412包含组成长名的字符6~11
LDIR_FstClusLO262必为0
LDIR_Name3284包含租场长名的字符12~13

下面说明长名目录条目如何存储:(7.3示例)

EntryOrdinal
第N个长名目录项LAST_LONG_ENTRY (0x40) | N
第1个长名目录项1
与前面的长名称目录条目集关联的短名称目录条目N/A

7.1 序号生成

N的生成规则:

  1. 集合中的第一个条目中LDIR_Ord=1
  2. 随后LDIR_Ord的值必须单调递增
  3. 第N个(最后一个)成员必须包含值 ( N | LAST_LONG_ENTRY)

如果长名称目录条目集合中的任何成员不遵循上述规则,则认为该集合已损坏。

7.2 校验和生成

当短、长目录条目创建时,一个8比特的校验和有短目录条目的名称计算。所有11个字节的名字都用于计算校验和。校验和放在长名目录条目的LDIR_Chksum字段中。如果有任何的校验和与名字匹配则认为长名目录条目已损坏。

// ChkSum()
// 
unsigned char ChkSum(unsigned char* pFcbName)
{
  short FcbNameLen = 11;
  unsigned char Sum = 0;
  for (; FcbNameLen != 0; FcbName--)
  {
    //            将sum向右边循环移动1位
    Sum = ((Sum & 1) ? 0x80 : 0) + (Sum >> 1) +    *PfcbName++;
  }
  return Sum;
}

7.3 长存储实例

名称以NULL结尾,后面以0xFFFF补齐目的时检查长名字段是否损坏。如果名称刚好填满长命(13个字节)则NULL和0xFFFF不是必须的。

NAME:”The quick brown.fox"
在这里插入图片描述
长文件名称的命名限制和字符集
长文件名限制在255个字符,不包括结尾的NULL。字符组合可以是短文件名定义的任意组合(see 6.1),.也可以使用多次。 下面的6个特殊字符在长文件名中是合法的(在短文件名中是不合法的)+ , ; = [ ]

长文件名中插入的空格也是允许的。开头和结尾的空格是被忽略的。开头和插入的.是允许的结尾的空格是忽略的。存储在长目录项中的长文件名用UNICODE编码。UNICODE字符是16位的字符。在短名目录项中存储UNICODE是不可能的,因为它存的是8位的DBCS字符。传递给文件系统的长文件名不会转换为大写,而是保留其原始大小写值。UNICODE通过始终将小写字符转换为单个唯一的大写字符,解决了某些OEM代码页中普遍存在的案例映射问题。

7.4 管理名称创建和匹配的规则

所有长文件名和短文件名的联合被定义为卷中包含的对象的命名空间。
该命名空间,必须遵守以下规则:

  • 任意在某个目录中的名字,无论是短名或长名,只能出现一次(大小写不同是会忽略的,这样的名字是冲突的)【这个规则在Windows11上也是,无法命名一个仅有大小写不一样的短文件名】
  • 当介质上的字符(无论是存储在OEM字符集中还是UNICODE中)无法转换为OEM或ANSI代码页中的相应字符时,它总是被转换为“”(下划线)字符——该字符在介质上不会被修改。“”字符在所有OEM代码页和ANSI中都是相同的。

案例:512字节大小扇区 128 MB HDD FAT32格式的BPB

在这里插入图片描述

案例:512字节大小扇区 128 MB HDD FAT16格式的BPB

在这里插入图片描述


小结

卷——分区——文件系统
暂且认为三者一一对应

一个字节为8位,由于硬盘物理限制无法单字节访问。
通常情况下512字节组成一个扇区,卷中大都以扇区为单位计量大小,多个扇区组成一个族,即为最小可分配空间(blocksize)。


FAT中的一些规则被沿用到了今天的Windows上

references

https://jyywiki.cn/pages/OS/manuals/MSFAT-spec.pdf

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值