このドキュメントは、exFATファイルシステムについてUS. Pat. App. Pub. No. 2009/0164440 A1を元に実際のシステムの挙動を調査して作成されています。先に公開されている「FATファイルシステムのしくみと操作法」の追補版として書かれているので、そちらとセットとしてご利用ください。なお、このドキュメントの内容には意図しない(または不正確な認識による)誤りが混入している可能性があります。実際にexFATファイルシステムをインプリメントする際は、必ず一次資料の情報および標準システムの動作を確認しながら行うこと。
exFATファイルシステム(Microsoft Extended FAT Filesystem)は、リムーバブルメディアの事実上の標準ファイルシステムであるFATファイルシステム(FAT/FAT32)を置き換える目的で開発されました。このような事情から、exFATファイルシステムは各システムが容易に対応できるように多くの部分でFATファイルシステムと同じ技術を採用しています。このドキュメントでもFATファイルシステムとの違いを中心に解説していきますので、読むに当たってはFATファイルシステムについて十分に理解している必要があります。FATファイルシステムに対するexFATファイルシステムのアドバンテージのうち主なものを次に示します。
"0x"で始まる数値は16進数とし、それ以外の数値は10進数とします。
各単位の接頭辞"K"は210とします。同様に"M"は220、"G"は230、"T"は240とします。
このドキュメントに含まれるプログラムコードの断片はC言語を想定して書かれていますが、文法には厳密ではありません。
プログラムコードの断片には、32ビット値と16ビット値が適当に混在しています。プログラマは型変換によるデータの欠落を認識しそれを避ける方法を知っていることとします。また、全てのデータ型は符号無しとします。符号付きで計算すると意図しない結果になることがあるので行ってはいけません。
exFATファイルシステムの論理ボリュームは3つの領域で構成され、各領域は複数のセクタで構成されます。それぞれの領域はボリューム上に次の順に配置されます。(exFATボリュームマップ)
ボリューム管理データは、FATファイルシステムと同様にブートセクタに記録されます。exFATボリュームでは、12セクタで一つのブートセクタを構成し、これがメインとバックアップの2セット計24セクタでボリューム先頭のブート領域に配置されます。各領域の配置やクラスタ数などのパラメータは明確に記録され、FATファイルシステムのような不明瞭さは無くなっています。
フィールド名 | Offset | Size | 解説 |
---|---|---|---|
JumpBoot | 0 | 3 | BootCodeへのジャンプ命令(x86命令)。このフィールドの値は、0xEB,0x76,0x90でなければならない。 |
FileSystemName | 3 | 8 | ファイルシステムの名前。このフィールドの値は、"EXFAT "でなければならない。 |
MustBeZero | 11 | 53 | このフィールドは、ゼロで埋められていなければならない。(FATファイルシステムではBPBが配置される部分) |
PartitionOffset | 64 | 8 | このexFATボリュームの物理ドライブ先頭からのセクタ単位のオフセット。0のときは、このフィールドは無意味であることを示す。 |
VolumeLength | 72 | 8 | このexFATボリュームのセクタ単位のサイズ。FATファイルシステムとは異なり、コンテナ(MBR形式ならその区画、SFD形式なら物理ドライブ)のサイズに正確に一致していなければならない。また、ボリュームサイズは、1MB以上でなければならない。 |
FatOffset | 80 | 4 | FAT領域の開始セクタ。ボリューム先頭からのオフセットで表される。 |
FatLength | 84 | 4 | FAT一つ当たりのサイズ(セクタ数)。 |
ClusterHeapOffset | 88 | 4 | クラスタヒープ(データ領域のこと)の開始セクタ。ボリューム先頭からのオフセットで表される。 |
ClusterCount | 92 | 4 | データ領域上の総クラスタ数。最大値は、0xFFFFFFF5。 |
FirstClusterOfRootDirectory | 96 | 4 | ルートディレクトリの開始クラスタ番号。 |
VolumeSerialNumber | 100 | 4 | ボリュームシリアル番号。 |
FileSystemRevision | 104 | 2 | ファイルシステムのリビジョン。上位バイトがメジャー番号で下位バイトがマイナー番号(例えば、0x020Bなら2.11)。本ドキュメントは、exFAT 1.00について解説している。 |
VolumeFlags | 106 | 2 | 次に示すビットフィールドがある。 bit0(ActiveFat): 0のときは1st FATを使用する。1のときは2nd FATを使用する(TexFATオプション)。 bit1(VolumeDirty): マウント時に1をセット、アンマウント時に元の値をセット。つまり、マウント時に1のときは論理的エラーの存在の可能性を示す。 bit2(MediaFailure): ハードエラーが発生したとき1をセットする。つまり、1のときはメディアの物理的障害の可能性を示す。 bit3-15: 予約(0)。 |
BytesPerSectorShift | 108 | 1 | バイト単位のセクタサイズ。底を2とする対数で表され、有効値は9~12(つまり512~4096バイト)。メディアの読み書き単位と一致するべきである。 |
SectorsPerClusterShift | 109 | 1 | セクタ単位のクラスタサイズ。底を2とする対数で表され、有効値は0~25-BytesPerSectorShift(つまり最大32MB)。 |
NumberOfFats | 110 | 1 | FATの数。有効値は1または2。TexFATにおいて2で、リムーバブルメディアでは1である。 |
DriveSelect | 111 | 1 | ドライブセレクト。システム依存フィールドで、通常は0x80。 |
PercentInUse | 112 | 1 | パーセント単位のボリューム使用率。0xFFのときはこの値が無効なことを示す。 |
Reserved | 113 | 7 | 予約。ボリューム作成時はゼロで埋め、以降参照すべきではない。 |
BootCode | 120 | 390 | ブートコード。システム依存フィールドで、未使用時はゼロで埋める。 |
BootSignature | 510 | 2 | 0xAA55。このセクタが有効なブートセクタであることを示すシグネチャ。 |
512 | セクタサイズが512バイトを越える場合、残りのフィールドは未定義。通常は、ゼロで埋められる。 |
フィールド名 | Offset | Size | 解説 |
---|---|---|---|
ExtendedBootCode | 0 | 2BytesPerSectorShift-4 | 拡張ブートコード。システム依存フィールドで、未使用時はゼロで埋められる。 |
ExtededBootSignature | 2BytesPerSectorShift-4 | 4 | 0xAA550000。このセクタが有効なブートセクタであることを示すシグネチャ。 |
セクタ9はOEMパラメータ、セクタ10は予約です。これらもシステム依存フィールドで、未使用時は全てゼロで埋められます。セクタ11はセクタ0~10の32ビットチェックサム値で埋められます。以上12セクタで一つのブートセクタを構成し、セクタ12から同じ内容のセット(バックアップ)がもう一つ続きます。チェックサムの計算方法は次の通りです。なお、計算範囲のセクタは連続したバイト列として扱われ、実際にはVolumeFlagsとPercentInUseの各フィールドは除外(飛ばして計算)されます。
/* 32ビットチェックサム生成アルゴリズム */
uint32_t sum32 (const void* p, uint32_t n)
{
uint32_t sum = 0;
const uint8_t *dp = (const uint8_t*)p;
do {
sum = ((sum & 1) ? 0x80000000 : 0) + (sum >> 1) + *dp++;
} while (--n);
return sum;
}
/* 16ビットチェックサム生成アルゴリズム */
uint16_t sum16 (const void* p, uint32_t n)
{
uint16_t sum = 0;
const uint8_t *dp = (const uint8_t*)p;
do {
sum = ((sum & 1) ? 0x8000 : 0) + (sum >> 1) + *dp++;
} while (--n);
return sum;
}
FATエントリのサイズは32ビットで、各エントリの全ビットが使用されること以外はFAT32と同じですが、exFATではアロケーションビットマップが新たに採用されました。exFATでは、各クラスタの状態(使用中か空きか)をFATではなくビットマップで管理しています。ビットマップの先頭ビット(先頭バイトのLSB)が先頭クラスタ(クラスタ2)に対応します。あるクラスタに対応するビット値が1のときは、そのクラスタが使用中であることを示します。ビット値が0のときは未使用であることを示し、このときFAT値は意味を持ちません。これは、FAT値0でクラスタ未使用を示していたFATファイルシステムとの大きな違いとなっています。なお、クラスタが使用中でもファイルが特定の状態のときFAT値が意味を持たない(つまりFATアクセスの必要が無い)場合があります。これについては、後のセクションで解説します。次の表にある一つのクラスタにおいて、そのビット値とFAT値によって示されるクラスタの状態を示します。
ビット値 | FAT値 | クラスタの状態 |
---|---|---|
0 | 値は意味を持たない | 未使用 |
1 | 0~1 | (未定義) |
1 | 2~ClusterCount+1 | 使用中(値は後続リンク) |
1 | ClusterCount+2~0xFFFFFFF6 | (未定義) |
1 | 0xFFFFFFF7 | 不良クラスタ |
1 | 0xFFFFFFF8~0xFFFFFFFE | (未定義) |
1 | 0xFFFFFFFF | 使用中(最終リンク) |
アロケーションビットマップは、ファイルと同様にデータ領域に配置され、開始クラスタやサイズなどの配置情報は、ルートディレクトリ上のエントリ(後述)に記録されます。なお、アロケーションビットマップのサイズは、(ClusterCount + 7) / 8 バイトとなります。
ファイル名の扱いにおいて、FATファイルシステムのLFN拡張と同様に記録は大文字小文字を保持、マッチングは大文字小文字を無視という仕様になっています。このため、ディレクトリ検索時は全ての文字について大文字情報を得る必要があります。FATドライバでは大文字変換テーブルをドライバ側で用意していましたが、exFATでは「ボリューム上に保持」するようになっています。たぶん、新たに文字が定義されたときに備えているのでしょうが、exFATボリューム同士や他のファイルシステムとの互換性がどうなるのかは不明です。変換テーブルは、少なくとも a~z → A~Z の大文字情報を持つことになっていて、それ以外の文字についてはオプションです。Windowsのフォーマッタで作成したexFATボリュームを調べたところ、FATファイルシステムやNTFSと同じ変換となっていましたが、将来どうなるかは分かりません。
変換テーブルはBMPにのみ対応しているようで、U+0000~U+FFFFの単純なテーブルです。でも、65536項目ベタのテーブルで記録されているわけではなく、簡単な圧縮がかけられています。ロード手順は次の通りです。
void load_upcase ( uint16_t dst[], /* 展開先変換テーブル(U+0000~U+FFFF) */ uint16_t src[], /* ボリューム上の圧縮テーブル */ uint16_t n_src /* 圧縮テーブルのサイズ[項目数] */ ) { uint16_t c, si, di; /* 変換テーブルをデフォルト値で埋める */ c = 0; do dst[c] = c; while (++c); si = di = 0; do { c = src[si++]; /* 変換値を1文字読み出し */ if (c == 0xFFFF && si < n_src) { di += src[si++]; /* 途中にU+FFFFが現れたら続く値で示す範囲をスキップ */ } else { dst[di++] = c; /* 変換値をストア */ } } while (si < n_src); }
大文字変換テーブルは、ファイルと同様にデータ領域に配置され、開始クラスタやサイズなどの配置情報は、ルートディレクトリ上のエントリ(後述)に記録されます。
ルートディレクトリは、FAT32と同様にデータ領域に配置され、開始クラスタ番号はFirstClusterOfRootDirectoryで指定されます。ディレクトリエントリのサイズはFATファイルシステムと同じく32バイトで、ディレクトリの最大長は、256MB(8Mエントリ)です。各エントリの先頭バイト(EntryType)は、そのエントリのタイプを示します。このバイトは次の表に示すように、4つのフィールドから成ります。
フィールド | 説明 |
---|---|
InUse(bit7) | 0:エントリは未使用。1:エントリは使用中。 |
TypeCategory(bit6) | 0:プライマリエントリ。1:セカンダリエントリ(プライマリエントリに付随するエントリ)。 |
TypeImportance(bit5) | 0:重要な情報。1:重要でない情報。 |
TypeCode(bit4-0) | 0~31:タイプコード。 |
このようにEntryTypeバイトはフィールドが細かく分かれているものの重要なのはその値です。次の表に主なエントリタイプの値を示します。最後の4タイプは通常のアクセスでは必要なかったり、exFAT 1.00では未定義だったりするオプションエントリなので、本ドキュメントでは説明しません。
値 | エントリタイプ |
---|---|
0x81 | アロケーションビットマップ |
0x82 | 大文字変換テーブル |
0x83 | ボリュームラベル |
0x85 | ファイル・ディレクトリ(ファイルのアトリビュートやタイムスタンプ) |
0xC0 | ストリーム拡張(ファイルの配置情報) |
0xC1 | ファイル名(ファイルの名前) |
0xC2 | Windows CE access control list |
0xA0 | Volume GUID |
0xA1 | TexFAT Padding |
0xA2 | Windows CE access control table |
各タイプで唯一共通なフィールドがEntryTypeで、それ以外のフィールドはタイプ毎に異なります。エントリを削除するときはFATファイルシステムのように0xE5を書き込むのではなく、InUseビットをクリアすることによって行います。また、FATファイルシステム同様、EntryTypeが0x00のときは、それ以降のエントリも全て0x00であることが保証され、無駄なアクセスを避けることができます。
アロケーションビットマップの配置情報が記録されるエントリです。このエントリはルートディレクトリに単独で配置されます。
フィールド名 | Ofs | Size | 機能 |
---|---|---|---|
EntryType | 0 | 1 | 0x81。 |
BitMapFlags | 1 | 1 | Bit0: 0=1stビットマップ、1=2ndビットマップ。 Bit7-1:予約(0)。 |
Reserved | 2 | 18 | 予約(0)。 |
FirstCluster | 20 | 4 | アロケーションビットマップの開始クラスタ番号。 |
DataLength | 24 | 8 | ビットマップのサイズ[バイト]。 |
大文字テーブルの配置情報が記録されるエントリです。このエントリはルートディレクトリに単独で配置されます。
フィールド名 | Ofs | Size | 機能 |
---|---|---|---|
EntryType | 0 | 1 | 0x82。 |
Reserved1 | 1 | 3 | 予約(0)。 |
TableChecksum | 4 | 4 | 大文字テーブルの32ビットチェックサム。 |
Reserved2 | 8 | 12 | 予約(0)。 |
FirstCluster | 20 | 4 | 大文字テーブルの開始クラスタ番号。 |
DataLength | 24 | 8 | テーブルのサイズ[バイト]。 |
ボリュームラベルが記録されるエントリです。このエントリはルートディレクトリに単独で記録されます。存在しない場合または文字数が0の場合、ボリュームラベルはありません。ラベルに使用可能な文字は、ドットを含むファイルに使用可能なもの全てです。
フィールド名 | Ofs | Size | 機能 |
---|---|---|---|
EntryType | 0 | 1 | 0x83。 |
CharacterCount | 1 | 1 | ボリューム ラベルの文字数。有効値は、0~11。 |
VolumeLabel | 2 | 22 | ボリュームラベル文字列(UTF-16LE)。FATファイルシステムとは異なり、大文字小文字は保持され、名前末尾のスペースも有効。 |
Reserved | 24 | 8 | 予約(0)。 |
ファイルの情報が記録されるエントリセットを構成するエントリの一つで、セットの開始を示すとともに主に属性やタイムスタンプ等の情報を保持します。エントリセットとは複数の連続したエントリのブロックのことで、ファイル・ディレクトリエントリ(1個)、ストリーム拡張エントリ(1個)、名前拡張エントリ(1~17個)の順に配置され、一つのエントリセットを構成します。
フィールド名 | Ofs | Size | 機能 |
---|---|---|---|
EntryType | 0 | 1 | 0x85。 |
SecondaryCount | 1 | 1 | 続くエントリの個数。このエントリセットのサイズは、SecondaryCount + 1エントリとなる。 |
SetCheckSum | 2 | 2 | このエントリセットの16ビットチェックサム。サム計算の際、このフィールドのみ除外(飛ばして計算)する。サムの合わないエントリセットは破損と見なされる。 |
FileAttribute | 4 | 2 | ファイルアトリビュート。各ビットの意味はFATファイルシステムのそれと同じ。 bit0: Read-Only。 bit1: Hidden。 bit2: System。 bit3: 予約(0)。 bit4: Directory。 bit5: Archive。 bit6-15: 予約(0)。 |
Reserved1 | 6 | 2 | 予約(0)。 |
CreateTimestamp | 8 | 4 | ファイル作成時の日時で、下位16ビットが時刻、上位16ビットが日付。それぞれのフィールドはFATファイルシステムと同じ。 |
LastModifiedTimestamp | 12 | 4 | 最後にファイルが変更されたときの日時。 |
LastAccessedTimestamp | 16 | 4 | 最後にファイルにアクセスしたときの日時。 |
Create10msIncrement | 20 | 1 | CreateTimestampのサブセコンド情報。有効値は0~199。FATファイルシステムと同じく、2秒未満の時間分解能を補う。 |
LastModified10msIncrement | 21 | 1 | LastModifiedTimestampのサブセコンド情報。 |
CreateTZOffset | 22 | 1 | CreateTimestampのタイムゾーン。15分単位でMSBを1にマスクした値。例えば、JST(+9:00)のときは+9 * 4 | 0x80 = 0xA4、PST(-7:00)なら-7 * 4 | 0x80 = 0xE0となる。これにより、UTCのタイムスタンプを得ることができる。タイムゾーンはオプションで、これを使用しないときは0x00をセットする。 |
LastModifiedTZOffset | 23 | 1 | LastModifiedTimestampのタイムゾーン。 |
LastAccessedTZOffset | 24 | 1 | LastAccessedTimestampのタイムゾーン。 |
Reserved2 | 25 | 7 | 予約(0)。 |
ファイルの情報が記録されるエントリセットを構成するエントリの一つで、主にアロケーション情報を保持します。
フィールド名 | Ofs | Size | 機能 |
---|---|---|---|
EntryType | 0 | 1 | 0xC0。 |
GeneralSecondaryFlags | 1 | 1 | ファイルのアロケーション状態を示すフラグ。 bit0(AllocationPossible): 0=クラスタ割り当て不可、1=クラスタ割り当て可。 bit1(NoFatChain): 0=FATチェーン有効、1=FATチェーン無効。 bit2-15: 予約(0)。 AllocationPossibleビットが0の時(実際は常に1)はValidDataLength, FirstCluster, DataLengthの各フィールドは無効。FATチェーンが連続のときは、NoFatChainフラグをセットしてFATへの記録を省略できる。 |
Reserved1 | 2 | 1 | 予約(0)。 |
NameLength | 3 | 1 | ファイル名の長さ(文字数)。有効値は1~255。 |
NameHash | 4 | 2 | ファイル名のハッシュ値(16ビットチェックサム)。大文字変換されたファイル名文字列(UTF-16LE)を単純なバイト列としてサムを生成する。後にファイルの検索でこの値を参照することで、殆どの不一致ファイルの文字列比較を省略できる。 |
Reserved2 | 6 | 2 | 予約(0)。 |
ValidDataLength | 8 | 8 | 有効データ長[バイト]。有効値は、0~DataLengthで、実際に書き込まれたデータのサイズを示す。これは、fallocate()等を効果的に実装するための機能で、これを越える領域のデータは未定義で、読み出しにはゼロを返すべきである。サブディレクトリの場合は、常にDataLengthと同じでなければならない。 |
Reserved3 | 16 | 4 | 予約(0)。 |
FirstCluster | 20 | 4 | ファイルのデータの開始クラスタ番号。DataLengthが0のときはクラスタは割り当てられず、この値も0となる。 |
DataLength | 24 | 8 | データ長[バイト]。ファイルの実体のサイズを示す。 |
ファイルの情報が記録されるエントリセットを構成するエントリの一つで、名前文字列を保持します。ファイル名に使用可能な文字はFATファイルシステムのLFN拡張と同じで、制御文字(U+0000~U+001F,U+007F)と" * / : < > ? \ |を除く全ての文字が使用可能です。8.3形式ファイル名は廃止されています。
フィールド名 | Ofs | Size | 機能 |
---|---|---|---|
EntryType | 0 | 1 | 0xC1。 |
GeneralSecondaryFlags | 1 | 1 | 常に0。 |
FileName | 2 | 30 | ファイル名文字列(UTF-16LE)を保持する。余ったフィールドは0で埋める。ファイル名が15文字を越える場合は複数個((NameLength + 14) / 15)のエントリが使用される。FATファイルシステムのLFNエントリとは異なり、エントリは昇順で配置される。 |
参考までに実際のexFATボリュームのディレクトリのダンプを示します。ここで説明した各タイプのエントリがテーブルに格納される様子が分かると思います。
ファイルを作成するときは、ディレクトリテーブルの空きを探してその名前でエントリセットを作成します。エントリセットのFileAttributeフィールドにはArchiveビットをセットし、DataLengthフィールドの初期値は0、AllocationPossibleビットは常に1、NoFatChainビットの初期値は0です。
ファイルに初めて新しいクラスタが割り当てられたとき、そのクラスタ番号がFirstClusterにセットされます。このとき、NoFatChainビットには1をセットし、以降クラスタチェーンが連続している限りFATへの書き込みは行いません。チェーンの伸張に伴いチェーンが分断されたときは、FAT上に有効なチェーンを作成してNoFatChainビットをクリアします。なお、ファイルとサブディレクトリ以外の全てのクラスタチェーンは、常に有効なFATチェーンを持ちます。
サブディレクトリを作成するときは、その名前でエントリセットを作成し、FileAttributeフィールドにはDirectoryビットをセットします。ディレクトリには最初にクラスタを1個割り当て、割り当てられたクラスタは、全体を0で初期化(全て未使用状態)します。FATファイルシステムと異なり、exFATではサブディレクトリも有効なサイズ情報を持ちます。NoFatChainビットの扱いはファイルと同じです。テーブルの延長はクラスタ単位で行い、サイズは256MB(8Mエントリ)を越えることはできません。
FATボリュームのサブディレクトリ上に必ずあったドットエントリ(".", "..")はexFATでは廃止され、ファイルAPI上だけの論理的な存在になっています。このため、FATボリュームのようにこれを使って親ディレクトリを辿ることはできません。
ファイルを削除するときはそのファイルのエントリセットの各エントリのInUseビットをクリアしてエントリを解放します。ファイルにクラスタが割り当てられている場合、全て解放します。この際、FATを書き換える必要はなく、アロケーションビットマップの該当ビットをクリアするだけでOKです。
サブディレクトリの削除はファイルと同じですが、そのディレクトリにファイルが存在するときは削除することができません。もし空でないディレクトリが削除された場合、そのディレクトリ下にあるファイルの占めるクラスタが全てロストクラスタになってしまいます。ディレクトリを削除するときは、そのノード以下全てのオブジェクトを削除しなければなりません。
FATファイルシステムでの解説と同じで、区画テーブル上のシステム種別の値はexFATでは0x07(NTFSと同じ)になります。ただし、2TB超のストレージはMBR形式(32ビットLBAで管理)で扱えないため、SFD形式でボリュームを配置するか、GPT形式でパーテーショニングする必要があります。