SDカードドライバの設計(3)

FATの構造(2)

前回に続き、FATの構造を理解します。

BIOS Parameter Block

前回説明したMaster Boot RecordでfirstSectorの位置にはBIOS Parameter Blockがあります。その構造体は、FAT12/FAT16の場合、以下のようになっています。なおFAT32だと、定義が異なります。

struct BIOSParameterBlock {
	BYTE jmpOpCode[3];
	BYTE OEMName[8];
	WORD bytesPerSector;	/* bytes/sector (512) */
	BYTE sectorsPerCluster;	/* sectors/cluster */
	WORD reservedSectors;	/* reserved sector for BPB */
	BYTE numberOfFATs;	/* the number of file allocation tables */
	WORD rootEntries;	/* the number of root entries (512) */
	WORD totalSectors;	/* the number of secters for this partition */
	BYTE mediaDescriptor;	/* 0xf8: Hard Disk */
	WORD sectorsPerFAT;	/* number of sectors for FAT */
	WORD sectorsPerTrack;	/* sector/track (not used) */
	WORD heads;		/* heads number (not used) */
	DWORD hiddenSectors;	/* hidden sector number */
	DWORD bigTotalSectors;	/* total sector number */
	BYTE driveNumber;
	BYTE unused;
	BYTE extBootSignature;
	DWORD serialNumber;
	BYTE volumeLabel[11];
	BYTE fileSystemType[8];	/* "FAT1?   " */
	BYTE loadProgramCode[448];
	WORD sig;		/* 0x55, 0xaa */
};

ここで重要なのは、以下の情報です。

bytesPerSector

セクタあたりのバイト数。SDカードなら通常512です。フロッピーや昔のハードディスクで256の場合があります。SD-OSではここが512でない場合は異常と判断しています。以降、この値は512として話を進めます。

reservedSectors

BPBに割り当てられているセクタ数。BPBのセクタ位置にこの数を足すと、FATのセクタ位置が求まります。

sectorsPerFAT

1つのFATに割り当てられているセクタ数。

numberOfFATs

FATの数。通常FATは2組あるので、この数値は2です。FATのセクタ位置にsectorsPerFAT * numberOfFATsを足すと、ルートディレクトリエントリのセクタ位置が求まります。

sectorsPerCluster

1クラスタに含まれるセクタ数。FATではデータの格納位置をクラスタ番号で管理するので、この数は重要です。

totalSectors, bigTotalSectors

総セクタ数。totalSectorsは16ビットしかないので、約32MBまでしか表せません。これが昔あった「32MBの壁」ですね。これより大きな場 合はtotalSectorsは0で、bigTotalSectorsに総セクタ数が入っています。この数値は32ビットなので、2TBまで表せます。あ くまでセクタ数の話で、他にクラスタ番号の制約があるので話はそんなに簡単ではないですが。

rootEntries

ルートディレクトリのエントリ数。すなわちルートディレクトリに入れることのできるファイル・ディレクトリの数です。FAT16まではこの数値は512固定となります。

FAT

BIOS Parameter Blockの次にはFATがあります。ここには、「あるクラスタの次に格納されているデータ位置を示すクラスタ番号」が入っています。そのクラスタ番号は 12/16/32ビットで表され、これがFAT12/FAT16/FAT32と言われる所以です。

FAT12だとクラスタ番号は0x000~0xfff=4096個あり、各エントリは12ビットなので、FATの最大サイズは

4,096(エントリ) * 12(bit) / 8(bits/byte) = 6,144バイト = 6kB = 12セクタ

となります。FAT16なら、

65,536(エントリ) * 16(bit) / 8(bits/byte) = 131,072バイト = 128kB = 256セクタ

あ ります。実際のサイズはBIOS Parameter BlockのsectorsPerFAT * bytesPerSectorで求めます。FATが占めるディスク領域は、やはりBIOS Parameter BlockのsectorsPerFAT * numberOfFATsで求められます。

各エントリは以下のような意味を持っています(FAT12/FAT16)。

  • 0x000 / 0x0000
    未使用
  • 0x001 / 0x0001
    予約
  • 0x002 - 0xff6 / 0x0002 - 0xfff6
    現在のクラスタの次のデータが格納されているクラスタ番号
  • 0xff7 / 0xfff7
    不良クラスタ
  • 0xff8 - 0xfff / 0xfff8 - 0xffff
    現在のクラスタでデータが最終することを示す

与えられたクラスタ番号に相当するFATエントリを見つけるには、

FAT開始位置 + クラスタ番号 * クラスタエントリの大きさ

となります。16ビットFATなら、2バイトごとにリトルエンディアンで格納されているので話は簡単です。

00000000: F8 FF FF FF 12 34 56 78 ...
エントリ: 00 00 11 11 22 22 33 33 ...

上記の場合、クラスタ番号0のエントリは 0xfff8、クラスタ番号3のエントリは0x7856です。

12ビットFATでは、各エントリが1.5バイトと半端なので、ちょっと計算が必要となります。たとえば、FATが開始位置から以下のようになっているとします。

00000000: F8 FF FF 12 34 56 78 9A ...
エントリ: 00 10 11 22 32 33 44 54 ...

ここで、クラスタ番号0(偶数番号)のエントリは1バイト目と2バイト目の下位4ビットを合わせて F8 F -> 0xff8 となります。

クラスタ番号3(奇数番号)のエントリは5バイト目の上位4ビットと6バイト目を合わせて 3 56 -> 0x563 となります。

やはりリトルエンディアンなので注意してください。

Root Directory Entry

FATの次に格納されているのがルートディレクトリエントリです。

Root Directory Entryの開始セクタは、以下で求められます。

FATの開始セクタ + sectorsPerFAT * numberOfFATs

FAT12/FAT16ではエントリ数が512個固定(BIOS Parameter BlockのrootEntries)となっています。

ディレクトリの1エントリは32バイトなので、Root Directory Entryが占めるディスク領域は、512 * 32バイトで、32セクタです。エントリの詳細については以下で説明します。

Data Area

Root Directory Entryの次からはデータエリアとなります。
データエリアの開始セクタは、以下で求められます。

Root Directory Entryの開始セクタ + 32 * rootEntries / bytesPerSector

通常rootEntriesおよびbytesPerSectorはともに512なので、結局、

Root Directory Entryの開始セクタ + 32

ということになります。

FATファイルシステムでは、データ領域はディレクトリエントリまたはファイルデータそのものです。

デー タはクラスタ単位で記録されています。あるクラスタを使い切ったら、そのクラスタ番号に対応するFATのエントリを参照することで、次のクラスタ番号を求 めることができます。次のクラスタ番号が0xff8(FAT12)/0xfff8(FAT16)以上なら、次のクラスタはなく、そこでファイルやディレク トリのデータが終わることを意味します。

クラスタ番号に対応するセクタ番号を求めるには、最初の2つのクラスタ番号が予約されているので、

(クラスタ番号 - 2) / sectorsPerCluster + データエリア開始セクタ

となります。すなわち、データエリアの最初のクラスタ番号は2です。

Directory Entry

Root Directory Entryまたはデータエリア内でディレクトリを表す場合、各ディレクトリエントリは32バイト固定の定義でできています。その構造は以下のとおりです。

struct DirEntry {
	unsigned char name[8];		/* name */
	unsigned char extension[3];	/* extension */
	BYTE attribute;			/* attribute [arc dir vol hid sys ro] */
	BYTE reserved;
	BYTE createTimeMs;		/* for VFAT */
	WORD createTime;		/* for VFAT */
	WORD createDate;		/* for VFAT */
	WORD accessDate;		/* for VFAT */
	WORD clusterHighWord;		/* for FAT32 */
	WORD updateTime;
	WORD updateDate;
	WORD cluster;			/* start cluster number */
	DWORD fileSize;			/* file size */
};

重要なフィールドは以下のとおりです。

name, extension

ファイル名と拡張子です。

最初のバイト(name[0])が0であれば、それはディレクトリの終了を示し、0xe5であれば、削除されたエントリを示します。

nameには"."および".."のエントリが存在し、それぞれカレントディレクトリ、親ディレクトリを表します。これらのエントリにも、以下に出てくるクラスタ番号は正しく格納されているので、普通のディレクトリエントリとして扱うことが出来ます。

attribute

各ビットがこのファイルエントリの属性を格納します。MSB -> LSBの順で、

N/A N/A Archive Directory Volume Hidden System Readonly

となります。VFATのロングネームエントリの場合、ここが0x0fとなります。VFATのファイル名は32バイトでは表しきれない場合があるので、複数のディレクトリエントリを使って表現することが可能です。
ロングネームを持つファイルは、常にMS-DOS互換形式のエントリも持ちます。たとえば、

a long file name.long extension

というファイル名は、VFATのエントリ(attribute=0x0fのエントリ)を1個以上持ち、かつ

ALONGF~1.LON

というエントリも格納されているので、こちらのみでもファイルの識別は可能です。

実際、SD-OSではattributeが0x0fのものは無視します。

cluster

このファイルの最初のデータが格納されているクラスタ番号です。ファイルを読み込む場合は、このクラスタからデータを読み込みます。

fileSize

ファイルサイズがバイトで入っています。ここが32ビットなので、FATで格納できる最大ファイルサイズは2^32=4GBということになります。このエントリがディレクトリの場合は、常にサイズは0です。

おわりに

大体このくらいで、FATは終わりです。FAT32やVFATはサポートしませんが、これでFAT対応のドライバを書くことができるでしょう。

なお、この記事はFAT FS フォーマットの実装についての覚え書きからかなりの情報をいただきました。ありがとうございます。

こちらもよく読まれています