ELM Home Page


更新: 2016. 5. 22

exFATファイルシステムのしくみと操作法


このドキュメントは、exFATファイルシステムについてUS. Pat. App. Pub. No. 2009/0164440 A1を元に実際のシステムの挙動を調査して作成されています。先に公開されている「FATファイルシステムのしくみと操作法」の追補版として書かれているので、そちらとセットとしてご利用ください。なお、このドキュメントの内容には意図しない(または不正確な認識による)誤りが混入している可能性があります。実際にexFATファイルシステムをインプリメントする際は、必ず一次資料の情報および標準システムの動作を確認しながら行うこと。

  1. はじめに
  2. exFATボリュームの構成
  3. ブートセクタ
  4. FATとアロケーションビットマップ
  5. 大文字変換テーブル
  6. ディレクトリエントリ
  7. ディレクトリの操作
  8. ストレージ(物理ドライブ)のパーテーショニング

はじめに

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ボリュームの構成

exFATファイルシステムの論理ボリュームは3つの領域で構成され、各領域は複数のセクタで構成されます。それぞれの領域はボリューム上に次の順に配置されます。(exFATボリュームマップ)

  1. ブート領域 (ボリュームの構成データ)
  2. FAT領域 (不連続チェーンの記録のみ)
  3. データ領域 (ファイル、ディレクトリ、その他システム構造体が格納される)

ブートセクタ

ボリューム管理データは、FATファイルシステムと同様にブートセクタに記録されます。exFATボリュームでは、12セクタで一つのブートセクタを構成し、これがメインとバックアップの2セット計24セクタでボリューム先頭のブート領域に配置されます。各領域の配置やクラスタ数などのパラメータは明確に記録され、FATファイルシステムのような不明瞭さは無くなっています。

ブートセクタ(セクタ0)
フィールド名OffsetSize解説
JumpBoot03 BootCodeへのジャンプ命令(x86命令)。このフィールドの値は、0xEB,0x76,0x90でなければならない。
FileSystemName38 ファイルシステムの名前。このフィールドの値は、"EXFAT   "でなければならない。
MustBeZero1153 このフィールドは、ゼロで埋められていなければならない。(FATファイルシステムではBPBが配置される部分)
PartitionOffset648 このexFATボリュームの物理ドライブ先頭からのセクタ単位のオフセット。0のときは、このフィールドは無意味であることを示す。
VolumeLength728 このexFATボリュームのセクタ単位のサイズ。FATファイルシステムとは異なり、コンテナ(MBR形式ならその区画、SFD形式なら物理ドライブ)のサイズに正確に一致していなければならない。また、ボリュームサイズは、1MB以上でなければならない。
FatOffset804 FAT領域の開始セクタ。ボリューム先頭からのオフセットで表される。
FatLength844 FAT一つ当たりのサイズ(セクタ数)。
ClusterHeapOffset884 クラスタヒープ(データ領域のこと)の開始セクタ。ボリューム先頭からのオフセットで表される。
ClusterCount924 データ領域上の総クラスタ数。最大値は、0xFFFFFFF5。
FirstClusterOfRootDirectory964 ルートディレクトリの開始クラスタ番号。
VolumeSerialNumber1004 ボリュームシリアル番号。
FileSystemRevision1042 ファイルシステムのリビジョン。上位バイトがメジャー番号で下位バイトがマイナー番号(例えば、0x020Bなら2.11)。本ドキュメントは、exFAT 1.00について解説している。
VolumeFlags1062 次に示すビットフィールドがある。
bit0(ActiveFat): 0のときは1st FATを使用する。1のときは2nd FATを使用する(TexFATオプション)。
bit1(VolumeDirty): マウント時に1をセット、アンマウント時に元の値をセット。つまり、マウント時に1のときは論理的エラーの存在の可能性を示す。
bit2(MediaFailure): ハードエラーが発生したとき1をセットする。つまり、1のときはメディアの物理的障害の可能性を示す。
bit3-15: 予約(0)。
BytesPerSectorShift1081 バイト単位のセクタサイズ。底を2とする対数で表され、有効値は9~12(つまり512~4096バイト)。メディアの読み書き単位と一致するべきである。
SectorsPerClusterShift1091 セクタ単位のクラスタサイズ。底を2とする対数で表され、有効値は0~25-BytesPerSectorShift(つまり最大32MB)。
NumberOfFats1101 FATの数。有効値は1または2。TexFATにおいて2で、リムーバブルメディアでは1である。
DriveSelect1111 ドライブセレクト。システム依存フィールドで、通常は0x80。
PercentInUse1121 パーセント単位のボリューム使用率。0xFFのときはこの値が無効なことを示す。
Reserved1137 予約。ボリューム作成時はゼロで埋め、以降参照すべきではない。
BootCode120390 ブートコード。システム依存フィールドで、未使用時はゼロで埋める。
BootSignature5102 0xAA55。このセクタが有効なブートセクタであることを示すシグネチャ。
512 セクタサイズが512バイトを越える場合、残りのフィールドは未定義。通常は、ゼロで埋められる。
拡張ブートセクタ(セクタ1~8)
フィールド名OffsetSize解説
ExtendedBootCode02BytesPerSectorShift-4 拡張ブートコード。システム依存フィールドで、未使用時はゼロで埋められる。
ExtededBootSignature2BytesPerSectorShift-44 0xAA550000。このセクタが有効なブートセクタであることを示すシグネチャ。

セクタ9はOEMパラメータ、セクタ10は予約です。これらもシステム依存フィールドで、未使用時は全てゼロで埋められます。セクタ11はセクタ0~10の32ビットチェックサム値で埋められます。以上12セクタで一つのブートセクタを構成し、セクタ12から同じ内容のセット(バックアップ)がもう一つ続きます。チェックサムの計算方法は次の通りです。なお、計算範囲のセクタは連続したバイト列として扱われ、実際にはVolumeFlagsPercentInUseの各フィールドは除外(飛ばして計算)されます。

/* 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とアロケーションビットマップ

FATエントリのサイズは32ビットで、各エントリの全ビットが使用されること以外はFAT32と同じですが、exFATではアロケーションビットマップが新たに採用されました。exFATでは、各クラスタの状態(使用中か空きか)をFATではなくビットマップで管理しています。ビットマップの先頭ビット(先頭バイトのLSB)が先頭クラスタ(クラスタ2)に対応します。あるクラスタに対応するビット値が1のときは、そのクラスタが使用中であることを示します。ビット値が0のときは未使用であることを示し、このときFAT値は意味を持ちません。これは、FAT値0でクラスタ未使用を示していたFATファイルシステムとの大きな違いとなっています。なお、クラスタが使用中でもファイルが特定の状態のときFAT値が意味を持たない(つまりFATアクセスの必要が無い)場合があります。これについては、後のセクションで解説します。次の表にある一つのクラスタにおいて、そのビット値とFAT値によって示されるクラスタの状態を示します。

ビット値FAT値クラスタの状態
0値は意味を持たない未使用
10~1(未定義)
12~ClusterCount+1使用中(値は後続リンク)
1ClusterCount+2~0xFFFFFFF6(未定義)
10xFFFFFFF7不良クラスタ
10xFFFFFFF8~0xFFFFFFFE(未定義)
10xFFFFFFFF使用中(最終リンク)

アロケーションビットマップは、ファイルと同様にデータ領域に配置され、開始クラスタやサイズなどの配置情報は、ルートディレクトリ上のエントリ(後述)に記録されます。なお、アロケーションビットマップのサイズは、(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つのフィールドから成ります。

EntryTypeのフィールド
フィールド説明
InUse(bit7)0:エントリは未使用。1:エントリは使用中。
TypeCategory(bit6)0:プライマリエントリ。1:セカンダリエントリ(プライマリエントリに付随するエントリ)。
TypeImportance(bit5)0:重要な情報。1:重要でない情報。
TypeCode(bit4-0)0~31:タイプコード。

このようにEntryTypeバイトはフィールドが細かく分かれているものの重要なのはその値です。次の表に主なエントリタイプの値を示します。最後の4タイプは通常のアクセスでは必要なかったり、exFAT 1.00では未定義だったりするオプションエントリなので、本ドキュメントでは説明しません。

EntryTypeの値とエントリタイプ
エントリタイプ
0x81アロケーションビットマップ
0x82大文字変換テーブル
0x83ボリュームラベル
0x85ファイル・ディレクトリ(ファイルのアトリビュートやタイムスタンプ)
0xC0ストリーム拡張(ファイルの配置情報)
0xC1ファイル名(ファイルの名前)
0xC2Windows CE access control list
0xA0Volume GUID
0xA1TexFAT Padding
0xA2Windows CE access control table

各タイプで唯一共通なフィールドがEntryTypeで、それ以外のフィールドはタイプ毎に異なります。エントリを削除するときはFATファイルシステムのように0xE5を書き込むのではなく、InUseビットをクリアすることによって行います。また、FATファイルシステム同様、EntryTypeが0x00のときは、それ以降のエントリも全て0x00であることが保証され、無駄なアクセスを避けることができます。

アロケーションビットマップエントリ

アロケーションビットマップの配置情報が記録されるエントリです。このエントリはルートディレクトリに単独で配置されます。

フィールド名OfsSize機能
EntryType010x81。
BitMapFlags11Bit0: 0=1stビットマップ、1=2ndビットマップ。
Bit7-1:予約(0)。
Reserved218予約(0)。
FirstCluster204アロケーションビットマップの開始クラスタ番号。
DataLength248ビットマップのサイズ[バイト]。

大文字テーブルエントリ

大文字テーブルの配置情報が記録されるエントリです。このエントリはルートディレクトリに単独で配置されます。

フィールド名OfsSize機能
EntryType010x82。
Reserved113予約(0)。
TableChecksum44大文字テーブルの32ビットチェックサム。
Reserved2812予約(0)。
FirstCluster204大文字テーブルの開始クラスタ番号。
DataLength248テーブルのサイズ[バイト]。

ボリュームラベルエントリ

ボリュームラベルが記録されるエントリです。このエントリはルートディレクトリに単独で記録されます。存在しない場合または文字数が0の場合、ボリュームラベルはありません。ラベルに使用可能な文字は、ドットを含むファイルに使用可能なもの全てです。

フィールド名OfsSize機能
EntryType010x83。
CharacterCount11ボリューム ラベルの文字数。有効値は、0~11。
VolumeLabel222ボリュームラベル文字列(UTF-16LE)。FATファイルシステムとは異なり、大文字小文字は保持され、名前末尾のスペースも有効。
Reserved248予約(0)。

ファイル・ディレクトリエントリ

ファイルの情報が記録されるエントリセットを構成するエントリの一つで、セットの開始を示すとともに主に属性やタイムスタンプ等の情報を保持します。エントリセットとは複数の連続したエントリのブロックのことで、ファイル・ディレクトリエントリ(1個)、ストリーム拡張エントリ(1個)、名前拡張エントリ(1~17個)の順に配置され、一つのエントリセットを構成します。

フィールド名OfsSize機能
EntryType010x85。
SecondaryCount11続くエントリの個数。このエントリセットのサイズは、SecondaryCount + 1エントリとなる。
SetCheckSum22このエントリセットの16ビットチェックサム。サム計算の際、このフィールドのみ除外(飛ばして計算)する。サムの合わないエントリセットは破損と見なされる。
FileAttribute42ファイルアトリビュート。各ビットの意味はFATファイルシステムのそれと同じ。
bit0: Read-Only。
bit1: Hidden。
bit2: System。
bit3: 予約(0)。
bit4: Directory。
bit5: Archive。
bit6-15: 予約(0)。
Reserved162予約(0)。
CreateTimestamp84ファイル作成時の日時で、下位16ビットが時刻、上位16ビットが日付。それぞれのフィールドはFATファイルシステムと同じ。
LastModifiedTimestamp124最後にファイルが変更されたときの日時。
LastAccessedTimestamp164最後にファイルにアクセスしたときの日時。
Create10msIncrement201CreateTimestampのサブセコンド情報。有効値は0~199。FATファイルシステムと同じく、2秒未満の時間分解能を補う。
LastModified10msIncrement211LastModifiedTimestampのサブセコンド情報。
CreateTZOffset221CreateTimestampのタイムゾーン。15分単位でMSBを1にマスクした値。例えば、JST(+9:00)のときは+9 * 4 | 0x80 = 0xA4、PST(-7:00)なら-7 * 4 | 0x80 = 0xE0となる。これにより、UTCのタイムスタンプを得ることができる。タイムゾーンはオプションで、これを使用しないときは0x00をセットする。
LastModifiedTZOffset231LastModifiedTimestampのタイムゾーン。
LastAccessedTZOffset241LastAccessedTimestampのタイムゾーン。
Reserved2257予約(0)。

ストリーム拡張エントリ

ファイルの情報が記録されるエントリセットを構成するエントリの一つで、主にアロケーション情報を保持します。

フィールド名OfsSize機能
EntryType010xC0。
GeneralSecondaryFlags11 ファイルのアロケーション状態を示すフラグ。
bit0(AllocationPossible): 0=クラスタ割り当て不可、1=クラスタ割り当て可。
bit1(NoFatChain): 0=FATチェーン有効、1=FATチェーン無効。
bit2-15: 予約(0)。
AllocationPossibleビットが0の時(実際は常に1)はValidDataLength, FirstCluster, DataLengthの各フィールドは無効。FATチェーンが連続のときは、NoFatChainフラグをセットしてFATへの記録を省略できる。
Reserved121予約(0)。
NameLength31ファイル名の長さ(文字数)。有効値は1~255。
NameHash42ファイル名のハッシュ値(16ビットチェックサム)。大文字変換されたファイル名文字列(UTF-16LE)を単純なバイト列としてサムを生成する。後にファイルの検索でこの値を参照することで、殆どの不一致ファイルの文字列比較を省略できる。
Reserved262予約(0)。
ValidDataLength88有効データ長[バイト]。有効値は、0~DataLengthで、実際に書き込まれたデータのサイズを示す。これは、fallocate()等を効果的に実装するための機能で、これを越える領域のデータは未定義で、読み出しにはゼロを返すべきである。サブディレクトリの場合は、常にDataLengthと同じでなければならない。
Reserved3164予約(0)。
FirstCluster204ファイルのデータの開始クラスタ番号。DataLengthが0のときはクラスタは割り当てられず、この値も0となる。
DataLength248データ長[バイト]。ファイルの実体のサイズを示す。

名前拡張エントリ

ファイルの情報が記録されるエントリセットを構成するエントリの一つで、名前文字列を保持します。ファイル名に使用可能な文字はFATファイルシステムのLFN拡張と同じで、制御文字(U+0000~U+001F,U+007F)と" * / : < > ? \ |を除く全ての文字が使用可能です。8.3形式ファイル名は廃止されています。

フィールド名OfsSize機能
EntryType010xC1。
GeneralSecondaryFlags11常に0。
FileName230ファイル名文字列(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形式でパーテーショニングする必要があります。