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?

CUIOS自作 COM実行機能2

Posted at

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

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

ソースコードはこちら:
https://github.com/ooe1220/sourouOS/tree/20250816

初めに

生活に大きな変化があり、前回の更新からかなり時間が開きました。
前回の記事でHDDからCOMファイルを読み込み実行する機能を実装しました。
https://qiita.com/earthen94/items/5f15587444a663004761

しかし前回実装したソースではCOMファイルが保存されている位置(セクタ)を直接指定してメモリ上へ読みこむようになっていました。そこで今回はFAT16のルートディレクトリに記録されている開始クラスタを読み取り、HDD上の位置を計算して読みこむように変更しました。
見た目上変化はありませんが動作している様子を以下に載せます。

图片.png

hello.asm
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からシステムコールを呼び出せるようにする。

kernel.asm
    mov word [0x21 * 4], int21_handler
    mov word [0x21 * 4 + 2], cs    

返り先の設定

int21でCOMファイルがOSへ制御を戻す時に返る場所を設定する。

※本来はこの場所に戻すのは宜しくないが、筆者がINT呼び出しをした場合のスタック処理をまだ理解しておらず、偶然この場所へ戻った場合に正常に動作したため、一旦このように書いている。次回INT呼び出し時のスタックを深く理解した上でkernel_returnを然るべき場所へ移す。

kernel.asm
    ; COM実行後ここへ返る    
    kernel_return:
    xor ax, ax
    mov ds, ax
    mov es, ax

外部コマンドの実行

HELLO.COMは実行ファイルだが、外部コマンド扱い。
今後殆どのコマンドをCOMファイルとして実装し、OS自体を軽くするつもりでいる。

1.内部コマンドとの処理の違い

入力されたコマンドがどの内部コマンドにも該当しない場合、その名前のCOMファイルが無いかどうか検索する処理へ跳ぶ。

command.asm
    ; 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ファイルの検索
  1. 入力された文字を拡張子つき8.3形式に変換する "hello"→"HELLO COM"
    内部コマンドと比較する時点で既に大文字へと変換されている為ここでは変換不要
  2. find_com_and_runを呼び出し同名のCOMファイルがあれば実行する
command.asm
; 外部コマンド判定処理
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(入力されたコマンド)を比較している。

command/run.asm
    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)が入っていることが確認出来る。
图片.png

この開始クラスタ19をHDD上の248セクタへと変換し、read_sectors関数でメモリ上へ読みこんだ後にjmp 0x0200:0x0100で実行を開始する。

command/run.asm
.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を然るべき場所へ移す必要がある。

syscall/int21.asm
; AH=4Ch 終了処理(カーネルに戻る等)
int21_exit:
    pop ds
    popa
        
    ; PSPセグメントを一時的にDSにセット
    mov ax, 0x0200
    mov ds, ax

    ; far jump でPSPから復帰先を取得してカーネルに戻る
    jmp far [ds:0]
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?