🦊

FAT32ファイルシステム読解

2022/12/29に公開

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方式のみ説明します。

https://whatamilookingfor.hatenadiary.org/entry/20120624/1340530660

目的のパーティションの先頭セクタを求める

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ファイルシステムは通常、FAT1FAT2という二つのテーブルが存在します。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_Attr0x0Fを示します。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