# 文件系统基本概念
扇区(Sector):扇区是存储设备上用于读写的最小数据块单位。传统上一个硬盘扇区的大小是 512 字节。但在高级格式化硬盘中,物理扇区大小是 4096 字节,不过依然向下支持 512 字节。存储设备上的每个扇区都由一个扇区号寻址,该扇区号从存储设备顶部依次排列。
块(Block)/ 簇(Cluster):块(簇)是 操作系统与磁盘(硬盘)交互的最小数据单元(在 Linux 中称为块,在 windows 中称为簇), 块的大小在硬盘格式化时被指定,一般有 1K,2K,4K(最常用)。如果块的大小设置为 4K,那么磁盘要读取 8 个扇区之后,才将数据块传给操作系统。另外,块也是 DOS 下数据存储的最小单元。例如,如果一个文件的大小为 1K,而块的大小为 4K,那么该文件还是会占用一个块,块中剩下的 3K 被空闲出来,不能用于存储其他数据。
扇区 vs. 簇
扇区是对硬盘而言的,在物理层;块和簇是对文件系统而言的,是逻辑层。
簇和块是操作系统操作的最小单位,扇区是磁头从磁盘中读取数据的最小单位。
一般来说操作系统会将物理磁盘中的多个连续扇区合并成簇来管理,每个簇一般包括 2 的整数幂(2、4、8 等)个扇区。
分区(partition):故名思意就是将一块儿磁盘上的一些连续扇区划分成独立的逻辑区域,一般情况下可以将一个磁盘分为多个分区。每个分区可以格式化为一个 “卷”。分区被限制在一块磁盘中,不能跨磁盘建立分区。分区的记录存储在磁盘的第一个扇区中,它是一种较低层次的概念。
卷(volume):一种更高级、更抽象的分区,可以包含多个分区或多个物理硬盘,可以跨硬盘使用,是操作系统眼中的 “硬盘的样子”,每个卷有自己的文件系统并支持许多高级功能(如快照、压缩等)。这样我们可以认为,“卷” 是 “分区” 的扩展,“分区” 是 “卷” 的一种特殊情况。
# FAT32 详解
FAT 文件系统用 “簇” 作为数据单元。一个 “簇” 由一组连续的扇区组成,簇所含的扇区数必须是 2 的整数次幂。簇的最大值为 64 个扇区,即 32KB。在 FAT 文件系统中,同时使用 “扇区地址” 和 “簇地址” 两种地址管理方式。这是因为只有存储用户数据的数据区使用簇进行管理,其他文件系统管理数据区域是不以簇进行管理的,这部分区域使用扇区地址进行管理。文件系统的起始扇区为 0 号扇区(逻辑 0 号扇区,因为卷可以不在物理磁盘的第一个分区)。
请仔细区分 FAT卷
和 FAT表
的不同, FAT卷
指的是 FAT 文件系统,而 FAT表
指的是 FAT 文件系统中 FAT 区域。
# FAT32 整体布局
FAT32 文件系统由保留扇区,FAT1,FAT2 和 DATA 四个部分组成,其结构如下图:
这些结构是在分区被格式化时创建出来的,含义解释如下:
保留区域:包括引导扇区和保留扇区,是文件系统的隐藏区域,记录着文件系统相关的信息。
FAT1:FAT 的含义是文件分配表,FAT32 一般有两份 FAT,FAT1 是第一份,也是主 FAT。
FAT2:FAT2 是 FAT32 的第二份文件分配表,也是 FAT1 的备份。
DATA:数据区,是 FAT32 文件系统的主要区域,其中包含目录区域。
# FAT32 保留区
保留区包括 “引导扇区”(主引导扇区),” 备份引导扇区 “和 “保留扇区”。
# 引导扇区(Boot Sector)
在 FAT 文件系统中,文件系统的数据记录在 “引导扇区” 中。引导扇区从整个文件系统的 0 号扇区开始,长度为 3 个扇区,也称其为 DBR(DOS Boot Recorder——DOS 引导记录)扇区,DBR 中记录着文件系统的起始位置、大小、FAT 表个数及大小等相关信息,这些对于 FAT 至关重要的配置参数所在区域也被称为 BPB(BIOS Parameter Block),由于历史原因,BPB 的定义一直在变化。下表列出了引导扇区的数据字段,任何以 BPB_
命名的字段都是 BPB 的一部分。任何以 BS_
为标题的字段都不是 BPB 的一部分,而只是引导扇区的一部分:
字段名 | 偏移 | 大小 | 描述 |
---|---|---|---|
BS_JmpBoot | 0 | 3 | 跳转到操作系统启动过程使用的引导代码(x86 指令)的指令。该字段有两种格式,首选前一种格式。 - 0xEB, 0x??, 0x90 (Short jump + NOP) - 0xE9, 0x??, 0x?? (Near jump) ?? 是任意值,取决于跳转的位置。如果使用这些格式以外的任何格式,Windows 将无法识别卷。 |
BS_OEMName | 3 | 8 | 推荐使用 MSWIN 4.1 ,而 MSDOS 5.0 也经常被使用。关于该字段存在许多误解。这仅仅是一个名字,微软的操作系统并不会关注这个字段,但一些 FAT 驱动程序会参考它。推荐使用上述字符串是因为它被认为可以最大程度地减少兼容性问题。你也可以设置其他值,但某些 FAT 驱动程序可能无法识别该卷。此字段通常表示创建该卷的系统名称。 |
BPB_BytsPerSec | 11 | 2 | 字节为单位的扇区大小。此字段的有效值为 512 、 1024 、 2048 或 4096 。微软操作系统能够正确支持这些扇区大小。一些 FAT 驱动程序假设扇区大小为 512 ,并且不会检查此字段。因此,为了最大兼容性,应使用 512 。不过,不要误解为此字段仅与兼容性有关。此值必须与包含 FAT 卷的存储设备的扇区大小一致。 |
BPB_SecPerClus | 13 | 1 | 每个分配单元的扇区数。在 FAT 文件系统中,分配单元被称为簇 (Cluster)。簇是由一个或多个连续扇区组成的数据块,数据区以此单元进行管理。每个簇的扇区数必须是 2 的幂,因此有效值为 1、2、4、... 以及 128。不过,簇大小 ( BPB_BytsPerSec * BPB_SecPerClus ) 超过 32 KB 的值不应使用。尽管现代系统(如 Windows)支持超过 32 KB 的簇大小(如 64 KB 、 128 KB 和 256 KB),但 MS-DOS 或旧版磁盘工具无法正确识别此类卷。 |
BPB_RsvdSecCnt | 14 | 2 | 保留区中的扇区数。此字段不能为 0 ,因为包含此 BPB 的引导扇区本身就在保留区域中。为避免兼容性问题,在 FAT12/16 卷上此字段应为 1 ,这是因为一些旧版 FAT 驱动程序忽略此字段,并假设保留区域的大小为 1 。在 FAT32 卷上,通常设置为 32 。微软的操作系统能正确支持值为 1 或更大的任何值。 |
BPB_NumFATs | 16 | 1 | FAT 表的数量。此字段的值应始终为 2 。任何值大于或等于 1 都是有效的,但强烈建议不要使用除 2 以外的值以避免兼容性问题。微软的 FAT 驱动程序能正确支持非 2 的值,但一些工具和 FAT 驱动程序会忽略此字段,并假设 FAT 表的数量为 2 。设置为 2 是标准值,用于为 FAT 数据提供冗余。通常从第一个 FAT 表读取条目,并将更改同步到所有 FAT 副本。如果 FAT 区域的某个扇区损坏,数据不会丢失,因为另一份 FAT 中有备份,从而最大程度降低数据丢失风险。在非磁盘存储(如存储卡)中,此冗余功能无意义,因此可将 FAT 表数量设置为 1 来节省空间。但某些 FAT 驱动程序可能无法正确识别此类卷。 |
BPB_RootEntCnt | 17 | 2 | 在 FAT12/16 卷上,此字段表示根目录中 32 字节目录项的数量。该值应设置为使根目录的大小对齐到扇区的边界,即 BPB_RootEntCnt * 32 是 BPB_BytsPerSec 的整倍数。为最大兼容性, FAT16 卷上此字段应设置为 512 。在 FAT32 卷上,此字段必须为 0 。 |
BPB_TotSec16 | 19 | 2 | 旧的 16 位字段中卷的总扇区数。该值包括卷的所有四个区域的扇区数。当 FAT12/16 卷的扇区数为 0x10000 或更大时,此字段将设置为无效值 0 ,而实际值存储在 BPB_TotSec32 中。在 FAT32 卷中,此字段必须始终为 0 。 |
BPB_Media | 21 | 1 | 此字段的有效值为 0xF0 、 0xF8 、 0xF9 、 0xFA 、 0xFB 、 0xFC 、 0xFD 、 0xFE 和 0xFF 。 0xF8 是非可移动磁盘的标准值, 0xF0 常用于未分区的可移动磁盘。另一个重要点是,同样的值必须设置在 FAT[0] 的低 8 位。这源于 MS-DOS 1.0 的媒体确定方法,但现在不再有任何实际用途。 |
BPB_FATSz16 | 22 | 2 | FAT 表占用的扇区数。此字段仅适用于 FAT12/16 卷。在 FAT32 卷上,该字段必须是无效值 0 ,改用 BPB_FATSz32 。 FAT 区域的大小为 BPB_FATSz?? * BPB_NumFATs 扇区。 |
BPB_SecPerTrk | 24 | 2 | 每磁道的扇区数。此字段仅与具有几何结构的介质相关,并且仅用于 IBM PC 的磁盘 BIOS。 |
BPB_NumHeads | 26 | 2 | 磁头数量。此字段仅与具有几何结构的介质相关,并且仅用于 IBM PC 的磁盘 BIOS。 |
BPB_HiddSec | 28 | 4 | FAT 卷前的隐藏物理扇区数。通常与 IBM PC 磁盘 BIOS 访问的存储相关,此字段的值取决于平台。如果卷从存储的起始处开始(如未分区磁盘,例如软盘),此字段应始终为 0 。 |
BPB_TotSec32 | 32 | 4 | 新的 32 位字段中 FAT 卷的总扇区数。该值包括卷的所有四个区域的扇区数。当 FAT12/16 卷的扇区数小于 0x10000 时,此字段必须为无效值 0 ,而实际值存储在 BPB_TotSec16 中。在 FAT32 卷中,此字段始终有效,而旧字段不再使用。 |
BPB_FATSz32 | 36 | 4 | FAT 表的大小(以扇区为单位)。 FAT 区域的总大小为 BPB_FATSz32 * BPB_NumFATs 扇区。 此字段是唯一一个需要在确定 FAT 类型前被引用的字段,但该字段仅存在于 FAT32 卷中。 |
BPB_ExtFlags | 40 | 2 | 扩展标志位,用于管理 FAT 表的活动状态和镜像。 Bit3-0: 活动 FAT,从 0 开始计数。当 Bit7 为 1 时有效。 Bit6-4: 保留位(应设置为 0)。 Bit7: 0 : 所有 FAT 表都处于活动状态并被镜像。 1 : 仅由 Bit3-0 指定的 FAT 表处于活动状态。Bit15-8-4: 保留位(应设置为 0)。 |
BPB_FSVer | 42 | 2 | FAT32 版本号。高字节为主版本号,低字节为次版本号。此字段为 FAT32 文件系统未来扩展预留,实际 FAT32 卷的更新已停止,所以这里一直是默认值 0.0 。 |
BPB_RootClus | 44 | 4 | 根目录的第一个簇号。通常设置为 2 (即卷的第一个簇),但不一定总是 2 。 |
BPB_FSInfo | 48 | 2 | FSInfo 结构的扇区偏移量(相对于 FAT32 卷的起始位置)。通常设置为 1 ,即位于引导扇区的下一个扇区。 |
BPB_BkBootSec | 50 | 2 | 备份引导扇区的扇区偏移量(相对于 FAT32 卷的起始位置)。通常设置为 6 ,即紧邻引导扇区的位置。不推荐设置其他值。 |
BPB_Reserved | 52 | 12 | 保留字段,必须填充为 0。 |
BS_DrvNum | 64 | 1 | 磁盘 BIOS 使用的驱动器编号,适用于 IBM PC。此字段在 MS-DOS 启动加载程序中使用:0x00 : 软盘驱动器0x80 : 固定磁盘(硬盘)实际上此值取决于操作系统。 |
BS_Reserved | 65 | 1 | 保留字段,Windows NT 使用。创建卷时,此字段应设置为 0 。 |
BS_BootSig | 66 | 1 | 扩展引导签名(值为 0x29 )。此签名字节表示接下来的三个字段( BS_VolID 、 BS_VolLab 和 BS_FilSysType )存在。 |
BS_VolID | 67 | 4 | 卷序列号,与 BS_VolLab 配合使用,用于在可移动存储介质上跟踪卷。FAT 驱动程序通过此字段检测错误的媒体更换。通常在格式化时,根据当前时间和日期自动生成。 |
BS_VolLab | 71 | 11 | 卷的 11 字节标签,需与根目录中记录的卷标签匹配。 当根目录中的卷标签发生更改时,FAT 驱动程序应更新此字段。 MS-DOS 会自动更新此字段,但 Windows 不会更新。 如果卷标签不存在,应设置为 "NO NAME " (共 11 字节,右侧填充空格)。 |
BS_FilSysType | 82 | 8 | 文件系统类型字符串,通常为 "FAT12" , "FAT16" , "FAT32" 或 "FAT " 。许多人误认为此字符串影响 FAT 类型的确定,实际上这是一个误解。 从字段名称可知,它并不属于 BPB (BIOS 参数块)。 此字符串经常被设置不正确或未设置,因此 Microsoft 的 FAT 驱动程序不会使用此字段来确定 FAT 类型。 然而,一些旧的 FAT 驱动程序依赖此字段来确定 FAT 类型,因此,为避免兼容性问题,应该根据卷的 FAT 类型正确设置此字段。 |
BS_BootCode32 | 90 | 420 | 引导程序。它与平台有关,不使用时填充 0 。 |
BS_Sign | 510 | 2 | 应为固定值 0xAA55 ,表示这是一个有效引导扇区的引导签名。 |
512 | 当扇区大小大于 512 字节时,扇区中的其余部分应为零。 |
我们来看一个实例并解释一些重点自动
0xEB 0x58 0x90
:汇编指令跳转到0x58
偏移处,这个偏移是相对于当前指令的,所以加上本身0xEB 0x58
这两个字节指令,正好跳转到0x5A
处找到引导程序的开头,也就是标号 10 的位置。- 扇区大小为
0x200
也就是512
字节。14983*2+2802= - 一个簇内含有
0x10
个扇区,也就是一个簇中有16
个扇区. - 保留区域中的扇区数为
0xAF2=2802
个,由于扇区是从 0 开始编号的,也就是说FAT1
从第 2802 号扇区开始。 - 有
2
个 FAT 表。 - FAT 卷前的隐藏物理扇区数为
0x800=2048
,也就是说在 FAT 卷前还有2048
个物理扇区,即逻辑上的 0 号扇区其实对应着物理磁盘中的第2048
号扇区。 - FAT 卷的总扇区数为
0x1D4B7D0=30717904
个,总扇区包括保留区
、FAT1
、FAT2
和DATA
四个分区的所有扇区,是 FAT 卷能管理的所有扇区的数量。 - FAT 表的大小为
0x3A87=14983
个扇区。FAT表大小 * FAT表个数 + 保留区大小 = DATA区开始地址
。 - 卷序列号为
0x2AEB8680
,用于在可移动存储介质上跟踪卷。
# 一些计算
每一个区域的偏移和大小都是由 BPB 参数计算出来的:
FAT 区在保留区的后面,其位置和大小(扇区数)的计算为:
Fat1StartSector = BPB_ResvdSecCnt;
FatSectors = BPB_FATSz32 * BPB_NumFATs;
FAT2 区在
FAT1
区的后面,其位置和大小(扇区数)的计算为:Fat2StartSector = BPB_ResvdSecCnt + FatSectors;
Fat2Sectors = FatSectors = BPB_FATSz32 * BPB_NumFATs;
DATA 区在所有 FAT 区的后面,其位置和大小(扇区数)的计算为:
DataStartSectors = BPB_FATSz32 * BPB_NumFATs + BPB_ResvdSecCnt;
DataSectors = BPB_TotSec32 - DataStartSectors;
如果
FAT卷
不是从硬盘的第一个扇区开始的,那么FAT卷
中的每个扇区对应的物理扇区为:BPB_HiddSec + 逻辑扇区
# FSINFO 信息扇区
FAT32
在保留区中增加了一个 FSINFO
扇区,用以记录文件系统中空闲簇的数量以及下一可用簇的簇号等信息,使得操作系统可以快速找到可用的簇而无需在每次使用时重新扫描整个 FAT卷
。 FSINFO
扇区的位置由 BPB_FSInfo
指定,通常是 1
,也就是 1 号扇区,紧邻着引导扇区,其结构如下:
字段名 | 偏移 | 大小 | 描述 |
---|---|---|---|
FSI_LeadSig | 0 | 4 | 固定值 0x41615252 ,引导签名,用于验证该扇区确实是一个 FSInfo 扇区。 |
FSI_Reserved1 | 4 | 480 | 保留字段。此字段应始终初始化为零。 |
FSI_StrucSig | 484 | 4 | 固定值 0x61417272 ,另一个签名,更加局部地标识了与该扇区中使用的字段相关的位置。 |
FSI_Free_Count | 488 | 4 | 表示卷上最后一次更新后已知的空闲簇数。如果该值为 0xFFFFFFFF ,则表示空闲簇数未知。这不一定是正确的,因此 FAT 驱动程序需要确保该值对卷是有效的。 |
FSI_Nxt_Free | 492 | 4 | 此字段为 FAT 驱动程序提供一个提示,指示驱动程序应从哪个簇开始查找空闲簇。 由于 FAT32 的 FAT 很大,如果开始查找时有很多已分配的簇,则从 FAT 卷的第一个簇开始查找空闲簇可能会非常耗时。 通常,此值设置为驱动程序上次分配的最后一个簇号。如果值为 0xFFFFFFFF ,则表示没有提示,驱动程序应从簇 2 开始查找。 此值可能不准确,因此 FAT 驱动程序需要确保它对卷是有效的。 |
FSI_Reserved2 | 496 | 12 | 保留字段。此字段应始终初始化为零。 |
FSI_TrailSig | 508 | 4 | 固定值 0xAA550000 ,结尾签名,用于验证该扇区确实是一个 FSInfo 扇区。 |
512 | 如果扇区大小大于 512 字节,则其余字节应初始化为零。 |
一个详细的示例如下:
- FAT 卷最后一次更新后已知的空闲簇数为
0x1817A8=1578920
个。也即还有 82% 的空余。 - 最靠前的一个空闲簇号为
0x2BDB=11227
号。
# 2 号扇区
有关 2 号扇区的内容网络上的描述比较少,似乎 FAT 文件系统标准中并没有对 2 号扇区做太多规范,唯一的规范是,由于其仍属于引导扇区的一部分,所以该扇区结尾 4 个字节仍必须是引导扇区签名 0xAA550000
。然而在现在文件系统中,该扇区一般会存放一些 0 号扇区中放不下的引导加载程序的一些额外代码,该部分的代码会在 0 号扇区中通过 jmp 400h
的汇编指令直接跳转到达,该部分的代码也可以在 Intel 80386 实模式下的 16 位模式进行反汇编。
# 备份引导扇区
FAT32
中会将引导扇区的 3 个扇区都做一份备份用于冗余,当主引导扇区损坏时可以通过备份引导扇区恢复文件系统,备份引导扇区的位置由 0 号扇区中的 BPB_BkBootSec
字段指定,一般是 6
,也就是从 6
号扇区开始。 6
、 8
号扇区的内容是 0
、 2
号扇区的原样拷贝,然而第 7
号扇区,也就是 FSINFO
的备份扇区的内容与原扇区内容不太一样,是该扇区的未初始化版本,也就是说当操作系统使用备份扇区时需要重新搜索空间簇数量和位置,这也是非常合理的,当发生硬盘损坏时原本的空闲簇信息可能也不在可靠了,需要重新搜索。
# 保留扇区
保留区的剩下扇区都时保留扇区,供使用者灵活使用。
# FAT32 FAT 区
FAT 区位于保留区后,一般是两个完全相同的 FAT(File Allocation Table,文件分配表)表组成, FAT 文件系统的名字也是因此而来。
FAT 文件系统之所以有 12,16,32 不同的版本之分,其根本在于 FAT 表项用来记录一簇空间所占用的二进制位数。以 FAT16
为例,每一簇在 FAT 表中占据 16 bit
(2 字节)。所以,FAT16 最大可以表示的簇号为 0xFFFF
( 65535
),如果以 32K
(最多只支持 64 个扇区)为簇的大小的话, FAT16
可以管理的最大磁盘空间为: 32KB×65535=2048MB
,这就是为什么 FAT16
不支持超过 2GB
分区的原因(DOS 下)。注意:
- 对于文件系统来说,FAT 表有两个重要作用:描述簇的分配状态以及标明文件或目录的下一簇的簇号。
- 通常情况下,一个 FAT 文件系统会有两个 FAT 表,但有时也允许只有一个 FAT 表,FAT 表的具体个数记录在引导扇区的
BPB_NumFATs
字段处。 FAT32
的 FAT 表由一系列大小相等的 FAT 表项组成,每个表项占32bit
(4 字节)。表项从 FAT 区的起始位置开始进行编号,FAT 区第一个扇区的头 4 个字节为 0 号地址,依次类推。0 号地址与 1 号地址被系统保留并存储特殊标志内容,从 2 号表项开始,每个地址对应一个数据区的簇号,FAT 表中的地址编号与数据区中的簇号相同。
FAT 表的每个条目记录如下 5 种信息中的一种:
- 文件下一个簇的地址
- 一个特殊的簇链结束符(EOC,End Of Cluster-chain,或称 End Of Chain)符号指示文件的结束
- 一个特殊的符号标示坏簇
- 一个特殊的符号标示保留簇
- 0 来表示空闲簇
其条目值得具体规范如下:
FAT 32 表项值 | 描述 |
---|---|
0x?0000000 | 空闲簇 |
0x?0000001 | 保留簇 |
0x?0000002 - 0x?FFFFFEF | 被占用的簇;指向下一个簇 |
0x?FFFFFF0 - 0x?FFFFFF6 | 保留值 |
0x?FFFFFF7 | 簇中含有坏扇区,会将整个簇标记为坏簇 |
0x?FFFFFF8 - 0x?FFFFFFF | 文件最后一个簇 |
⚠️ 注意 FAT32
只使用 32 位中的 28位
。高 4 位通常是 0
,但它们是保留位,不要更改它们。在上面的表中它们用问号表示。
一个具体的例子如下:
- 0 号表项(标号 1)的值总是
0x0FFFFFF8
。 - 1 号表项(标号 2)可能被用于记录脏标志,以说明文件系统没有被正常卸载或者磁盘表面存在错误。不过这个值并不重要。正常情况下 1 号表项的值为
0xFFFFFFFF
或0xFFFFFFF
。 - 如果某个簇未被分配使用,它所对应的表项值即用
0
进行填充,表示该 FAT 表 项所对应的簇未被分配。当某个簇已被分配使用时,则它对应的表项值是该文件的下一个存储位置的簇号。如果该文件结束于该簇,则在它的 FAT 表项中记录的是一个文件结束标记,对于 FAT32 而言,代表文件结束的 FAT 表项值为0x0FFFFFFF
。这里的标志说明 2 号簇内的文件比较小,单独一个 2 号簇的空间就已经可以存储整个文件了。 - 7 号簇的表项值说明该簇内存储的文件非常大,一个簇存储不下,存储不下的内容需要去
0x8
号簇去寻找,依次类推,直到找到一个簇的表项值为文件结束标志。
当文件系统被创建,也就是进行格式化操作时,分配给 FAT 区域的空间将会被清空,然后在 FAT1
与 FAT2
的 0 号表项与 1 号表项写入上述特定值。由于创建文件系统的同时也会创建根目录,也就是为根目录分配了一个簇空间,通常为 2 号簇,所以 2 号簇所对应的 2 号 FAT 表项也会被写入一个结束标记。如果某个簇存在坏扇区,则整个簇会用 FAT 表项值 0xFFFFFFF7
标记为坏簇,不再使用,这个坏簇 标记就记录在它所对应的 FAT 表项中。
# FAT32 DATA 区
数据区时真正用于存放用户数据的区域。数据区紧跟在 FAT2 之后,被划分成一个个的簇。所有的簇 从 2 开始进行编号。也就是说,2 号簇的起始位置就是数据区的起始位置。
# 目录表
目录表是一个表示目录的特殊类型文件(现今通常称为文件夹)。它里面保存的每个文件或目录使用表中的 32字节
条目表示。每个条目记录名字、扩展名、属性(档案、目录、隐藏、只读、系统和卷)、创建的日期和时间、文件/目录数据第一个簇的地址,最后是文件/目录的大小。在 FAT32
中所有目录表都存在数据区域(DATA 区)中。目录表项遵循以下格式:
字段名 | 偏移 | 大小 | 描述 |
---|---|---|---|
DIR_Name | 0 | 11 | 短文件名(包括文件名和扩展名, 8-11 位为文件扩展名,使用空格补齐)第一个字节可以是下面的特殊数值: - 0x00:这个条目未使用并且后面没有被占用条目(后面所有条目该项也为 0) - 0x05:最初字符确实是 0xE5 时使用该值替代 - 0x2E:' 点 ' 条目;'.' 或者 '..' - 0xE5:这个条目曾经被删除不再有用。取消删除文件工具作为取消删除的一步必须使用一个正常的字符取代它。 |
DIR_Attr | 11(0B) | 1 | 文件属性标志位(高 2 位是保留位且必须置为 0)- 0x01 :只读- 0x02 :隐藏- 0x04 :系统- 0x08 :卷标- 0x10 :子目录- 0x20 :档案(只要完成了写操作并已关闭,则该位置位 1)- 0x0F : 长文件条目(Long File Name Entry,LFN Entry) |
DIR_NTRes | 12(0C) | 1 | NT 保留标志,指示短文件名 (SFN) 的大小写信息: - 0x08 : 名称主体中的所有字母均为小写。- 0x10 : 扩展名中的所有字母均为小写。 |
DIR_CrtTimeTenth | 13(0D) | 1 | 与 DIR_CrtTime 对应的子秒信息(可选)。 DIR_CrtTime 的时间分辨率为 2 秒,因此此字段以 10 毫秒为单位表示子秒计数,其有效值范围为 0 到 199。如果不支持,则将其设置为 0,并且之后不要更改。 |
DIR_CrtTime | 14 | 2 | 文件创建时间(可选)。如果不支持,则将其设置为 0,并且之后不要更改。小时、分钟和秒根据后面的图示描述进行编码: - 15-11位 :小时(0-23)- 10-5位 :分钟(0-59)- 4-0位 :秒 / 2 (0-29) |
DIR_CrtDate | 16 | 2 | 文件创建日期(可选)。如果不支持,则将其设置为 0,并且之后不要更改。年、月和日根据后面的图示编码: - 15-9位 :年(0 = 1980, 127 = 2107)- 8-5位 : 月(1 =1 月, 12 = 12 月)- 4-0位 :日(1 - 31) |
DIR_LstAccDate | 18 | 2 | 文件最近访问日期(可选)。分辨率为 1 天。 如果不支持,则将其设置为 0,并且之后不要更改。 |
DIR_FstClusHI | 20 | 2 | 簇号的高 16 位。在 FAT12/16 卷上始终为零。 |
DIR_WrtTime | 22 | 2 | 文件的最后写入时间(通常在文件关闭时更新),编码格式与 DIR_CrtTime 相同。 |
DIR_WrtDate | 24 | 2 | 文件的最后写入日期(通常在文件关闭时更新),编码格式与 DIR_CrtDate 相同。 |
DIR_FstClusLO | 26 | 2 | 簇号的低 16 位。当文件大小为 0 时,不分配簇,此项必须为 0。 如果是目录,则此值始终为有效值。 |
DIR_FileSize | 28 | 4 | 文件的大小,以字节为单位。如果是目录,此字段未使用,并且其值必须始终为 0。 |
当 DIR_Attr
为 0x0F
时,该目录表的字段含义为:
字段名 | 偏移 | 大小 | 描述 |
---|---|---|---|
LDIR_Ord | 0 | 1 | 序号 (1-20),用于标识此条目在组成 LFN 的所有条目中的位置。 0x01 表示 LFN 的起始部分。任何带有 LAST_LONG_ENTRY 标志 ( 0x40 ) 的值表示 LFN 的最后一部分。 |
LDIR_Name1 | 1 | 10 | 文件名的第 1-5 个字符(UTF-16),未使用的部分先填充两个字节的 0x00 ,然后用 0xFF 填充。 |
LDIR_Attr | 11 | 1 | LFN 目录属性标志 0x0F |
LDIR_Type | 12 | 1 | 保留未使用 |
LDIR_Chksum | 13 | 1 | 文件名校验和 |
LDIR_Name2 | 14 | 12 | 文件名的第 6-11 个字符(UTF-16),未使用的部分先填充两个字节的 0x00 ,然后用 0xFF 填充。 |
LDIR_FstClusLO | 26 | 2 | 保留 |
LDIR_Name3 | 28 | 4 | 文件名的第 12-13 个字符(UTF-16),未使用的部分先填充两个字节的 0x00 ,然后用 0xFF 填充。 |
一个实例如下:
通过绿色的
0x08
标志确定第一个目录项类型为卷标,即该 FAT 卷标名为UBUNTU-SERV
,实际情况也确实如此,该 U 盘为 Ubuntu-24.04 的系统盘。一般来说卷标名存储在 DATA 数据区的开头,我们也可以计算一下进行验证,DATA 区的地址应该为0x200 * (FAT区扇区数 * 2 + 保留区扇区数) = 0x1000000
,与实际相符。同时我们也可以计算出该卷最后一次修改的时间与日期:时间:
0x9E55 = (10011)(110 010)(10101)
即(19):(50):(21*2)
,也就是 19:50:42。年份:
0x596B = (0101100)(1 011)(01011)
即(44)(11)(11)
,也就是 2024 年 11 月 11 日。注意:卷标目录项结构与普通短文件名目录项结构完全相同,但没有创建时间和访问时间,只有一个最后修改时间。另外,卷标目录项也没有簇号和大小值,这些字节位置全部这只为
0
。绿色标志位确定这两项都是
LFN
项,LFN 有一些特殊的规则:如果一个文件的文件名超过了 8 个字符,则会将其名字截短后为其建立短文件名(Short File Name, SFN)。将短文件名存储在短文件名目录项中。长文件名则存放在长文件名目录项中。
LFN 项中的文件名使用 UTF-16 编码,存储 13 个 Unicode 字符的文件名,每个字符占用两个字节,如果文件名长于 13 个字符,则继续为其分配 LFN 项,知道够用为止。
一个文件的所有 LFN 项按倒序排列在它的 SFN 项前面,即文件名的第一部分距离 SFN 是最近的。
LFN 项第一个字节为序号位,距离 SFN 最近的一个 LFN 项序号为 1, 如果后续还有 LFN 项,则依次向后递增为 2, 3,... 最后一个 LFN 块的序号需要异或上
0x40
,在上图中,由于第二个 LFN 项就是最后一个 LFN 项,所以其序号为0x42 = 0x40 | 0x02
。每个 LFN 条目在
LDIR_Chksum
中都有一个校验和,同一个文件的所有 LFN 项的校验和都相同,此外,校验和还被用于确保 LFN 和 SFN 之间的相关性。校验和的生成算法如下:uint8_t create_sum (const DIR* entry)
{
int i;
uint8_t sum;
for (i = sum = 0; i < 11; i++) { /* Calculate sum of DIR_Name[] field */
sum = (sum >> 1) + (sum << 7) + entry->DIR_Name[i];
}
return sum;
}
我们可以写一个简单的小程序来验证一下
#include <stdint.h>
#include <stdio.h>
int main(void)
{
int i;
uint8_t sum;
uint8_t DIR_name[11] = {0x53, 0x59, 0x53, 0x54, 0x45, 0x4D, 0x7e, 0x31, 0x20, 0x20, 0x20};
for (i = sum = 0; i < 11; i++) {
sum = (sum >> 1) + (sum << 7) + DIR_name[i];
}
printf("sum = %#X\n", sum);
}
// Output: sum = 0X72
与棕色标出的校验和一致。
当文件名没有占满一个 LFN 项时,需要先填充两个字节的
0x00
,也就是0x3E-0x3F
位置的填充的内容,如果仍有空余空间,则应该在所有字符位填充0xFF
。
对于短文件名(SFN)目录项来说,如果文件名不足 8 个字符,用 0x20 进行填充。当文件名超过 8 个字符时则会被截短,因为短文件名目录项中没有足够的空间记录超出的部分。截短的方法是取文件名的前 6 个字符加上
~1 (0x7E 0x31)
(如果有同名文件,则会依次递增该数值),然后加上其扩展名。这里绿色标志位确定改项为一个子目录项,且具有两个属性:隐藏和系统。当目录项类型为子目录时,则将扩展名部分用 “0x20” 进行填充。可以看到该子目录所在簇号为 3 号,由于在该文件系统中一个簇包含 16 个扇区,所以在
0x10020000
偏移处就是 3 号簇,其中记录着System Volume Information
目录内的内容。
注意:从 windows95 开始,不管文件名的长度是否超过 8 个字符,都会同时为其创建 SFN 目录项和 LFN 目录项 ,因为短文件名不区分大小写,而长文件名则是区分大小写的。这一点也可以在下列的 [boot]
子目录项的格式中得到验证。(之所以在 SFN 中 [boot]
被替换为 _BOOT_
是因为 []
并不是合法的 DOS 文件名字符,所以发生了替换,合法的 DOS 字符包括:
0~9 A~Z ! # $ % & ' ( ) - @ ^ _ \` { } ~ |
# FAT32 工作过程
# 建立文件
假设在跟目录下由一个子目录,名字为 test
,我们要在其下建立一个文件 example.txt
。使用 FAT32
文件系统,一个簇的大小为 4096
字节(一个簇 8
个扇区,每个扇区 512
字节),我们要建立的文件大小为 5000
字节。步骤如下:
- 读取位于卷 0 号扇区的引导扇区,根据引导扇区中的信息定位 FAT 表、数据区和根目录的位置。
- 查看根目录下的每个目录项,寻找名字为
test
且具有目录属性的目录项。找到后,查看它的起始簇号为 3。 - 读取 3 号簇的内容,查找每个目录项,直到找到一个未分配的目录项(第一个字节为
0x00
)。 - 找到可用项后写入文件名 “example.txt”,并将文件大小和当前时间写入相应的位置。
- 为文件内容分配簇空间。转到 FAT 表,寻找空闲的位置。发现 4 号 FAT 表项未使用,这就说明 4 号簇是空闲的。将 4 号簇分配给文件,并在 4 号簇的 FAT 表项内写入结束标记。
- 将簇号 4 写入
example.txt
文件目录项的起始簇号区域。将文件的前 4096 字节写入到 4 号簇中,还剩下904
字节,所以还需要再为其分配一个簇。 - 在 FAT 表中继续寻找未分配簇,找到 5 号簇为空闲未使用( FAT 表项为 0)。
- 将
example.txt
的第一个簇(即 4 号簇)的 FAT 表项值改写为 5,将文件的最后904
字节写入 5 号簇。 - 在 5 号簇的 FAT 表项内写入结束标记。
# 删除文件
将我们之前创建的 /test/example.txt
文件删除。步骤如下:
- 从卷 0 号扇区读取引导扇区,根据引导扇区中的信息定位 FAT 表、数据区和根目录的位置。
- 在根目录下寻找名字为
test
且具有目录属性的目录项。 - :由
test
的目录项中获取它的起始簇号为 3,到 3 号簇查看test
文件夹内的内容,从中找到文件example.txt
的目录项,提取出它的起始簇号,为 4 号簇。 - 到 FAT 表中找到该文件的簇链,确定他的存储位置为 4 号簇和 5 号簇。
- 将 4 号簇和 5 号簇的 FAT 项设置为 0,表示这两个簇未被占用,可以被重新分配。
- 将文件
example.txt
的目录项的第一个字节改为0xE5
。
# 索引文件
索引文件的过程其实就是删除文件的前 4 步,比较简单,就不赘述了。