「手探りでCUI OS作成に挑む」連載
この記事は「手探りでCUI OS作成に挑む」連載の一部です。
全体の目次は「手探りでCUI OS作成に挑む」連載目次を御覧下さい。
ソースコードはこちら:
https://github.com/ooe1220/sourouOS/tree/20250816
初めに
生活に大きな変化があり、前回の更新からかなり時間が開きました。
前回の記事でHDDからCOMファイルを読み込み実行する機能を実装しました。
https://qiita.com/earthen94/items/5f15587444a663004761
しかし前回実装したソースではCOMファイルが保存されている位置(セクタ)を直接指定してメモリ上へ読みこむようになっていました。そこで今回はFAT16のルートディレクトリに記録されている開始クラスタを読み取り、HDD上の位置を計算して読みこむように変更しました。
見た目上変化はありませんが動作している様子を以下に載せます。
org 0x100 ; COMは必ず0x100から開始(セグメントは自由)
mov ah, 0009h
mov dx, msg
int 21h
mov ax, 4C00h
int 21h
msg db 'Hello, World!$'
times 512 - ($ - $$) db 0 ; 1セクタ分埋める
HDD上のデータ分布
セクタ | 大きさ | 項目 | 開始クラスタ番号 |
---|---|---|---|
0 | 1セクタ | MBR | - |
63 | 1セクタ | VBR | - |
64 | 48セクタ | ルートディレクトリ(32) + FAT1(8) + FAT2(8) | - |
112 | 128セクタ | カーネル | 2 |
240 | 8セクタ | TEST.TXT | 18 |
248 | 8セクタ | HELLO.COM | 19 |
※1クラスタ=8セクタとして設計しているため、8セクタに満たないファイルは0埋め
解説
関連ソース
sourouOS/
├─ kernel.asm ; OS本体、初期化とメインループ
├─ command.asm ; 入力に応じてコマンドを実行
├─ boot/
│ └─ fat16_init.asm ; FAT16初期化・ルートディレクトリをバイナリで生成
└─ command/
└─ run.asm ; COMファイル探索・実行処理
処理の流れ
int21ハンドラの登録
MSDOSに倣い、int21経由でCOMからシステムコールを呼び出せるようにする。
mov word [0x21 * 4], int21_handler
mov word [0x21 * 4 + 2], cs
返り先の設定
int21でCOMファイルがOSへ制御を戻す時に返る場所を設定する。
※本来はこの場所に戻すのは宜しくないが、筆者がINT呼び出しをした場合のスタック処理をまだ理解しておらず、偶然この場所へ戻った場合に正常に動作したため、一旦このように書いている。次回INT呼び出し時のスタックを深く理解した上でkernel_returnを然るべき場所へ移す。
; COM実行後ここへ返る
kernel_return:
xor ax, ax
mov ds, ax
mov es, ax
外部コマンドの実行
HELLO.COMは実行ファイルだが、外部コマンド扱い。
今後殆どのコマンドをCOMファイルとして実装し、OS自体を軽くするつもりでいる。
1.内部コマンドとの処理の違い
入力されたコマンドがどの内部コマンドにも該当しない場合、その名前のCOMファイルが無いかどうか検索する処理へ跳ぶ。
; help実行
mov si, input_buffer
mov di, cmd_help
call strcmp
cmp ax, 0
je .do_help
...中略...
; dir実行
mov di,cmd_dir
call strcmp
je .do_dir
; 外部コマンド(COM)判定
jmp check_external_command
1.入力コマンドと一致するCOMファイルの検索
- 入力された文字を拡張子つき8.3形式に変換する "hello"→"HELLO COM"
内部コマンドと比較する時点で既に大文字へと変換されている為ここでは変換不要 - find_com_and_runを呼び出し同名のCOMファイルがあれば実行する
; 外部コマンド判定処理
check_external_command:
; ここで input_buffer → 8.3形式に変換
mov si, input_buffer
mov di, file_name_8_3 ; 11バイトバッファ(カーネル側に確保しておく)
call convert_to_8_3_com
; file_name_8_3 を使ってルートディレクトリ検索・COMファイル読込・実行
call find_com_and_run
cmp ax, 0
jne .found
; 見つからなければエラー表示
mov si, unknown_cmd
call print_string
jmp .done
.found:
; 実行して戻ってきた時の処理(必要なら)
jmp .done
.done:
popa
ret
2.検索+実行部分
ルートディレクトリを走査して名前が一致するかを検索する。
1ファイル32バイトのため、毎回32バイト進めて次のファイル処理へ移る。
cmpsbでDS:SI(ルートディレクトリ上)とES:DI(入力されたコマンド)を比較している。
mov si, 0xB000 ; ルートディレクトリ先頭
mov cx, 512 ; エントリ数
search_loop:
push cx
push si
mov di, file_name_8_3
mov cx, 11
repe cmpsb
je .found_entry
pop si
add si, 32 ; 次のエントリへ
pop cx
loop search_loop
; 見つからなかった場合
mov ax, 0
jmp done
筆者の自作OSではCOMファイルをメモリ0x0200:0x0100上へ読みこむ。
0x0200:0x000から0x0200:0x0FFはPSPと呼ばれ、COMに関する情報を読みこむ領域。
MSDOSでは恐らく様々な情報を登録していると思われるが、今回はCOMファイル実行後の返りアドレスのみを保存する。
call print_ax_hexのコメントアウトを外し、HELLO.COMを呼び出すと、AXに開始クラスタ19(0x13)が入っていることが確認出来る。
この開始クラスタ19をHDD上の248セクタへと変換し、read_sectors関数でメモリ上へ読みこんだ後にjmp 0x0200:0x0100で実行を開始する。
.found_entry:
; SIは次の位置なので戻す
pop si
pop cx
; PSP準備
push es
mov ax, 0x0200 ; PSPのセグメント
mov es, ax
mov word [es:2], cs ; PSP:2 にカーネルのCS
mov word [es:0], kernel_return ; PSP:0 にカーネル復帰IP
pop es
; クラスタ→LBA変換(仮に1クラスタ8セクタ、データ領域先頭LBA=112)
add si, 26
mov ax, [ds:si] ; 開始クラスタ取得
;call print_ax_hex ; 開始クラスタが取得出来ているかの確認用
; 開始クラスタ→開始セクタへ変換する
mov bx, ax ; BX = Cluster
sub bx, 2 ; (Cluster - 2)
mov ax, 8 ; SectorsPerCluster
mul bx ; DX:AX = (Cluster-2) * 8
add ax, 112 ; DataAreaStart = 112
adc dx, 0 ; 繰り上がり処理(念のため)
; COMファイルをメモリ上へ読みこむ(xp /512 0x2100)
mov cx, dx ; LBA上位16bit
mov si, ax ; LBA下位16bit
mov dx, 0x0200 ; 保存先セグメント
mov bx, 0x0100 ; 保存先オフセット
mov al, 8 ; 読み込むクラスタ数 = 1(1クラスタ=8バイト)
call read_sectors
; COM実行
jmp 0x0200:0x0100
クラスタ→セクタ変換の補足
; 開始クラスタ→開始セクタへ変換する
mov bx, ax ; BX = Cluster
sub bx, 2 ; (Cluster - 2)
mov ax, 8 ; SectorsPerCluster
mul bx ; DX:AX = (Cluster-2) * 8
add ax, 112 ; DataAreaStart = 112
adc dx, 0 ; 繰り上がり処理(念のため)
sub bx,2 … FATの仕様で「クラスタ2=データ領域の最初」だから2を引く。
mul bx … (Cluster-2) * 8 を計算(= データ領域からのオフセットセクタ数)。
add ax,112 … データ領域の開始位置は セクタ112 だから、ベースを足す。
adc dx,0 … 32bit加算の繰り上げ処理。結果は DX:AX に 32bit LBAが入る。
例:クラスタ = 0x13 (19)
(19 - 2) * 8 = 17 * 8 = 136
112 + 136 = 248
だから LBA = 248 → 目的の場所。
今後の課題
現在INT21ハンドラ中の終了処理にjmp farを使用している。
書いたときは知らなかったがINTを呼び出した時に、スタックに何かの値が詰まれるようなので、IRETを使ってカーネルへ戻らないとスタックが壊れる模様。
今は偶然動いているが、ここを直してkernel_returnを然るべき場所へ移す必要がある。
; AH=4Ch 終了処理(カーネルに戻る等)
int21_exit:
pop ds
popa
; PSPセグメントを一時的にDSにセット
mov ax, 0x0200
mov ds, ax
; far jump でPSPから復帰先を取得してカーネルに戻る
jmp far [ds:0]