FAT16ファイルシステムを手書きで作る

## 「手探りでCUI OS作成に挑む」連載
この記事は「手探りでCUI OS作成に挑む」連載の一部です。
全体の目次は[「手探りでCUI OS作成に挑む」連載目次](https://qiita.com/earthen94/items/b5d77f98f0cf85a13346)を御覧下さい。

## 前回やったこと
前回MBR・VBRを自作してHDDへ書き込み、Windows上でFAT形式のディスクとして認識されることに成功しました。
しかし実際にはファイルを保存する仕組みは実装しておらず、中身のないはりぼてでファイルを書き込めない状態でした。
https://qiita.com/earthen94/items/958fbd1a52031123bc0b

ソースを再掲します。
<details>
<summary>mbr.asm</summary>

```mbr.asm
[BITS 16]
[ORG 0x7C00]

start:
    ; 1. MBRを安全な領域（0x0600）へ退避
    xor ax, ax
    mov ds, ax
    mov si, 0x7C00
    mov di, 0x0600
    mov cx, 512
    rep movsb

    ; 2. セグメントレジスタの初期化
    xor ax, ax
    mov ds, ax
    mov es, ax
    mov ss, ax
    mov sp, 0x7C00
    
    ; 0x0600にジャンプして処理を続行
    jmp 0x0000:(continue - start + 0x0600)

continue:

    mov si, msg_loading
    call print_string

    ; 4. VBRを0x7C00に読み込む（このコードを上書き）
    mov ah, 0x02
    mov al, 1       ; 読み込むセクタ数
    mov ch, 0       ; シリンダ番号
    mov cl, 1       ; セクタ番号（1〜63）
    mov dh, 1       ; ヘッド番号
    mov dl, 0x80    ; 一台目のHDDを読み込む
    mov bx, 0x7C00
    int 0x13
    jc disk_error

    ; 5. 0x7C00に読み込んだVBRへ跳ぶ
    jmp 0x0000:0x7C00

print_string:
    lodsb
    or al, al
    jz .done
    mov ah, 0x0E
    int 0x10
    jmp print_string
.done:
    ret

disk_error:
    mov si, msg_disk_error
    call print_string
    jmp $

msg_loading db "Loading VBR...", 0
msg_disk_error db "Disk read error!", 0

; MBRの残りを埋める
times 446-($-$$) db 0

; パーティションテーブル（FAT16のパーティション）
db 0x80               ; アクティブパーティション
db 0x01, 0x01, 0x00   ; CHS開始位置（0,1,1）
db 0x04               ; パーティションタイプ（FAT16）
db 0xFF, 0xFF, 0xFF   ; CHS終了位置（最大値）
dd 63                 ; 開始LBA（63セクタ目）
dd 4096               ; セクタ数（約2MB）

; 残りの3つの空パーティションを埋める
times 16 * 3 db 0

dw 0xAA55
```
</details>

<details>
<summary>vbr.asm</summary>

```vbr.asm
[BITS 16]
[ORG 0x7C00]  ; VBRはアドレス0x7C00に読み込まれる

; FAT16のBPB
jmp start
nop
db "MSWIN4.1"     ; OEM名
dw 512           ; セクタあたりのバイト数
db 1             ; クラスタあたりのセクタ数
dw 1             ; 予約セクタ数
db 2             ; FATテーブルの数
dw 512           ; ルートディレクトリ項目数
dw 4096          ; 総セクタ数（16ビット）
db 0xF8          ; 媒体種別（HDD等）
dw 8             ; 各FATのセクタ数
dw 63            ; トラックあたりのセクタ数
dw 255           ; ヘッド数
dd 63            ; 隠しセクタ数
dd 0             ; 総セクタ数（32ビット、16ビットが0の場合に使用）
db 0x80          ; BIOSドライブ番号（0x80＝HDD）
db 0             ; 予約（使用されない）
db 0x29          ; 拡張ブート用
dd 0x12345678    ; ボリューム番号
db "FAT16DISK "  ; ボリュームラベル
db "FAT16   "    ; ファイルシステムの種類

start:

    mov si, msg_loaded
    call print_string

    ; FAT16ファイルシステムのマウント（今後の拡張用）
    jmp $       ; 無限ループ（実際にはファイルシステムを読み込む）

print_string:
    lodsb
    or al, al
    jz .done
    mov ah, 0x0E
    int 0x10
    jmp print_string
.done:
    ret

msg_loaded db "FAT16 VBR Loaded!", 0

; 残りの領域を512バイトまで埋める
times 510-($-$$) db 0

dw 0xAA55

```
</details>



## FAT表及びルートディレクトリ
バイナリで書くのは骨が折れるため、NASMを用いてデータを生成してDD命令でHDDへ書き込みました。
```fat16_init.asm
; FAT表及びルートディレクトリを初期化する。
; DDで64セクタ目からの位置に書き込む。
; 固定でKERNEL.BINを一つ登録しておく。
; 前提：
;   - MBRは0セクタ目に設置
;   - VBRは63セクタ目に設置（パーティションはMBR中で63セクタ目から始まるように設定してある）
;   - FATの情報は64から始まる（VBRの直後に設置）

bits 16

; ------------------------------
; 設定値。VBRと矛盾しないよう注意
; ------------------------------
sectors_per_fat    equ 8      ; 一つのFAT表は8セクタを占める
bytes_per_sector   equ 512    ; 1セクタは512バイト
root_dir_entries   equ 512    ; ルートディレクトリに登録可能なファイル数
kernel_start_cluster equ 2    ; KERNEL.BINが始まるクラスタ番号
kernel_size        equ 65536  ; ファイルの大きさ（バイト）
%define cluster_count (kernel_size / bytes_per_sector) ; KERNEL.BINが何セクタを占めるか

; ------------------------------
; FAT1表LBA 64〜始まる　(DDでこの場所に置く)
; ------------------------------
fat1:
    ; 初めの2クラスタ分は実質固定
    dw 0xFFF8 ; F8はHDD、FF固定
    dw 0xFFFF ; ファイルの最後のクラスタの目印

    ; KERNEL.BINクラスタの連なり（2 → 3 → ... → EOF）
    %assign i kernel_start_cluster
        %rep cluster_count
        %if i == kernel_start_cluster + cluster_count - 1 ;最後のクラスタには目印0xFFFFを置く
            dw 0xFFFF
        %else
            dw i + 1
        %endif
        %assign i i + 1
    %endrep

    ; FAT表の剰余分を0で埋める (8セクタ×512バイト-既に書き込んだ分のバイト数)
    times sectors_per_fat * bytes_per_sector - ($ - fat1) db 0

; ------------------------------
; FAT2表（FAT表1の予備　FAT1の直後）簡略化の為に0埋め
; ------------------------------
fat2:
    ; 表1個8セクタ×512バイト/表
    times sectors_per_fat * bytes_per_sector db 0

; ------------------------------
; ルートディレクトリ（FAT2の直後，LBA 80）
; ------------------------------
root_dir:
    ; KERNEL.BINの登録（32字节）
    db 'KERNEL  BIN'          ; ファイル名（8.3形式）
    db 0x20                   ; 属性（0X20は通常のファイルを意味する）
    db 0                      ; 保留
    db 0                      ; 作成時間（ミリ秒）
    dw 0x0000                 ; 作成時間（16:00:00）
    dw 0x2100                 ; 作成日時（2023-01-01）
    dw 0x2100                 ; 最終変更日時
    dw 0                      ; EA索引
    dw 0x0000                 ; 最終変更時間
    dw 0x2100                 ; 最終変更日時
    dw kernel_start_cluster   ; 開始クラスタ
    dd kernel_size            ; ファイルの大きさ（バイト）

    ; ルートディレクトリが32セクタとなるように0で埋める。
    times root_dir_entries * 32 - ($ - root_dir) db 0
```
## ルートディレクトリ
ルートディレクトリの中には以下の用にファイルやディレクトリが登録されています。
開始クラスタを元にFAT表を参照してファイルを読み込みます
| 項目番号 | ファイル名     | 開始クラスタ | ファイルの大きさ | 意味・備考               |
|--------------|----------------|---------------|----------------|--------------------------|
| 0            | FILEA.TXT      | 2             | 3072           | 3クラスタ分（1024B×3）   |
| 1            | FILEB.BIN      | 5             | 2048           | 2クラスタ分（1024B×2）   |
| 2〜511       | （未使用）     | 0x0000        | 0              | 空き             |


## FAT表に関して
以下の用に一つのファイルが複数の塊(クラスタ)に分かれて鎖の用に連なった形式で登録されています。
FAT登録値の位置のデータを読み込み繋げることで記憶媒体の各地に分かれて保存されているファイルを復元できます。
この例では分かり易くする為に連続して保存されている例を示しています。
| クラスタ番号 | FAT登録値 | 意味                                 |
|--------------|---------------|--------------------------------------|
| 0            | 0xFFF8        | 媒体識別（予約）                 |
| 1            | 0xFFFF        | 予約                                 |
| 2            | 0x0003        | ファイルAの先頭 → 次はクラスタ3     |
| 3            | 0x0004        | ファイルAの中間 → 次はクラスタ4     |
| 4            | 0xFFFF        | ファイルAの終端                      |
| 5            | 0x0006        | ファイルBの先頭 → 次はクラスタ6     |
| 6            | 0xFFFF        | ファイルBの終端                      |
| 7            | 0x0000        | 空きクラスタ                         |
| 8            | 0x0000        | 空きクラスタ                         |


## 書き込み命令
```
# コンパイル
nasm -f bin mbr.asm -o mbr.bin
nasm -f bin vbr.asm -o vbr.bin

dd if=/dev/zero of=virtual_disk.img bs=1M count=1024  # 1GBの仮想HDD生成

# MBRを先頭512バイトへ書き込み
dd if=mbr.bin of=virtual_disk.img bs=512 count=1 conv=notrunc

# VBRを63セクタ目へ書き込み「第1パーティションの先頭512バイト」
dd if=vbr.bin of=virtual_disk.img bs=512 seek=63 conv=notrunc

# >>>>>>>>今回追加>>>>>>>>

# コンパイル
nasm -f bin ​fat16_init.asm -o fat16_init.bin
# 64セクタ目〜の領域へ書き込み
dd if=fat16_init.bin of=virtual_disk.img bs=512 seek=64 conv=notrunc

# <<<<<<<<今回追加<<<<<<<<

```

## 認識されるかの確認
WindowsXPでの検証コマンド
```
  qemu-system-i386 \
  -m 1024 \
  -hda xp.img \
  -hdb virtual_disk.img \
  -boot c \
  -cpu host \
  -smp 2 \
  -net nic -net user \
  -vga std \
  -rtc base=localtime \
  -enable-kvm \
  -usbdevice tablet
```
Eドライブの中にKERNEL.BINのファイルが入っていることが確認できます。
![图片.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4054974/bc8d8284-90b4-4024-8a74-6df78026c013.png)

## 今後の予定
次回は実際にKERNEL.BINの中でOSの中核部分を実装し、VBRから起動する処理を実装します。
MSDOSのDIRコマンドのようなファイル一覧を表示できる機能をまず実装できればと考えています。
サブディレクトリは少し複雑なため、一旦飛ばします。