FAT32ファイルシステム読解
1. はじめに
FAT32 ファイルシステムを実装する必要があった (趣味) ため、本記事では FAT ファイルシステム (メインはFAT32) の仕様を出来るだけ分かり易くまとめました。
なぜ FAT32 なのか?
- 全てのOS (Windows, Linux, MacOS) や Raspberry Pi4 のブート用ファイルシステムでもサポートされおり、非常に使い勝手が良い
- 少なくともファイルのリード操作だけに限定した場合に実装がとても簡単で、Raspberry Pi4 のデバイスで SD カード上のファイルにリードアクセスする場合に便利
ただし、FAT は暗号系の機能がサポートされていなかったりするので、実際の組み込み機器の Linux 系で利用するファイルシステムだと Ext2/Ext3/Ext4 など他のファイルシステムの利用が多いとは思います。
2. FATファイルシステム概要
FAT (File Allocation Table) ファイルシステムは1980年代の MS-DOS 時代から利用されてきた Windows が標準でサポートするファイルシステムの一つです。現在でもリムーバルメディア (USBメモリなど) で広く利用されています。
FAT には、FAT12, FAT16, VFAT, FAT32, exFATの5種類が存在します。FAT32, exFAT以外のものは現在では実質使われないと思うため、本記事では説明は FAT32 にフォーカスしています。基本的に下位互換性が担保された仕様になっています(仕様の拡張で新しいフォーマットが策定されています)。
なお、FAT は厳密にはファイルシステムではなく、ファイルを管理するための領域のことを指します。凄く正確に文章を書いているわけでは無いため、おそらく記事の中でも不適切な文章の部分があると思います。
FAT12
FAT 初期の仕様です。
- 最大ファイルサイズ: 32MB
- 最大ファイル数: 4077
- 最大ボリュームサイズ: 32MB
FAT16
MS-DOS や PC-98 などで利用されていた様子です。
- 最大ファイルサイズ: 2GB
- 最大ファイル数: 65,517
- 最大ボリュームサイズ: 2GB
- 最大ファイル名長: 8文字
VFAT
Virtual FAT の略称で、FAT のファイル名長が最大11文字であった制約を最大ファイル名長が255文字に拡張された FAT です。
FAT32
Windows95 以降でサポートされている FAT です。
- 最大ファイルサイズ: 4GB
- 最大ファイル数: 268,435,437
- 最大ボリュームサイズ: 2TB
- 最大ファイル名長: 255文字
exFAT
exFAT は FAT32 で1ファイルの最大サイズが4GBであった制約を実質無くしたFATです。
- 最大ファイルサイズ: 16EB (ただし最大ボリュームサイズの制約から256TBが実質上限)
- 最大ファイル数: 2,796,202 (ディレクトリ毎)
- 最大ボリュームサイズ: 256TB
- 最大ファイル名長: 255文字
3. FATファイルシステムの内部構造
FAT ファイルシステムの全体概要図を以下に示します。FAT は最大で4パーティション持つことが可能です。
4. MBR (Master Boot Record) 構造
MBR は、512バイト固定領域で必ず先頭の物理セクタ (ブロックとも呼ぶ) に配置され、OSやブートローダー等が起動するためのコードが格納されるブートストラップ領域
と、4つのパーティションの管理情報を格納するパーティションテーブル
、そしてMRBが有効であることを示すシグネチャ
から構成されます。
FAT32 ファイルシステムに限る話ではありませんが、多くの場合はまずはこの MBR を参照して何のパーティションが存在するか確認し、目的のパーティションの情報を取得します。
名称 | オフセット[Byte] | サイズ[Byte] | 用途 |
---|---|---|---|
MBR_bootcode | 0 | 466 | ブートストラップ領域 |
MBR_Partation1 | 446 | 16 | パーティションテーブル1の管理情報 |
MBR_Partation2 | 462 | 16 | パーティションテーブル2の管理情報 |
MBR_Partation3 | 478 | 16 | パーティションテーブル3の管理情報 |
MBR_Partation4 | 494 | 16 | パーティションテーブル4の管理情報 |
MBR_Sig | 510 | 2 | MBRが有効であれば0xaa55が格納されています。それ以外の場合は無効 |
ブートストラップ領域
必要に応じて起動コードが格納されますが、ブート出来るかどうか含めて利用する側のシステム依存です。そのため、システムがブートした後にファイルシステムとして参照する場合には無視して良い領域です。
パーティションテーブル
名称 | オフセット[Byte] | サイズ[Byte] | 用途 |
---|---|---|---|
PT_BootID | 0 | 1 | ブート出来るかどうか 0x00: ブート不可 0x80: ブート可 |
PT_StartChs | 1 | 3 | パーティション開始位置(CHS方式) |
PT_System | 4 | 1 | パーティションタイプ 0x00: 無し(空きエントリ) 0x01: FAT12 (DOS) 0x04: FAT16 (32MB以下) 0x05: 拡張領域 0x06: FAT16 (32MB以上) 0x07: HPFS/NTFS/exFAT 0x0B: FAT32 0x0C: FAT32 (LBA) 0x0E: FAT16 (LBA) 0x0F: 拡張領域 (LBA) |
PT_EndChs | 5 | 3 | パーティション終了位置(CHS方式) |
PT_LbaOfs | 8 | 4 | 開始セクタ番号(LBA方式) |
PT_LbaSize | 12 | 4 | パーティションサイズ(LBA方式) |
CHS方式とLBA方式
パーティションの開始位置やサイズ情報はCHS (Cylinder Head Sector)
方式とLBA (Logical Block Addressing)
方式の2種類で格納されています。現在はLBA方式が主流でCHS方式は使われていないと思いますので、本記事ではLBA方式のみ説明します。
目的のパーティションの先頭セクタを求める
PT_LbaOfs
にパーティションの開始セクタ番号(1~0xFFFFFFFF)が格納され、PT_LbaSize
にパーティションのサイズが格納されます。そのため、ターゲットのパーティションの先頭にシークする場合は、PT_LbaOfsの情報を参照すれば良いです。目的のパーティションの先頭位置は以下で求めるとことが出来ます。
目的のパーティションの先頭セクタ = PT_LbaOfs x 1セクタのサイズ;
5. FATパーティション構造
前述のPT_LbaOfs
の情報から、ターゲットのボリューム (パーティション) のデータ (先頭セクタ) を取得します。
ボリュームは、管理情報が格納されているBPB (BIOS Parameter Block)
領域、ファイルとセクタの対応関係などのテーブル情報のFAT
領域、そして実際のファイルデータであるユーザデータ
領域から構成されます。
BPB (BIOS Parameter Block)
BPBは、MBRのパーティション・テーブルに従ってパーティションが作成され、そのパーティションの先頭セクタに存在 (BPBのサイズは512バイト固定) します。まずはこのBPBの情報をチェックします。
名称 | オフセット[Byte] | サイズ[Byte] | 用途 |
---|---|---|---|
BS_JmpBoot | 0 | 3 | ブートストラップコード。通常はBS_BootCode32へのジャンプ命令が格納 |
BS_OEMName | 3 | 8 | |
BPB_BytsPerSec | 11 | 2 | 1セクタのバイト数を示す。多くの場合は512であるが、有効な値としては512, 1024, 2048, 4096が有り得る |
BPB_SecPerClus | 13 | 1 | クラスタあたりのセクタ数 |
BPB_RsvdSecCnt | 14 | 2 | BPB領域を含む、FATパーティションの先頭からFAT1/FAT2領域直前までのセクタ数を示す |
BPB_NumFATs | 16 | 1 | FATテーブルの数を示します。通常は2が設定され、障害時の復旧用として同じFATが二重化されます。 |
BPB_RootEntCnt | 17 | 2 | ルートディレクトリ直下に存在するディレクトリ数を示す(FAT16のみ。FAT32の場合は0) |
BPB_TotSec16 | 19 | 2 | |
BPB_Media | 21 | 1 | |
BPB_FATSz16 | 22 | 2 | 1つのFATが占めるセクタ数を示す(FAT12/FAT16の場合にのみ有効) |
BPB_SecPerTrk | 24 | 2 | |
BPB_NumHeads | 26 | 2 | |
BPB_HiddSec | 28 | 4 | |
BPB_TotSec32 | 32 | 4 | ユーザ領域(ボリューム)の総セクタ数 |
BPB_FATSz32 | 36 | 4 | 1つのFATが占めるセクタ数を示す(FAT32の場合にのみ有効) |
BPB_ExtFlags | 40 | 2 | |
BPB_FSVer | 42 | 2 | |
BPB_RootClus | 44 | 4 | ルートディレクトリの先頭クラスタ番号を示す。通常は2が設定されているはずです。 |
BPB_FSInfo | 48 | 2 | |
BPB_BkBootSec | 50 | 2 | |
BPB_Reserved | 52 | 12 | 未使用 |
BS_DrvNum | 64 | 1 | |
BS_Reserved | 65 | 1 | 未使用 |
BS_BootSig | 66 | 1 | 拡張ブートシグネチャ。0x29が設定されている場合、後続の3つのフィールドが存在することを示す |
BS_VolID | 67 | 4 | 本パーティション(ボリューム)のシリアル番号 |
BS_VolLab | 71 | 11 | 本ボリュームのラベル名 |
BS_FilSysType | 82 | 8 | ファイルシステムの種類に応じて、"FAT12 ", "FAT16 "または"FAT "のうちいずれかの文字列が設定される |
BS_BootCode32 | 90 | 420 | ブートストラップのコード |
BS_BootSign | 510 | 2 | 0xaa55であれば有効なブートセクタであることを示す。それ以外の場合は無効 (未フォーマット) であることを示す |
FATテーブル
FATファイルシステムは通常、FAT1
とFAT2
という二つのテーブルが存在します。FAT2は、FAT1と全く同じ値を値を持つミラーで障害発生時のために多重化されています。FATの多重化は、BPB_NumFATs
の値を見れば分かります (通常は2が設定されている)。データの書き込み時、まずはFAT2を更新してその後で変更内容をFAT1にも反映します。
次に、FATテーブルの先頭セクタとFAT領域 (多重化されているものも含む) のセクタ数は、BPBの情報から以下のように計算することが出来ます。
FAT先頭セクタ = BPB_RsvdSecCnt;
FAT領域のセクタ数 = BPB_FATSz32 * BPB_NumFATs;
クラスタ
FAT領域にある情報が実際に何であるかを説明するために、まずはクラスタというキーワードを説明する必要があります。クラスタとは、一定数のセクタをグループ化したものです。1クラスタあたりのセクタ数はBPB_SecPerClus
で設定されています。
このクラスタ単位で、ユーザ領域にある実際の各ファイルへの割り当てが行われます。ここで、1ファイルが1クラスタに収まることはないので、あるファイルの対応するクラスタをデイジーチェーンで先頭から最後のクラスタまで繋いで管理します。この管理情報(あるクラスタの次のクラスタはどれという情報)をFATで管理します。
FATの実際のデータは、1エントリーあたり4バイト
で各データが現在のクラスタの場所には次のクラスタ番号が格納されている単純な配列構造です。
ここで、クラスタ番号は以下の通り、少し特殊な意味を持つものがあります。それゆえ、FAT内のクラスタ0, 1も利用することが出来ないReserved扱いになっており、最初の有効なクラスタ番号は2から始まります。
クラスタ番号 | 意味 |
---|---|
0 | 未使用クラスタ |
1 | Reserved |
2 - 0x0FFF_FFF6 | 有効なクラスタ |
0x0FFF_FFF7 | 不良クラスタ |
0x0FFF_FFF8 - 0x0FFF_FFFF | クラスタ終端、つまり最後のクラスタであることを示す (End of Cluster) |
RDE (Root Directory Entry)
FAT32の場合、BPB_RootClus
で設定されているクラスタ番号のデータを見て、そこに存在するファイルやディレクトリ等を探索していくような流れになります。
FAT32ではRDE
というキーワードは実質忘れても良いのですが、FAT16まではRDEというルートディレクトリ直下のディレクトリ情報をクラスタのチェーンとは別で管理した専用の領域がFATテーブルの次に確保されていました。しかし、FAT32ではこの領域はサイズ0になるので実質的に無いものとみなして良いです。
RDEの先頭セクタ番号とセクタ数は以下で求めることが出来ます。32
という数値は後述するディレクトリの管理情報のデータエントリーのサイズです。
RDE先頭セクタ = FAT先頭セクタ + FAT領域のセクタ数;
RDE領域のセクタ数 = (32 x BPB_RootEntCnt + BPB_RootEntCnt - 1) / BPB_BytsPerSec;
クラスタ番号と対応するセクタ番号を求める
クラスタ番号から実際のユーザデータ領域にあるデータのセクタ番号を求める方法について説明します。
まず、ユーザデータ領域はRDE領域の次の領域であるため(隙間を空けない)、先頭セクタは以下で求める事が出来ます。
ユーザデータ先頭セクタ = RDE先頭セクタ + RDE領域のセクタ数;
目的のクラスタの先頭セクタ番号は以下で求める事が出来ます。
クラスタの先頭セクタ = ユーザデータ先頭セクタ + (目的のクラスタ番号 - 2) * BPB_SecPerClus;
ファイルサイズがクラスタサイズよりも大きい場合
ファイルサイズが1つのクラスタサイズよりも大きい場合には、FATを参照しながら現在のクラスタの次のクラスタ番号を計算し、そのクラスタ番号に対応するセクタのデータを参照していくことになります。
ディレクトリエントリー
クラスタ番号から対応するセクタのデータを参照しますが、まずはルートディレクトリ直下に存在するファイルやディレクトリ等の情報を確認する必要があります。この情報を管理するデータがディレクトリエントリー
(32バイト) と呼ばれます。
このディレクトリエントリーのフォーマットは以下です。
名称 | オフセット[Byte] | サイズ[Byte] | 用途 |
---|---|---|---|
DIR_Name | 0 | 11 | 先頭の8バイトがファイル名前、残りの3バイトが拡張子を示します。ただし最初の1バイトは特殊な意味を持ちます(後述) |
DIR_Attr | 11 | 1 | ファイル属性 0x01: 書き込み禁止 0x02: 隠しファイル 0x04: システム 0x08: ボリュームラベル 0x10: ディレクトリ 0x20: アーカイブ 0x0F: LFNエントリ |
DIR_NTRes | 12 | 1 | ファイル名の小文字判定 0x08: ファイル名8文字が小文字 0x10: 拡張子が小文字 |
DIR_CrtTimeTenth | 13 | 1 | ファイル作成時刻 (10ms単位) |
DIR_CrtTime | 14 | 2 | ファイル作成時刻 |
DIR_CrtDate | 16 | 2 | ファイル作成年月日 |
DIR_LstAccDate | 18 | 2 | 最終アクセス年月日 |
DIR_FstClusHI | 20 | 2 | このファイルの先頭クラスタ番号の上位16ビット |
DIR_WrtTime | 22 | 2 | ファイル更新時刻 |
DIR_WrtDate | 24 | 2 | ファイル更新年月日 |
DIR_FstClusLO | 26 | 2 | このファイルの先頭クラスタ番号の下位16ビット |
DIR_FileSize | 28 | 4 | このファイルのファイルサイズ(バイト) |
BPB_RootClus
で示されるクラスタ番号に対応するセクタ番号の場所から、このディレクトリエントリーのデータを参照します。なお、ディレクトリであるかどうかは、DIR_Attr
を見て0x10
かどうかで判断することが出来ます。
ファイルもしくはディレクトリに対応するクラスタ番号は以下の計算で求める事が出来ます。
ファイルのクラスタ番号 = (DIR_FstClusHI << 16) | DIR_FstClusLO;
このクラスタ番号が示す先のデータが実際のファイルのデータになります (エントリーがディレクトリの場合は再度同じ探索をしていくことになります)。
当然のことながら、ルートディレクトリ直下には複数のファイルやディレクトリが存在する事が可能なので、最初のディレクトリエントリーからその次のディレクトリエントリーと、順番に探索をしていく必要があります。この探索時に利用する重要な情報として、DIR_Name
の最初のデータ (DIR_Name[0])が以下のように特別な意味を持ちます。
DIR_Name[0] | 意味 |
---|---|
0x00 | 空 (未使用) エントリーを意味します。また、探索中にこれが来たらここで探索を打ち切るという、エントリーの終端の意味にもなります。 |
0x05 | 日本語ファイル名を示す。ファイル名として参照する場合には0xe5に置き換える必要がある。下の0xe5と区別するためです。 |
0xe5 | 削除されて現在は未使用のエントリーを意味します。 |
ディレクトリエントリーの探索
目的のファイルを見つけた場合には、そこで探索を終了しても良いのですが、とりあえず何のファイルが存在するか全部確認する場合には、ディレクトリエントリーが終端であることを確認するか、もしくは、探索中のクラスタ番号が無効(有効ではない)クラスタ番号になるまで探索してい感じになります。
ファイル名の取得
ファイル名 (ディレクトリ含む) はDIR_Name
を参照することで取得出来ますが、DIR_Nameは11バイトしかないため以下のように格納されています。
- ドット(.)は省略して格納
- 未使用の場合はスペースを入れる (下の2番目の例)
見て分かる通り、これでは8文字+3文字の拡張子以上の長さのファイル名を保存することが出来ません。そこで考えられたのが次に説明するLFN (Long File Name)
です。ちなみに、この8文字+3文字の短い長さのファイル名をSFN
と呼びます。
LFN (Long File Name)
8文字+3文字の拡張子以上の長さの長いファイル名 (LFN
) の場合には、複数の連続したディレクトリエントリーを利用して構成されます。
ディレクトリエントリーがLFNの場合は、DIR_Attr
が0x0F
を示します。DIR_Attr=0x0F
が来た場合には、何もせずに次のディレクトリエントリーを確認します。LFNの場合にはファイル名の長さに応じて複数のディレクトリエントリーが連続してDIR_Attr=0x0Fとなることがありますので、これを繰り返します。そして最後のディレクトリエントリーは通常のファイルの短いファイル名 (SFN) になります。
ちょっと面倒なのですが、この (最後の) SFNから前の方向にLFNのエントリーを戻って行って(降順)、全てのディレクトリエントリーのDIR_Nameを結合することで最終的なファイル/ディレクトリ名を求める必要があります。
また、これらの LFN (最後のSFNは含まない!) のディレクトリエントリーは、通常のディレクトリエントリーと別の以下のLFN専用のディレクトリエントリーのフォーマットを利用します。
名称 | オフセット[Byte] | サイズ[Byte] | 用途 |
---|---|---|---|
LDIR_Ord | 0 | 1 | このLFNエントリーが全体の中でどの位置なのかを示すシーケンス番号(1-20)。開始は1で、0x40が設定されt |
LDIR_Name1 | 1 | 10 | 名前 (1-5文字目) |
LDIR_Attr | 11 | 1 | DIR_Attrと同じ。LFNの場合は0x0Fでなければならない |
LDIR_Type | 12 | 1 | このLFNのタイプ。詳細は未確認ですが、必ず0である必要があるらしい |
LDIR_Chksum | 13 | 1 | このLFNエントリーのチェックサム値 |
LDIR_Name2 | 14 | 12 | 名前 (6-11文字目) |
LDIR_FstClusLO | 26 | 2 | 詳細は未確認ですが、0がセットされるらしいです。 |
LDIR_Name3 | 28 | 4 | 名前 (12-13文字目) |
LDIR_Name1
, LDIR_Name2
, LDIR_Name3
の文字コードは Unicode (UTF-16LE) です。全ての LFN エントリーの LDIR_Name* の文字列を結合することで、最終的なファイル/ディレクトリ名を得ることが出来ます。
LDIR_Chksum (チェックサム)
LDIR_Chksumに設定されているチェックサム値は、LFNに関連しているSFN (最後のディレクトリエントリー) のDIR_Nameに対して以下の操作で求める事が出来ます。
uint8_t sum = 0;
for (int i = 0; i < 11; i++) {
sum = (sum >> 1) + (sum << 7) + DIR_Name[i];
}
6. ファイルを参照する例
例として、boot
パーティションに存在している config.txt
ファイル (/boot/config.txt) を参照する場合の動作例を以下に示します。
これまで説明してきた通り、目的のパーティションの BPB に定義されている BPB_RootClus
を参照し、ルートディレクトリのクラスタ番号を取得します。そのクラスタ番号から、ディレクトリエントリーを確認していき、ルートディレクトリに存在しているファイルやディレクトリ情報を取得します。
その後、config.txt ファイルはクラスタ番号6に存在することが分かったため、そのクラスタのデータを取得していきます。config.txt は1クラスタに収まらないデータだったため、FAT テーブルのクラスタチェーンの情報からクラスタ7と9のデータも取得することで、最終的な config.txt のデータを取得することが出来ます。
7. 参考文献
ここに記載している多くの情報はFATファイルシステムのしくみと操作法を参考にさせて頂きました。その情報を読み解いてFATファイルシステムを利用するために必要な部分の情報を自分用に分かり易くまとめました。
Discussion