Closed21

ゼロからOS自作入門 17章

ackyacky

ファイルシステム

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

ddコマンド

「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 処理バイト数や処理速度を表示しない
ackyacky

mkfs.fat

create an MS-DOS filesystem under Linux

Linux環境下でFAT形式でフォーマットする。FATタイプも複数指定可能である

ackyacky

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

ackyacky

FAT形式での8文字を超えるファイルの記録形式がおもしろい

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

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_BytsPerSec \times BPB_SecPerClus が32Kバイトを越す値は使用すべきではない
BPB_RsvdSecCnt 14 2 予約領域のセクタ数
このフィールドは0であってはならない(少なくともこのBPBを含むブートセクタそれ自身が存在する)
互換性問題を避けるため、FAT12/16ボリュームでは1にするべきである
BPB_NumFATs 16 1 FATの数
このフィールドの値は常に2に設定すべきである。1以上の何らかの値もまた有効だが、互換性問題を避けるため2以外の値は使用しないことが強く推奨されている
BPB_RootEntCnt 17 2 FAT12/16ボリュームでは、ルートディレクトリに含まれるディレクトリエントリの数を示す
このフィールドには、ディレクトリテーブルのサイズが2セクタ境界にアライメントする値、つまり、 BPB_RootEntCnt \times 32 がBPB_BytsPerSecの偶数倍になる値を設定すべきである(32というのはディレクトリエントリ1個のサイズ)
最大の互換性のためには、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_FATSz32 * BPB_NumFATs セクタとなる
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。有効なブートセクタであることを示すブートシグネチャ

ルートディレクトリのブロック番号を求める計算式

BPB_RsvdSecCnt + BPB_NumFATs \times BPB_FATSz32 + (BPB_RootClus - 2) \times BPB_SecPerClus = 2064

ackyacky

ディレクトリエントリ

  • ディレクトリエントリの構造体
名前 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 このフラグの組み合わせは、そのエントリが長いファイル名の一部であることを示す
このフラグは常に単独使用する
ackyacky

ボリュームを読み出す

ボリューム情報を読み出すためにすること

  • BPBからルートディレクトリの位置を取得する
  • そこからディレクトリエントリの構造に従ってデータを読み出す

まずはブートローダーの修正か

ackyacky

なぜか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

これでビルド環境は整ったと

ackyacky

企画書だと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();
    }
ackyacky

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を開く

このあたりの関数の呼び出しは仕様書を読まないといけなないな

ackyacky

あとは、ブートローダーで取得したボリュームイメージをカーネル側でいじると

  // 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 の内容になっているんだな

ackyacky

lsコマンド

ついにlsコマンドだ

ackyacky

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;
  }
}
ackyacky

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形式に入らないのはダメ
ackyacky

今回はこのルールを適用しているのか

"DOG.AVI"         "DOG     AVI"     8+3バイトに満たないときはスペースでパディング
"File.Txt"        "FILE    TXT"     ASCII小文字は全て大文字に置き換えられる
"file.TXT"        "FILE    TXT"     ↑に同じ

だから表示されているファイルリストが大文字なのか。なるほど

ackyacky

これに従ってクラスタの先頭アドレスを出しているんだな

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;
}
ackyacky

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

このスクラップは2021/08/25にクローズされました