WinAPIで0セクタを読み込む

シンプルに 0セクタ 512Byte をMBRとして読み込み、パーティションテーブルの値を出力してみる。

#include <windows.h>
#include <iostream>

#include <vector>
#include <string>

#pragma pack(push, 1)

// パーティションエントリ構造体
struct PARTITION_ENTRY {
    BYTE  bootIndicator;
    BYTE  startHead;
    BYTE  startSector;
    BYTE  startCylinder;
    BYTE  partitionType;
    BYTE  endHead;
    BYTE  endSector;
    BYTE  endCylinder;
    DWORD startLBA;      // PBRが存在する先頭LBAセクタ番号
    DWORD totalSectors;
};

// MBR構造体
struct MBR {
    BYTE            bootCode[446];
    PARTITION_ENTRY partitionTable[4];
    WORD            signature;
};

#pragma pack(pop)

int readSector0()
{
    // 物理ドライブ0を開く(管理者権限が必要)
    HANDLE hDevice = CreateFileW(
        L"\\\\.\\PhysicalDrive0",
        GENERIC_READ,
        FILE_SHARE_READ | FILE_SHARE_WRITE,
        NULL,
        OPEN_EXISTING,
        FILE_ATTRIBUTE_NORMAL,
        NULL
    );

    if (hDevice != INVALID_HANDLE_VALUE)
    {
        // MBR を読み込む
        MBR mbr = { 0 };
        DWORD bytesRead = 0;

        if (ReadFile(hDevice, &mbr, sizeof(MBR), &bytesRead, NULL) != 0 && bytesRead == sizeof(MBR) && mbr.signature == 0xAA55)
        {
            for (int i = 0; i < 4; i++)
            {
                std::wcout << "Partition: " << (DWORD)i << std::endl;
                std::wcout << "  boot: " << std::format(L"0x{:02x}", (DWORD)mbr.partitionTable[i].bootIndicator) << std::endl;
                std::wcout << "  type: " << std::format(L"0x{:02x}", (DWORD)mbr.partitionTable[i].partitionType) << std::endl;
                std::wcout << "  startLBA: " << (DWORD)mbr.partitionTable[i].startLBA << std::endl;
                std::wcout << "  totalSectors: " << (DWORD)mbr.partitionTable[i].totalSectors << std::endl;
            }
        }

        CloseHandle(hDevice);
    }

    return 0;
}

int main()
{
	return readSector0();
}

色々と試してみる

FATの仕組みが、概ね分かったところで、
一つ一つ目で追っていくのも、そろそろ限界なので、
小さいプログラムで、ディレクトリの情報などが取得出来ないか、試してみよう。

ひとまず、Windowsで、ディスクの生情報(セクタ0)を取得するには、
物理ドライブとしてオープンする必要があるらしいので、
まずは、その物理ドライブの一覧を取得してみる。

#include <windows.h>
#include <iostream>
#include <vector>
#include <string>

int checkPhysicalDrives()
{
    for (int i = 0; i < 32; ++i)        // 物理ドライブ0〜31までをスキャン
    {
        std::wstring drivePath = L"\\\\.\\PhysicalDrive" + std::to_wstring(i);

        // 物理ドライブをオープン(管理者権限が望ましい)
        HANDLE hDevice = CreateFileW(
            drivePath.c_str(),
            0, // アクセス権なし(情報の取得のみ)
            FILE_SHARE_READ | FILE_SHARE_WRITE,
            NULL,
            OPEN_EXISTING,
            0,
            NULL
        );

        if (hDevice != INVALID_HANDLE_VALUE)
        {
            std::wcout << "Found!!: " << drivePath << std::endl;

            CloseHandle(hDevice);
        }
    }

    return 0;
}

int main()
{
	return checkPhysicalDrives();
}

要するに、
"\\\\.\\PhysicalDrive0"
"\\\\.\\PhysicalDrive1"
"\\\\.\\PhysicalDrive2"
 :
と連番で増えていく訳だけど、上記のコードでは、オープン出来るか試しているだけで、
実際に、どの物理ドライブに繋がっているのかは、連番だけでは分からない。
デバイスハンドルから物理ドライブの情報を取得する手段の一つが、DeviceIoControl による問い合わせとなるようだ。

#include <windows.h>
#include <iostream>
#include <vector>
#include <string>

int getPhysicalDriveInfo()
{
    for (int i = 0; i < 32; ++i)        // 物理ドライブ0〜31までをスキャン
    {
        std::wstring drivePath = L"\\\\.\\PhysicalDrive" + std::to_wstring(i);

        // 物理ドライブをオープン(管理者権限が望ましい)
        HANDLE hDevice = CreateFileW(
            drivePath.c_str(),
            0, // アクセス権なし(情報の取得のみ)
            FILE_SHARE_READ | FILE_SHARE_WRITE,
            NULL,
            OPEN_EXISTING,
            0,
            NULL
        );

        if (hDevice != INVALID_HANDLE_VALUE)
        {
            std::wcout << "Found!!: " << drivePath << std::endl;

            // ドライブの詳細(モデル名やシリアル番号など)を取得したい場合
            STORAGE_PROPERTY_QUERY query = {};
            query.PropertyId = StorageDeviceProperty;
            query.QueryType = PropertyStandardQuery;

            BYTE buffer[1024] = {};
            DWORD bytesReturned = 0;

            if (DeviceIoControl(
                hDevice,
                IOCTL_STORAGE_QUERY_PROPERTY,
                &query,
                sizeof(query),
                buffer,
                sizeof(buffer),
                &bytesReturned,
                NULL)
                )
            {
                STORAGE_DEVICE_DESCRIPTOR* desc = (STORAGE_DEVICE_DESCRIPTOR*)buffer;

                std::cout << "  Version: "            << (DWORD)desc->Version            << std::endl;
                std::cout << "  Size: "               << (DWORD)desc->Size               << std::endl;
                std::cout << "  DeviceType: "         << (DWORD)desc->DeviceType         << std::endl;
                std::cout << "  DeviceTypeModifier: " << (DWORD)desc->DeviceTypeModifier << std::endl;
                std::cout << "  RemovableMedia: "     << (DWORD)desc->RemovableMedia     << std::endl;
                std::cout << "  CommandQueueing: "    << (DWORD)desc->CommandQueueing    << std::endl;

                if (desc->VendorIdOffset != 0) {
                    char* pString = (char*)buffer + desc->VendorIdOffset;
                    std::cout << "  VendorID: " << pString << std::endl;
                }

                if (desc->ProductIdOffset != 0) {
                    char* pString = (char*)buffer + desc->ProductIdOffset;
                    std::cout << "  ProductID: " << pString << std::endl;
                }

                if (desc->ProductRevisionOffset != 0) {
                    char* pString = (char*)buffer + desc->ProductRevisionOffset;
                    std::cout << "  ProductRevision: " << pString << std::endl;
                }

                if (desc->SerialNumberOffset != 0) {
                    char* pString = (char*)buffer + desc->SerialNumberOffset;
                    std::cout << "  SerialNumber: " << pString << std::endl;
                }

                std::cout << "  BusType: " << desc->BusType << std::endl;
                std::cout << "  RawPropertiesLength: " << desc->RawPropertiesLength << std::endl;
            }

            CloseHandle(hDevice);
        }
    }

    return 0;
}

int main()
{
	return getPhysicalDriveInfo();
}

実際に覗いてみる

試しに、手元のUSBメモリを覗いてみよう。

データ用で使用しているのだが、ブートコードっぽい物もあるようなので、
とりあえず、追ってみる。

0x7c00	FA		cli				; 割り込み禁止
0x7c01	B8 00 00	mov ax, 0			; AX=0
0x7c04	8E D0		mov ss, ax			; SS=0
0x7c06	BC 00 7C	mov sp, 0x7c00			; SP=0x7C00
0x7c09	8B F4		mov si, sp			; SI=0x7C00
0x7c0b	50		push ax				; 
0x7c0c	07		pop es				; ES=0
0x7c0d	50		push ax				;
0x7c0e	1F		pop ds				; DS=0
0x7c0f	FB		sti				; 割り込み許可
0x7c10	FC		cld				; DF=0(正方向)
0x7c11	BF 00 06	mov di, 0x600			; DI=0x0600 => ES:[DI] 0x0000:0x0600
0x7c14	B9 00 01	mov cx, 0x100			; CS=0x0100
0x7c17	F3 A5		rep movsw word ptr es:[di], word ptr [si]
0x7c19	EA 1E 06 00 00	ljmp 0:0x61e			; 0x0000:0x061Eへロングジャンプ

冒頭の処理は、見慣れた、お約束の手順で、
今回は、0x0600以降に自分自身をコピーしてから、処理を継続している。
なので、ロングジャンプ以降は、0x0000:0x0600からコードが始まっている前提になる。

0x061e	BE BE 07	mov si, 0x7be			; 0x0600 から相対で見ると 0x01BE つまり、パーティションテーブルの先頭
0x0621	80 3C 80	cmp byte ptr [si], 0x80		; ブート可能かチェック(0x80=ブート可能)
0x0624	74 02		je 0x628			; 可能であれば、0x0628へジャンプ
0x0626	CD 18		int 0x18			; BIOS CALL => システムブート失敗時のフォールバック処理
0x0628	56		push si
0x0629	53		push bx
0x062a	06		push es
0x062b	BB 00 7C	mov bx, 0x7c00			; BX=0x7C00
0x062e	B9 01 00	mov cx, 1			; CH=0x00, CL=0x01
0x0631	BA 00 00	mov dx, 0			; DH=0x00, DL=0x00
0x0634	B8 01 02	mov ax, 0x201			; AH=0x02, AL=0x01
0x0637	CD 13		int 0x13			; BIOS CALL => セクタ読み込み
0x0639	07		pop es
0x063a	5B		pop bx
0x063b	5E		pop si
0x063c	B2 80		mov dl, 0x80
0x063e	72 0B		jb 0x64b			; CF=1(読み込み失敗)であれば、0x064Bへジャンプ
0x0640	BF BC 7D	mov di, 0x7dbc			; 0x7C00 から相対で見ると 0x01BC つまり、パーティションテーブル直前の2Byte?
0x0643	81 3D 55 53	cmp word ptr [di], 0x5355	; リトルエンディアンとすると"US"?
0x0647	75 02		jne 0x64b			; 一致しなければ、0x064Bへジャンプ
0x0649	B2 00		mov dl, 0
0x064b	BF EB 06	mov di, 0x6eb			; DI=0x06EB
0x064e	88 15		mov byte ptr [di], dl		; 0x06EB=DL
0x0650	8A 74 01	mov dh, byte ptr [si + 1]	; ヘッド番号
0x0653	8B 4C 02	mov cx, word ptr [si + 2]	; シリンダ+セクタ番号
0x0656	8B EE		mov bp, si			; BP=SI
0x0658	EB 15		jmp 0x66f

ドライブ番号0x00(フロッピーディスク)から1セクタ読み込みしているようだけど、かなり、謎な処理に見える。
とりあえず、フロッピーディスクから読み込みが成功した上で、特定のシグネチャが存在すれば、そのまま、フロッピーディスクから、
そうでなければ、ハードディスクから... のような流れに見える。

何故か、ここから、0x066Fへジャンプしていて、
後続のコードは、処理的には、エラーメッセージを表示した後、無限ループに入る。
エラーメッセージ:"Invalid partition table"

0x065a	BE 9B 06	mov si, 0x69b			; 0x0600 から相対で見ると 0x009B エラーメッセージの先頭
0x065d	AC		lodsb al, byte ptr [si]		; AL=SIが指す一文字分
0x065e	3C 00		cmp al, 0			; NULLチェック
0x0660	74 0B		je 0x66d			; NULLなら0x066Dへジャンプ(終了)
0x0662	56		push si
0x0663	BB 07 00	mov bx, 7			; BH=0x00, BL=0x07
0x0666	B4 0E		mov ah, 0xe			; AH=0x00, AL=0x0E
0x0668	CD 10		int 0x10			; BIOS CALL => テレタイプ式1文字出力
0x066a	5E		pop si
0x066b	EB F0		jmp 0x65d			; 0x065Dへジャンプ (繰り返し処理)
0x066d	EB FE		jmp 0x66d			; 無限ループ (多分、HLTを挟んだ方が良いと思うけど...)

続きは、結局、指定ドライブから1セクタ読み込んで、
ちょいちょい、意味不明な処理があるけど、
ブートシグネチャがあれば、そのまま、ロングジャンプする。

0x066f	BB 00 7C	mov bx, 0x7c00
0x0672	B8 01 02	mov ax, 0x201
0x0675	CD 13		int 0x13
0x0677	73 05		jae 0x67e
0x0679	BE B3 06	mov si, 0x6b3			; 0x0600 から相対で見ると 0x00B3 エラーメッセージの先頭
0x067c	EB DF		jmp 0x65d			; "Error loading operating system"
0x067e	BE D2 06	mov si, 0x6d2			; 0x0600 から相対で見ると 0x00D2 エラーメッセージの先頭
0x0681	BF FE 7D	mov di, 0x7dfe
0x0684	81 3D 55 AA	cmp word ptr [di], 0xaa55	; ブートシグネチャのチェック
0x0688	75 D3		jne 0x65d			; "Missing operating system"
0x068a	BF 24 7C	mov di, 0x7c24			; BPB_SecPerTrk
0x068d	BE EB 06	mov si, 0x6eb			; 
0x0690	8A 04		mov al, byte ptr [si]		; AL=*0x06EB
0x0692	88 05		mov byte ptr [di], al
0x0694	8B F5		mov si, bp
0x0696	EA 00 7C 00 00	ljmp0:0x7c00

まぁ、微妙に怪しいコードなんですが、パーティションテーブルを見てみると...

要素 16進値 10進値 補足
ブートフラグ 0x00 0 ブート不可
先頭セクタ[0] 0xBC 188
先頭セクタ[1] 0x3D 61
先頭セクタ[2] 0x00 0
タイプ 0x0B 11 FAT32
最終セクタ[0] 0xBC 188
最終セクタ[1] 0xB8 184
最終セクタ[2] 0x84 132
先頭セクタLBA 0x00002E80 11,904
全セクタ数 0x1D0D5180 487,412,096

ブートフラグが、0x00なので、そもそも、ブート不可。
ブートコードが、怪しかろうが、関係なかった...
全セクタ数が、487,412,096(≒232GiB)なので、
CHS形式の方は、役に立たないので、多分、適当な値になっていると思われる。
3Byteフルに使っても、全然、足りないもんね...

パーティション先頭までの予約領域?が、11,904セクタってのも、大容量メディアには、ありがちなのだろうか?
ほぼ、6MiB... 小さくはないけれど、100Gi超えの容量では、誤差程度か?
誤差でフロッピー6枚分だもんなぁ... 隔世の感があるよなぁ...

何は、ともあれ、11,904セクタを覗いてみると、ちゃんとBPBが確認出来る。

オフセット 要素 サイズ 16進値 10進値 補足
0x00 ジャンプコード 3 EB 58 90 --- JMP 0x7C5A, NOP
0x03 OEM名称 8 --- --- "MSDOS5.0"
0x0B セクタサイズ 2 0x0200 512 1セクタ=512Byte
0x0D クラスタサイズ 1 0x20 32 1クラスタ=32セクタ(16KiB)
0x0E 予約セクタ 2 0x4FB8 20,408 これまた、でかいな...
0x10 FATの数 1 0x02 2
0x11 ルートディレクトリのエントリ最大数 2 0x0000 0 FAT32だからね
0x13 全セクタ数(16bit) 2 0x0000 0 FAT32だからね
0x15 メディア 1 0xF8 ---
0x16 FATサイズ(16bit) 2 0x0000 0 FAT32だからね
0x18 トラックあたりのセクタ数 2 0x003F 63
0x1A ヘッド数 2 0x00FF 256
0x1C 隠しセクタ数 4 0x00002E80 11,904 パーティションテーブルの内容と一致
0x20 全セクタ数(32bit) 4 0x1D0D5180 487,412,096 パーティションテーブルの内容と一致
0x24 FATサイズ(32bit) 4 0x0001D0B4 118,964 一つのFATが必要とするセクタ数
0x28 拡張フラグ 2 0x0000 0 FATミラーリング有効
0x2C ルートクラスタ 4 0x00000002 2
0x30 FSInfo 2 0x0001 1
0x32 ブートセクタバックアップ 2 0x006 6
0x34 予約 12 ALL 0 0
0x40 ドライブ番号 0x00 0 フロッピー扱い?
0x41 予約 1 0x00 0
0x42 シグネチャ 1 0x29 --- お約束
0x43 ボリュームID 4 0x5819BFB2
0x47 ボリュームラベル 11 ALL 0x20 --- スペース埋め(NO NAME じゃなかったっけ?)
0x53 ファイルシステム 8 "FAT32 " ---

一応、ブートコードっぽいバイナリが続くが、
ブート不可ボリュームだし、内容が怪しいので、今回は、スルー。
予約セクタが、20,408セクタってのも、デカすぎる気がするが、そういうもんなのか?
ひとまず、これを信じると、(隠しセクタ数) 11,904 + (予約セクタ数) 20,408 = 32, 312 が、FATの先頭になるはず。
実際に見てみると...

F8 FF FF FF
FF FF FF FF
FF FF FF 0F
FF FF FF 0F

それっぽい...
F8は、BPB_Media と同じ値。
1クラスタ目は、特殊フラグとして利用される場合もあるらしいが、ALL 1 であれば、異常なし。
2クラスタ目、3クラスタ目は、クラスタチェーンの終端となっている。
2クラスタ目は、ルートディレクトリのはず。

(FATサイズ) 118,964 x 2 = 237,928

なので...

(FAT先頭) 32,312 + 237,928 = 270,240

2クラスタ目の先頭は、ここになる。
以降、Nクラスタ目は、270,240 + 32 * (N - 2) という事になる。
とりあえず、これで、ディレクトリ情報は、追えそうな感じ。

そしてFAT32へ...

1990年代に入り、HDDの容量は、数百MBとなり、1990代後半には、GB時代に突入する。
1995年、Windows95 で、ロングファイルネームなどの導入により、FAT16は、一つの完成形に至ったが、
既に、HDDの大容量化に対して、限界が見えていた。
1996年、Windows95 OSR(OEM Service Release) 2 で、FAT32が、初めて実装され、
1998年、Windows98 において、FAT32 が、標準となる。

もう一度、復習...

年 MS-DOS Windows HDD平均容量 FAT 出来事
1980 1.0 --- 5MB〜10MB FAT12 まだまだフロッピーがメイン
1983 2.0 --- 10MB〜20MB 属性フラグ確立、サブディレクトリが使用可能に
1984 3.0 --- FAT16A HDDの普及が進む
同年 3.1 --- ネットワーク対応
1985 1.0 MS-DOS向けGUI環境。まだ「タイル表示」のみ。
1986 3.2 20MB〜40MB 3.5インチフロッピー標準サポート
1987 3.3 2.0 FAT16B(OEM) ウィンドウの重ね合わせが可能に...
1988 4.0 2.1 FAT16B(正式)
1990 3.0 40MB〜120MB 「プログラムマネージャ」や「ファイルマネージャ」が登場。
プロテクトモードサポートによりメモリ領域拡大。
1991 5.0 HMAやUMBなどのメモリ管理機能が強化された
1992 3.1 TrueTypeフォントの標準サポート。
安定性が向上し日本でも広く普及(1993年〜)。
1993 6.0 200MB〜540MB MS-DOS単体製品としては、最後のバージョン
同年 (NT 3.1) NTの誕生&NTFS(Ver1.0)の初採用
1994 (NT 3.5) 大幅な軽量化&最適化
1995 7.0 Windows95 1GB〜2GB MS-DOSは、Windowsに内蔵された
同年 (NT 3.51) PowerPCやMIPSなどのx86以外のCPUにも対応
1996 7.1 Windows95 OSR2 FAT32(OEM)
同年 (NT 4.0) Windows95のGUI要素を取り込む
1998 Windows98 4GB〜20GB FAT32(正式)
2000 8.0 WindowsMe 20GB〜80GB 最後のMS-DOS
同年 Windows 2000 (NT 5.0)
2001 Windows XP (NT 5.1) 9x系とNT系を完全統合

なんか、MSの年表みたいになってしまったが、こうして並べてみると、HDDの容量増加が、本当にえげつない。
ある程度、容量増加を見込んでFATの改良を続けてきたのだろうが、全く追い付いていない感じだ。
並行して、NTFSなど、新規のファイルシステムが登場し、実需は、そちらに移行していった。
ただ、NTFSなど、プロプライエタリ製品については、仕様が非公開であるため、基本的にWindows専用であって、
FATは、その点、至る所で採用された結果、広く互換性を発揮する事となった。

現在、HDDの容量は、数TB〜数十TBとなり、さすがにFATでは無理があるが、
USBメモリやSDカードなど、数GB〜数十GBクラスでは、未だに現役である。
しかし、フラッシュメモリでもTBクラスが登場するなど、大容量化は続いており、限界は近いかもしれない。

ともあれ、FAT32は、HDDの容量が、数GB〜数十GBの時代となり、
FAT16における限界を打破するために登場した。
これは、FAT12からFAT16への移行と、ほぼ同じ流れと考えられる。
FAT16が、クラスタサイズを32Kとしても、最大2Gまでしか対応できず、
しかも、数バイトのファイルでも1クラスタ(32K)を消費してしまい、利用効率の悪化に繋がった。
そこで、クラスタ数を32bit(仕様上28bitに制限)に拡張する事で、
クラスタサイズを512Byteとしても100G程度まで対応可能とした。

FAT32では、BPBの構成にも大きく手が入り、ルートディレクトリの固定化も廃止された。

オフセット サイズ 要素 FAT16 FAT32 内容
0x00 3 BS_JmpBoot ○ ○ ブートローダーへのジャンプコード
0x03 8 BS_OEMName ○ ○ OEM名称
0x0B 2 BPB_BytsPerSec ○ ○ セクタあたりのバイト数
0x0D 1 BPB_SecPerClus ○ ○ クラスタあたりのセクタ数。[1,2,4,8,16,32,64,128]
0x0E 2 BPB_RsvdSecCnt ○ ○ 予約セクタ数 (FAT1領域の直前までのセクタ数 FAT32では32が多い)
0x10 1 BPB_NumFATs ○ ○ FATの数 (常に2が望ましい)
0x11 2 BPB_RootEntCnt ○ 未使用(0固定) ルートディレクトリエントリの最大数 (FAT16では512が多い)
0x13 2 BPB_TotSec16 ○ 未使用(0固定) 全セクタ数 (古い16ビットフィールド)
0x15 1 BPB_Media ○ ○ 0xF8(固定) or 0x0F(リムーバブル) これと同じ値をFAT[0]の下位8bitに置く
0x16 2 BPB_FATSz16 ○ 未使用(0固定) 一つのFATが占めるセクタ数
0x18 2 BPB_SecPerTrk ○ ○ トラックあたりのセクタ数 (現在では、ほとんど意味がない)
0x1A 2 BPB_NumHeads ○ ○ ヘッド数 (現在では、ほとんど意味がない)
0x1C 4 BPB_HiddSec ○ ○ 隠しセクタ数 (現在では、ほとんど意味がない)
0x20 4 BPB_TotSec32 FAT16Bで追加 ○ 全セクタ数 (新しい32ビットフィールド)
0x24 4 BPB_FATSz32 --- 新規 一つのFATが占めるセクタ数
0x28 2 BPB_ExtFlags --- 新規 拡張フラグ(下位1ByteがFATミラーリングフラグ)
0x2A 2 BPB_FSVer --- 新規 FAT32ボリュームのバージョン
0x2C 4 BPB_RootClus --- 新規 ルートディレクトリの先頭クラスタ番号
0x30 2 BPB_FSInfo --- 新規 予約領域中でFSINFO構造体の置かれるセクタ番号
0x32 2 BPB_BkBootSec --- 新規 予約領域中でブートセクタのバックアップが配置されるセクタ
0x34 12 BPB_Reserved --- 新規 予約
0x40 1 BS_DrvNum ○ ○ ドライブ番号 (FDDでは0x00,HDDでは0x80)
0x41 1 BS_Reserved ○ ○ 予約 (通常は0x00を設定)
0x42 1 BS_BootSig ○ ○ 拡張ブートシグネチャ(有効であれば0x29)
0x43 4 BS_VolID ○ ○ ボリュームシリアル番号
0x47 11 BS_VolLab ○ ○ ボリュームラベル 8+3 文字 (無い場合は"NO NAME ")
0x52 8 BS_FilSysType ○ ○ この文字列は情報提供のみを目的としている

FAT(8)12から連綿と続く歴史の積み重ねと見れば、感慨深くもあるが、
いきなり、3Byte, 8Byte と刻まれると、今の時代、アラインメントが〜!!と頭を抱えがち。
互換性維持というのも、なかなかに難しい物である。

FAT16までは、ブートセクタ(先頭セクタ)の直後からFATが配置されていたが、
FAT32では、通常、16KiB(512Byteセクタで32セクタ分)の予約領域が置かれている。
内訳は、以下の通り。

セクタ番号(LBA)(512Byte) 領域 内容
0 ブートセクタ ブートコード、BPB
1 FSInfo 検索高速化用
2 ブートコード拡張 ブートコード用の追加領域
3〜5 予約領域 0x00埋め
6 バックアップ0 セクタ0の完全コピー
7 バックアップ1 セクタ1の完全コピー
8 バックアップ2 セクタ2の完全コピー
9〜31 予約領域 0x00埋め


LBA1セクタには、FSInfoが置かれる。
これは、FATの肥大化により、空きクラスタの走査などに時間が掛かるようになり、
その対応として、空き領域や空きクラスタの情報を記録するために用意されている。
また、重要セクタの完全コピーを用意する事で、多少、障害時の復旧能力が向上している。
とは言え、重要セクタが密集しているので、ダメな時はダメだろう。(当たり前だが...)

FAT32では、クラスタ数を32bit(仕様上、28bit)で管理するので、
FAT領域は、総クラスタ数✕4Byte(32bit)必要となる。
1クラスタ=1セクタ(512Byte)として、単純に計算すれば、
1セクタ(512Byte) で128クラスタ分の領域となり、64KiBのデータ領域を管理出来る。
1024セクタで64MiB、1024*1024セクタで64GiB。
そもそも、28bit長の最大値が、256*1024*1024なので、2*1024*1024セクタで最大となり、最大容量は、128GiBとなる。
しかし、この場合、FATサイズだけで1GiBとなり、
通常は、これよりも、ずっと少ないクラスタ数で収まるよう、セクタサイズが調整される。
セクタサイズを大きくすると、利用効率の悪化に繋がるという問題を繰り返しているようだが、
ディスク全体のサイズが大きくなり過ぎたり、一度の読み書き単位が大きくなったりで、
必ずしも1セクタ単位での管理が効率的とは限らなくなっている。
何より、FATをメモリ上に展開する上で、さすがに、1GiBでは、大き過ぎる。
現実的には、数十KiBから数百KiB程度... 大きくても1MiB程度に収まるサイズが多いようだ。
フォーマット済みのディスクであれば、BPB_FATSz32パラメータに、一つのFATが占めるセクタ数が、セットされている。

それらを踏まえ、ボリューム先頭の配置は、以下のようになった。

用途 LBA セクタ数 サイズ 備考
BPB+予約領域 0 32 16KiB ブートローダー, FSInfo, バックアップ含む
FAT 33 BPB_FATSz32 ✕ 2 BPB_BytsPerSec ✕ BPB_FATSz32 ✕ 2 FAT x 2セット
データ領域 FAT直後〜 可変 可変

ルートディレクトリは、BPB_RootClus から始まるクラスタチェーンに配置される。
多くは、実質の先頭クラスタとなる2がセットされている。
ディレクトリエントリ構造体は、これまで通りであり、LFN仕様も、そのまま引き継がれている。

サブディレクトリとロングファイルネーム

再度、復習になってしまうが、1981年にFAT12が導入された際、
属性フラグも特に定められておらず、予約領域を含めて、未使用状態に近い状態であった。

要素 サイズ 備考
ファイル名 8 スペース埋め
拡張子 3 スペース埋め
属性フラグ 1 MS-DOS 2.0 〜
予約領域 10
最終更新時刻 2 時:5bit, 分:6bit, 秒:5bit(2秒単位) (リトルエンディアン)
最終更新日付 2 年:7bit(1980+a), 月:4bit, 日:5bit (リトルエンディアン)
開始クラスタ番号 2
ファイルサイズ 4

1983年の MS-DOS 2.0 で、以下の属性フラグが導入された。

ビット 16進数値 内容
0 0x01 Read-Only (読み取り専用=書き込み禁止)
1 0x02 Hidden (隠しファイル)
2 0x04 System (システムファイル=特殊ファイル)
3 0x08 Volume Label (ボリュームラベル)
4 0x10 Directory (ディレクトリ)
5 0x20 Archive (アーカイブフラグ)
6 0x40 Device (デバイスフラグ)
7 0x80 Reserved (予約)

0〜3ビットについては、暗黙的に排他関係にあると解釈された。
4ビット目のディレクトリ属性により、サブディレクトリの作成が可能となった。
5ビット目以降は、特殊な用途として使用される。

ディレクトリ属性を持つエントリは、ディレクトリとして扱われ、
開始クラスタ番号が指すクラスタに、ディレクトリ配下のファイルエントリが作成される。
サブディレクトリのエントリには、自動的に、特殊なディレクトリがいくつか用意される。

ファイル名 属性 開始クラスタ
. (ドット) 0x80 (ディレクトリ) 自分自身のクラスタ番号
.. (ドットドット) 0x80 (ディレクトリ) 親のクラスタ番号。ルートの場合は、0。

ドットディレクトリは、いわゆる、エイリアスのような物であり、
実際のディレクトリ名称を得るには、ディレクトリ階層を遡り、本体の情報を再取得する必要があるので、
多くの実装では、キャッシュを用意するなど、工夫を凝らしているようである。

1クラスタ=1セクタの512Byteとすると、32Byteエントリが、16個入る事になるが、
クラスタチェーンを繋ぐ事で、必要なだけ増やす事が出来る。
当然、連続的にクラスタを確保出来るとは限らないので、断片化が進むとバラバラに配置される可能性がある。

一方、予約領域は、1995年、Windows95の登場時点で、ほぼ埋まった。

要素 サイズ 備考
ファイル名 8 スペース埋め
拡張子 3 スペース埋め
属性フラグ 1
予約フラグ 1 NTなどによる大文字・小文字区別など
作成時刻(10ms単位) 1 0-199。2秒単位の作成時刻と合わせて補正する。
作成時刻 2 時:5bit, 分:6bit, 秒:5bit(2秒単位) (リトルエンディアン)
作成日付 2 年:7bit(1980+a), 月:4bit, 日:5bit (リトルエンディアン)
最終アクセス日付 2 年:7bit(1980+a), 月:4bit, 日:5bit (リトルエンディアン)
予約領域 2 まだ、2Byte 残していた...
最終更新時刻 2 時:5bit, 分:6bit, 秒:5bit(2秒単位) (リトルエンディアン)
最終更新日付 2 年:7bit(1980+a), 月:4bit, 日:5bit (リトルエンディアン)
開始クラスタ番号 2
ファイルサイズ 4

こう見てみると、日付や時刻のアバウトさは、そのまま引き継がれているんだな...

そして、ここで、ロングファイルネーム(LFN)も導入された。
LFNでは、これまで排他扱いされてきた属性フラグを逆手に取り、
0〜4bit を全て1(0x0F)とした値をLFN用とした。
古いプログラムでは、これらの組み合わせを異常とみなし、除外される想定であり、
極力互換性を維持しつつ拡張を行おうとした苦労が垣間見える。

ファイルネームの格納方法も、ファイル情報の32Byte領域を流用した形で、
本体となるファイル情報の手前に逆順に連続して配置する。
一つの32Byte領域は、以下のような内訳となっている。

要素 サイズ 備考
シーケンス番号 1 1〜20 (0x01〜0x14) + 最終エントリのみ 0x40
文字データ(1) 10 LFNの 1〜5 文字目
属性フラグ 1 0x0F
予約フラグ 1 NTなどによる大文字・小文字区別など
チェックサム 1 ショートファイル名から計算した値
文字データ(2) 12 LFNの 6〜11 文字目
開始クラスタ番号 2 0x0000
文字データ(3) 4 LFNの 12〜13 文字目

32Byte領域一つで、UNICODE(UTF-16LE)13文字分を格納する。
ファイル名の最大長は、255文字となっており、NULL(0x0000)で終わる事になっているので、
最大で20個(260文字分)の領域を消費する事になる。
尚、NULL終端以降の空き領域は、0xFFFFで埋める事になっているようだ。
ロングファイルネーム使用時、ショートファイルネームには、以下の内容が設定される。

1.ロングファイルネームから空白・禁止文字・複数のドットを取り除く。
2.拡張子は、最後のドットの直後にある文字列のみを取り出す。
3.全て大文字に変換し、先頭の6文字を切り出す。
4.チルダ+連番(~1など)を連結して8文字とする。
5.ファイル名が重複する場合は、連番を増やしていく。

また、チェックサムには、ショートネームから以下の手順で算出した値が設定される。(全てに同じ値が設定される)

1.チェックサム=0
2.ファイル名+拡張子の11文字(ドットは除く)を1文字ずつ取り出す
3.チェックサムを右ローテート(最下位ビットを最上位ビットへ移動)
4.チェックサムに文字コードを加算。
5.11文字分繰り返す

今更だが、ショートファイルネームは、基本的に、全て大文字として扱われてきた。
MS-DOSが、ファイルネームの大文字・小文字を区別しないのも、FAT由来であると考えられる。
従って、8.3文字であっても、小文字のファイル名は、LFNとして扱われていたようである。

知れば知るほど... なんと言うか、無理に無理を重ねたような仕様になってるよなぁ... ^^;;;

FAT12からFAT16へ

FAT12が、1980年代初頭に登場したが、1980年代中頃には、数十MBのハードディスクが普及し始めた。
しばらくは、クラスタサイズを大きくして凌いだが、それも早々に限界に達した。
そして、1984年にFAT16の初期バージョン(A)が導入された。
基本的には、FAT管理が12bitから16bitに拡張されたのみで、大部分の構成は、FAT12から、そのまま引き継がれている。
そのため、BPBの総セクタ数が2Byteのままで、これが最大容量の制限となった。(32Mの壁)

年 MS-DOS Ver 内容
1981 1.0 FAT12導入
1983 2.0 属性フラグ確立、サブディレクトリが使用可能に
1984 3.0 FAT16A導入
1987 3.3 拡張パーティション導入
1988 4.0 FAT16B導入

一方で、ハードディスクの容量は、着々と増加していき、
拡張パーティションの導入などで、多数のパーティションに分割出来るようになった物の、
やはり、取り回しの不便さは否めず、
とうとう、BPBに4Byte長の総セクタ数フィールドを新たに追加する事となった。
これが、いわゆる、FAT16Bである。
これにより、追加されたフィールド以降のパラメータが、4Byte分ずれる事になった。
つまり、FAT12、及び、FAT16AとFAT16B以降では、完全互換ではない。
尚、現在、FAT16と言えば、ほぼFAT16Bを指すと考えて良いだろう。
ともかく、これで、最大容量が2G(OSの拡張対応により4G)まで拡張された。
また、容量増加に合わせ、ルートディレクトリエントリも512エントリ(32セクタ=16KiB)が標準的となった。

FAT12の復習

FATは、そもそも、8インチフロッピーディスクの管理用に考案された物であり、
最初のバージョンは、言うなれば、FAT8と呼べるような代物で、
クラスタ番号を8bitで管理していたようである。
8bitとなると、0〜255となるが、仕様上、管理に割り当て可能な数値は、それよりも少なくなる。
1S (片面単密)でも77(トラック)✕26(セクタ)=2,002となり、必然的にクラスタサイズを大きくする事になる。

メディア種別 変調方式 トラック数 セクタ数 セクタサイズ フォーマット容量 主な用途
1S (片面単密) FM 77 26 128 Byte 約 243KB 〜 256KB IBM 標準, CP/M 等

セクタサイズが、128Byteなので、1クラスタ=8セクタとすると、1,024となり、クラスタ数も250程度に収まる。
単純に8bitで256エントリ分の領域を確保すると、そのまま、256Byteとなり、128Byteセクタであれば、2セクタ分となる。
当時から、FATを二重化していたとしても、4セクタ分。
また、当初のファイル情報は、16Byte構成で、ファイル名も6文字+3文字、ファイルサイズも、恐らく、2Byte程度だったと考えられる。
Gemini君に、何度か尋ねてみたところ、概ね、以下のような構成だったようだ。

要素 サイズ 備考
ファイル名 9 恐らく、6文字+3文字
予約 2 謎...
属性フラグ 1 プロテクトフラグなど?
開始クラスタ番号 1 8bitFATたる所以
ファイルサイズ 3 理論上16MBまで

メインメモリが数十KB程度であったので、最大64KBでも、実用上十分だったと思うが、3Byte割り当てていたのか?
謎な予約領域2Byteを、素直にファイル名にあてれば、8文字+3文字になる気もするが...
まぁ、この頃は、試行錯誤の繰り返しだったと思うので、これは、ほんの一例と捉えるべきか...
勿論?まだ、ディレクトリ管理機能は用意されておらず、ファイルのみの構成だったようだ。
128Byteセクタであれば、1セクタ分で、8ファイル分となり、この辺は、必要に応じて調整していたようだ。

用途 セクタ数 サイズ 備考
IPL 2 256Byte 足りるのか?
FAT 4 512Byte 8bit x 256エントリ x 2セット
ファイル情報 4 512Byte 32ファイル分
データ領域 1,992 254,976Byte 249クラスタ分

例えば、こんな配置だとすれば、綺麗に収まる。
管理領域が、1KBちょっと。
全容量の1%にも満たないサイズで、よくやっている。
とは言え、この時点で、全セクタ数に対して、8bitFATでは、不足気味で、
86-DOS(後のMS-DOS)へ採用するタイミングで12bitFATへと拡張される事になったようだ。
同時に、ファイル情報が、32Byteに拡張され、ファイル名も8文字+3文字となり、
FAT12フォーマットが、ここで確立された事になる。

要素 サイズ 備考
ファイル名 8 スペース埋め
拡張子 3 スペース埋め
属性フラグ 1 色々とある
予約領域 10 ざっくり確保されていたらしい
最終更新時刻 2 時:5bit, 分:6bit, 秒:5bit(2秒単位) (リトルエンディアン)
最終更新日付 2 年:7bit(1980+a), 月:4bit, 日:5bit (リトルエンディアン)
開始クラスタ番号 2 12bitFATだが、ここでは、16bitある
ファイルサイズ 4 一応、理論上4GiBまで

当初は、予約領域として、ざっくり10Byte分が確保されていたらしいが、
後々、これらは、機能拡張の度に切り崩されていった。
そう考えると、この予約領域のお陰で、救われた部分も少なくないように思う。
日付・時刻は、bit単位で詰め込まれ、秒に関しては、半分にして無理やり収めている。
なんとも涙ぐましいではないか...

FATの方は、12bitに拡張され、これを3Byteに2つ分詰め込む事になった。
この3Byteは、リトルエンディアン扱いなのだが、度々、混乱の元となりがちである。
機械(CPU)も人も、基本的には、小さい桁から処理を行っているのだが、
人が数値を表記する場合、右側に小さい桁がある事に慣れすぎていて、
左側が上の位、右側が下の位、という並びを自然に感じる。(ビッグエンディアン)
ところが、機械(CPU)からすると、アドレスが増える方向に桁が上がる方が都合が良い。
アドレスを表記する場合、左から右に増えていくのが一般的なので、
左側が下の位、右側が上の位、という並びに見えてしまう。(リトルエンディアン)
おまけに1Byteの数値を16進数で表記すると2桁となり、部分的にビッグエンディアンとなってしまう。
これらが入り乱れる事で、分かりづらくなっていると思われる。
例えば、3, 4 というクラスタ情報は、0x03, 0x40, 0x00 というバイト並びになるのだが、
これは、逆順に並べて右(下の桁)から読むと分かりやすくなる(と思う)。
つまり、0x004003となり、0x004|0x003 <こっちが先。

ともかくも、クラスタ管理を12bitで行う事になり、以下のように割り当てられた。

クラスタNo16進 クラスタNo10進 役割
0x000 0 未使用(Free Cluster)
0x001 1 予約
0x002 〜 0xFF0 2 〜 4080 通常のクラスタ(Data)
0xFF1 〜 0xFF6 4081 〜 4086 予約(Reserved)
0xFF7 4087 不良クラスタ(Bad Cluster)
0xFF8 〜 0xFFF 4088 〜 4095 終端(End Of File)

フロッピーディスクの物理条件を考えれば、4,000 あれば、まずまず十分と考えられた(のだろう)。
これらを踏まえて、代表的な?2HD(1.44)のフロッピーディスクのレイアウトは、以下のようになった。

用途 LBA セクタ数 サイズ 備考
BPB 0 1 512Byte ブートローダーを含む場合もある
FAT 1 18 9KiB 12bit x 2,880 x 2セット
ルートディレクトエントリ 19 14 7KiB 32Byte x 224エントリ
データ領域 33〜 2,847 約1.3MiB

FATサイズは、本来、全体のクラスタ数から決まる訳だが、
ここでは、1クラスタ=1セクタとなっているので、必然的に2,880クラスタで計算している。
ルートディレクトリエントリの個数も、FAT12の仕様では、任意の個数で良いはずだけど、
PC9801シリーズ用の1.2Mフォーマットで、224個とすると都合が良かったらしく(トラック境界に合うように)、
そのまま、その個数が定着してしまったようである。

また、FATの先頭2クラスタ分は、仕様により、未使用領域になる訳だが、
クラスタ0には、メディア記述子、クラスタ1には、Dirty Flag などが書き込まれるようである。

ファイル情報の先頭バイト、つまり、ファイル名の先頭バイトは、フラグを兼ね、
0x00であれば、未使用領域となり、0xE5であれば、削除済みとなる。
日本語ファイル名の場合、0xE5から始まる場合があるため、
削除済みマークとの衝突を避けるため、書き込み時に0x05に置き換えられる。
従って、読み込み時に0x05から、0xE5に再度置き換えする必要がある。