FAT数据结构(FAT Data Structure)
接下来一个重要的数据结构就是FAT表(Fat Allocation Table),它是——对应于数据区簇号的列表。
文件系统分配磁盘空间是按照簇来分配的。因此,文件占用磁盘空间时,基本单位不是字节而是簇,即使某个文件只有一个字节,操作系统也会给它分配一个最小存储单元——簇。为了可以将磁盘空间有序地分配给相应的文件,而读取文件的时候又能够从相应的地址读出文件,我们把数据区空间分为BPB_BytesPerSec(每扇区字节数)×BPB_SecPerClus(每簇扇区数)字节长的簇来管理,FAT表项的大小与FAT类型有关,FAT12的表项为12bit,FAT16的表项为16bit,而FAT32的表项则为32bit。对于大文件,需要分配多个簇。同一个文件的数据并不一定完整地存放在磁盘中一个连续的区域内!而往往会分成若干段,像链条一样存放。这种存储方式称为文件的链式存储。为了实现文件的链式存储,文件系统必须准确地记录哪些簇已经被文件占用,还必须为每个已经占用的簇指明存储后继内容的下一个簇的簇号,对于文件的最后一个簇,则需要指明本簇无后继簇。这些都是由FAT表来保存的,FAT表的对应表项中记录着它所代表的簇的有关信息:诸如是否空,是否坏簇,是否已经是某个文件的尾簇等等。
以FAT16为例说明FAT区的结构如下:
FAT的项数与硬盘上的总簇数相关(因为每一个项要代表一个簇,簇越多,当然需要的FAT表项越多),每一项占用的字节数也与总簇数有关(因为其中需要存放簇号,簇号越大,当然每项占用的字节数就越大)。
这里说一下FAT目录,其实它和普通文件并没有什么不一样的地方,只是多了一个表示它是目录的属性(attrib),另外就是目录所链接的内容是一个32字节的目录项(32-byte FAT directory entries后面有具体讨论)。除此之外,目录和文件没什么区别。FAT表是根据簇数和文件对应的。第一个存放数据的簇是簇2。
簇2的第一个扇区(磁盘的数据区)根据BPB来计算,首先我们计算根目录所占的扇区数:
RootDirSectors=((BPB_RootEntCnt × 32) + (BPB_BytesPerSec - 1))/BPB_BytesPerSec;
根目录扇区数=((根目录数 × 32) + (每扇区字节数 - 1))/每扇区字节数(根目录数代表目录的项数,每个项目长度为32bytes)
上图中BPB_RootEntCnt=512,BPB_BytesPerSec=512,则按照上述计算公式,可以得出RootDirSectors=32,也就是说,这个U盘根目录扇区数为32,约为16KB。
因为FAT32的BPB_RootEntCnt(根目录项数)为0,所以对于FAT32卷RootDirSectors的值也一定是0。上式中的32是每个目录项所占的字节数。计算结果四舍五入。
数据区的起始地址,簇2的第一个扇区由下面的公式计算:
If(BPB_FATSz16 != 0) (如果FAT12/16一个FAT表所占的扇区数不为0,则该FAT必为FAT12/16)
FATSz = BPB_FATSz16;
Else
FATSz = BPB_FATSz32;
FirstDataSector = BPB_RsvdSecCnt + (BPB_NumFATs × FATSz) + RootDirSectors;
仍以上图为例,我们来计算一下这个U盘的数据区第一个扇区号。
上图中BPB_RsvdSecCnt=4,BPB_NumFATs=2,FATSz=246,RootDirSectors已经算出,为32。则数据区起始扇区号=4+246×2+32=528.换言之,数据区紧跟在根目录后,根目录的最后一个扇区号为527。
数据区第一个扇区 = 保留扇区数 + (FAT表的份数 × FAT表所占的扇区数) + 根目录扇区数。
NOTE:扇区号指的是针对卷中包含BPB的第一个扇区的偏移量(包含BPB的第一个扇区是扇区0),并不是必须直接和磁盘的扇区相对应。因为卷的扇区0并不一定就是磁盘的扇区0。
给一个合法的簇号N,该簇的第一个扇区号(针对FAT卷扇区0的偏移量)由下式计算:
FirstSectorofCluster = ((N-2)×BPB_SecPerClust) + FirstDataSector;(-2是因为起始标志F8 FF FF FF占用两个存放簇号的位置的缘故)。
例如:下图中是U盘的FAT1截图(WinHex)
要注意,这里的簇号指的是相当于FAT1的偏移位置,而非指磁盘的最小单位。
根据上面提供的公式:我们计算出任意簇号对应的第一个扇区号。比如,簇号为7。那么簇7的第一个扇区号=((7-2)×64)+528=848。(这里的BPB_SecPerClust为64)。在WinHex中按ctrl-G,弹出如下对话框。在Cluster中输入7,可以看到扇区号直接变成了848,验证了结果的正确。
NOTE:因为BPB_SecPerClus总是2的整数次方(1,2,4,8,……)这意味着BPB_SecPerClus的乘除法运算可以通过移位(SHIFT)来进行。在当前Intel x86架构2进制的乘法(MULT)和除法(DIV)的机器指令非常的繁杂和强大,而使用移位来运算则会相对的快很多。
FAT类型辨别
这是一个经常产生错误的地方,并且常常会出现诸如“off by 1”,“off by 2”,“off by 10”和“massively off”的错误,事实上,FAT类型的检测十分简单,FAT的类型——FAT12,或是FAT16或是FAT32——只能通过FAT卷中簇的数量来判定,没有其他办法。
请仔细阅读本段的每一个细节,每个词都很关键。比如“簇数(count of cluster)”并不是指“最大可取得的簇的数量(maximum valid cluster number)”,因为数据区的第一个簇是簇2而不是0或1。
首先我们讨论这个“簇数”是如何计算的,它完全根据BPB的内容来确定,我们先计算根目录所占的扇区数(前面已经有叙述)。
RootDirSectors = ((BPB_RootEntCnt × 32) + (BPB_BytesPerSec - 1))/BPB_BytesPerSec;
FAT32的RootDirSectors为0。
接下来我们检测数据区中的扇区数:
If(BPB_FATSz16 != 0)
FATSz = BPB_FATSz16;
Else
FATSz = BPB_FATSz32;
If(BPB_TotSec16 != 0)
TotSec = BPB_TotSec16;
Else
TotSec = BPB_TotSec32;
DataSec = TotSec – (BPB_RsvdSecCnt + (BPB_NumFATs × FATSz) + RootDirSectors);
数据扇区 = 总扇区数 – (保留扇区数 + (FAT数 × FAT表所占扇区数) + 根目录扇区数)
仍然以上图中的U盘数据为例,由于U盘的文件系统是FAT16,所以FAT表的大小即BPB_FATSz16,又因为图示U盘中BPB_TotSec16为0,所以FAT卷上四个基本区的总扇区数取决于BPB_TotSec32,这里的BPB_TotSec32的值为42029408。BPB_RsvdSecCnt为4,BPB_NumFATs为2,FATSz为246,RootDirSectors上面已经计算出为32,则数据扇区数=42029408-(4+(2×246)+32)=4028880。
计算簇数:
CountofClusters = DataSec / BPB_SecPerClus;
簇数 = 数据扇区数 / 每簇扇区数
同样,数据区的簇数=4028880/64=62951。这个计算结果和实际结果吻合!
请记住计算结果四舍五入。
现在我们就可以判定FAT的类型了,这部分请仔细阅读,否则会导致off by 1的错误。
在下面的程序中,“<”和“<=”是不一样的,另外注意数字不要弄错。
If(CountofCluster < 4085) {
}
Else if(CountofCluster < 65525) {
}
Else {
}
这是检测FAT类型的唯一办法。世界上不存在簇数大于4084的FAT12卷,也不存在簇数小于4085或是大于65524的FAT16卷,同样没有哪个FAT32卷的簇数小于65525。如果你坚持要违背这个规则来创建一个FAT卷,那么Microsoft的操作系统将无法对此卷进行操作,因为它不认为这是FAT文件系统。
NOTE:如前面所说,目前有很多FAT的代码有一些错误,常常会出现 off by 1,2,8,10或是off by 16的错误。因为,为了和现存的代码取得最好的兼容性,强烈建议在格式化FAT文件系统时,尽量使簇数的取值不要接近4085或65525,最好能和这个分割点的值相差16或更多。
同时请注意这里的簇数(Count of Cluster)是指数据区所占簇的数量(the count of data clusters),从簇2开始算起,而“最大可用的簇数”(Maximum valid cluster number for the volume)是簇数+1,“包括保留簇的簇数(count of cluster including the two reserved cluster)”则为簇数+2。
FAT的另一个重要计算公式:给一个簇号N,它位于FAT表的什么位置呢?对于FAT16和FAT32都比较容易计算,而FAT12则会复杂一些:
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_RsvdSecCnt + (FATOffset / BPB_BytesPerSec); //FAT表中包含簇N的扇区数 = 保留扇区数 + (簇N位于FAT表的位置/每扇区字节数)
ThisFATEntOffset = REM (FATOffset / BPB_BytesPerSec);
这里要注意,簇的概念,既然FAT分区中存储数据的最小单位是簇,那么现在就是以簇来看待FAT表,对于FAT16来说,每个簇号占据两个字节,所以簇N前的字节数就是(N-1+1)*2,也就是N*2,N*2/每扇区的字节数就可以得到簇N前的扇区数,再加上保留扇区数,就得到簇号在FAT表中的位置。
例如:仍然以U盘的BPB为例。现在想要知道簇128位于FAT表中的什么位置。则按照以上的计算公式,有:
该FAT扇区号=保留扇区+(FAT偏移地址N*2/每扇区字节数)=4+(128×2/512)=4。这与实际结果吻合(看winhex的info panel)。
REM(…)为求余符号,就是求FATOffset除以BPB_BytesPerSec的余数。ThisFATSecNum是FAT表中包含簇N的扇区数,如果你想得到第二个FAT表中的扇区数,只要加上FATSz(FAT表大小)就是了,如果想得到第三个FAT表中的扇区数,只需要加上FATSz × 2,依此类推。
现在你得到扇区数ThisFATSecNum(记住这是针对FAT卷扇区0的偏移量),假设把该值读入到一个指定的8-bit SecBuff,同时假定数据类型WORD是一个16-bit的带符号类型,而DWORD是一个32-bit的无符号类型。
If(FATType == FAT16)
FAT16ClusEntryVal = *((WORD * ) & SecBuff[ThisFATEntOffset]);// 返回FAT16类型下ThisFATEntOffset的内容。
Else
FAT32ClusEntryVal = (*((DWORD *) & SecBuff[ThisFATEntOffset])) & 0x0FFFFFFF;//返回FAT32类型下ThisFATEntOffset的内容。
这里要注意:&是取址运算符。运算方向从右到左。加*号后变成指针变量,字节型被强制转换成WORD型。这样,对于FAT16,可以读出一个字的内容,而对于FAT32,可以读出双字——32位。
取得该扇区的内容。
设置该扇区的值使用如下算式:
If(FATType == FAT16)
*((WORD *) & SecBuff[ThisFATEntOffset]) = FAT16ClusEntryVal;
Else {
FAT32ClusEntryVal = FAT32ClusEntryVal & 0x0FFFFFFF; //舍弃高4位,实际上FAT32的FAT表项被使用的只有28位。
*((DWORD *) & SecBuff[ThisFATEntOffset]) = (*((DWORD *) & SecBuff[ThisFATEntOffset])) & 0xF0000000;//取出高4位的内容,并清空低28bit的内容。
*((DWORD *) & SecBuff[ThisFATEntOffset]) = (*((DWORD *) & SecBuff[ThisFATEntOffset])) | FAT32ClusEntryVal; //放入低28位的内容。
}
我们看看上述FAT代码是如何工作的,实际上每个FAT32的FAT表项只有28-bit可以使用,它的高4位保留,这4位只有在被格式化的时候会被使用到,在格式化时整个FAT32单元的32bit都被设置为0,包括高位的4-bit。
另外要说明的一点,这也是经常被混淆的地方,因为FAT32表项实际上被使用的只有28-bit而不是32-bit。比如,以下几个FAT32簇的值为0x10000000,0xF0000000和0x00000000都表示该簇为空,因为程序忽略了高位4-bit的值。如果当前簇的值为0x30000000,你想要把数值0x0FFFFFF7写入当前簇来标记坏簇,那么当你的操作结束后该簇的实际值为0x3FFFFFF7,因为你必须舍去0x0FFFFFF7这个坏簇标记高位的4-bit。
因为BPB_BytesPerSec的值一定能够被2和4整除,对于FAT16/FAT32来说,你不必担心元素会超越扇区的边界,但对于FAT12,你就必须小心了。
FAT12的代码会显得复杂一点,因为它每个元素(簇号)占1.5个字节(12-bit)。