「手探りでCUI OS作成に挑む」連載
この記事は「手探りでCUI OS作成に挑む」連載の一部です。
全体の目次は「手探りでCUI OS作成に挑む」連載目次を御覧下さい。
ソース
やりたいこと
MSDOSの用なDIRコマンドを実装します。
いきなりサブディレクトリを実装するのは難しいため、一旦ルートディレクトリのみの実装とします。
よってcdコマンドを使ってフォルダ内のファイルを表示するといったことはまだ出来ません。
動作
今回追加したソース
ファイル | 関数名 | 機能 |
---|---|---|
driver/disk.asm |
read_multi_sector |
指定LBAから複数セクタ分をメモリへ読み込む |
filesystem/fat16.asm |
read_fat |
FAT表およびルートディレクトリをメモリ上へ配置 |
command/dir.asm |
dir |
ルートディレクトリを参照しファイル名を表示する |
今回ファイルの中身は参照しないためFAT表はメモリ上へ読み込むのみで使用しません。
検証用データ
今までkernel.binのみ仮想HDDに登録していましたが、TEST1.TXT及びTEST2.TXTをルートディレクトリに追加しました。
boot/fat16_init.asm
...省略...
; TEST1.TXTの登録
db 'TEST1 TXT' ; ファイル名(8.3形式)
db 0x20 ; 属性
db 0
db 0
dw 0x0000
dw 0x2100
dw 0x2100
dw 0
dw 0x0000
dw 0x2100
dw 100 ; 開始クラスタ番号(例:適当に100番とする)
dd 1234 ; ファイルサイズ(バイト)
; TEST2.TXTの登録
db 'TEST2 TXT'
db 0x20
db 0
db 0
dw 0x0000
dw 0x2100
dw 0x2100
dw 0
dw 0x0000
dw 0x2100
dw 110 ; 開始クラスタ番号
dd 5678 ; ファイルサイズ
...省略...
ソース
何セクタ目から何セクタ分をメモリの何番地へ読み込むかを指定して、HDD上のデータを読み込みます。
driver/disk.asm
driver/disk.asm
; read_multi_sector:
; 複数セクタの読み込み
; BX = LBA下位16bit (LBA 0-15)
; CX = LBA上位8bit (LBA 16-23)
; SI = 読み込みセクタ数
; ES:DI = 転送先アドレス
; 現状CXの値には対応しておらず65536セクタを越えると0に戻る
; 今後対応する
; 初めはread_sectorを呼び出さず直接複数セクタを呼び出す関数を作る
; 予定であったがどうしても1セクタ以上読み込まれずこの形になった。
read_multi_sector:
pusha ; 全レジスタ退避
.next_sector:
; 現在のLBAで1セクタ読み込み
call read_sector
jc .error ; エラーあれば終了
; 次のセクタへLBAインクリメント
inc bl ; LBA下位8bit++
cmp bl, 0
jne .skip_bh_inc
inc bh ; 下位16bitの上位8bit(bh)++
.skip_bh_inc:
; 転送先アドレスを512バイト進める
add di, 512
dec si
jnz .next_sector
popa
clc ; 成功フラグクリア
ret
.error:
mov si, disktest
call print_string
popa
stc ; エラーフラグセット
ret
; =========================================================
; read_sector - IDE HDDから1セクタ(512バイト)読み込み
;
; 機能:
; 指定したLBAアドレスから1セクタ(512バイト)を
; メモリ(ES:DI)へ転送する
;
; 呼び出し前の設定:
; BX = LBAアドレス 下位16ビット(LBA 0-15)
; CX = LBAアドレス 上位8ビット(LBA 16-23)
; ES:DI = 転送先メモリアドレス(512バイト以上の領域)
;
; 戻り値:
; CF = 0 正常終了
; CF = 1 エラー発生
;
; 補足:
; 常に512バイト固定の読み込み
; =========================================================
read_sector:
pusha
push ds
push es
; 引数を保存
mov [lba_low16_bx], bx ; BXレジスタをスタックに保存(bl、bhを含む)
mov [lba_high8_cx], cx ; CXレジスタをスタックに保存(clを含む)
; ===== 1. HDDが使用可能か =====
mov dx, 0x1F7 ; 状態レジスタポート
mov cx, 0xFFFF ; 最大試行回数
.wait_not_busy:
in al, dx
test al, 0x80 ; 最高位ビット=1かどうか(0x80 = 1000 0000)
jz .ready ; 1でなければ使用可能 →readyへ遷移
dec cx ; 試行回数を減算
jnz .wait_not_busy ; 使用不可の場合は待ち続ける
jmp .error ; 時間切れ
.ready:
; ===== 2. どのドライブを選択するかとLBAアドレスの上位4ビットを設定 =====
mov dx, 0x1F6 ; 「ドライブ/ヘッドレジスタ」
mov al, 0xE0 ; 本文を参照
or al, bh ;
and al, 0xEF ; bit4=0(固定)
out dx, al
; ===== 3. 読み込みセクタ数の設定 =====
mov dx, 0x1F2 ; セクタ数レジスタ:転送するセクタ数を指定するために使用
mov al, 1 ; 1セクタ読み込み
out dx, al
; ===== 4. LBAアドレスの指定 =====
; 引数を取り出す
mov bx, [lba_low16_bx]
mov cx, [lba_high8_cx]
mov dx, 0x1F3 ; LBA 0-7
mov al, bl ; bl = LBA 0-7
out dx, al
mov dx, 0x1F4 ; LBA 8-15
mov al, bh ; bh = LBA 8-15
out dx, al
mov dx, 0x1F5 ; LBA 16-23
mov al, cl
out dx, al
; ===== 5. 読み込み命令送信 =====
mov dx, 0x1F7
mov al, 0x20 ; 0x20: セクタ読み込み (参考 0x30: セクタ書き込み)
out dx, al ; 一般的な書き方 outb(0x1F7, 0x20) → 読み込み開始
; ===== 6. データ転送が可能となるのを待つ =====
mov cx, 0xFFFF ; 最大試行回数
.wait_data_ready:
in al, dx
test al, 0x80 ; 読み込み可能か
jnz .wait_data_ready
test al, 0x08
jnz .data_ready
dec cx
jnz .wait_data_ready
jmp .error
.data_ready:
; ===== 7. エラー確認 =====
test al, 0x01 ; 状態レジスタ 0bit目=1はエラー
jnz .error
; ===== 8. データ読み込み =====
mov cx, 256 ; 512バイト(WORD単位で÷2) IDEデータレジスタ(0x1F0)は16ビット(2バイト)単位でデータを扱う
mov dx, 0x1F0 ; データレジスタ(読み書きに使用される)
cld ; 念のため明示的にDF=0に設定し、insw実行時にメモリアドレス(DI)を自動増加(+2)させる。std(DF=1)だと、アドレスが減少(-2)
rep insw ; ES:DIへ読み込む cxの回数だけinswを繰り返し、
pop es
pop ds
popa
clc ; 成功
ret
.error:
pop es
pop ds
popa
stc ; 失败
ret
disktest db 'disktest', 0x0D, 0x0A, 0
lba_low16_bx dw 0 ; LBA 下位16bit バッファ
lba_high8_cx dw 0 ; LBA 上位8bit バッファ(chは実質無意味)
FAT表及びルートディレクトリをメモリ上へ読み込む。
filesystem/fat16.asm
ilesystem/fat16.asm
; FAT表及びルートディレクトリを0x0000:9000へ読み込む
; 8 (FAT1) + 8 (FAT2) + 32 (ルートディレクトリ) = 48セクタ
; 64セクタ目から48セクタを読み込む(パーティション第63セクタ目〜、63セクタ目にはVBRを設置)
; メモリ上の分布
; FAT1は64セクタ目から8セクタ分、メモリの 0x0000:0x9000 に読み込む
; FAT2はその次の8セクタ分、つまり72〜79セクタ目、メモリの 0x0000:0xA000 に読み込む
; ルートディレクトリは80セクタ目から32セクタ分、メモリの 0x0000:0xB000 に読み込む
; 番地計算方法
; セクタサイズは512バイト(0x200)
; FAT1のサイズ:8セクタ × 512 = 4096 = 0x1000バイト
; FAT2の開始アドレスは FAT1の終了アドレス + 0x1000 → 0x9000 + 0x1000 = 0xA000
; ルートディレクトリ開始アドレスは FAT2の終了アドレス + 0x1000 → 0xA000 + 0x1000 = 0xB000
read_fat:
pusha
push es
mov bl, 0x40 ; LBA 0-7 64セクタ目〜
mov bh, 0x00 ; LBA 8-15
mov cl, 0x00 ; LBA 16-23
mov si, 48 ; 読み込みセクタ数 48
xor ax, ax
mov es, ax
mov di, 0x9000 ; 転送先 0x0000:9000
call read_multi_sector
mov si, msg_fat_loaded
call print_string
pop es
popa
ret
msg_fat_loaded db '[Kernel] FAT info loaded at 0x0000:0x9000', 0x0D, 0x0A, 0
ルートディレクトリを参照し、ファイルの一覧を表示します。
command/dir.asm
command/dir.asm
; いづれは外部コマンド(DIR.COM)にする予定
; ファイル一覧を表示する
;
dir:
; ルートディレクトリの先頭番地
mov si, 0xB000
; 全512エントリを順番に見る
mov cx, 512
.dir_loop:
push cx ; ループカウンタ退避
push si ; SI退避
; 最初の1バイトを確認(0なら未使用、0xE5は削除済み)
mov al, [si]
cmp al, 0
je .skip_entry ; 0ならスキップ
cmp al, 0xE5
je .skip_entry ; 削除済みエントリはスキップ
; ファイル名(8+3)を表示
mov bx, si
call print_filename_83 ; ファイル名表示サブルーチン(後述)
; 改行
mov si, newline
call print_string
.skip_entry:
pop si
add si, 32 ; 次のエントリへ
pop cx
loop .dir_loop
ret
; 入力: BX = エントリ先頭番地
print_filename_83:
pusha
mov si, bx ; SI = エントリ先頭
; 8文字のファイル名表示
mov cx, 8
.print_name:
mov al, [si]
cmp al, ' ' ; スペースなら終端
je .skip_spaces
call putchar_direct
.skip_spaces:
inc si
loop .print_name
; ドットを表示
mov al, '.'
call putchar_direct
; 拡張子(3文字)
mov cx, 3
.print_ext:
mov al, [si]
cmp al, ' '
je .skip_ext_spaces
call putchar_direct
.skip_ext_spaces:
inc si
loop .print_ext
popa
ret
XPにて検証
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