0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

「手探りでCUI OS作成に挑む」実装篇 FAT12 ファイル一覧表示

Posted at

「手探りでCUI OS作成に挑む」連載

この記事は「手探りでCUI OS作成に挑む」連載の一部です。
全体の目次は「手探りでCUI OS作成に挑む」連載目次を御覧下さい。

初めに

この記事では以下のようにFAT12のファイル一覧を表示する機能を実装します。

TEST.TXT 12
TEST2.TXT 45

段階的に実装するため、今回はルートディレクトリ上のファイル及びディレクトリの表示のみに止め、ディレクトリ内部の構造は表示できません。

開発中のOSコードにいきなり追加すると可読性が下がるため、一旦個別に開発し、その後結合するつもりです。

予備知識

フロッピーはシリンダ、ヘッダ、セクタを指定して読み書きを行いますが、ここに関しては割愛させて下さい。
詳細については、インターネット上に多くの解説図や資料がありますので、そちらも参考にしてみてください。

FAT12とは

特徴と用途

FAT12は主にフロッピーに使われていた形式です。
現在でもUSB等で拡張形のFAT32が広く使われています。

なぜ必要なのか

簡単にいうとフロッピー上に保存されたファイルの管理の仕組みです。
元々境目のない土地を便宜上地番で区切って、登記簿に登録することで個人や法人が土地を所有できる仕組みに似ています。

全てのデータは0と1の羅列です。
モールス信号のようなもので読み出す時に法則を決めて都合よく解釈しているだけです。
フロッピー、HDDなどの記憶装置は0と1の羅列しか記憶できず境目もありません。
写真や文章などのファイルを保存する場合、当然どこかに保存される訳ですが、それを見つけられないと再生はできません。
しかも一つのデータは連続した場所にずらっと保存される訳ではなく幾つもの欠片に分けて空いている場所へ保存されます。

具体的に言うと0011010100101010100101010101......0010101010001010101111010101001
のように連続した0と1の羅列の中から欲しいデータを探しださなければなりません。

そうすると、管理台帳のようなものにファイルの一覧、そして各ファイルの欠片の分布表が記載された番地分布表のようなものが必要です。

便宜上、固定の長さ(今回は512バイト、即ち512×8個の0または1)毎に番地を振ります。
そして管理台帳には写真1、音楽1、写真2のデータが登録されているとします。
番地分布表には写真1の欠片は○番地、○番地、......、音楽1は、○番地、○番地、......、等のようにどこに分散しているかが記載されています。
この台帳及び番地分布表を参照すると各地に分散していても目的のファイルを復元することができます。

幾つもの欠片に分けて保存するのは複雑さが増すだけかとも思われるかもしれませんが、こうしないとまずいことが起きます。
例えば1GB分の細かいデータを保存して、500MB分消したとします。
しかし空いた領域は連続しているとは限らず小さな隙間が大量にある状態となり、せっかく空いた領域に大きなデータを保存できなくなってしまいます。
またある程度まとまった区域毎に番地を振らないと番地分布表に登録ができません。

そしてどんなメーカーの製品でもフロッピーを読み込めるように、管理台帳、番地分布表の置かれる場所、形式は全て決まっています。その1つがFAT12です。
各社が独自の規格で開発してしまったら互換性が全くなくなってしまいます。

仕組み

FAT12形式でフロッピーを初期化するとFAT表、目録が作られます。
上で説明した番地分布表がFAT表、目録が管理台帳に当たります。

今回はファイル一覧しか実装しないため、目録の説明のみに止めます。
ファイルの中身を再生する時に初めてFAT表が必要となります。

FAT12で初期化(USBメモリをフォーマットするのと同じです)すると目録はフロッピー上の19セクタ目から32セクタ目までの14セクタに生成されます。

目録の構造

FAT表は今回使用しないため割愛します。
1項目=32ビット、全224項目

オフセット サイズ (バイト) 内容 説明
0x00 8 ファイル名(8文字) 満たない場合は空白埋め。大文字英数字。
0x08 3 拡張子(3文字) 満たない場合は空白埋め。
0x0B 1 属性 ファイル属性ビット
0x0C 1 予約 常に0、使用不可
0x0D 1 作成時間(10ミリ秒単位) 通常は無視される
0x0E 2 作成時間 16bit DOS形式時間
0x10 2 作成日 16bit DOS形式日付
0x12 2 最終アクセス日 ディレクトリでは重要(更新用)
0x14 2 高位クラスタ番号(FAT32用) FAT12では無視してよい
0x16 2 最終更新時刻 通常の更新時刻
0x18 2 最終更新日 通常の更新日付
0x1A 2 ファイルの開始クラスタ番号 FATのチェーン開始位置
0x1C 4 ファイルサイズ(バイト) ディレクトリなら0、ファイルサイズ

実装

完全なコード

boot.asm
boot.asm
org 0x7C00
bits 16

jmp short start
nop

; FAT12 BIOSパラメータブロック (BPB)
OEMLabel        db "MYFAT12 "  ; 8バイト、スペースで埋める必要あり
BytesPerSector  dw 512         ; 1セクタあたりのバイト数
SectorsPerCluster db 1         ; クラスタあたりのセクタ数
ReservedSectors dw 1           ; 予約セクタ数
NumberOfFATs    db 2           ; FATテーブルの数
RootDirEntries  dw 224         ; ルートディレクトリエントリ数(224エントリ)
TotalSectors    dw 2880         ; 総セクタ数(2880セクタ=1.44MB)
MediaDescriptor db 0xF0        ; メディア記述子(0xF0=3.5インチフロッピー)
SectorsPerFAT   dw 9           ; FATテーブルあたりのセクタ数(9セクタ)
SectorsPerTrack dw 18          ; 1トラックあたりのセクタ数
HeadsPerCylinder dw 2          ; 1シリンダあたりのヘッド数
HiddenSectors   dd 0           ; 隠しセクタ数
LargeSectors    dd 0           ; LBA用総セクタ数(未使用)
DriveNumber     db 0           ; ドライブ番号(0=フロッピー)
Reserved        db 0           ; 予約領域
ExtendedBootSig db 0x29        ; 拡張ブートシグネチャ
VolumeSerial    dd 0x12345678  ; ボリュームシリアル(任意の値)
VolumeLabel     db "MYVOLUME   " ; 11バイトのボリュームラベル
FileSystem      db "FAT12   "  ; 8バイトのファイルシステムタイプ

start:
    xor ax, ax          ; AXレジスタをゼロ初期化
    mov ds, ax          ; データセグメントを0に設定
    mov es, ax          ; エクストラセグメントを0に設定
    mov ss, ax          ; スタックセグメントを0に設定
    mov sp, 0x7C00      ; スタックポインタをブートローダ位置に設定

    ; CHSパラメータを使用してLBA 100を直接読み込み
    mov ah, 0x02         ; BIOS ディスク読み込み機能
    mov al, 1            ; 読み込むセクタ数(1セクタ)
    mov ch, 2            ; シリンダ番号=2(LBA 100に対応する計算値)
    mov cl, 11           ; セクタ番号=11(LBA 100に対応する計算値)
    mov dh, 1            ; ヘッド番号=1(LBA 100に対応する計算値)
    mov dl, 0x00         ; ドライブ番号(0x00=フロッピードライブA)
    mov bx, 0x7E00       ; ES:BX = 読み込み先アドレス 0x0000:0x7E00
    int 0x13            ; ディスクサービス割り込み
    jc disk_error       ; エラー発生時はdisk_errorへジャンプ

    jmp 0x0000:0x7E00   ; 読み込んだプログラムへジャンプ

disk_error:
    jmp $

times 510-($-$$) db 0
dw 0xAA55
load.asm
load.asm
org 0x7E00

;mov si, debug_msg
;call print_str

call load_root_directory
call list_root_directory

mov si, debug_msg_hlt
call print_str
cli
hlt


; --- 目録をES:BXへ読み込む ---
load_root_directory:
    ; ES:BXの値を1000:0000に初期化する
    mov ax, 0x1000     ; ES = 0x1000
    mov es, ax
    xor bx, bx         ; ES:BX = 1000:0000

    mov si, 0          ; sector index (0 ~ 13)
.load_sector_loop:
    mov ax, 19         ; 19セクタ目から読み込む
    add ax, si

    push si

    call read_sect_lba ; 1セクタ分ES:BXへ読み込む

    pop si
    add bx, 512        ; 512バイト足すと次のES:BXがセクタを指す(1セクタ=512バイト)
    inc si             ; 読み込みセクタ数+1
    cmp si, 14         ; 14セクタ分読み込んだか調べる
    jl .load_sector_loop ; 14セクタに達していない場合は次のセクタを読み込む

    ret

; --- 目録の内容を画面に表示する ---
; 入力: 目録は既に0x1000:0000へ読み込まれている
; 出力: 画面上にファイル及びディレクトリを表示
list_root_directory:
    mov si, 0          ; 現在読み込み中の項目(目録の項目は0~223)
    mov di, 0          ; 目録の相対アドレス(オフセット)(0 ~ 32*224)

.next_entry:
    ; ES:DI 現在の目録項目
    ; 項目1バイト目が0x00であればその下には他のファイル無し,0xE5は削除済み
    mov al, byte [es:di]
    cmp al, 0x00
    je .done           ; 最後のファイル、読み込み終了
    cmp al, 0xE5
    je .skip_entry     ; 削除済み、跳ばす

    ; 各項目11バイト目の属性を確認
    mov al, byte [es:di+11]
    test al, 0x08
    jnz .skip_entry    ; ドライブ自体の情報は表示しない為、跳ばす

    ; ファイル名表示(ファイル名8+拡張子3) 例:FILE.TXT
    push si
    push di
    
    mov ax, es      ; ES保存
    push ax

    mov si, di
    add si, 0x00         ; ファイル名
    call print_filename  ; ファイル名表示
    
    pop ax          
    mov es, ax

    ; 属性を見てディレクトリかどうかを確認
    mov al, byte [es:di+11]
    test al, 0x10
    jnz .print_dir

    ; ファイルの大きさ(0x1C-0x1F)
    mov ax, word [es:di+0x1C]
    call print_num

    jmp .done_print

.print_dir:
    mov si, msg_dir
    call print_str

.done_print:
    ;call print_hex
    call print_newline

    pop di
    pop si

.skip_entry:
    add di, 32
    inc si
    cmp si, 224
    jl .next_entry

.done:
    ;mov si, debug_msg_done
    ;call print_str
    ret

; --- 8+3ファイル名表示 ---
; 入力: ES:SIが指している項目のファイル名を表示
; 出力: FILE.TXTのような形式で画面表示
print_filename:
    ; DS:SI ファイル名を指している
    push cx
    push ax
    push es
    push ds
    
    push es
    pop ds       ; DS = ES
    
    mov cx, 8
.next_char1:
    mov al, byte [es:si]
    cmp al, ' '      ;8バイト未満は空白で埋められるので、空白は表示しない
    je .skip_char1
    call print_char  ;空白でなければ画面に表示する
.skip_char1:
    inc si
    loop .next_char1

    mov al, '.'      ;例:FILE.TXTの'.'
    call print_char

    ;拡張子表示(処理はファイル名と同じ)
    mov cx, 3
.next_char2:
    mov al, byte [es:si]
    cmp al, ' '
    je .skip_char2
    call print_char
.skip_char2:
    inc si
    loop .next_char2

    mov al, ' '
    call print_char

    pop ds
    pop es
    pop ax
    pop cx
    ret

; --- 指定されたセクタをES:BXへ読み込む ---
read_sect_lba:
    ; 入力: AX = 読み込むセクタのアドレス
    ; 出力: 1セクタ分データを読み込み`ES:BX`へ読み込む

    pusha
    
    ;mov si, debug_msg_reading_sector
    ;call print_str

    ; LBA→CHS変換 フロッピー一周18セクタ、円盤1枚2面
    mov cx, 36           ; 1シリンダ36セクタ
    xor dx, dx
    div cx               ; AX = シリンダ番号,DX = 余剰セクタ (0-35)

    mov ch, al           ; CH = シリンダ番号
    mov ax, dx           ; AX = 余剰セクタ (0-35)
    mov cl, 18
    div cl               ; AL = ヘッダ番号(0或は1),AH = 余剰セクタ (0-17)

    mov dh, al           ; DH = ヘッダ番号
    mov cl, ah
    inc cl               ; セクタは0でなく1番から始まる(BIOS仕様)
    mov dl, 0            ; フロッピーは0を指定

    mov ax, 0x0201       ; AH=0x02(セクタ読み込み),AL=1(1セクタ分読み込み)
    int 0x13
    jc .error

    ;mov si, debug_msg_sector_ok
    ;call print_str

    popa
    ret

.error:
    mov si, disk_error_msg
    call print_str
    cli
.halt_loop:
    hlt
    jmp .halt_loop

debug_msg          db 'Stage2: Booting from floppy...', 0x0D, 0x0A, 0
debug_msg_done     db 'Root directory scan completed.', 0x0D, 0x0A, 0
debug_msg_hlt      db 'System halted. Remove disk.', 0x0D, 0x0A, 0
debug_msg_entering_load db 'Entering load_root_directory...', 0x0D, 0x0A, 0
debug_msg_loaded db 'Root directory loaded!', 0x0D, 0x0A, 0
disk_error_msg db 'Disk error! Status=', 0
debug_msg_reading_sector db 'Reading LBA=', 0
debug_msg_sector_ok db ' - OK', 0x0D, 0x0A, 0

; --- 1文字表示(AL = ASCII)---
; 入力: AL = 表示する文字のASCIIコード
print_char:
    pusha
    mov ah, 0x0E        ; BIOS teletype
    mov bx, 0x0007      ; ページ 0,色 7
    int 0x10
    popa
    ret

; --- 文字列表示 ---
; 入力: DS:SI = 終端文字0で終わる文字列
print_str:
    pusha
.next_char:
    lodsb               ; [DS:SI] を ALへ読み込む
    test al, al
    jz .done
    call print_char
    jmp .next_char
.done:
    popa
    ret

; --- 改行 ---
print_newline:
    push si
    mov si, newline_str
    call print_str
    pop si
    ret

newline_str db 0x0D, 0x0A, 0

; --- 10進数数字表示 ---
print_num:
    pusha
    mov cx, 0           

.divide_loop:
    xor dx, dx        ; DXを0に
    mov bx, 10        ; 除数10をBXに設定
    div bx            ; AX = DX:AX / 10, DX = 余り
    push dx           ; 余り(桁)をスタックに保存
    inc cx            ; 桁数カウンタを増加
    test ax, ax       ; AXが0かチェック
    jnz .divide_loop  ; 0でなければループ継続

.print_loop:
    pop ax            ; スタックから桁を取り出し
    add al, '0'       ; 数値→ASCII文字に変換
    call print_char   ; 文字を表示
    loop .print_loop  ; CXが0になるまでループ

    popa
    ret
    
msg_dir: db ' <DIR>', 0

load.asmの要となる処理

load_root_directory関数

FAT12で初期化すると自動的に19セクタ目 ~ 32セクタ目、計14セクタに目録が書き込まれ、
ファイルやディレクトリを追加するとこの目録に情報が追加される。
この目録をES:BXへ読み込むのが以下のプログラム。
実際にfloppyからデータを読み込む処理はread_sect_lbaの中で実装している。

load_root_directory:
    ; ES:BXの値を1000:0000に初期化する
    mov ax, 0x1000     ; ES = 0x1000
    mov es, ax
    xor bx, bx         ; ES:BX = 1000:0000

    mov si, 0          ; sector index (0 ~ 13)
.load_sector_loop:
    mov ax, 19         ; 19セクタ目から読み込む
    add ax, si

    push si

    call read_sect_lba ; 1セクタ分ES:BXへ読み込む

    pop si
    add bx, 512        ; 512バイト足すと次のES:BXがセクタを指す(1セクタ=512バイト)
    inc si             ; 読み込みセクタ数+1
    cmp si, 14         ; 14セクタ分読み込んだか調べる
    jl .load_sector_loop ; 14セクタに達していない場合は次のセクタを読み込む

    ret

read_sect_lba

FAT12では、データの読み書きにCHS(Cylinder-Head-Sector)方式が使われています。
LBA(Logical Block Addressing)はより高機能な方法で、セクタ番号だけでアクセスできる形式ですが、フロッピーでは対応していません。
将来的な拡張性を考慮し、本関数ではLBA形式でセクタ番号を受け取り、内部でCHS形式へ変換して処理を行っています。

※団地の一部屋を特定する場合の例
CHS:〇棟、〇階、〇番目の部屋
LBA:〇号室

この関数内ではセクタ番号から、シリンダ、ヘッダ、セクタを計算し、ES:BXへデータを読み込みます。

読み込むセクタ番号 LBA
一周のセクタ数 (​​SectorsPerTrack​​, SPT)
ヘッド数 (​​Heads​​, H)
$$
\text{シリンダ} = \left\lfloor \frac{\text{LBA}}{\text{SPT} \times H} \right\rfloor
$$

$$
\text{ヘッド} = \left\lfloor \frac{\text{LBA}}{\text{SPT}} \right\rfloor \bmod H
$$

$$
\text{セクタ} = (\text{LBA} \bmod \text{SPT}) + 1
$$

CHS変換の具体例:LBA = 19(フロッピーの19番目のセクタ)
  1. シリンダ (C) の計算

$$
C = \left\lfloor \frac{19}{18 \times 2} \right\rfloor = \left\lfloor \frac{19}{36} \right\rfloor = 0
$$

  1. ヘッド (H) の計算

$$
H = \left\lfloor \frac{19}{18} \right\rfloor \bmod 2 = 1 \bmod 2 = 1
$$

  1. セクタ (S) の計算

$$
S = (19 \bmod 18) + 1 = 1 + 1 = 2
$$

read_sect_lba:
    ; 入力: AX = 読み込むセクタのアドレス
    ; 出力: 1セクタ分データを読み込み`ES:BX`へ読み込む

    pusha
    
    ;mov si, debug_msg_reading_sector
    ;call print_str

    ; LBA→CHS変換 フロッピー一周18セクタ、円盤1枚2面
    mov cx, 36           ; 1シリンダ36セクタ
    xor dx, dx
    div cx               ; AX = シリンダ番号,DX = 余剰セクタ (0-35)

    mov ch, al           ; CH = シリンダ番号
    mov ax, dx           ; AX = 余剰セクタ (0-35)
    mov cl, 18
    div cl               ; AL = ヘッダ番号(0或は1),AH = 余剰セクタ (0-17)

    mov dh, al           ; DH = ヘッダ番号
    mov cl, ah
    inc cl               ; セクタは0でなく1番から始まる(BIOS仕様)
    mov dl, 0            ; フロッピーは0を指定

    mov ax, 0x0201       ; AH=0x02(セクタ読み込み),AL=1(1セクタ分読み込み)
    int 0x13
    jc .error

    ;mov si, debug_msg_sector_ok
    ;call print_str

    popa
    ret

.error:
    mov si, disk_error_msg
    call print_str
    cli
.halt_loop:
    hlt
    jmp .halt_loop

list_root_directory

目録を走査し一覧を表示する

; 入力: 目録は既に0x1000:0000へ読み込まれている
; 出力: 画面上にファイル及びディレクトリを表示
list_root_directory:
    mov si, 0          ; 現在読み込み中の項目(目録の項目は0~223)
    mov di, 0          ; 目録の相対アドレス(オフセット)(0 ~ 32*224)

.next_entry:
    ; ES:DI 現在の目録項目
    ; 項目1バイト目が0x00であればその下には他のファイル無し,0xE5は削除済み
    mov al, byte [es:di]
    cmp al, 0x00
    je .done           ; 最後のファイル、読み込み終了
    cmp al, 0xE5
    je .skip_entry     ; 削除済み、跳ばす

    ; 各項目11バイト目の属性を確認
    mov al, byte [es:di+11]
    test al, 0x08
    jnz .skip_entry    ; ドライブ自体の情報は表示しない為、跳ばす

    ; ファイル名表示(ファイル名8+拡張子3) 例:FILE.TXT
    push si
    push di
    
    mov ax, es      ; 保存ES
    push ax

    mov si, di
    add si, 0x00         ; ファイル名
    call print_filename  ; ファイル名表示
    
    pop ax          
    mov es, ax

    ; 属性を見てディレクトリかどうかを確認
    mov al, byte [es:di+11]
    test al, 0x10
    jnz .print_dir

    ; ファイルの大きさ(0x1C-0x1F)
    mov ax, word [es:di+0x1C]
    call print_num

    jmp .done_print

print_filename

; 入力: ES:SIが指している項目のファイル名を表示
; 出力: FILE.TXTのような形式で画面表示
print_filename:
    ; DS:SI ファイル名を指している
    push cx
    push ax
    push es
    push ds
    
    push es
    pop ds       ; DS = ES
    
    mov cx, 8
.next_char1:
    mov al, byte [es:si]
    cmp al, ' '      ;8バイト未満は空白で埋められるので、空白は表示しない
    je .skip_char1
    call print_char  ;空白でなければ画面に表示する
.skip_char1:
    inc si
    loop .next_char1

    mov al, '.'      ;例:FILE.TXTの'.'
    call print_char

    ;拡張子表示(処理はファイル名と同じ)
    mov cx, 3
.next_char2:
    mov al, byte [es:si]
    cmp al, ' '
    je .skip_char2
    call print_char
.skip_char2:
    inc si
    loop .next_char2

    mov al, ' '
    call print_char

    pop ds
    pop es
    pop ax
    pop cx
    ret

動作確認

フロッピーの内部構造

1セクタは512バイト。1.44MBフロッピーは、1,474,560バイト ÷ 512バイト/セクタ = 2880セクタ

用途 物理位置(LBAセクタ番号)
ブートセクタ(boot.bin) LBA 0
FAT1 表 LBA 1–9
FAT2 表(予備) LBA 10–18
ルートディレクトリ LBA 19–32(全14セクタ)
ファイル領域 LBA 33〜
load.bin LBA 100(FAT・ファイル領域を避けた任意配置)

FAT12で初期化しプログラム及びファイルを書き込む作業

Linux端末上で、FAT12形式の仮想フロッピーに、ブートローダとプログラムを配置し、QEMUで起動確認するまでの手順を記します。

nasm -f bin boot.asm -o boot.bin # コンパイル
nasm -f bin load.asm -o load.bin

# フロッピーイメージを作成(1.44MB = 512バイト × 2880セクタ)
dd if=/dev/zero of=testdisk.img bs=512 count=2880 

# FAT12ファイルシステムで初期化
# -F 12: FAT12形式を指定
# -v: 詳細情報を表示(verbose)
mkdosfs -F 12 -v testdisk.img

# ブートセクタを書き込み(最初の1セクタ)
dd if=boot.bin of=testdisk.img bs=512 count=1 conv=notrunc
# load.binは100セクタ目に配置
dd if=load.bin of=testdisk.img bs=512 seek=100 conv=notrunc

# 不要になった一時ファイルを削除
rm boot.bin
rm load.bin

# マウントポイントを作成
#sudo mkdir /mnt/floppy

# 作成したFAT12イメージをマウントして中身を編集
sudo mount -o loop,fat=12 testdisk.img /mnt/floppy

# 動作確認用ファイルを作成(ルートディレクトリにファイルとディレクトリを追加)
echo "Hello FAT12!" | sudo tee /mnt/floppy/test.txt
echo 'Hello FAT12!aaaaa' | sudo tee /mnt/floppy/test2.txt #2komeha singuru
sudo mkdir /mnt/floppy/TEST_DIR

# 書き込んだ結果を確認
ls -l /mnt/floppy

# マウント解除(編集完了後)
sudo umount /mnt/floppy

# QEMUでフロッピーイメージを起動して動作確認
qemu-system-i386 -fda testdisk.img

動作画面

image.png

詰まったところ

FAT12で初期化するとブートローダーが上書きされる→ブートローダー内でFAT12のヘッダー情報を定義。
フロッピーをLBA方式で読み込もうとして失敗。
CPUを停止させるためにjmp $と書き。ファンが激しく回る。→cli hltと書き換え解決。
関数内でレジスタの値が書き換えられ、原因が分からず詰まった。

最後に

説明が難しくうまく説明できていないところもあると思います。
ネット上にもっと分かり易い説明もあるので、興味のある方は調べてみて下さい。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?