ELM Home Page

作成: 2009. 5. 24
更新: 2020. 10. 31

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


このドキュメントは、FAT32ファイルシステム仕様書🔗(以下FAT仕様書)を読み解くためのガイドとして書かれています。オリジナルの内容に沿った記述になっていますが、一部省略されていたり動作解析によるオリジナルには無い解説も多く加えられていたりします。標準システム(DOS/Windows)の動作がFAT仕様書と異なる場合は、実際の動作に基づいて説明しています。また、このドキュメントの内容には意図しない(または不正確な認識による)誤りが混入している可能性があります。実際にFATファイルシステムをインプリメントする際は、必ず一次資料の情報および標準システムの動作を確認しながら行うこと。exFATについては、こちらも併せて参照してください。

  1. はじめに
  2. FATファイルシステムの基本
  3. ブートセクタとBPB
  4. 各領域の計算
  5. FATとクラスタ
  6. FATタイプの決定
  7. FATエントリへのアクセス
  8. ファイルとクラスタの結びつけ
  9. FAT32のFSInfo構造体とバックアップブートセクタ
  10. ディレクトリ構造体
  11. ディレクトリの操作
  12. 日付・時刻フィールドのフォーマット
  13. 長いファイル名
  14. 名前の範囲とコードページ
  15. LFNとSFNのマッチング
  16. SFNの生成方法
  17. LFNエントリの抑止
  18. システム間の互換性
  19. ストレージ(物理ドライブ)のパーテーショニング

はじめに

ファイルシステムとは、一般的にはストレージ(補助記憶)上のデータを管理するためのシステム全体のことを指しますが、このドキュメントでは特にFATファイルシステムにおけるストレージ上のデータフォーマットを指し、これについて解説していきます。

FATファイルシステムは1980年前後に端を発し、最初にMS-DOSでサポートされたファイルシステムです。当初、FATファイルシステムは、500Kバイト以下のフロッピーディスクに適したシンプルなファイルシステムとして開発されました。その後、より大容量のストレージをサポートするため拡張されてきました。FATとは、File Allocation Tableというデータ領域の割り当てを管理するための配列のことを指し、それがそのままファイルシステムの名称となっています。現在は、3つのサブタイプ(FAT12,FAT16,FAT32)があります。これらの基本的な違いはFAT上のエントリ(配列要素)のサイズで、それぞれの名前に示されるように12/16/32ビットになります。これらは順に開発され、それぞれ完全に下位互換が保たれています(FAT16対応システムは必ずFAT12も包含し、FAT32対応システムは必ずFAT12/16も包含する)。

ドキュメント中の表記について

"0x"で始まる数値は16進数とし、それ以外の数値は10進数とします。

各単位の接頭辞"K"は210とします。同様に"M"は220、"G"は230、"T"は240とします。

このドキュメントに含まれるプログラムコードの断片はC言語を想定して書かれていますが、文法には厳密ではありません。

プログラムコードの断片には、32ビット値と16ビット値が適当に混在しています。プログラマは型変換によるデータの欠落を認識しそれを避ける方法を知っていることとします。また、全てのデータ型は符号無しとします。符号付きで計算すると意図しない結果になることがあるので行ってはいけません。

FATファイルシステムの基本

セクタ

メモリ(主記憶)とは異なり、ストレージデバイス(ブロックデバイスとも呼ばれる)では、ある決まったサイズのデータブロックを最小単位に読み書きが行われます。FATファイルシステムではこれをセクタと呼んでいます。一般的には512バイトのセクタサイズが用いられ、一部のストレージデバイスではより大きなセクタサイズも用いられます。それぞれのセクタはセクタ番号で識別され、ストレージの先頭を0として順に割り振られます。ボリュームはストレージの先頭から配置されるとは限らないため、このドキュメントでは、単に「セクタ番号」と言った場合は、ボリュームの先頭セクタを0とした相対的な番号とします。ストレージ先頭からの絶対的な位置を示す場合は、「物理セクタ番号」と表記します。

FATボリューム

ある一つの完結したFATファイルシステムを論理ボリューム(または論理ドライブ)と呼びます。FATの論理ボリュームは3つまたは4つの領域で構成され、各領域は1個または複数のセクタで構成されます。それぞれの領域はボリューム上に次の順に配置されます。(FATボリュームマップ)

  1. 予約領域 (ボリュームの構成データ)
  2. FAT領域 (データ領域の割り当て表)
  3. ルートディレクトリ領域 (FAT32ボリュームでは存在しない)
  4. データ領域 (ファイルやディレクトリの内容が格納される)

データ形式

FATファイルシステムは、最初はx86プロセッサを搭載するIBM PC向けに開発されてきました。つまり、FATボリューム上において数値データは、全てリトルエンディアンでストアされます。もし、対象プラットフォームのアーキテクチャがビッグエンディアンのときは、ストレージ上のBPB/FAT/ディレクトリ構造体にアクセスする際にエンディアン変換が必要になります。また、16/32ビット長のデータフィールドは、そのワード境界にアライメントしているとは限りません。対象プロセッサがリトルエンディアンであっても、非アライメントのワードアクセスが正しく行えない場合は、やはりバイト単位でアクセスする必要があります。このような理由から、FATボリューム上のデータを構造体メンバとしてアクセスする方法は、これらにパッキングオプションの環境依存問題も加わり、きわめてポータビリティの悪いコードになります。

これはファイルシステムに限ったことではなく通信システムなどにおいても同様で、システムポータブルな構造体を扱う上では常に問題となります。FATボリュームを単純なバイト配列とみなし、バイト単位でアクセスする方法は、可読性の低下と引き替えに最良のポータビリティが得られます。

ブートセクタとBPB

FATボリュームで最も重要なデータ構造体はBPB(BIOS Parameter Block)で、そのFATボリュームに関する構成パラメータが記録されます。BPBはブートセクタに配置されます。このセクタはよくVBR(Volume Boot Record)やPBR(Private Boot Record)などと呼ばれていますが、重要なのは単純にそれが予約領域の先頭セクタ(つまりボリュームの先頭セクタ)であるということです。

BPBは、FATファイルシステムの機能追加のたびに拡張されてきました。しかし、明確なバージョン管理が無かったため、たびたび混乱をもたらしてきました。BPBは最初からあったわけではなく、MS-DOS Ver.1ではブートセクタにBPBが存在しませんでした。この最初のバージョンのFATファイルシステムでは、限られた構成のフォーマットしか存在せず、どのタイプか決定するのはブートセクタ直後から始まるFATの先頭バイト(FAT[0]の下位8ビット)の値を調べることで行われていました。

この手のフォーマット決定方法は、MS-DOS Ver.2でのBPBの新設により取って代わられ、サポートされなくなりました。現在は全てのFATボリュームは、ブートセクタにBPBを置かなければなりません。BPBはその後も拡張され、そのたびにFATボリュームの認識に関して混乱(どれが正しいパラメータなのか?)をもたらしました。

まず、ハードディスクの普及によりFAT12の最大クラスタ数の制限によるディスク利用効率の悪化やファイル数の制限が現れ、MS-DOS Ver.3で新たにFAT16がサポートされました(1回目の変更)。しかし、すぐに新たな問題が発生しました。ボリュームのサイズを示すフィールドが16ビットだったため、65536セクタ(セクタサイズ512バイトで32Mバイト)未満のボリュームにしか対応できなかったのです。このため、MS-DOS Ver.3.31でBPBに32ビットのフィールドが追加され、FAT12で128Mバイト、FAT16で2Gバイトまでのボリューム(32Kバイト/クラスタの場合)に対応できるようになりました(2回目の変更)。

最後の変更は、FAT32ファイルシステムが現れたWindows 95 OSR2です。当時、FAT16もファイル数や容量の限界に近づいていたため、FAT32のサポートにより2Gバイトを越えるボリュームサイズを実現しました。FAT32は仕様上は最大2Tバイト(512バイト/セクタの場合)まで対応し、FATファイルシステムの最終形と言えます。なお、マイクロソフトは32Gバイトを超えるボリュームにおいては、FAT以外のファイルシステム(NTFSやexFATなど)の使用を強制しているため、実用面ではFATボリュームの最大容量は32Gバイトとなります。

次の表にブートセクタのデータフィールドを示します。表に示すBPB_で始まる名前のフィールドは、BPBの一部です。BS_で始まる名前のフィールドはBPBとは関係なく、単にブートセクタを構成するフィールドに過ぎません。

FAT32ボリュームのBPBは、BPB先頭からBPB_TotSec32フィールドまでFAT12/FAT16ボリュームと共通で、それ以降はFATタイプがFAT32かFAT12/FAT16かで異なります。36バイト目以降のフィールドは、どちらか一方のフィールドのみ存在することになります。FATタイプの決定方法については後に解説。

FAT12/16/32共通フィールド(オフセット0~35)
名前OffsetSize解説
BS_JmpBoot03 ブートストラップコードへのジャンプ命令(x86命令)。このフィールドには次の2つのフォーマットがあり、前者が一般的。
0xEB, 0x??, 0x90 (ショートジャンプ+NOP)
0xE9, 0x??, 0x?? (ニアジャンプ)
??はジャンプ先により異なる任意の値。これらから外れたフォーマットの場合、そのボリュームはWindowsで認識されない。
BS_OEMName38 "MSWIN4.1"が推奨される。ほかに"MSDOS5.0"などがよく使われる。このフィールドに関しては、多くの誤解がある。これは単なる名前である。マイクロソフトのOSはこのフィールドに何ら注意を払わないが、いくつかのFATドライバは何らかの参照を行う。この文字列が推奨されるのは、それが互換性問題を最小にする設定であることが理由である。何か違う値を設定しても良いが、いくつかのFATドライバはそのボリュームを認識できないかも知れない。このフィールドは、たいていはそのボリュームを作成したシステムを示している。
BPB_BytsPerSec112 バイト単位のセクタサイズ。有効な値は、512, 1024, 2048または4096である。マイクロソフトのOSはこれらのセクタサイズを適切にサポートする。しかし、サポートするセクタサイズを512に限定していても、このフィールドが512であることをチェックしないFATドライバが多く存在するため、最大限の互換性が要求されるときは512を使うべきである。ただし、それは単に互換性に関することであるというのを誤解しないこと。この値は、そのボリュームを格納するストレージのセクタサイズと同じでなければならない。
BPB_SecPerClus131 アロケーションユニット(割り当て単位)当たりのセクタ数。FATファイルシステムでは、アロケーションユニットのことをクラスタと呼んでいる。これは1個以上の連続したセクタのブロックのことで、データ領域はこれを単位に管理される。クラスタ当たりのセクタ数は、2の累乗でなければならない。したがって、有効な値は、1, 2, 4, ..., 128ということになるが、クラスタサイズ(BPB_BytsPerSec * BPB_SecPerClus)が32Kバイトを越す値は使用すべきではない。最近のシステム(例えばWindows98やNT系Windows)では 64K, 128K, 256Kバイトといったクラスタサイズも扱えるが、そのようなボリュームは古いシステムやディスクユーティリティに正しく認識されないかもしれない。
BPB_RsvdSecCnt142 予約領域のセクタ数。このフィールドは0であってはならない(少なくともこのBPBを含むブートセクタそれ自身が存在する)。互換性問題を避けるため、FAT12/16ボリュームでは1にするべきである。なぜなら、FAT12/16の予約領域は1セクタであると決め打ちし、このフィールドを無視するツールやFATドライバが存在するからである。FAT32ボリュームでは代表的には32である。マイクロソフトのOSは1以上の値を適切にサポートする。
BPB_NumFATs161 FATの数。このフィールドの値は常に2に設定すべきである。1以上の何らかの値もまた有効ではあるが、互換性問題を避けるため2以外の値は使用しないことが強く推奨される。マイクロソフトのFATドライバは2以外の値も適切にサポートするが、いくつかのツールやFATドライバにはこのフィールドを無視してFAT数2で動作するものがある。
このフィールドの標準値が2であるのはFATに冗長性を持たせるためで、もしFAT中の一部のセクタが破損しても多重化されたもう一つのFATがあるので、ファイルが失われる危険が少なくなる。メモリカードのような非ディスクストレージではそのような冗長性は無用な機能なので1でも良いが、そのようなボリュームは互換性が低下するかも知れない。
BPB_RootEntCnt172 FAT12/16ボリュームでは、ルートディレクトリに含まれるディレクトリエントリの数を示す。このフィールドには、ディレクトリテーブルのサイズが2セクタ境界にアライメントする値、つまり、BPB_RootEntCnt * 32BPB_BytsPerSecの偶数倍になる値を設定すべきである(32というのはディレクトリエントリ1個のサイズ)。最大の互換性のためには、FAT16では512に設定すべきである。FAT32ボリュームではこのフィールドは使われず、常に0でなければならない。
BPB_TotSec16192 ボリュームの総セクタ数(古い16ビットフィールド)。この値は、ボリュームの4つの領域全てを含んだセクタ数である。FAT12/16でボリュームのセクタ数が0x10000以上になるときは、このフィールドには無効値(0)が設定され、真の値がBPB_TotSec32に設定される。FAT32ボリュームでは、このフィールドは必ず無効値でなければならない。
BPB_Media211 区画分けされたハードディスクでは0xF8が標準値である。区画分けされないリムーバブルメディアでは0xF0がしばしば使われる。このフィールドに有効な値は、0xF0, 0xF8, 0xF9, 0xFA, 0xFB, 0xFC, 0xFD, 0xFEおよび0xFFで、ほかに重要な点はこれと同じ値をFAT[0]の下位8ビットに置かなければならないということだけである。これは、MS-DOS 1.xでメディアタイプの設定に遡り、既に使われていない。
BPB_FATSz16222 1個のFATが占めるセクタ数。このフィールドはFAT12/FAT16ボリュームでのみ使われる。FAT32ボリュームでは必ず無効値(0)でなければならず、代わりにBPB_FATSz32が使われる。FAT領域のサイズは、この値 * BPB_NumFATsセクタとなる。
BPB_SecPerTrk242 トラック当たりのセクタ数。このフィールドは、ジオメトリを持つストレージにのみ関係し、IBM PCのディスクBIOSで使用される。それ以外では意味がない。
BPB_NumHeads262 ヘッド数。このフィールドは、ジオメトリを持つストレージにのみ関係し、IBM PCのディスクBIOSで使用される。それ以外では意味がない。
BPB_HiddSec284 ストレージ上でこのボリュームの手前に存在する隠れた物理セクタの数。一般的にIBM PCのディスクBIOSでアクセスされるストレージに関するものであり、どのような値が入るかはシステム依存。ボリュームがストレージの先頭から始まる場合(つまりフロッピーディスクなど区画分けされていないもの)では常に0であるべきである。
BPB_TotSec32324 ボリュームの総セクタ数(新しい32ビットフィールド)。この値は、ボリュームの4つの領域全てを含んだセクタ数である。FAT12/16ボリュームで総セクタ数が0x10000未満のとき、このフィールドは無効値(0)でなければならなず、真の値はBPB_TotSec16に設定される。FAT32ボリュームでは常に有効値が入る。

ここから先のフィールドはそのボリュームがFAT12/16かFAT32かで異なるので、これらを参照する前にFATタイプを決定しなければなりません。また、FAT32ボリュームではFAT12/16ボリュームに存在しないフィールドもあります。

FAT12/16におけるオフセット36以降のフィールド
名前OffsetSize解説
BS_DrvNum361 IBM PCのディスクBIOSで使われるドライブ番号。このフィールドは、MS-DOSのブートストラップで使われ、フロッピーディスクでは0x00、固定ディスクでは0x80である。実際にはOS依存。
BS_Reserved371 予約(WindowsNTで使用)。フォーマットするときは常に0を設定すべきである。
BS_BootSig381 拡張ブートシグネチャ (0x29)。これは、続く3つのフィールドが存在することを示す。
BS_VolID394 ボリュームシリアル番号。このフィールドとBS_VolLabでリムーバブルストレージにおけるボリュームの追跡をサポートする。これらの値はFATドライバが不正なメディア交換を検出するのを助ける。このIDは大抵は現在時刻から生成される。
BS_VolLab4311 ボリュームラベル。このフィールドは、ルートディレクトリに記録される11バイトのボリュームラベルに一致する。FATドライバは、ルートディレクトリのボリュームラベルを更新したら、この値にも反映させるべきである。ボリュームラベルが無い場合は、"NO NAME "を設定する。
BS_FilSysType548 "FAT12   ", "FAT16   "または"FAT     "のうちいずれかの文字列。多くの人はこの文字列がFATタイプの決定に何らかの関与をしていると思っているが、それは明確に間違いである。このフィールドの名前から、これはBPBの一部ではないことが分かると思う。この文字列は不正確だったり設定されていなかったりすることがしばしばあるので、マイクロソフトのFATドライバはFATタイプの決定にこのフィールドを使わない。しかし、一部のFATドライバはこの文字列を使用しているので、互換性問題を避けるためこの文字列はそのボリュームのFATタイプに基づいて設定されるべきである。
BS_BootCode62448 ブートストラップコード。システム依存フィールドで、未使用時はゼロで埋める。
BS_Sign5102 0xAA55。有効なブートセクタであることを示すブートシグネチャ。
512 512バイトを越えるセクタサイズの場合、残りはゼロで埋める。
FAT32におけるオフセット36以降のフィールド
名前OffsetSize解説
BPB_FATSz32364 1個のFATが占めるセクタ数。FAT領域のサイズは、この値 * BPB_NumFATsセクタとなる。このフィールドに限りFATタイプ決定前に参照する必要があるが、BPB_FATSz16がFAT12/16で必ず有効、FAT32で必ず無効になるので問題はない。
BPB_ExtFlags402 ビット3~0: 0から始まるアクティブなFAT。ビット7が1のとき有効。
ビット6~4: 予約。
ビット7: 0は全てのFATがミラーリングされることを示す。1はビット3~0で示される1個のFATだけがアクティブであることを示す。
ビット15~8: 予約。
BPB_FSVer422 FAT32ボリュームのバージョン。上位バイトがメジャーバージョン番号、下位バイトがマイナーバージョン番号。これは、将来古いFAT32ドライバでマウントされることを考慮することなくFAT32を拡張するためのもの。なお、このドキュメントでは、バージョン0.0について解説している。
ディスクユーティリティは、設計された時点より新しいバージョンのFAT32ボリュームに対しては操作を行うべきでない。また、FAT32ドライバはこのフィールドをチェックし、その値がドライバの書かれた時点より新しいバージョンの場合、そのボリュームをマウントすべきではない。
BPB_RootClus444 ルートディレクトリの先頭クラスタ番号。大抵は2(つまり先頭クラスタ)が設定されるが、2である必要はない。
しかし、ルートディレクトリの位置を変える場合、なるべく先頭で正常なクラスタに置くようにすべきである。これは、もしこのフィールドが誤ってクリアされたとき、ディスク修復ツールが容易にルートディレクトリを見つけられるようにするためである。
BPB_FSInfo482 FAT32ボリュームの予約領域中でFSINFO構造体の置かれるセクタ番号。常に1(つまりブートセクタの次)。
BPB_BkBootSec502 0以外の場合、FAT32ボリュームの予約領域中でブートセクタのバックアップが置かれるセクタを示す。大抵は6(つまりブートセクタの6セクタ先)で、この値以外は推奨されない。
BPB_Reserved5212 将来の拡張のために予約。フォーマット時はゼロを設定すべきである。
BS_DrvNum641 FAT12/16での説明に同じ。
BS_Reserved651 FAT12/16での説明に同じ。
BS_BootSig661 FAT12/16での説明に同じ。
BS_VolID674 FAT12/16での説明に同じ。
BS_VolLab7111 FAT12/16での説明に同じ。
BS_FilSysType828 常に"FAT32   "。このフィールドはFATタイプの決定には関与しない。
BS_BootCode3290420 ブートストラップコード。システム依存フィールドで、未使用時はゼロで埋める。
BS_Sign5102 0xAA55。有効なブートセクタであることを示すブートシグネチャ。
512 512バイトを越えるセクタサイズの場合、残りはゼロで埋める。

ブートセクタには、もう一つ重要なことがあります。ブートシグネチャ(BS_Sign)の存在をもってそのブートセクタが有効であると認識されます。この値が確認できないときは、そのブートセクタは無効(つまり、未フォーマットと判断される)です。多くのFATドキュメントは、「ブートセクタの末尾にブートシグネチャがある」という誤った解説をしています。これは、セクタサイズが512のときは正しいのですが、それ以外の場合は誤ったものとなります。ブートシグネチャは、セクタサイズに関わらず常にオフセット510に置かれなければなりません(これとセクタの末尾の両方に置いてもよい)。マイクロソフトのフォーマッタは、オフセット512以降を全て0で埋めます。また、初期のMS-DOS時代に作成された古いFATボリュームではブートシグネチャが設定されていないことがあります。

ボリュームのサイズ(BPB_TotSec16/32フィールドの値)は、そのボリュームの置かれているコンテナ(ストレージや区画)のサイズより小さくても、全く問題ではありません。アライメントの関係でそのような状態になることは良くあります。

このようなアライメントホールは、ストレージ容量の無駄にはなりますが、FATボリューム自体の不正を意味するものではありません。しかし、BPB_TotSec16/32の値がそのボリュームのコンテナのサイズより大きい場合は、FATボリュームの破損または不正な設定など深刻な状態と言えます。このようなボリュームを操作した場合、壊滅的なデータ破壊をもたらすことになるので、そのような状態を検出したときは、FATドライバはそのボリュームのマウントを拒否すべきです。

各領域の計算

各領域の開始位置とサイズは、次に示すようにBPBのフィールドから算出されます。

FAT領域は予約領域の次なので、当然ながら開始位置とサイズは

FatStartSector = BPB_ResvdSecCnt;
FatSectors = BPB_FATSz * BPB_NumFATs;

となります。ルートディレクトリの開始位置とサイズは、

RootDirStartSector = FatStartSector + FatSectors;
RootDirSectors = (32 * BPB_RootEntCnt + BPB_BytsPerSec - 1) / BPB_BytsPerSec;

32というのは、ディレクトリエントリ1個のサイズ(バイト)です。端数が出た場合は切り上げて引っかかるセクタも含めるように計算しています(端数が出るような構成は推奨されていませんが)。FAT32ボリュームでは、BPB_RootEntCntは常に0なので、ルートディレクトリ領域のサイズも0になります。そして、データ領域はこれらの残りとなり、次のように求められます。

DataStartSector = RootDirStartSector + RootDirSectors;
DataSectors = BPB_TotSec - DataStartSector;

ストレージが区画分けされている場合など、ボリュームがストレージの先頭から始まらない場合は、これらの開始セクタ番号は物理セクタ番号と一致しません。

FATとクラスタ

次に重要な領域はFATそれ自身です。FATの役目は、ファイルのデータ領域における配置(クラスタチェーン)を記録・管理することです。

データ領域は、クラスタという一定のセクタ数(BPB_SecPerClus)のブロックに区切られ、これを単位に各ファイルへのデータ領域の割り当てが行われます。各クラスタはクラスタ番号で識別され、FATの各要素(FATエントリ)は各クラスタと1:1で対応付けされています。FATの値は対応するクラスタの状態を示します。ただし、FATの先頭2エントリ(FAT[0], FAT[1])は予約で、クラスタには結びつけられていません。FAT[2]がデータ領域の先頭クラスタ(クラスタ番号2)に対応します。このように有効なクラスタ番号は2から始まるため、「クラスタ数N」のボリュームにおいて、「有効なクラスタ番号は 2~N+1」、「FATのサイズは N+2エントリ」となります。FATに記録されるデータについては、ファイルとクラスタの結びつけを参照してください。

FATはセクタが破損したときの影響が大きいため、多重化されています。多重化数はBPB_NumFATsで示されるので、FAT領域のサイズはBPB_FATSz * BPB_NumFATsとなります。通常は先頭のFATが参照され、更新は全てのコピーに対して行われます。

FATタイプの決定

これについては、どのように行うのが正しいのか相当な混乱があり、大小様々なエラーをもたらしています。実際のところ、FATタイプの決定は次に定義されるようにきわめてシンプルなロジックで行われます。

FATタイプ(FAT12/16/32)は、ボリューム上のクラスタ数によってのみ決定され、それ以外の手段は「無い」。

クラスタ数はデータ領域に存在できるクラスタの数、つまりデータ領域のサイズをクラスタサイズで割った値となります。

CountofClusters = DataSectors / BPB_SecPerClus;

端数が出た場合は、切り捨てます。クラスタ数が分かればFATタイプを決定できます。これは次のように行います。

これがFATタイプ決定の唯一の手段です。FAT12ボリュームが4085を超えるクラスタを持つことはありませんし、FAT16ボリュームが4086クラスタ未満だったり、65525クラスタを超えたりすることもありません。もしもこのルールを破った不正なFATボリュームの作成を試みても、正しく設計されたFATドライバは意図したものとは異なるFATタイプと認識し、結果としてそれを扱うことができません。

しかし、この境界値は明確に決まっているわけではありません。クラスタ番号の取り得る値からすると上記のようになりますが、実際に出回っている情報やFATドライバではその多くにバラツキ(1,2,8,10または16クラスタ)があります。たとえば、FAT12の最大クラスタ数一つ取っても、FAT仕様書では4084としている一方、MSDNの解説では4085、そして実際のWindowsの動作では4085(ドライバ)や4086(ディスクツール)などとなっています。基準となるべき仕様書やシステムでさえこのようにバラバラな状態なので、存在する全てのFATコードにおいて最大限の互換性を確保するため、クラスタ数が境界値付近となるボリュームの作成は避けることが推奨されています。FAT仕様書では境界値から16は離したクラスタ数に設定すべきとしています。

また、このルールを知らずに書かれたFATドライバは、クラスタ数ではなくBS_FilSysTypeフィールドの文字列でFATタイプを決定しているようです。そのような不正なFATドライバの救済のためにも、BS_FilSysTypeには実際のFATタイプに矛盾しない文字列の設定が推奨されています。

FATタイプがクラスタ数で決まるということは、ボリュームのサイズによって成り得るFATタイプも決まってくるということが分かります。例えば、セクタサイズを512バイト、クラスタサイズの範囲を512~32Kバイトとした場合、

FATタイプボリュームサイズ
FAT12~128Mバイト
FAT162M~2Gバイト
FAT3232M~2Tバイト

と言うことができます(境界は概値)。

FATエントリへのアクセス

FATに関連してもう一つ重要な点は、FATにアクセスする手順です。まず、クラスタ番号NのエントリはFAT中のどこに置かれるのか知る必要があります。FAT16/32の場合これははきわめて単純で、FATは一般的な16/32ビット整数の配列そのものです。メモリ上の配列と違う点は、各要素は連続した領域ではなく、FAT領域の先頭セクタから連続した複数のセクタにまたがっているということです。そのクラスタ番号NのFATエントリ(FAT[N])がどのセクタのどのバイト位置かは、次の計算で求められます。

/* FAT16エントリ位置計算 */
    ThisFATSecNum = BPB_ResvdSecCnt + (N * 2 / BPB_BytsPerSec);

    ThisFATEntOffset = (N * 2) % BPB_BytsPerSec;
FAT32エントリ位置計算
    ThisFATSecNum = BPB_ResvdSecCnt + (N * 4 / BPB_BytsPerSec);

    ThisFATEntOffset = (N * 4) % BPB_BytsPerSec;

これにより求められたセクタ番号のバイト位置から2バイト(FAT16)または4バイト(FAT32)がそのFATエントリの値です。バイト順はリトルエンディアンです。FAT16/32ボリュームでは、FATエントリがセクタ境界にまたがることはありません。

FAT32に関してはもう一つ重要な点があります。FAT32ボリュームのFATエントリは32ビットを占めますが、実際にはその上位4ビットは予約で、下位28ビットだけが有効な値です。予約ビットはフォーマット時に0を設定され、それ以降は変更してはいけません。このため、FAT32ボリュームのFATエントリから値を取り出すときは、0x0FFFFFFFで上位4ビットをANDマスクする必要があります。また、FATエントリに何らかの値を設定するときは、上位4ビットの値を保存する必要があります。

/* FAT32エントリ取り出し */
    ReadSector(SecBuff, ThisFATSecNum);

    ThisEntryVal = *(uint32*)&SecBuff[ThisFATEntOffset] & 0x0FFFFFFF;
/* FAT32エントリ設定 */
    ReadSector(SecBuff, ThisFATSecNum);

    tmp = *(uint32*)&SecBuff[ThisFATEntOffset];
    tmp = (tmp & 0xF0000000) | (NewEntryVal & 0x0FFFFFFF);
    *(uint32*)&SecBuff[ThisFATEntOffset] = tmp;

    WriteSector(SecBuff, ThisFATSecNum);

FAT12では、FATエントリのサイズが12ビット(1バイト半)なので、少しだけ複雑になります。クラスタ番号が偶数か奇数かによってバイト中のビット配置が異なるため、それぞれ処理を分ける必要があります。

/* FAT12エントリ位置 */
    ThisFATSecNum = BPB_ResvdSecCnt + ((N + (N / 2)) / BPB_BytsPerSec);
    ThisFATEntOffset = (N + (N / 2)) % BPB_BytsPerSec;
/* FAT12エントリ取り出し */
    ReadSector(SecBuff, ThisFATSecNum);

    if (N & 1) {    /* 奇数クラスタ番号 */
        ThisEntryVal = (SecBuff[ThisFATEntOffset] >> 4)
                     | ((uint16)SecBuff[ThisFATEntOffset + 1] << 4);
    } else {        /* 偶数クラスタ番号 */
        ThisEntryVal = SecBuff[ThisFATEntOffset]
                     | ((uint16)(SecBuff[ThisFATEntOffset + 1] & 0x0F) << 8);
    }
/* FAT12エントリ設定 */
    ReadSector(SecBuff, ThisFATSecNum);

    if (N & 1) {    /* 奇数クラスタ番号 */
        SecBuff[ThisFATEntOffset] = (SecBuff[ThisFATEntOffset] & 0x0F)
                                  | (NewEntryVal << 4);
        SecBuff[ThisFATEntOffset + 1] = NewEntryVal >> 4;
    } else {        /* 偶数クラスタ番号 */
        SecBuff[ThisFATEntOffset] = NewEntryVal;
        SecBuff[ThisFATEntOffset + 1] = (SecBuff[ThisFATEntOffset + 1] & 0xF0)
                                      | ((NewEntryVal >> 8) & 0x0F);
    }

    WriteSector(SecBuff, ThisFATSecNum);

しかし、残念ながらこのコードは正しく動作しません。なぜなら、ThisFATEntOffsetがセクタの最終バイトになるとき、そのエントリがセクタ境界にまたがるからです。このような例外も適切に処理されるように何らかの工夫をする必要があります。

ここまでのまとめとして、各FATタイプ別のFATエントリの格納状態を次の図に示します。

FAT entries

ファイルとクラスタの結びつけ

FATボリューム上のファイルは、ディレクトリという管理テーブルによって管理されます(ディレクトリの詳細は後述)。ディレクトリエントリにはそのファイルの名前とともにデータの「先頭クラスタ番号」が記録され、これがFATへの入り口になります。先頭クラスタ番号は、そのファイルのデータの記録開始クラスタ番号を意味しています。サイズが0のファイルはクラスタは割り当てられず、ディレクトリエントリに記録される先頭クラスタ番号は、0になります。ある有効なクラスタ番号から対応するデータ領域内のクラスタの位置を得るには、次のように計算します。たとえば、クラスタ番号がNの場合、そのクラスタの先頭セクタの位置は次のようになります。

FirstSectorofCluster = DataStartSector + (N - 2) * BPB_SecPerClus;

ファイルサイズがクラスタサイズより大きい場合は、ファイルデータは複数のクラスタにまたがって記録されます。そのクラスタに対応するFATエントリの値は、次のクラスタ番号を示しているので、それを順に辿っていくことにより、ファイル中の任意のデータセクタにアクセスすることができます。逆に辿ることはできません。このクラスタチェーンの最後のクラスタのFATエントリには、チェーンの終了を示す特別な値(EOCマーク)が入ります。この値はどのクラスタ番号にも一致しない値で、各FATタイプにより次に示す値になります。普通は代表値が使われますが、指定範囲内の値ならどれでも同じです。

もう一つ特別な値として、不良クラスタマークがあります。不良クラスタとは、メディアの不良などによる使用不能セクタを含むクラスタのことです。ボリュームのフォーマット、精査、修復などの際に発見された不良クラスタはFATに記録され、以降そのクラスタがファイルに割り当てられることはありません。不良クラスタマークの値は、FAT12では0xFF7、FAT16では 0xFFF7、FAT32では0x0FFFFFF7になります。

FAT12/16では有効クラスタ番号がこれらの値に一致することはありません。しかし、FAT32ではクラスタ数の上限が定義されていないので、有効クラスタ番号が不良クラスタマークに一致する可能性があります。このようなボリュームはディスクユーティリティ等の混乱を招くので、作成を避けるべきです。このため、FAT32のクラスタ数の上限は、事実上26843545(約256M)クラスタとなります。

一部のシステムでは、インプリメント上の理由からクラスタ数に制限のある場合があります。たとえば、Windows9X/MeではFATサイズの上限が16Mバイトになっているので、FAT32において約4Mクラスタ以上のボリュームが扱えません。

FATエントリの値が0のクラスタは、未使用でどのファイルにも割り当てられていないことを示しています。0以外の値は、そのクラスタが既に使用中か不良で、新たな割り当ては不可能であることを意味します。未使用クラスタの数はどこにも記録されていないので、ボリュームの空きサイズを得るには、全てのFATエントリを調べなければなりません。FAT32ではクラスタ数が膨大になるので、これを補助する手段(FSInfo)が提供されます。

FATの先頭2個のエントリ(FAT[0], FAT[1])は、データクラスタには結びつけられていません。これらの内容は、フォーマット時に次のように初期化されます。

??は、BPB_Mediaと同じ値が入ります。FAT[0]の値は特別な機能を持ちませんが、FAT[1]の特定のビットは、FAT16/32において次のような機能を持ちます。

これらのビットは、ボリュームの不正状態の可能性を示すフラグです。これをサポートするOSは、ボリュームの状態をこれらのフラグに記録し、次回起動時にこれらのフラグをチェックしてディスク修復ツールの自動起動を行ったりします。Windows9X/Meはこれをサポートしますが、NT系Windowsはこれの代わりにBS_Reservedを使って同様な機能を実装しています。

FAT領域に関してさらに2つの重要な点があります。一つは、FATの最終セクタは必ずしも全て使われるわけではないということです。FATはセクタの途中で終わっていることがほとんどです。FATドライバは、未使用部分の値に関して何の前提も持ってはいけません。ボリューム作成時は、FAT領域はこの部分を含めてゼロで初期化されます (FAT[0], FAT[1]を除く)。もう一つは、BPB_FATSz16/32に示されるFATのセクタ数は、FATが実際に必要とするセクタ数より大きい場合もあるということです。つまり、各FATの後に全く使われないセクタが続く場合もあります。これは、データ領域のアライメント調整などの目的で挿入されることがあります。これらのセクタもゼロで初期化されます。

ここまでのまとめとして、各FATタイプにおけるFATエントリの値の範囲とその意味を示します。

FAT12FAT16FAT32解説
0x0000x00000x00000000未使用クラスタ
0x0010x00010x00000001予約
0x002:0xFF60x0002:0xFFF60x00000002:0x0FFFFFF6使用中クラスタ (値は次のクラスタ番号)
0xFF70xFFF70x0FFFFFF7不良クラスタ
0xFF8:0xFFF0xFFF8:0xFFFF0x0FFFFFF8:0x0FFFFFFF使用中クラスタ (チェーンの終端)

FAT32のFSInfo構造体とバックアップブートセクタ

FATのサイズは、FAT12で最大6Kバイト、FAT16で最大128Kバイトでしたが、FAT32では数Mバイト以上に達することもよくあります。こうした理由から、空きクラスタの検索や空きクラスタ数の取得などでFAT全域をアクセスするのをなるべく避けるため、FAT32ボリュームではFSInfo構造体がサポートされます。この構造体はBPB_FSInfoで示されるFSInfoセクタに記録されます。

FAT32 FSInfoセクタ
名前OffsetSize解説
FSI_LeadSig04 0x41615252。このセクタが実際にFSInfoセクタであることを示すシグネチャ1。
FSI_Reserved14480 予約。フォーマット時に0で初期化し、使用してはならない。
FSI_StrucSig4844 0x61417272。このセクタが実際にFSInfoセクタであることを示すシグネチャ2。
FSI_Free_Count4884 空きクラスタ数をセットする。参考値で、必ずしも正しいとは限らない。0xFFFFFFFFの場合は実際に無効であることを示す。使用するときは少なくともそのボリュームのクラスタ数として有効かどうかチェックするべきである。
FSI_Nxt_Free4924 最後に割り当てられたクラスタ番号をセットする。参考値で、必ずしも正しいとは限らない。0xFFFFFFFFの場合は実際に無効であることを示す。この値は、FATドライバが新しいクラスタチェーンを作成するとき、空き検索を開始するクラスタ番号として使用できる。無効なときは多くはFAT[2]から検索する。
FSI_Reserved249612 予約。フォーマット時に0で初期化され、以降使用してはならない。
FSI_TrailSig5084 0xAA550000。このセクタが実際にFSInfoセクタであることを示すシグネチャ3。
512 512バイトを越えるセクタサイズの場合、残りは0で埋める。

これ以外のFAT32の機能として、バックアップブートレコードというものがあります。これは、FAT12/16ボリュームでは1個だけだったブートセクタに冗長性を持たせるための機能です。これにより、何らかの理由でブートセクタが破損した場合の回復の可能性を高めることができます。このセクタの位置は、BPB_BkBootSecに記録されています。この値は6が強く推奨されているので、ブートローダやFATドライバはブートセクタの読み込みに失敗したとき、セクタ#6からブートレコードの読み込みを試みるようにコーディングされています。

FAT32ボリュームのブートセクタは、実際には3つの連続したセクタがブートセクタとされているので、ブートセクタから連続した3セクタが保存領域にコピーされることになります。

ディレクトリ構造体

まずはFATファイルシステム当初からの基本機能である短いファイル名について解説します。後の拡張で長いファイル名も使えるようになりましたが、これについてはこの後で解説します。FATボリュームではディレクトリの実体は、静的に確保された領域または特殊なアトリビュートを持ったファイルとして存在し、内容はディレクトリエントリ構造体を格納するテーブルです。ディレクトリエントリのサイズは32バイトで、テーブル長の最大値は65536エントリ(2Mバイト)です。

ボリューム中唯一の特殊なディレクトリで必ず存在しなければならないのがルートディレクトリで、そのボリュームの最上位のノードとなります。ルートディレクトリは、FAT12/16ボリュームではFAT領域直後のディレクトリ領域として静的に配置され、テーブル長はBPB_RootEntCntに示される固定長となります。FAT32ボリュームではルートディレクトリもサブディレクトリ(ルートディレクトリ下のすべてのディレクトリ)と同様にデータ領域に配置されます。サブディレクトリと違う点は、名前など(それを指すディレクトリエントリ)を持たないということで、ルートディレクトリの開始クラスタ番号はBPB_RootClusで指定されます。このほかにルートディレクトリがサブディレクトリと違う点は、サブディレクトリには必ずあるドットエントリ(".", "..")を持たないことです。

次の表にFATボリュームのディレクトリエントリ構造体のフィールドを示します。

ディレクトリエントリ構造体
名前OffsetSize解説
DIR_Name011 短いファイル名のボディと拡張子。
DIR_Attr111 ファイルアトリビュート。次のフラグのコンビネーションで表現される。上位2ビットは未使用で0でなければならない。
0x01: ATTR_READ_ONLY (書き込み禁止)
0x02: ATTR_HIDDEN (隠し)
0x04: ATTR_SYSTEM (システム)
0x08: ATTR_VOLUME_ID (ボリュームラベル)
0x10: ATTR_DIRECTORY (ディレクトリ)
0x20: ATTR_ARCHIVE (アーカイブ)
0x0F: ATTR_LONG_FILE_NAME (LFNエントリ)
DIR_NTRes121 短いファイル名の小文字情報を記録するフラグ(オプション)。
0x08: ボディがすべて小文字。
0x10: 拡張子がすべて小文字。
DIR_CrtTimeTenth131 DIR_CrtTimeのサブセコンド情報。DIR_CrtTimeの分解能は2秒であるが、それをさらに200分割した値(0~199)が入る。サポートしない場合は、作成時に0を設定し、以降変更しない。
DIR_CrtTime142 このファイルが作成された時刻(オプション)。サポートしない場合は、作成時に0を設定し、以降変更しない。
DIR_CrtDate162 このファイルが作成された日付(オプション)。サポートしない場合は、作成時に0を設定し、以降変更しない。
DIR_LstAccDate182 このファイルを最後にオープンした日付(オプション)。時刻情報は無い。書き込みアクセスの場合はDIR_WrtDateと同じ値を設定すべきである。サポートしない場合は、作成時に0を設定し、以降変更しない。
DIR_FstClusHI202 このファイルのデータの先頭クラスタ番号の上位16ビット。FAT12/16では常に0。詳細はDIR_FstClusLOを参照。
DIR_WrtTime222 このファイルのデータが最後に変更された(変更後クローズされた)時刻。サポート必須。
DIR_WrtDate242 このファイルが最後に変更された日付。サポート必須。
DIR_FstClusLO262 このファイルのデータの先頭クラスタ番号の下位16ビット。ファイルサイズが0のときはクラスタは割り当てられず、常に0。ディレクトリの場合は常に1個以上のクラスタが割り当てられ、有効値が入る。
DIR_FileSize284 ファイルの場合、バイト単位のサイズ(0~0xFFFFFFFF)。ディレクトリの場合は使用されず、常に0。

DIR_Nameフィールドの先頭バイトDIR_Name[0]は、そのエントリの状態を示す重要なデータです。この値が0xE5または0x00の場合は、そのエントリは空で新たに使用可能です。それ以外の値の場合は、そのエントリは使用中です。値が0x00のときはテーブルの終端を示し、それ以降のエントリも全て0x00であることが保証されるので、無駄な検索を省くことができます。ファイル名の先頭バイトが0xE5になる場合は、代わりに0x05をセットします。

DIR_Nameフィールドは11バイトの文字列で、本体と拡張子の8+3バイトの文字列として格納されます。その際、本体と拡張子を区切るドットは除去されます。本体8バイト、拡張子3バイトに満たない場合は、それぞれ残りの部分にスペース(0x20)が詰められます。文字コードには、ローカルなコードセット(日本語の場合はCP932 Shift_JIS)が使われます。次に実際の例を示します。

ファイル名         DIR_Name          解説
"FILENAME.TXT"    "FILENAMETXT"     ドットは除去される
"DOG.AVI"         "DOG     AVI"     8+3バイトに満たないときはスペースでパディング
"File.Txt"        "FILE    TXT"     ASCII小文字は全て大文字に置き換えられる
"file.TXT"        "FILE    TXT"     ↑に同じ
"蜃気楼.JPG"      "・気楼  JPG"      "蜃"の第一バイトが0xE5なので0x05に置き換え
"NO_EXT"          "NO_EXT     "     拡張子無し
"NO_EXT."         "NO_EXT     "     ↑に同じ
".cnf"                              ※本体無しは不可
"new file.txt"                      ※スペースは使用不可
"file[1].c++"                       ※[]や+はダメ文字
"longext.jpeg"                      ※8+3形式に入らないのはダメ
"arch.tar.gz"                       ※8+3形式に入らないのはダメ

ファイル名に使用可能な文字は、ASCII文字の
0~9 A~Z ! # $ % & ' ( ) - @ ^ _ ` { } ~
と、拡張文字(0x80~0xFF)です。ASCII小文字 a~z は大文字に置き換られた上で記録・検索されます。拡張文字の大文字変換についてはそのシステムのコードページ毎に扱いが異なり、Ää→ÄÄ (CP852)だったり、Ää→AA (CP850)だったりします。このように拡張文字の扱いはコードページ間で異なるので、異なるコードページ間での互換性がありません。日本語環境での拡張文字の扱いについては、この先の「システム間の互換性」を参照してください。

ある一つのディレクトリの中では、全てのファイル名がユニークです。そのディレクトリの中にはDIR_Nameに同じ文字列を含むエントリは2つ以上存在することはありません。DIR_Attrフィールドは、次の表に示すようにそのファイルの属性を示します。

ファイル属性
フラグ解説
ATTR_READ_ONLY書き込み禁止。このファイルへの書き込みや消去は、拒否されるべきである。
ATTR_HIDDEN通常のディレクトリ表示でこのファイルを表示しない。(扱いはシステム依存)
ATTR_SYSTEMシステム関連の重要ファイルであることを示す。(扱いはシステム依存)
ATTR_DIRECTORYこのファイルはサブディレクトリのコンテナである。以下で解説。
ATTR_ARCHIVEファイルへの何らかの変更が発生したとき、FATドライバによってセットされる。主にバックアップツールが変更されたファイルを見つけるために使用される。
ATTR_VOLUME_IDこの属性を持つエントリは、ルートディレクトリに1個だけ存在できる。このエントリの持つ名前は、そのボリュームのラベル名である。DIR_FstClusHIDIR_FstClusLOおよびDIR_FileSizeは常に0でなければならない。このフラグは常に単独使用であるが、一部のシステムではATTR_ARCHIVEを立てることがある。
ATTR_LONG_NAMEこのフラグの組み合わせは、そのエントリが長いファイル名の一部であることを示す。このフラグは常に単独使用。これについては後ろのセクションで解説。

ディレクトリの操作

ファイルの作成

ファイルを作成するときは、ディレクトリテーブルの空きを探してその名前のエントリを作成します。テーブルがクラスタの最後まで埋まっているときは、新たなクラスタを割り当ててチェーンを延ばします。ただし、エントリ数は65536を越えることはできません。静的ディレクトリテーブル(FAT12/16のルートディレクトリ)の場合は、テーブルが満杯になったらそれ以上のエントリは作成できません。エントリのDIR_AttrフィールドにはATTR_ARCHIVEフラグを設定し、DIR_FstClusHI, DIR_FstClusLO, DIR_FileSizeは0が初期値となります。ファイルにデータが書き込まれ、ファイルサイズが0→1以上に変化するとき新しいクラスタが割り当てられ、DIR_FstClusHI, DIR_FstClusLOにそのクラスタ番号がセットされます。以降、ファイルサイズが増大してクラスタがあふれるたびに新しいクラスタが割り当てられ、クラスタチェーンが延びていきます。

ディレクトリの作成

サブディレクトリを作成するときは、その名前のエントリを作成し、DIR_AttrフィールドにはATTR_DIRECTORYフラグを設定します。ディレクトリとして特別なことは、その内容がディレクトリエントリのテーブルであることだけで、それ以外はファイルに似ています。ディレクトリはサイズ情報を持たずDIR_FileSizeには常に0を設定しなければなりません。ディレクトリには最初にクラスタを1個割り当て、先頭クラスタ番号のフィールドDIR_FstClusHI, DIR_FstClusLOには、割り当てたクラスタ番号をセットします。割り当てられたクラスタは、全体を0で初期化(全て未使用状態)します。

サブディレクトリのテーブル先頭の2エントリには、必ずドットエントリ(".", "..")を作成します。ドットエントリは、ルートディレクトリには存在せず、サブディレクトリにだけ存在します。DIR[0]DIR_Nameには、".       "を、DIR[1]には"..      "をセットします。これらはATTR_DIRECTORY属性を持つエントリですが、それ自身はテーブルは持たずに、他のエントリが持つテーブルを指すだけのエントリになっています。"."エントリの先頭クラスタ番号のフィールドには、それが置かれているディレクトリ自身(つまり今作成した)の開始クラスタ番号を指します。".."エントリは、親ディレクトリ(このディレクトリのエントリが置かれたディレクトリ)の開始クラスタ番号を指します。親ディレクトリがルートディレクトリの場合は、0をセットします(たとえFAT32の場合でも)。

ファイルの削除

ファイルを削除するときはDIR_Name[0]0xE5をセットしてディレクトリエントリを解放します。ファイルがクラスタチェーンを持っている場合、チェーンを辿って全て解放(FATエントリに0をセット)します。

ディレクトリの削除

サブディレクトリの削除はファイルと同じですが、テーブルが空(ドットエントリ以外に何も無い状態)でないときは、削除することができません。もしも、空でないディレクトリを単に削除した場合、そのディレクトリ下にあるファイルの占めるクラスタが全てロストクラスタになってしまいます。ディレクトリを削除するときは、そのノード以下全てのオブジェクトを削除しなければなりません。

ボリュームラベル

FATボリュームにはボリュームラベルという名前を付けることができます。ボリュームラベルは、属性フィールドにATTR_VOLUME_ID(ATTR_ARCHIVEの有無は任意)を持つ1個のディレクトリエントリとしてルートディレクトリに記録されます。ボリュームラベルは単にボリュームに付けられる名前であり、ファイルではありません。他のディレクトリエントリとは独立した名前空間を持ち、他のエントリと重複する名前を持つこともできます。ボリュームラベルに使用可能な文字はファイルのSFNエントリとほぼ同じで、最大11バイトの単純な文字列として扱われます。また、最後尾を除く任意の位置にスペースを含むことができ、ドットは使えません

ボリュームラベルに対してはLFN拡張機能は適用されません。MS-DOSではボリュームラベルを設定する際、ルートディレクトリの項目とブートセクタのフィールドBS_VolLabを同期しますが、Windowsはそれをしません。また、Windowsでは0xE5で始まるボリュームラベルの扱いに問題(0x05への置き換えをせず、書き込みが無かったことになってしまう)があります。このため、そのようなボリュームラベルは使用するべきではありません。

日付・時刻フィールドのフォーマット

ディレクトリエントリには、時間に関するフィールドがいくつかあります。多くのFATドライバはサポート必須フィールドであるDIR_WrtTime, DIR_WrtDateのみサポートし、ほかのオプションフィールドはサポートしません。サポートしないフィールドはエントリ作成時に0で初期化し、以降変更するべきではありません。日付と時刻のフィールドは次のようなフォーマットになっています。時刻は全てローカル時間で記録され、UTCはサポートされません。

フィールドビット割り当て
DIR_WrtDate
DIR_CrtDate
DIR_LstAccDate
Bit 15~9: 1980年から起算した年(0~127)。1980~2107年。
Bit 8~5: 月 (1~12)。
Bit 4~0: 日 (1~31)。
DIR_WrtTime
DIR_CrtTime
Bit 15~11: 時 (0~23)。
Bit 10~5: 分 (0~59)。
Bit 4~0: 秒を2で割った値 (0~29)。0~58秒。

長いファイル名

FATファイルシステムにLFN(長いファイル名)を付加するには、既存のシステムでの下位互換性を保ったまま拡張する必要があります。具体的には次に示すようなことが要求されます。

これらの条件を満たすため、LFNは特殊なアトリビュートを持ったディレクトリエントリとして定義されています。前のセクションで既に説明していますが、LFNを構成するエントリとしてのアトリビュート(ATTR_LONG_NAME)は、既存のアトリビュートの組み合わせ(ATTR_READ_ONLY | ATTR_HIDDEN | ATTR_SYSTEM | ATTR_VOLUME_ID)として表現されます。また、あるエントリが LFNの一部であるかどうかを調べるためのマスク値(ATTR_LONG_NAME_MASK)は、ATTR_READ_ONLY | ATTR_HIDDEN | ATTR_SYSTEM | ATTR_VOLUME_ID | ATTR_DIRECTORY | ATTR_ARCHIVEとして定義されています。この値でDIR_AttrをANDマスクし、ATTR_LONG_NAMEに一致したら、そのエントリは LFNエントリの一部です。このディレクトリエントリを見つけたときは、次に示すようにSFNエントリとは異なるフィールドとして解釈します。

LFNを構成するディレクトリエントリ構造体
名前OffsetSize解説
LDIR_Ord01 このエントリがLFNエントリ(1個のLFNを構成するエントリ群)のどの部分かを示すシーケンス番号(1~20)。1がLFNの先頭部を意味する。LAST_LONG_ENTRYフラグ(0x40)が立っているときは、LFNエントリの開始であることを示す。
LDIR_Name1110 名前。1文字目~5文字目。Unicode(UTF-16LE)で格納される。
LDIR_Attr111 LFNアトリビュート。このエントリがLFNエントリの一部であることを示すため、ATTR_LONG_NAMEでなければならない。
LDIR_Type121 LFNのタイプ。常に0でなければならず、0以外は予約。
LDIR_Chksum131 このLFNエントリと結びつけられているSFNエントリのチェックサム。
LDIR_Name21412 名前。6文字目~11文字目。
LDIR_FstClusLO262 古いディスクユーティリティによる危険の可能性を避けるため、0がセットされる。
LDIR_Name3284 名前。12文字目~13文字目。

LFNエントリは対応するSFNエントリに付随するもので、LFNエントリ単独で存在することはありません。それぞれのファイルはSFNのみ、またはSFNとLFNの2つの名前を持つということです。これは、MS-DOSなどのLFN非対応システムでの下位互換性を保つためです。LFNがある場合はそれがファイルの主たる名前となります。LFN非対応システムはLFNエントリを認識できませんが、SFNでそのファイルをアクセスできます。LFN対応システムの場合、LFNとSFNのどちらでもファイルアクセスできます。LFNエントリはLFNの文字列情報だけしか持ちません。もしもSFNエントリとの関連づけの切れたLFNエントリが存在した場合、もはやそのLFNエントリは無効です。次の図にLFNエントリ(3個のエントリにまたがる)とSFNエントリのセットがディレクトリテーブルに格納される様子を示します。

"MultiMediaCard System Summary.pdf" の格納
格納位置先頭バイト名前フィールドアトリビュート内容
     
DIR[N-3]0x43ary.pdf--VSHRLFN 3/3(27~33文字目)
DIR[N-2]0x02d System Summ--VSHRLFN 2/3(14~26文字目)
DIR[N-1]0x01MultiMediaCar--VSHRLFN 1/3(1~13文字目)
DIR[N]'M'MULTIM~1PDFA-----SFNエントリ
     

LFNは、それを構成するエントリ1個にUnicodeで最大13文字まで格納され、それを越える文字数のLFNでは複数のエントリを占有します。LFNの最大文字数(正確にはUTF-16のエンコーディングユニット数)は255なので、最大20エントリを占有することになります。たとえば、例に示す33文字のLFNエントリの場合は、LDIR_Ord0x01, 0x02, 0x43を持つ3つのディレクトリエントリで構成されます。文字数が13の倍数に一致しない場合、名前はヌル文字(U+0000)で終端され、後ろの空きはU+FFFFでパディングします。SFNエントリの名前は、与えられたファイル名から自動生成されます(アルゴリズムは後述)。

LFNエントリは関連づけられたSFNエントリの直前に降順で配置されます。LDIR_Ordの値は1から連続していなければならず、これらのグループは連続したエントリに配置されます。これらの条件が一つでも満たされない場合、そのLFNエントリは無効になります。

さらにチェックサムを使ってLFNエントリとSFNエントリの関連性が確認されます。LFNエントリを構成するエントリには、関連づけられているSFNエントリのチェックサムがそれぞれのLDIR_Chksumに記録されます。チェックサムの値は、次のアルゴリズムでSFNエントリのDIR_Nameフィールドから生成されます。

uint8_t create_sum (const DIR* entry)
{
    uint8_t i, sum;

    for (i = sum = 0; i < 11; i++) {
        sum = (sum >> 1) + (sum << 7) + entry->DIR_Name[i];
    }
    return sum;
}

チェックサムが一つでも一致しない場合は、そのLFNエントリは無効になります。これはLFN非対応システムがディレクトリテーブルを操作(削除・作成・名前の変更など)した結果、不正な関連づけになるのを防ぐためです。しかし、無効になったLFNエントリはゴミになってディレクトリを占有し続けるので、そのような状態が繰り返し発生するとボリュームの利用効率がどんどん悪化していきます。特にテーブル長の限られるFAT12/16のルートディレクトリでは切実な問題です。このため、LFNを持つファイルに対してはLFN非対応システムで削除や名前変更を行わないことが推奨されます。無効になったLFNエントリはディスクユーティリティ等で除去されます。

名前の範囲とコードページ

短いファイル名

短いファイル名では、8バイトまでのボディとオプションのドット(.)+3バイトまでの拡張子に制限される、いわゆる8.3形式のファイル名が使用できます。パス名のトータル長は、3バイトのドライブ文字 + 64バイトのパス + 12バイトのファイル名の合計79バイトとなります。ファイル名を構成する文字は、ASCII英数と一部のASCII記号$%'-_@~`!(){}^#&および拡張文字(非ASCII文字)の任意の組み合わせが可能です。

短いファイル名は、SFNエントリにそのシステムのOEMコードページ(MS-DOSや古いWindowsで使用される)で格納されます。SFNエントリではASCII小文字が使用禁止なので、ファイル名に含まれる小文字は、必ず大文字に変換されてSFNエントリに格納されます。このため、SFNでは大文字小文字情報は失われます。

長いファイル名

LFNでは、255文字までのファイル名が使用可能です。使用可能な文字は、SFNで使用可能な文字全てに+,;=[]が加わっています。ドットはファイル名の中にいくつでも含めることができ、スペースもファイル名の任意の位置に挿入することができます。ただし、ファイル名の末尾に来るドットやスペースは無効で、API入力の段階で除去・無視されます。ファイル名の先頭に来るドットやスペースは有効ですが、UIによっては(たとえばWindowsコモンダイアログ)これらのファイル名を入力できません。LFNエントリには、大文字変換は行わずに入力のままの形のUnicode(UTF-16)文字列として格納されます。

このようにSFNエントリとLFNエントリでそれぞれ異なる文字コード(ANSI/OEMとUnicode)が使われるため、一般的な実装ではそれらのコードの相互変換が必要になります。コードページがSBCSの場合は特に問題にはなりませんが、DBCSの場合は変換テーブルのサイズが数十~数百Kバイトに膨れあがります。これはメモリの乏しい小規模な組み込みシステムでは重大な問題といえます。

LFNとSFNのマッチング

あるディレクトリ中のファイル名は、SFNエントリ/LFNエントリに関わらず異なるファイルで同じものは二つとありません。LFNエントリには小文字はそのまま保持されますが、マッチングの際には文字の大小は無視されます。したがって、"LongFileName.txt", "longfilename.txt", "LONGFILENAME.TXT"は互いに一致と見なされます。このように、LFN/SFNに関わらずファイル検索動作において文字の大小は無視されます。

入力ファイル名をディレクトリから検索するとき、そのファイル名が8.3形式に適合しているときは、LFNとSFNの両方のエントリに対してマッチングが行われます。8.3形式に適合していないときは、LFNエントリについてのみマッチングが行われます。

ディレクトリからファイル名のリストを出力するときは、特別な指定がない限りLFNを返します。LFNが無い場合や文字コードをOEMコードに変換できない(APIレベルでOEMコードのときに発生しうる)場合は、代わりにSFNを返します。

SFNの生成方法

LFN対応システムでは、ファイルシステムAPIに渡されたファイル名は常にLFNとして扱うこととします。ファイル作成時、SFNは常に自動生成される必要があり、LFNとSFNについてそれぞれ個別に指定することを許可するべきではありません。実際のところ名前の衝突さえなければどんなSFNでもよいのですが、SFNの統一性を保ち各システムのユーザやアプリケーション間での名前に関わる混乱を防ぐため、SFNの形式には一定のルールが設けられています。自動生成されるSFNは、次に示すようにボディ(+添字)(+.拡張子)で構成されます。

  1. 小文字は拡張文字を含めて大文字に変換する。
  2. スペースがあるときは全て取り除き、不可逆変換フラグを立てる。
  3. 先頭にドットがあるときは全て取り除き、不可逆変換フラグを立てる。
  4. ドットが2個以上あるときは最後を残して取り除き、不可逆変換フラグを立てる。
  5. SFNで使用できないASCII文字はアンダースコア(_)に置き換え、不可逆変換フラグを立てる。
  6. 入力がUnicodeの場合は、OEMコードに変換する。OEMコードに無い文字は取り除き、不可逆変換フラグを立てる。ボディが0バイトになった場合は代わりに16進4桁の乱数を挿入し、拡張子(あれば)が0バイトになった場合は残ったドットを取り除く。
  7. ボディと拡張子(あれば)の長さをそれぞれ8バイトと3バイトに切り詰める。切り詰めた場合は、不可逆変換フラグを立てる。

これでボディ(+.拡張子)のフォーマットでSFNが完成します。不可逆変換フラグが立っている(つまり与えられた名前が8.3形式に適合しない)場合は、続いてボディに添字を追加することで8.3形式でないLFNを持つことを暗示するSFNとします。添字のフォーマットは、1文字のチルダと1~6桁の十進数の組み合わせ(~n)です。添字でボディが8バイトを越えてしまう場合は、ボディを切り詰めて8バイトに収まるようにします。このようにして生成された名前がディレクトリ内の既存の名前と衝突する場合は、衝突しない名前を探します。そのアルゴリズムは実装依存で、98系Windowsでは~1から順に際限なく調べ、NT系Windowsでは~5で諦めて16進4桁の乱数で新たにボディを作成しています。このように、生成されるSFNは既存のファイル名やロケールの設定に影響されるので、ある一つの名前から生成されるSFNは一意的ではありません。例として、ある空のディレクトリに順にファイルを作成したときに生成されるSFNを次のテーブルに示します。

SFNの生成例
LFN(入力)SFN
File.txtFILE.TXT
foo.tar.gzFOOTAR~1.GZ
.confCONF~1
a+b=cA_B_C~1
💩.png3F04~1.PNG
Asakura Otome.jpegASAKUR~1.JPE
Asakura Yume.jpegASAKUR~2.JPE
  

LFNエントリの抑止

名前が8.3形式に適合する場合は、LFNとSFNは必ず同じ名前(大文字に変換されることを除き)になります。さらに、名前が次の条件を満たす場合は、LFNエントリは生成されません。

NT系Windowsではこれに加え次の条件のときにもLFNエントリが抑止されます。

この場合、小文字情報がSFNエントリのDIR_NTResフィールドに書き込まれ、LFNエントリは抑止されます。この条件を満たすファイル名は、"lower12.dll", "system32", "FDCMD.exe"などごく一般的なもので、NT系Windowsではシステムファイルとしても大量に存在する形式の名前です。これにより、ディレクトリテーブルの肥大化を抑えています。DIR_NTResフィールドをサポートしないLFNシステム(例えばWindows9X)ではこれらは単にLFNを持たないファイルとして認識され、全て大文字として見えることになります。FATファイルシステムでは名前のマッチングにおいて大文字小文字の区別はないので、小文字が大文字に化けてもシステム上は問題にはなりませんが、一部のUnix系アプリケーションは問題を起こすことがあります。

システム間の互換性

LFN非対応システム

LFNのサポートはハードディスクにおいて最も必要とされますが、リムーバブルディスクにおいてもまたサポートされます。リムーバブルディスクは複数のシステム間で使用されるため、下位互換性は特に重要なことです。LFN機能はLFN非対応システムでの互換性を損なうことなく実装されています。たとえば、LFNの使われているボリュームは、LFN非対応システムでアクセスすることができます。また、LFNは既存のボリュームに何の前処理もすることなく使用することができます。LFNエントリは、ディレクトリエントリを作成するとき(新規作成や名前の変更)に初めて作成されます。LFN名を持たないファイルにLFN名を付けるときは、エントリの配置の関係上SFNエントリは移動されます。LFNエントリは非対応システムに対しては隠しファイル以上に隠蔽されていて、通常の使い方では問題を起こすことはありません。

LFN非対応システムではLFNエントリの存在には一切関知しないので、ディレクトリ操作によってはLFNエントリが破壊されることがあります。この場合もファイルデータが失われることはありません。たとえば、LFNを持つファイルの名前が変更された場合、サムの不一致となりLFNエントリとSFNエントリの関連が解かれてそのファイルのLFNが失われます。また、ボリュームラベルを削除・変更した場合、どこかのLFNエントリが破壊され、いずれかのファイルのLFNが失われる可能性があります。この場合もそのファイルのデータへのダメージはありません。

日本語の大文字変換

SFNエントリの大文字変換規則は拡張文字に対しても適用されますが、日本語MS-DOSでは拡張文字(全角英字・ギリシャ文字・キリル文字)の小文字は変換されずに記録されていました。NT系Windowsからはこれらの拡張文字に対しても大文字変換されるようになっています。しかし、これにより深刻な互換性問題が起きています。たとえば、日本語MS-DOSで"Fast.TXT"というファイルを作成し、そのボリュームをWindows XPでマウントした場合、そのファイルを開くことができません。NT系Windowsでは、そのファイル名に対して"FAST.TXT"でSFNエントリを探すため、マッチせず見つけられないからです。対策としては、このようなファイル名の使用を避けるしかありません。9X系WindosもSFN拡張文字の大文字変換をしませんが、LFNエントリも作成されるのでNT系Windowsからアクセス可能です。

異なるOEMコードページ

いくつかのOEMコードページは、DBCSが採用されます。日本語環境の場合はShift_JISを使用することになっています。DBCSでは2バイト文字の2バイト目のコードが使用禁止文字と一致することがあります。これは、SBCSシステムでDBCSファイル名を扱おうとしたとき問題が発生します。同様に大文字変換の関係上、SBCS拡張文字を含む名前のファイルは異なるコードページのシステムで正常に開くことができません。異なるOEMコードページのシステム間で相互利用する場合は、ファイル名に拡張文字の使用を避ける必要があります。

LFNについてはUnicodeで記録されるため、コードページの問題はありません。しかし、ファイルAPI上の扱いがOEMコードの場合はLFN不完全対応となり、同様の問題が発生します。

Mac OS X

Mac OS Xでは、ドットやスペースで終わるファイル名が使用可能です。Mac OS XもFATボリュームへのアクセスが可能ですが、FATファイルシステムはこのようなファイル名を許可していません(普通はAPIレベルでそれらの文字は無視される)。そこで、FATボリュームに作成するファイル名がこれらの文字で終わる場合、Mac OS Xはそれらを特殊文字に置換(スペース→U+F028、ドット→U+F029)した名前で作成します。このため、Mac OS XとFATボリューム経由でファイル交換する場合は、この点を考慮する必要があります。当然ですが、これらのファイルをその名前で開くには、LFN機能にフル対応(つまりAPIレベルでUnicodeに対応)していなければなりません。もちろん、対応していなくてもSFNでなら開くことも可能です。

ストレージ(物理ドライブ)のパーテーショニング

物理ドライブのパーテーショニングに関してはFATファイルシステムのスコープを外れますが、組み込みシステムへの実装の際には知っている必要があるので、ここで簡単に解説しておきます。

ハードディスクは、いくつかのパーテーション(区画)に分割して使用されることがよくあります。たとえば、8Gバイトのハードディスクを3つの区画に分けて、4G、2G、2Gバイトの3つのボリュームを作成するなどといった使い方です。多くの場合は区画は1個だけ設けられ、1台の物理ドライブに1個のボリュームで使用されます。

パーテーショニングのルールには、MBR形式とSFD(Super Floppy Disk)形式の2つの種類があります。前者は物理ドライブの先頭物理セクタ(MBR: Master Boot Record)に区画テーブルが置かれ、そのドライブがどのように分割されているかが記録されます。後者は名前が示すようにフロッピディスクと同じで、単にパーテーショニングされていない形式です。ボリュームは物理ドライブの先頭物理セクタから始まり、ドライブ全体を占有します。

MBR形式はハードディスクで常に使用されるほか、多くのリムーバブルメディア(SDカード・CFカード・USBメモリ)でも標準的に使用されます。SFD形式は、光磁気ディスクや各種スーパーフロッピーメディアで使用されます。これら二つのフォーマットはどのシステムでもサポートされるわけではありません。例えば、Windows OSの場合、リムーバブルドライブではMBR形式の第2区画以降をサポートせず、ハードディスクではSFD形式をサポートしません(前者はWindows 10 1703で対応)。

次の表にMBRのフィールドを示します。

MBR(物理セクタ0)のフィールド
名前OffsetSize解説
MBR_bootcode0446 起動コード。システム依存。使用しないときは0で埋められる。
MBR_Partation144616 区画テーブルエントリ1。区画のステータスと範囲を示す。フィールドについては後述。
MBR_Partation246216 区画テーブルエントリ2。
MBR_Partation347816 区画テーブルエントリ3。
MBR_Partation449416 区画テーブルエントリ4。
MBR_Sig5102 0xAA55。MBRが有効であることを示す。この値が確認できないときは、MBRは無効である。

区画テーブルエントリのフィールドは次のようになっていて、このエントリがMBRに最大4個格納できます。これは、一つのドライブを最大4分割できるということです。拡張区画といってその中をさらに多くの区画に分けることもできますが、ここでは割愛します。

区画テーブルエントリのフィールド
名前OffsetSize解説
PT_BootID01 ブート標識。
0x00:ブート不可
0x80:ブート可
ブート可というのはこの区画から起動できることを示すが、実際にはシステム依存。ブート可に設定できるのは、いずれか一つの区画だけである。
PT_StartHd11 CHS形式で区画の開始物理セクタを示すヘッド番号(0~254)。
PT_StartCySc22 CHS形式で区画の開始物理セクタを示すシリンダ番号(下位10ビット:0~1023)とトラック内セクタ番号(上位6ビット:1~63)。
PT_System41 この区画の種類(代表的なもの)。
0x00: 無し(空きエントリ。他のフィールドも全てゼロでなければならない)
0x01: FAT12 (CHS/LBA, 65536セクタ未満)
0x04: FAT16 (CHS/LBA, 65536セクタ未満)
0x05: 拡張区画 (CHS/LBA)
0x06: FAT12/16 (CHS/LBA, 65536セクタ以上)
0x07: HPFS/NTFS/exFAT (CHS/LBA)
0x0B: FAT32 (CHS/LBA)
0x0C: FAT32 (LBA)
0x0E: FAT12/16 (LBA)
0x0F: 拡張区画 (LBA)
PT_EndHd11 CHS形式で区画の終了物理セクタを示すヘッド番号(0~254)。
PT_EndCySc22 CHS形式で区画の終了物理セクタを示すシリンダ番号(下位10ビット:0~1023)とトラック内セクタ番号(上位6ビット:1~63)。
PT_LbaOfs84 LBA形式で区画の開始物理セクタ番号を示す(1~0xFFFFFFFF)。
PT_LbaSize124 LBA形式で区画のサイズを示す(1~0xFFFFFFFF)。

区画テーブルは、常に全てのエントリが使われるわけではありません。通常は、区画が1個だけ作成され、ほかのエントリは空になっています。それぞれのエントリは、その物理ドライブ上の互いに重複しない任意の区画を指しています。そして、それぞれの区画の先頭セクタがボリュームの開始セクタになります。

区画の範囲の表現には、CHS形式とLBA形式の2通りがあり、それぞれで記録されています。CHS形式はドライブがジオメトリを持つときに使用されますが、実際はシステム依存です。LBAでのアクセスが可能なドライブの場合は、LBA形式のフィールド(区画先頭物理セクタ番号とセクタ数)を使用します。区画がCHSで表現可能な範囲を超えた部分(8Gバイト以降)にかかる場合、CHSフィールドは無効です。