x86リアルモードで動くモニタプログラムのHyfaxを作るお話の6回目です。今回は予告通りFATクラスタチェーンの追跡を取り込みたいと思います。
クラスタチェーン
FATはクラスタという単位で管理されています。クラスタは標準で4セクタ=0x800Byte=2048Byteになります。ここで、
- もしすべてのファイルサイズが2014Byte未満だったら
この場合は管理が簡単で、ディレクトリ管理領域にあるファイル名と開始LBAだけあれば内容にアクセスできます。 - もしファイルが1回書いたら消せないとしたら
この場合もファイル名と開始LBAがあればファイルにアクセスできます。
ところがわたしたちのやりたいことは、ファイルのサイズが自由に変えられて、削除追加ができることなんですね。例えば2048ByteのファイルAとB、4096ByteのファイルCがあったとします。ファイルA、Bと追加した後ファイルAを削除し、ファイルCを追加すると木のことを考えてみます。
ファイルAとファイルBを空きディスクに追加します。

すると感じになります。

次にファイルAを削除するとこう。

ここにファイルCを追加すると、空きエリアをできるだけ少なくするように(言い換えると効率的にディスクを使うために)ファイルCを分割して格納することになります。

結果としてディスクにはこのように格納されることになります。

このように異なるサイズのファイルを削除追加できるようにすると、場合によってファイルが分割され泣き別れになって格納されることになるわけです。このような場合、ファイルの先頭とサイズだけわかっていても全体を扱うことができません。ファイルが分割されたときに、その先がどこにあるかを記録しておく必要があります。これがFATです。FATでは特定のクラスタがどのクラスタにつながっているかが記録されています。開始クラスタから終端クラスタまでの単方向リストです。
あまりいい例ではないのですが、何度か見たディスクイメージのダンプを覗いてみます。

ディレクトリ領域は0x110800から始まります。最初のエントリはボリュームラベルです。『MONITOR BIN』のエントリは0x110820から始まり、開始クラスタが0x0002(エンジ色の部分)サイズが0x000008c5Byte(水色の部分)だとわかります。0x08c5=2245なので1クラスタを少し超えています。
次にFAT1を見ます。FATは2バイト=1エントリで、ゼロオリジンなので0x0002クラスタに対応する箇所は0x100804(エンジ色の部分)になります。そして1クラスタに入りきらないので次の空きクラスタに行くのですが、この例では隣が開いていたみたいでクラスタ0x0003に続いています。クラスタ0x0003ですべて格納できますのでクラスタ0x0003に続きはなく0xffff(EOF)で終了しています。
『MONITOR BIN」の次のファイルは『APP BIN』なのですが、同様に0x0004クラスタと0x0005クラスタの2クラスタ使用して格納されていることがわかります。
このような構造になっているのですが、使い方はそれほど複雑ではなく、ファイルの内容を読みに行くときにクラスタが終端ならばそこで終了、先があるなら先を読む。これを終端になるまで繰り返す。
言葉にしてしまえばこれだけなんです。
実装
コードにするとこんな感じになります。
前回のfind_dir_ent:から先を修正したものになります。
find_dir_ent:
; --- 一致 ---
; クラスタ・LBA変換
pop di
mov bx, es:[di+26] ; FirstCluster low
mov ax, es:[di+20] ; FirstCluster high
mov [TARGET_CLUS], bx ; ファイルのあるクラスタ
sub bx, 2
mov al, [SEC_PER_CLUS]
mov ah, 0
mul bx
mov bx, ax
mov ax, [DATA_LBA] ; データ開始LBA
add ax, bx
mov [FILE_LBA], ax ; ファイルのLBA
; ファイルサイズ・セクタ変換
mov ax, es:[di+28] ; File size low word
add ax, 511
shr ax, 9
mov [FILE_SECTOR], ax ; ファイルのセクタ数
mov cx, 0
file_read_loop:
PUTC '.'
mov si, dap
mov ax, [SEC_PER_CLUS]
mov word [si + DAP.NumBlocks], ax ; sectors
mov ax, [SEC_PER_CLUS]
mov bx, 0x200
mov [BYTE_PER_SEC], bx
mul bx
mul cx
add ax, MON_OFF
mov word [si + DAP.BufferOff], ax ; buffer offset
mov word [si + DAP.BufferSeg], MON_SEG ; buffer segment
mov ax, [FILE_LBA]
mov word [si + DAP.LBA_Low], ax ; LBA low Low
mov dword [si + DAP.LBA_High], 0 ; LBA high
mov dl, [BOOT_DRIVE]
mov ah, 0x42
int 0x13
jc disk_error
mov ax, [FILE_SECTOR]
sub ax, [SEC_PER_CLUS]
mov [FILE_SECTOR], ax
cmp ax, 0
jle exit_loop
inc cx
; ------------------------------------------------
; FAT16チェーンを辿って次のクラスタを取得する
; current cluster = [TARGET_CLUS]
; ------------------------------------------------
; ★ FAT16: エントリサイズ 2byte
; offset_bytes = cluster * 2
; FAT_sector = FAT_LBA + offset_bytes / BytesPerSec
; byte_offset = offset_bytes % BytesPerSec
; ------------------------------------------------
mov ax, [TARGET_CLUS] ; 現在のクラスタ番号
mov bx, 2
mul bx ; DX:AX = cluster * 2 (FAT内オフセット[byte])
mov bx, [BYTE_PER_SEC] ; 通常 512
div bx ; AX = FAT内セクタオフセット, DX = セクタ内byteオフセット
mov [FAT_TARGET_CLUS], ax ; FAT内セクタオフセットとして再利用
mov [FAT_TARGET_ENT], dx ; セクタ内byteオフセット
; FATセクタのLBA = FAT_LBA + FAT内セクタオフセット
mov ax, [FAT_LBA]
add ax, [FAT_TARGET_CLUS]
mov [TARGET_FAT_LBA], ax
; ------------------------------------------------
; FATセクタを1セクタだけBUFへ読み込み
; ------------------------------------------------
mov si, dap
mov word [si + DAP.NumBlocks], 1 ; FATは1セクタで十分
mov word [si + DAP.BufferOff], BUF_OFF
mov word [si + DAP.BufferSeg], BUF_SEG
mov ax, [TARGET_FAT_LBA]
mov word [si + DAP.LBA_Low], ax
mov dword [si + DAP.LBA_High], 0
mov dl, [BOOT_DRIVE]
mov ah, 0x42
int 0x13
jc disk_error
; ------------------------------------------------
; 読み込んだFATセクタから次クラスタ取得
; ------------------------------------------------
mov ax, BUF_SEG
mov es, ax
mov si, BUF_OFF
mov bx, [FAT_TARGET_ENT] ; セクタ内byteオフセット
add si, bx
mov ax, [es:si] ; FAT16なので2バイト読み
; ------------------------------------------------
; FAT16: 終端判定 (0xFFF8以上をEOFとみなす)
; ------------------------------------------------
cmp ax, 0xFFF8
jae exit_loop ; EOFなので読み込み終了
cmp ax, 0x0002
jb exit_loop ; 無効クラスタはとりあえず終了扱い
; ------------------------------------------------
; 次クラスタ → FILE_LBAへ変換
; clusterN → LBA = DATA_LBA + (clusterN - 2) * SEC_PER_CLUS
; ------------------------------------------------
mov [TARGET_CLUS], ax ; 現在クラスタを更新
mov bx, ax ; bx = clusterN
sub bx, 2
mov al, [SEC_PER_CLUS]
mov ah, 0
mul bx ; AX = (clusterN - 2) * SEC_PER_CLUS
mov bx, ax
mov ax, [DATA_LBA]
add ax, bx
mov [FILE_LBA], ax ; 次クラスタの先頭LBA
jmp file_read_loop
exit_loop:
ね?めんどくさいでしょ?
さてキリがいいところで今日はここまでです。実行結果は前回、前々回と変わらないので載せません。でも『結果が同じことを確認する』ことができるだけで、言い換えれば『ゴールが明確になる』だけで、かなり気持ち的に楽になります。
次回は『MONITOR BIN』に機能を追加しようかなとか思ってますが、どうなるかわかりません。未来のわたしに聞いてください♪