ゼロからOS自作入門 17章

ファイルシステム
ファイルシステムの説明はほぼFATファイルシステムのしくみと操作法のもの。自信の理解のために写経
ファイルシステムは、コンピュータのリソースを操作するための、オペレーティングシステムが持つ機能の一つ。ファイルとは、主に補助記憶装置に格納されたデータを指すが、デバイスやプロセス、カーネル内の情報といったものもファイルとして提供するファイルシステムもある。
ファイルシステムは抽象データ型の集まりであり、ストレージ、階層構造、データの操作/アクセス/検索のために実装されたものである。
FAT(File Allocation Table)
- Windowsの前身であるMS-DOSでも使用されていたファイルシステム
- もともとはフロッピーディスク上で使用するためのファイルシステム
- 拡張性に乏しい
- 最大のファイルサイズやフォルダ内のファイルの数に制限がある
FATは時代とともに拡張されている
FAT12 | FAT16 | FAT32 | exFAT | |
---|---|---|---|---|
最大ファイルサイズ | 32MiB | 2GiB/4GiB(NT) | 4GiB | 16EiB |
クラスタサイズ | 512byte~32KiB | 512byte~32KiB | 512byte~32KiB | 512byte~32MiB |
最大ファイル数 | 4077 | 65517 | 268435427 | 2796202(ディレクトリごと) |
最大ボリュームサイズ | 32MiB | 2GiB/4GiB(NT) | 2TiB | TBU |

- ブロックデバイス:ブロック単位でデータを保存する
- データ領域が固定で、ブロック単位で読み書きする
- データが1ブロックよりも大きい場合には複数のブロックを使う
- データサイズが変わらないものには有効
- データとブロック番号を紐付ける必要がある
- データが小さい場合には不利
- ファイル方式:ファイル単位でデータを保存する
- テーブルでファイルとデータ領域が紐づける
- このテーブルによって動的に領域の紐付けを更新できる

「dd」コマンドはファイルをブロック単位で読み出し、指定通り変換して出力する。処理するブロック数を指定でき、任意のサイズのファイルを作成するといった用途にも役立つ。
入力と出力にデバイスを指定できるため、次の用途で使われる
- HDDのパーティションをコピーする
- USBメモリやCD-ROMのバックアップを取る
オプション | 意味 |
---|---|
if=ファイル | 標準入力の代わりにファイルから読み出す。デバイスファイルも指定可能 |
of=ファイル | 標準出力の代わりにファイルへ書き込む。デバイスファイルも指定可能 |
bs=バイト数 | 1回に読み書きするブロックサイズ(バイト数) |
ibs=バイト数 | 1回に読み出すブロックサイズ(デフォルトは512バイト) |
obs=バイト数 | 1回に書き込むブロックサイズ(デフォルトは512バイト) |
count=個数 | ibsで指定したサイズのブロックを入力から個数分だけコピーする |
iflag=フラグ | フラグに従って読み出す。フラグは「,」で区切り、複数指定可能 |
oflag=フラグ | フラグに従って書き込む。フラグは「,」で区切り、複数指定可能 |
skip=ブロック数 | 入力時にibsで指定したサイズのブロックをブロック数分、先頭からスキップする |
seek=ブロック数 | 出力時にobsで指定したサイズのブロックをブロック数分、先頭からスキップする |
status=noxfer | 処理バイト数や処理速度を表示しない |

hexdumpは使ったことないなーって思っていたらバイナリエディタを使っていた。なんでバイナリエディタを使っていたんだろう?

FAT形式での8文字を超えるファイルの記録形式がおもしろい
- 8文字を超えるファイルは短縮して記録される
- 2箇所目に保存される場合は、短縮したものを大文字で記録する
- ファイルの内容は大文字・小文字が区別される

BIOSパラメータブロック
PBR(Private Boot Record)
- パーティションの先頭の1ブロックのこと
- パーティションの大きさ(ブロック数)や FAT のデータ構造がなんブロック目から始まるなどの情報が書かれている
VBR(Volume Boot Record)とも呼ばれるもので、予約領域の先頭セクタ(つまりボリュームの先頭セクタ)である
BPB(BIOS Parameter Block)
ブートセクターのうち、ブートストラップローダー領域に置かれているOSの管理情報テーブルのこと。FATボリュームに関する構成パラメータが記録されている。BPBはブートセレクタに配置される。
- FAT32のBPB構造
名前 | Offset | Size | 解説 |
---|---|---|---|
BS_JmpBoot | 0 | 3 | ブートストラップコードへのジャンプ命令(x86命令)。このフィールドには次の2つのフォーマットがあり、前者が一般的 0xEB, 0x??, 0x90 (ショートジャンプ+NOP) 0xE9, 0x??, 0x?? (ニアジャンプ) ・??はジャンプ先により異なる任意の値 これらから外れたフォーマットの場合、そのボリュームはWindowsで認識されない |
BS_OEMName | 3 | 8 | "MSWIN4.1"が推奨される。ほかに"MSDOS5.0"などがよく使われる マイクロソフトのOSはこのフィールドに何ら注意を払わないが、いくつかのFATドライバは何らかの参照を行う この文字列が推奨されるのは、それが互換性問題を最小にする設定であることが理由である 何か違う値を設定しても良いが、いくつかのFATドライバはそのボリュームを認識できない可能性がある このフィールドは、たいていはそのボリュームを作成したシステムを示している |
BPB_BytsPerSec | 11 | 2 | バイト単位のセクタサイズ 有効な値は、512, 1024, 2048または4096である マイクロソフトのOSはこれらのセクタサイズを適切にサポートが、サポートするセクタサイズを512に限定していても、このフィールドが512であることをチェックしないFATドライバが多く存在する。そのため、最大限の互換性が要求されるときは512を使うべきである この値は、そのボリュームを格納するストレージのセクタサイズと同じでなければならない |
BPB_SecPerClus | 13 | 1 | アロケーションユニット(割り当て単位)当たりのセクタ数 FATファイルシステムでは、アロケーションユニットのことをクラスタと呼んでいる これは1個以上の連続したセクタのブロックのことで、データ領域はこれを単位に管理される クラスタ当たりのセクタ数は、2の累乗でなければならない クラスタサイズ |
BPB_RsvdSecCnt | 14 | 2 | 予約領域のセクタ数 このフィールドは0であってはならない(少なくともこのBPBを含むブートセクタそれ自身が存在する) 互換性問題を避けるため、FAT12/16ボリュームでは1にするべきである |
BPB_NumFATs | 16 | 1 | FATの数 このフィールドの値は常に2に設定すべきである。1以上の何らかの値もまた有効だが、互換性問題を避けるため2以外の値は使用しないことが強く推奨されている |
BPB_RootEntCnt | 17 | 2 | FAT12/16ボリュームでは、ルートディレクトリに含まれるディレクトリエントリの数を示す このフィールドには、ディレクトリテーブルのサイズが2セクタ境界にアライメントする値、つまり、 最大の互換性のためには、FAT16では512に設定すべきである FAT32ボリュームではこのフィールドは使われず、常に0でなければならない |
BPB_TotSec16 | 19 | 2 | ボリュームの総セクタ数(古い16ビットフィールド) この値は、ボリュームの4つの領域全てを含んだセクタ数である FAT12/16でボリュームのセクタ数が0x10000以上になるときは、このフィールドには無効値(0)が設定され、真の値がBPB_TotSec32に設定される FAT32ボリュームでは、このフィールドは必ず無効値でなければならない |
BPB_Media | 21 | 1 | 区画分けされたハードディスクでは0xF8が標準値である 区画分けされないリムーバブルメディアでは0xF0がしばしば使われる このフィールドに有効な値は、0xF0, 0xF8, 0xF9, 0xFA, 0xFB, 0xFC, 0xFD, 0xFEおよび0xFFで、重要な点はこれと同じ値をFAT[0]の下位8ビットに置かなければならない |
BPB_FATSz16 | 22 | 2 | 1個のFATが占めるセクタ数 このフィールドはFAT12/FAT16ボリュームでのみ使われる FAT32ボリュームでは必ず無効値(0)でなければならず、代わりにBPB_FATSz32が使われる |
BPB_SecPerTrk | 24 | 2 | トラック当たりのセクタ数 このフィールドは、ジオメトリを持つストレージにのみ関係し、IBM PCのディスクBIOSで使用される |
BPB_NumHeads | 26 | 2 | ヘッド数 このフィールドは、ジオメトリを持つストレージにのみ関係し、IBM PCのディスクBIOSで使用される |
BPB_HiddSec | 28 | 4 | ストレージ上でこのボリュームの手前に存在する隠れた物理セクタの数 ボリュームがストレージの先頭から始まる場合(つまりフロッピーディスクなど区画分けされていないもの)では常に0であるべきである |
BPB_TotSec32 | 32 | 4 | ボリュームの総セクタ数(新しい32ビットフィールド) この値は、ボリュームの4つの領域全てを含んだセクタ数 FAT12/16ボリュームで総セクタ数が0x10000未満のとき、このフィールドは無効値(0)でなければならなず、真の値はBPB_TotSec16に設定される |
BPB_FATSz32 | 36 | 4 | 1個のFATが占めるセクタ数 FAT領域のサイズは、 |
BPB_ExtFlags | 40 | 2 | ビット3~0: 0から始まるアクティブなFAT。ビット7が1のとき有効 ビット6~4: 予約 ビット7: 0は全てのFATがミラーリングされることを示す。1はビット3~0で示される1個のFATだけがアクティブであることを示す ビット15~8: 予約 |
BPB_FSVer | 42 | 2 | FAT32ボリュームのバージョン 上位バイトがメジャーバージョン番号、下位バイトがマイナーバージョン番号 |
BPB_RootClus | 44 | 4 | ルートディレクトリの先頭クラスタ番号。大抵は2(つまり先頭クラスタ)が設定されるが、2である必要はない ルートディレクトリの位置を変える場合、なるべく先頭で正常なクラスタに置くようにすべきである フィールドが誤ってクリアされたとき、ディスク修復ツールが容易にルートディレクトリを見つけられるようにするために使用する |
BPB_FSInfo | 48 | 2 | FAT32ボリュームの予約領域中でFSINFO構造体の置かれるセクタ番号 常に1(つまりブートセクタの次) |
BPB_BkBootSec | 50 | 2 | 0以外の場合、FAT32ボリュームの予約領域中でブートセクタのバックアップが置かれるセクタを示す 大抵は6(つまりブートセクタの6セクタ先)で、この値以外は推奨されない |
BPB_Reserved | 52 | 12 | 将来の拡張のために予約。フォーマット時はゼロを設定すべきである |
BS_DrvNum | 64 | 1 | IBM PCのディスクBIOSで使われるドライブ番号 |
BS_Reserved | 65 | 1 | 予約領域。フォーマットするときは常に0を設定すべきである |
BS_BootSig | 66 | 1 | 拡張ブートシグネチャ (0x29)。これは、続く3つのフィールドが存在することを示す |
BS_VolID | 67 | 4 | ボリュームシリアル番号 このフィールドとBS_VolLabでリムーバブルストレージにおけるボリュームの追跡をサポートする |
BS_VolLab | 71 | 11 | ボリュームラベル このフィールドは、ルートディレクトリに記録される11バイトのボリュームラベルに一致する |
BS_FilSysType | 82 | 8 | 常に"FAT32 "。このフィールドはFATタイプの決定には関与しない |
BS_BootCode32 | 90 | 420 | ブートストラップコード。システム依存フィールドで、未使用時はゼロで埋める |
BS_BootSign | 510 | 2 | 0xAA55。有効なブートセクタであることを示すブートシグネチャ |
ルートディレクトリのブロック番号を求める計算式

ディレクトリエントリ
- ディレクトリエントリの構造体
名前 | Offset | Size | 解説 |
---|---|---|---|
DIR_Name | 0 | 11 | 短いファイル名のボディと拡張子 |
DIR_Attr | 11 | 1 | ファイルアトリビュート 次のフラグのコンビネーションで表現される。上位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_NTRes | 12 | 1 | 短いファイル名の小文字情報を記録するフラグ(オプション) 0x08: ボディがすべて小文字 0x10: 拡張子がすべて小文字 |
DIR_CrtTimeTenth | 13 | 1 | DIR_CrtTimeのサブセコンド情報 DIR_CrtTimeの分解能は2秒であるが、それをさらに200分割した値(0~199)が入る サポートしない場合は、作成時に0を設定し、以降変更しない |
DIR_CrtTime | 14 | 2 | このファイルが作成された時刻(オプション) サポートしない場合は、作成時に0を設定し、以降変更しない |
DIR_CrtDate | 16 | 2 | このファイルが作成された日付(オプション) サポートしない場合は、作成時に0を設定し、以降変更しない |
DIR_LstAccDate | 18 | 2 | このファイルを最後にオープンした日付(オプション) 時刻情報は無い 書き込みアクセスの場合はDIR_WrtDateと同じ値を設定すべきである サポートしない場合は、作成時に0を設定し、以降変更しない |
DIR_FstClusHI | 20 | 2 | このファイルの先頭クラスタ番号の上位16ビット FAT12/16では常に0 |
DIR_WrtTime | 22 | 2 | このファイルが最後に書き込まれた(書き込み後クローズされた)時刻 サポート必須 |
DIR_WrtDate | 24 | 2 | このファイルが最後に書き込まれた日付 サポート必須 |
DIR_FstClusLO | 26 | 2 | このファイルの先頭クラスタ番号の下位16ビット ファイルサイズが0のときは常に0 |
DIR_FileSize | 28 | 4 | このファイルのバイト単位のサイズ ディレクトリの場合は常に0 |
- FATファイルシステムのファイル属性
フラグ | 解説 |
---|---|
ATTR_READ_ONLY | 書き込み禁止 このファイルへの書き込みや消去は、拒否されるべきである |
ATTR_HIDDEN | 通常のディレクトリ表示でこのファイルを表示しない(扱いはシステム依存) |
ATTR_SYSTEM | システム関連の重要ファイルであることを示す(扱いはシステム依存) |
ATTR_DIRECTORY | このファイルはサブディレクトリのコンテナである |
ATTR_ARCHIVE | ファイルへの何らかの変更が発生したとき、FATドライバによってセットされる 主にバックアップツールが変更されたファイルを見つけるために使用される |
ATTR_VOLUME_ID | この属性を持つエントリは、ルートディレクトリに1個だけ存在できる このエントリの持つ名前は、そのボリュームのラベル名であるDIR_FstClusHI、DIR_FstClusLOおよびDIR_FileSizeは常に0でなければならない このフラグは常に単独使用であるが、一部のシステムではATTR_ARCHIVEを立てることがある |
ATTR_LONG_NAME | このフラグの組み合わせは、そのエントリが長いファイル名の一部であることを示す このフラグは常に単独使用する |

ボリュームを読み出す
ボリューム情報を読み出すためにすること
- BPBからルートディレクトリの位置を取得する
- そこからディレクトリエントリの構造に従ってデータを読み出す
まずはブートローダーの修正か

なぜかDocker環境だとインクルードパスが一部上手く設定できないから直書きと
#include "../kernel/elf.hpp"
#include "../kernel/frame_buffer_config.hpp"
#include "../kernel/memory_map.hpp"
あとは、ブートローダーのビルドと。。
$ unlink MikanLoaderPkg # 前に残っていたシンボリックリンクを削除
$ ln -s /mnt/30DayOS/workspace/ch17/MikanLoaderPkg/ ./
$ source edksetup.sh
$ build
これでビルド環境は整ったと

そうか。OS自作だからストレージへのマウントを含めてドライバがないのか。まずはそこを記述しないとか。
24.2 Block I/O Protocol Implementations これだな。あとはファイル読み書き@フルスクラッチで作る!UEFIベアメタルプログラミング

企画書だと13.5 File Protocolかな
メモリマップとkernel.elfと同じようにボリュームディスクの fat_disk
を読み出そうとしているんだな。
たしかにこの振る舞いは難しい
EFI_BLOCK_IO_PROTOCOL* block_io;
status = OpenBlockIoProtocolForLoadedImage(image_handle, &block_io);
if (EFI_ERROR(status)) {
Print(L"failed to open Block I/O Protocol: %r\n", status);
Halt();
}
EFI_BLOCK_IO_MEDIA* media = block_io->Media;
UINTN volume_bytes = (UINTN)media->BlockSize * (media->LastBlock + 1);
if (volume_bytes > 16 * 1024 * 1024) {
volume_bytes = 16 * 1024 * 1024;
}
Print(L"Reading %lu bytes (Present %d, BlockSize %u, LastBlock %u)\n",
volume_bytes, media->MediaPresent, media->BlockSize,
media->LastBlock);
status = ReadBlocks(block_io, media->MediaId, volume_bytes, &volume_image);
if (EFI_ERROR(status)) {
Print(L"failed to read blocks: %r\n", status);
Halt();
}

EFI_BOOT_SERVICESは4.4 EFI Boot Services Table か
Contains a table header and pointers to all of the boot services.
ブートローダーの情報が入っているんだな。そこからブロックデバイス情報を抽出か
関数としては
- Loadred Image Protocolをオープンする
- Loadred Image Protocolから得た値を使ってBlock I/O Protocolを開く
このあたりの関数の呼び出しは仕様書を読まないといけなないな

あとは、ブートローダーで取得したボリュームイメージをカーネル側でいじると
// dump_volume
uint8_t* p = reinterpret_cast<uint8_t*>(volume_image);
printk("Volume Image:\n");
for (int i = 0; i < 16; ++i) {
printk("%04x:", i * 16);
for (int j = 0; j < 8; ++j) {
printk(" %02x", *p);
++p;
}
printk(" ");
for (int j = 0; j < 8; ++j) {
printk(" %02x", *p);
++p;
}
printk("\n");
}
//--------------------------------------------
ほう。ボリュームイメージの先頭アドレスから順に書き出しているだけか
これが edk2\disk.img
の内容になっているんだな

lsコマンド
ついにlsコマンドだ

lsコマンドと
} else if (strcmp(command, "ls") == 0) {
auto root_dir_entries = fat::GetSectorByCluster<fat::DirectoryEntry>(
fat::boot_volume_image->root_cluster);
auto entries_per_cluster = fat::boot_volume_image->bytes_per_sector /
sizeof(fat::DirectoryEntry) *
fat::boot_volume_image->sectors_per_cluster;
char base[9], ext[4];
char s[64];
for (int i = 0; i < entries_per_cluster; ++i) {
ReadName(root_dir_entries[i], base, ext);
if (base[0] == 0x00) {
break;
} else if (static_cast<uint8_t>(base[0]) == 0xe5) {
continue;
} else if (root_dir_entries[i].attr == fat::Attribute::kLongName) {
continue;
}
if (ext[0]) {
sprintf(s, "%s.%s\n", base, ext);
} else {
sprintf(s, "%s\n", base);
}
Print(s);
}
}
- ルートディレクトリの先頭ポインタの取得
- 中身を先頭から読み出す
ここでは1クラスタに格納できる32個のファイルまでを呼び出せる
これで個々のファイル名を取得しているのか
void ReadName(const DirectoryEntry& entry, char* base, char* ext) {
memcpy(base, &entry.name[0], 8);
base[8] = 0;
for (int i = 7; i >= 0 && base[i] == 0x20; --i) {
base[i] = 0;
}
memcpy(ext, &entry.name[8], 3);
ext[3] = 0;
for (int i = 2; i >= 0 && ext[i] == 0x20; --i) {
ext[i] = 0;
}
}

FATファイルシステムのしくみと操作法の以下の部分を表現しているんだな
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形式に入らないのはダメ

今回はこのルールを適用しているのか
"DOG.AVI" "DOG AVI" 8+3バイトに満たないときはスペースでパディング
"File.Txt" "FILE TXT" ASCII小文字は全て大文字に置き換えられる
"file.TXT" "FILE TXT" ↑に同じ
だから表示されているファイルリストが大文字なのか。なるほど

上のサイトから引用

これに従ってクラスタの先頭アドレスを出しているんだな
uintptr_t GetClusterAddr(unsigned long cluster) {
unsigned long sector_num =
boot_volume_image->reserved_sector_count +
boot_volume_image->num_fats * boot_volume_image->fat_size_32 +
(cluster - 2) * boot_volume_image->sectors_per_cluster;
uintptr_t offset = sector_num * boot_volume_image->bytes_per_sector;
return reinterpret_cast<uintptr_t>(boot_volume_image) + offset;
}

難しいな。でも、このあたりは規定のルールに従ってだからまったく見通しがつかないってわけでないな