深入exFAT文件系统

Last Updated: 2023-06-26 09:26:12 Monday

-- TOC --

exFAT文件系统,extensible File Allocation Table,是FAT系列文件系统,FAT32的后续。exFAT突破了4G文件大小的限制,专利2019年到期,Linux和Mac上读写exFAT分区都没问题。

The exFAT file system is the successor to FAT32 in the FAT family of file systems.

exFAT (Extensible File Allocation Table) is a file system introduced by Microsoft in 2006 and optimized for flash memory such as USB flash drives and SD cards. exFAT was proprietary until 28 August 2019. 专利到期,大家就都支持了!

exFAT标准:https://learn.microsoft.com/en-us/windows/win32/fileio/exfat-specification

exFAT文件系统的设计目标:

BPB

struct exfat_bpb
{
  uint8_t jmp_boot[3];
  uint8_t oem_name[8];
  uint8_t mbz[53];
  uint64_t num_hidden_sectors;
  uint64_t num_total_sectors;
  uint32_t num_reserved_sectors;
  uint32_t sectors_per_fat;
  uint32_t cluster_offset;
  uint32_t cluster_count;
  uint32_t root_cluster;
  uint32_t num_serial;
  uint16_t fs_revision;
  uint16_t volume_flags;
  uint8_t bytes_per_sector_shift;
  uint8_t sectors_per_cluster_shift;
  uint8_t num_fats;
  uint8_t num_ph_drive;
  uint8_t PercentInUse;
  uint8_t reserved[7];
} GRUB_PACKED;

在U盘上创建一个exFAT分区(用mkfs.exfat命令,默认参数):

Device       Start      End  Sectors  Size Type
...
/dev/sdc2  2099200 60086271 57987072 27.7G Linux filesystem

BPB引导扇区内容如下:

$ sudo xxd -a -u -l 512 /dev/sdc2
00000000: EB76 9045 5846 4154 2020 2000 0000 0000  .v.EXFAT   .....
00000010: 0000 0000 0000 0000 0000 0000 0000 0000  ................
*
00000040: 0008 2000 0000 0000 00D0 7403 0000 0000  .. .......t.....
00000050: 0008 0000 C01B 0000 0028 0000 A0D2 0D00  .........(......
00000060: 0700 0000 5AE8 837D 0001 0000 0906 0180  ....Z..}........
00000070: 0000 0000 0000 0000 0000 0000 0000 0000  ................
*
000001f0: 0000 0000 0000 0000 0000 0000 0000 55AA  ..............U.

做几道小学数学题:

>>> (57987072-10240)/64
905888.0
>>> 10240-2048-7104
1088
>>> 2048+7104+1088+905888*64
57987072

在FAT表末尾到第1个cluster之间,还有1088个扇区闲置?!

MustBeZero

这段全0,就像一个保护字段,类似PMBR的效果。

The MustBeZero field shall directly correspond with the range of bytes the packed BIOS parameter block consumes on FAT12/16/32 volumes.

The valid value for this field is 0, which helps to prevent FAT12/16/32 implementations from mistakenly mounting an exFAT volume.

Volume Flags

这16个bit,标准目前只定了前4个:

Boot Checksum

checksum的作用是,代码需要首先通过checksum确认Boot Region的内容是可靠的。

The Main and Backup Boot Checksums each contain a repeating pattern of the four-byte checksum of the contents of all other sub-regions in their respective Boot regions. The checksum calculation shall not include the VolumeFlags and PercentInUse fields in their respective Boot Sector. The repeating pattern of the four-byte checksum fills its respective Boot Checksum sub-region from the beginning to the end of the sub-region.

checksum算法:

#include <stdio.h>
#include <stdlib.h>

int main() {
    unsigned int cks = 0;

    FILE *fdev = fopen("/dev/sdc2", "rb");
    if(!fdev){
        printf("fopen failed.\n");
        return 1;
    }

    unsigned char *p11s = malloc(512*11);
    fread(p11s, 512, 11, fdev);
    fclose(fdev);

    for(int i=0; i<512*11; ++i){
        if((i==106) || (i==107) || (i==112))
            continue;
        cks = ((cks&1)?0x80000000:0) + (cks>>1) + p11s[i];
    }
    free(p11s);
    printf("0x%X\n", cks);
    return 0;
}

sudo执行,输出0x7859FD56。exFAT文件系统定义的checksum,为什么要在一个sector内不断重复呢:

$ sudo xxd -a -u -s$((512*11)) -l 512 /dev/sdc2
00001600: 56FD 5978 56FD 5978 56FD 5978 56FD 5978  V.YxV.YxV.YxV.Yx
00001610: 56FD 5978 56FD 5978 56FD 5978 56FD 5978  V.YxV.YxV.YxV.Yx
00001620: 56FD 5978 56FD 5978 56FD 5978 56FD 5978  V.YxV.YxV.YxV.Yx
00001630: 56FD 5978 56FD 5978 56FD 5978 56FD 5978  V.YxV.YxV.YxV.Yx
00001640: 56FD 5978 56FD 5978 56FD 5978 56FD 5978  V.YxV.YxV.YxV.Yx
00001650: 56FD 5978 56FD 5978 56FD 5978 56FD 5978  V.YxV.YxV.YxV.Yx
00001660: 56FD 5978 56FD 5978 56FD 5978 56FD 5978  V.YxV.YxV.YxV.Yx
00001670: 56FD 5978 56FD 5978 56FD 5978 56FD 5978  V.YxV.YxV.YxV.Yx
00001680: 56FD 5978 56FD 5978 56FD 5978 56FD 5978  V.YxV.YxV.YxV.Yx
00001690: 56FD 5978 56FD 5978 56FD 5978 56FD 5978  V.YxV.YxV.YxV.Yx
000016a0: 56FD 5978 56FD 5978 56FD 5978 56FD 5978  V.YxV.YxV.YxV.Yx
000016b0: 56FD 5978 56FD 5978 56FD 5978 56FD 5978  V.YxV.YxV.YxV.Yx
000016c0: 56FD 5978 56FD 5978 56FD 5978 56FD 5978  V.YxV.YxV.YxV.Yx
000016d0: 56FD 5978 56FD 5978 56FD 5978 56FD 5978  V.YxV.YxV.YxV.Yx
000016e0: 56FD 5978 56FD 5978 56FD 5978 56FD 5978  V.YxV.YxV.YxV.Yx
000016f0: 56FD 5978 56FD 5978 56FD 5978 56FD 5978  V.YxV.YxV.YxV.Yx
00001700: 56FD 5978 56FD 5978 56FD 5978 56FD 5978  V.YxV.YxV.YxV.Yx
00001710: 56FD 5978 56FD 5978 56FD 5978 56FD 5978  V.YxV.YxV.YxV.Yx
00001720: 56FD 5978 56FD 5978 56FD 5978 56FD 5978  V.YxV.YxV.YxV.Yx
00001730: 56FD 5978 56FD 5978 56FD 5978 56FD 5978  V.YxV.YxV.YxV.Yx
00001740: 56FD 5978 56FD 5978 56FD 5978 56FD 5978  V.YxV.YxV.YxV.Yx
00001750: 56FD 5978 56FD 5978 56FD 5978 56FD 5978  V.YxV.YxV.YxV.Yx
00001760: 56FD 5978 56FD 5978 56FD 5978 56FD 5978  V.YxV.YxV.YxV.Yx
00001770: 56FD 5978 56FD 5978 56FD 5978 56FD 5978  V.YxV.YxV.YxV.Yx
00001780: 56FD 5978 56FD 5978 56FD 5978 56FD 5978  V.YxV.YxV.YxV.Yx
00001790: 56FD 5978 56FD 5978 56FD 5978 56FD 5978  V.YxV.YxV.YxV.Yx
000017a0: 56FD 5978 56FD 5978 56FD 5978 56FD 5978  V.YxV.YxV.YxV.Yx
000017b0: 56FD 5978 56FD 5978 56FD 5978 56FD 5978  V.YxV.YxV.YxV.Yx
000017c0: 56FD 5978 56FD 5978 56FD 5978 56FD 5978  V.YxV.YxV.YxV.Yx
000017d0: 56FD 5978 56FD 5978 56FD 5978 56FD 5978  V.YxV.YxV.YxV.Yx
000017e0: 56FD 5978 56FD 5978 56FD 5978 56FD 5978  V.YxV.YxV.YxV.Yx
000017f0: 56FD 5978 56FD 5978 56FD 5978 56FD 5978  V.YxV.YxV.YxV.Yx

Volume Structure

exFAT文件系统对分区的划分,与FAT32完全一样。

Main Boot Region  |   FAT Region  |   Data Region

Main Boot Region占用前12个扇区,结构如下描述:

Sub-region Name Offset (sector) Size (sectors) Comments
Main Boot Sector 0 1 BPB
Main Extended Boot Sectors 1 8 一般都是全0,但每个sector最后两个byte均为0x55AA
Main OEM Parameters 9 1 标准定义的很复杂,但我的U盘告诉我,全FF
Main Reserved 10 1 全0
Main Boot Checksum 11 1 4字节的校验,在整个sector重复128遍,如何计算校验,见下

紧接着的后面12个sector,是前12个sector的backup!

FAT Region

exFAT文件系统最多2个FAT表,在Flags中指定了那个是Active的。用mkfs.exfat格式化,默认就1个FAT。exFAT的FAT表与FAT32一样。

A FAT shall describe cluster chains in the Cluster Heap. A cluster chain is a series of clusters which provides space for recording the contents of files, directories, and other file system structures. A FAT represents a cluster chain as a singly-linked list of cluster indices. With the exception of the first two entries, every entry in a FAT represents exactly one cluster.

每4字节表示一个cluster,前2个entry有点特殊,按照标准,值是固定的,如下:

$ sudo xxd -a -u -s$((512*2048)) -l512 /dev/sdc2
00100000: F8FF FFFF FFFF FFFF 0300 0000 0400 0000  ................
00100010: 0500 0000 FFFF FFFF FFFF FFFF FFFF FFFF  ................
00100020: 0000 0000 0000 0000 0000 0000 0000 0000  ................
*
001001f0: 0000 0000 0000 0000 0000 0000 0000 0000  ................

发现问题了吗?exFAT的FAT表,没有定义如何表示空闲cluster。

Data Region

这一段就是一块块的cluster,历史原因,cluster的编号从2开始,因此最后一个cluster的index是ClusterCount+1(实际计算偏移时,要减2)。前面解析root cluster编号是7,我们从7号cluster开始,offset为7-2=5

Directory Entry

首先要搞懂Directory Entry这个多变的数据结构。

Directory Entry结构以32字节为单位,除了文件和文件夹,Allocation Bitmap,Volume Label,Up-case Table等都使用这个Entry来定位。

这32个字节被粗分为4个部分:

struct generic_directory_entry{
    uint8_t EntryType;
    uint8_t CustomDefined[19];
    uint32_t FirstCluster;
    uint64_t DataLength;
};

第1个字节很关键,EntryType,详细定义看标准,下面简述:

EntryType这8个bit,被分成了4个部分:

主要EntryType:

struct generic_primary_directory_entry{
    uint8_t EntryType;
    uint8_t SecondaryCount;
    uint16_t SetChecksum;
    uint16_t GeneralPrimaryFlags;
    uint8_t CustomDefined[14];
    uint32_t FirstCluster;
    uint64_t DataLength;
};

struct generic_second_directory_entry{
    uint8_t EntryType;
    uint8_t GeneralSecondaryFlags;
    uint8_t CustomDefined[18];
    uint32_t FirstCluster;
    uint64_t DataLength;
};

Allocation Bitmap

exFAT文件系统与FAT32显著不一样的地方,就是这个Allocation Bitmap。这是一块独立的区域,很可能会占用好几个cluster空间,专门用来记录cluster的占用情况。而FAT32文件系统,使用FAT表来记录,全0表示空闲。

In an exFAT volume, an Allocation Bitmap maintains the record of the allocation state of all clusters. This is a significant difference from exFAT's predecessors (FAT12, FAT16, and FAT32), in which a FAT maintained a record of the allocation state of all clusters in the Cluster Heap.

32字节的定义如下:

struct directory_entry{
    uint8_t EntryType = 0x81;
    uint8_t BitmapFlag;
    uint8_t reserved[18];
    uint32_t FirstCluster;
    uint64_t DataLength;
};

BitmapFlag也只有第1个bit被用到:

先看看7号root cluster的内容:

$ sudo xxd -a -u -s$((512*10240+32768*(7-2))) -l512 /dev/sdc2
00528000: 8300 0000 0000 0000 0000 0000 0000 0000  ................
00528010: 0000 0000 0000 0000 0000 0000 0000 0000  ................
00528020: 8100 0000 0000 0000 0000 0000 0000 0000  ................
00528030: 0000 0000 0200 0000 54BA 0100 0000 0000  ........T.......
00528040: 8200 0000 0DD3 19E6 0000 0000 0000 0000  ................
00528050: 0000 0000 0600 0000 CC16 0000 0000 0000  ................
00528060: 0502 6F8D 2000 0000 8A33 C956 8A33 C956  ..o. ....3.V.3.V
00528070: 8A33 C956 7F7F 8080 8000 0000 0000 0000  .3.V............
00528080: 4001 0009 7501 0000 0000 0000 0000 0000  @...u...........
00528090: 0000 0000 0000 0000 0000 0000 0000 0000  ................
005280a0: 4100 2E00 6400 6400 6400 6400 2E00 7300  A...d.d.d.d...s.
005280b0: 7700 7000 0000 0000 0000 0000 0000 0000  w.p.............
005280c0: 8502 77EC 2000 6F00 8C33 C956 8C33 C956  ..w. .o..3.V.3.V
005280d0: A233 C956 C1C1 8080 8000 0000 0000 0000  .3.V............
005280e0: C003 0004 2D28 0000 0B00 0000 0000 0000  ....-(..........
005280f0: 0000 0000 0900 0000 0B00 0000 0000 0000  ................
00528100: C100 6400 6400 6400 6400 0000 0000 0000  ..d.d.d.d.......
00528110: 0000 0000 0000 0000 0000 0000 0000 0000  ................
00528120: 8502 1792 2000 0000 7632 C956 7632 C956  .... ...v2.Vv2.V
00528130: 7632 C956 4040 8080 8000 0000 0000 0000  v2.V@@..........
00528140: C001 0003 2BF8 0000 0000 0000 0000 0000  ....+...........
00528150: 0000 0000 0000 0000 0000 0000 0000 0000  ................
00528160: C100 6300 6300 6300 0000 0000 0000 0000  ..c.c.c.........
00528170: 0000 0000 0000 0000 0000 0000 0000 0000  ................
*
005281f0: 0000 0000 0000 0000 0000 0000 0000 0000  ................

肉眼可见,第2个Entry,就是Allocation Bitmap:

00528020: 8100 0000 0000 0000 0000 0000 0000 0000  ................
00528030: 0000 0000 0200 0000 54BA 0100 0000 0000  ........T.......
>>> 905888/8
113236.0
>>> 905888/8/(1024*32)
3.4556884765625

一共需要4个cluster来存放Allocation Bitmap,现在看一下FAT表:

00100000: F8FF FFFF FFFF FFFF 0300 0000 0400 0000  ................
00100010: 0500 0000 FFFF FFFF FFFF FFFF FFFF FFFF  ................
00100020: 0000 0000 0000 0000 0000 0000 0000 0000  ................

即2,3,4,5号cluster被占用,5号cluster是最后一个,因此FAT表对应的值为全F。

最后看一下Allocation Bitmap里面的值:

$ sudo xxd -a -u -s$((512*10240+32768*(2-2))) -l512 /dev/sdc2
00500000: BF00 0000 0000 0000 0000 0000 0000 0000  ................
00500010: 0000 0000 0000 0000 0000 0000 0000 0000  ................
*
005001f0: 0000 0000 0000 0000 0000 0000 0000 0000  ................

第1个字节为0xBF,即0b1011 1111

Up-case Table

前面解析出来的root cluster是7号,2,3,4,5用allocation bitmap,中间的6号是干什么?它叫做Up-case Table。

00528040: 8200 0000 0DD3 19E6 0000 0000 0000 0000  ................
00528050: 0000 0000 0600 0000 CC16 0000 0000 0000  ................

exFAT对于文件名的处理,大小写不敏感,但是保留大小写信息(跟FAT32一样)。实现方式就是这个up-case表,filename存放的值有两个含义:

比如a,在filename存放0061,同时将0061当做up-case table的index,查到0041,即大写A。如此简单。

struct directory_entry{
    uint8_t EntryType = 0x82;  // Up-case Table
    uint8_t reserved1[3];
    uint32_t TableChecksum;
    uint8_t reserved2[12];
    uint32_t FirstCluster;
    uint64_t DataLength;
};

关于checksum,推荐的表内容,请见标准。

这个方法有效的一个前提:这个世界上的所有语言,所有的存在大小写符号,数量并不多!

Volume Label

00528000: 8300 0000 0000 0000 0000 0000 0000 0000  ................
00528010: 0000 0000 0000 0000 0000 0000 0000 0000  ................
struct directory_entry{
    uint8_t EntryType = 0x83;
    uint8_t CharacterCount;
    uint8_t VolumeLabel[22];
    uint8_t Reserved2[8];
};

File Entry Set

开始分析具体目录和文件的存储方式。

$ ll /mnt/u2
total 64K
drwxr-xr-x. 2 xinlin xinlin 32K Jun 10 10:19 .
drwxr-xr-x. 1 root   root    12 Jun  9 13:52 ..
-rwxr-xr-x. 1 xinlin xinlin   0 Jun  9 14:19 ccc
-rwxr-xr-x. 1 xinlin xinlin  11 Jun  9 14:28 dddd
...
005280c0: 8502 77EC 2000 6F00 8C33 C956 8C33 C956  ..w. .o..3.V.3.V
005280d0: A233 C956 C1C1 8080 8000 0000 0000 0000  .3.V............
005280e0: C003 0004 2D28 0000 0B00 0000 0000 0000  ....-(..........
005280f0: 0000 0000 0900 0000 0B00 0000 0000 0000  ................
00528100: C100 6400 6400 6400 6400 0000 0000 0000  ..d.d.d.d.......
00528110: 0000 0000 0000 0000 0000 0000 0000 0000  ................

dddd这个文件,对应3个entry,术语叫做Entry Set

struct directory_entry{
    uint8_t EntryType = 0x85;
    uint8_t SecondaryCount;
    uint16_t SetChecksum;
    uint16_t FileAttributes;
    uint16_t Reserved1;
    uint32_t CreateTimestamp;
    uint32_t LastModifiedTimestamp;
    uint32_t LastAccessedTimestamp;
    uint8_t Create10msIncrement;
    uint8_t LastModified10msIncrement;
    uint8_t CreateUtcOffset;
    uint8_t LastModifiedUtcOffset;
    uint8_t LastAccessedUtcOffset;
    uint8_t Reserved2[7];
};

其它全是各种时间。比FAT32多的是UTC offset。32bit的Timestamp如何解析,参考标准。

Stream Extension Entry

struct directory_entry{
    uint8_t EntryType = 0xC0;   // Stream Extension
    uint8_t GeneralSecondaryFlags;
    uint8_t Reserved1;
    uint8_t NameLength;
    uint16_t NameHash;
    uint16_t Reserved2;
    uint64_t ValidDataLength;
    uint32_t Reserved3;
    uint32_t FirstCluster;
    uint64_t DataLength;
};

File Name Entry

struct directory_entry{
    uint8_t EntryType = 0xC1;   // File Name
    uint8_t GeneralSecondaryFlags;
    uint8_t FileName[30];
};

每个File Name Entry可以最多存放15个符号(unicode),因此,文件名越长,需要的entry数量就越多。文件名最长255个unicode符号,即17个entry。

最后看一下这个示例文件dddd所在的9号cluster的内容:

$ cat /mnt/u2/dddd
1234567890
$ sudo xxd -a -u -s$((512*10240+32768*7)) -l32 /dev/sdc2
00538000: 3132 3334 3536 3738 3930 0A00 0000 0000  1234567890......
00538010: 0000 0000 0000 0000 0000 0000 0000 0000  ................

exFAT文件系统删除文件的本质

接上一节,如果将dddd文件删除,硬盘中的数据会出现哪些变化?可以预料的是,文件内容还在9号扇区中,只是root cluster中的一些entry数据会发生变化,FAT表的内容也不会有啥变化。

#  删除之前的root cluster
005280c0: 8502 77EC 2000 6F00 8C33 C956 8C33 C956  ..w. .o..3.V.3.V
005280d0: A233 C956 C1C1 8080 8000 0000 0000 0000  .3.V............
005280e0: C003 0004 2D28 0000 0B00 0000 0000 0000  ....-(..........
005280f0: 0000 0000 0900 0000 0B00 0000 0000 0000  ................
00528100: C100 6400 6400 6400 6400 0000 0000 0000  ..d.d.d.d.......
00528110: 0000 0000 0000 0000 0000 0000 0000 0000  ................
# 删除后的
005280c0: 0502 2DED 2000 6F00 8C33 C956 8C33 C956  ..-. .o..3.V.3.V
005280d0: E738 CC56 C1C1 8080 8000 0000 0000 0000  .8.V............
005280e0: 4003 0004 2D28 0000 0B00 0000 0000 0000  @...-(..........
005280f0: 0000 0000 0900 0000 0B00 0000 0000 0000  ................
00528100: 4100 6400 6400 6400 6400 0000 0000 0000  A.d.d.d.d.......
00528110: 0000 0000 0000 0000 0000 0000 0000 0000  ................

变化:

对EntryType的变化,都是将最高位直接置0。其它都没有变化,FAT表依然是0,9号cluster的内容没有变化:

$ sudo xxd -a -u -s$((512*10240+32768*7)) -l10 /dev/sdc2
00538000: 3132 3334 3536 3738 3930                 1234567890

mkfs.exfat命令

参考:mkfs.exfat命令

创建一个目录看看

创建一个名为abc(小写)的目录:

00528060: 8502 3682 1000 0000 651A CD56 081E CD56  ..6.....e..V...V
00528070: 081E CD56 9398 8080 8000 0000 0000 0000  ...V............
00528080: C003 0003 2BC8 0000 0080 0000 0000 0000  ....+...........
00528090: 0000 0000 0800 0000 0080 0000 0000 0000  ................
005280a0: C100 6100 6200 6300 0000 0000 0000 0000  ..a.b.c.........
005280b0: 0000 0000 0000 0000 0000 0000 0000 0000  ................

8号cluster,长度为2048,但此时目录为空,且全0。这里看到的是abc小写,到up-case table中查表,得到ABC大写。

0040h   0040h   0041h   0042h   0043h
0060h   0060h   0041h   0042h   0043h

如果创建大写的ABC,查表得到的还是大写。

本文链接:https://cs.pynote.net/hd/hdisk/202306081/

-- EOF --

-- MORE --