自作OS　簡単なコマンドの実装

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

この記事は「手探りでCUI OS作成に挑む」連載の一部です。
全体の目次は[「手探りでCUI OS作成に挑む」連載目次](https://qiita.com/earthen94/items/b5d77f98f0cf85a13346)を御覧下さい。

## 前回まで
MBR→VBR→カーネルの順に起動するところまで実装していました。
[MS-DOSのMBRの処理を追う](https://qiita.com/earthen94/items/9b5773ccaac1cd5008ec)
[MBR・VBR自作でFAT16として認識させる](https://qiita.com/earthen94/items/958fbd1a52031123bc0b)
[FAT16ファイルシステムを手書きで作る](https://qiita.com/earthen94/items/d227367cd6cb84f86e70)
[MBR→VBR→カーネルの順に起動する雛形（自作OS）](https://qiita.com/earthen94/items/19487698ae29aea71177)

## 今回やること
前に個別に実装していた処理を前回作ったカーネルに載せていきます。
`C:\> `と表示しているだけでしたが、キーボードから文字入力を受け付けて、`HELP`、`REBOOT`コマンドの実行機能をつけました。
BIOS依存をなるべく減らしていく予定なのでキーボード入力は直接ポートを指定して値を取得するようにしています。
詳細は以下の記事の中で説明しています。
[「シェル」の原型を実装](https://qiita.com/earthen94/items/26e6cff3082eb0935372)
[INT 16H風のキーボード割り込みを自作](https://qiita.com/earthen94/items/aa633f2bc22ef0e65d71)

## 実装コード
基本的には、以前の記事で個別に実装していた処理を、今回カーネル上に統合したものです。
細かい処理の方法については、以下に挙げる個別記事で詳しく解説していますので、そちらをご参照ください。
コードにはできる限り注釈を入れており、この記事単体でも流れが追えるよう心がけています。

<details>
<summary>mbr.asm(変更無し)</summary>

```mbr.asm
[BITS 16]
[ORG 0x7C00]

start:
    ; 1. MBRを安全な領域（0x0600）へ退避
    xor ax, ax
    mov ds, ax
    mov si, 0x7C00
    mov di, 0x0600
    mov cx, 512
    rep movsb

    ; 2. セグメントレジスタの初期化
    xor ax, ax
    mov ds, ax
    mov es, ax
    mov ss, ax
    mov sp, 0x7C00
    
    ; 0x0600にジャンプして処理を続行
    jmp 0x0000:(continue - start + 0x0600)

continue:

    mov si, msg_loading
    call print_string

    ; 4. VBRを0x7C00に読み込む（このコードを上書き）
    mov ah, 0x02
    mov al, 1       ; 読み込むセクタ数
    mov ch, 0       ; シリンダ番号
    mov cl, 1       ; セクタ番号（1〜63）
    mov dh, 1       ; ヘッド番号
    mov dl, 0x80    ; 一台目のHDDを読み込む
    mov bx, 0x7C00
    int 0x13
    jc disk_error

    ; 5. 0x7C00に読み込んだVBRへ跳ぶ
    jmp 0x0000:0x7C00

print_string:
    lodsb
    or al, al
    jz .done
    mov ah, 0x0E
    int 0x10
    jmp print_string
.done:
    ret

disk_error:
    mov si, msg_disk_error
    call print_string
    jmp $

msg_loading db "Loading VBR...", 0x0D, 0x0A, 0
msg_disk_error db "Disk read error!", 0x0D, 0x0A, 0

; MBRの残りを埋める
times 446-($-$$) db 0

; パーティションテーブル（FAT16のパーティション）
db 0x80               ; アクティブパーティション
db 0x01, 0x01, 0x00   ; CHS開始位置（0,1,1）
db 0x04               ; パーティションタイプ（FAT16）
db 0xFF, 0xFF, 0xFF   ; CHS終了位置（最大値）
dd 63                 ; 開始LBA（63セクタ目）
dd 4096               ; セクタ数（約2MB）

; 残りの3つの空パーティションを埋める
times 16 * 3 db 0

dw 0xAA55
```
</details>

<details>
<summary>vbr.asm(変更無し)</summary>

```vbr.asm
[BITS 16]
[ORG 0x7C00]  ; VBRはアドレス0x7C00に読み込まれる

; FAT16のBPB
jmp start
nop
db "MSWIN4.1"     ; OEM名
dw 512           ; セクタあたりのバイト数
db 1             ; クラスタあたりのセクタ数
dw 1             ; 予約セクタ数
db 2             ; FATテーブルの数
dw 512           ; ルートディレクトリ項目数
dw 4096          ; 総セクタ数（16ビット）
db 0xF8          ; 媒体種別（HDD等）
dw 8             ; 各FATのセクタ数
dw 63            ; トラックあたりのセクタ数
dw 255           ; ヘッド数
dd 63            ; 隠しセクタ数
dd 0             ; 総セクタ数（32ビット、16ビットが0の場合に使用）
db 0x80          ; BIOSドライブ番号（0x80＝HDD）
db 0             ; 予約（使用されない）
db 0x29          ; 拡張ブート用
dd 0x12345678    ; ボリューム番号
db "FAT16DISK "  ; ボリュームラベル
db "FAT16   "    ; ファイルシステムの種類

start:

    mov si, msg_loaded
    call print_string

    ; kernelは128セクタ分(65536バイト=64KB)，LBA=112
    ; KERNEL.BIN は LBA 112 セクタ目から始まる
    mov ah, 0x02
    mov al, 128 ; 128セクタ読み込む
    mov ch, 0 ; 
    mov cl, 50 ; セクタ = LBA 112 -> CHS
    mov dh, 1 ; ヘッド
    mov dl, 0x80 ; HDD
    mov bx, 0x8000 ; メモリ0x8000番地へ読み込む
    int 0x13
    jc load_error ; 

    jmp 0x0000:0x8000 ; kernelの開始アドレスへ跳ぶ

print_string:
    lodsb
    or al, al
    jz .done
    mov ah, 0x0E
    int 0x10
    jmp print_string
.done:
    ret
    
load_error:
    mov si, error_msg
    call print_string

msg_loaded db "FAT16 VBR Loaded!", 0x0D, 0x0A, 0
error_msg db "Failed to load KERNEL.BIN", 0x0D, 0x0A, 0

; 残りの領域を512バイトまで埋める
times 510-($-$$) db 0

dw 0xAA55
```
</details>

<details>
<summary>​fat16_init.asm(変更無し)</summary>

```​fat16_init.asm
; FAT表及びルートディレクトリを初期化する。
; DDで64セクタ目からの位置に書き込む。
; 固定でKERNEL.BINを一つ登録しておく。
; 前提：
;   - MBRは0セクタ目に設置
;   - VBRは63セクタ目に設置（パーティションはMBR中で63セクタ目から始まるように設定してある）
;   - FATの情報は64から始まる（VBRの直後に設置）

bits 16

; ------------------------------
; 設定値。VBRと矛盾しないよう注意
; ------------------------------
sectors_per_fat    equ 8      ; 一つのFAT表は8セクタを占める
bytes_per_sector   equ 512    ; 1セクタは512バイト
root_dir_entries   equ 512    ; ルートディレクトリに登録可能なファイル数
kernel_start_cluster equ 2    ; KERNEL.BINが始まるクラスタ番号
kernel_size        equ 65536  ; 文件大小（字节）
%define cluster_count (kernel_size / bytes_per_sector) ; KERNEL.BINが何セクタを占めるか

; ------------------------------
; FAT1表LBA 64〜始まる　(DDでこの場所に置く)
; ------------------------------
fat1:
    ; 初めの2クラスタ分は実質固定
    dw 0xFFF8 ; F8はHDD、FF固定
    dw 0xFFFF ; ファイルの最後のクラスタの目印

    ; KERNEL.BINクラスタの連なり（2 → 3 → ... → EOF）
    %assign i kernel_start_cluster
        %rep cluster_count
        %if i == kernel_start_cluster + cluster_count - 1 ;最後のクラスタには目印0xFFFFを置く
            dw 0xFFFF
        %else
            dw i + 1
        %endif
        %assign i i + 1
    %endrep

    ; FAT表の剰余分を0で埋める (8セクタ×512バイト-既に書き込んだ分のバイト数)
    times sectors_per_fat * bytes_per_sector - ($ - fat1) db 0

; ------------------------------
; FAT2表（FAT表1の予備　FAT1の直後）簡略化の為に0埋め
; ------------------------------
fat2:
    ; 表1個8セクタ×512バイト/表
    times sectors_per_fat * bytes_per_sector db 0

; ------------------------------
; ルートディレクトリ（FAT2の直後，LBA 80）
; ------------------------------
root_dir:
    ; KERNEL.BINの登録（32字节）
    db 'KERNEL  BIN'          ; ファイル名（8.3形式）
    db 0x20                   ; 属性（0X20は通常のファイルを意味する）
    db 0                      ; 保留
    db 0                      ; 作成時間（ミリ秒）
    dw 0x0000                 ; 作成時間（16:00:00）
    dw 0x2100                 ; 作成日時（2023-01-01）
    dw 0x2100                 ; 最終変更日時
    dw 0                      ; EA索引
    dw 0x0000                 ; 最終変更時間
    dw 0x2100                 ; 最終変更日時
    dw kernel_start_cluster   ; 開始クラスタ
    dd kernel_size            ; ファイルの大きさ（バイト）

    ; ルートディレクトリが32セクタとなるように0で埋める。
    times root_dir_entries * 32 - ($ - root_dir) db 0

```
</details>


<details>
<summary>kernel.asm</summary>

```kernel.asm
[BITS 16]
[ORG 0x8000]

start:
    ; レジスタ初期化
    xor ax, ax
    mov ds, ax
    mov es, ax
    
    ; スタック設定
    mov ss, ax
    mov sp, 0x7C00
    
    mov si, message
    call print_string
    
command_loop:
    ; c:\>を表示
    mov si, prompt
    call print_string
    
    ;キーボードから入力されるのを待つ(入力が終わるまで返ってこない) int 0x16未使用
    call read_input
    
    ; 入力された命令によって処理をする
    call parse_command
    
    ; command_loopへ戻りc:\>を表示して次の命令を待つ
    jmp command_loop

message db "Hello from 0x8000 kernel", 0x0D, 0x0A, 0
prompt db  'C:\> ', 0

; 他のソースをこの位置へ展開
%include "keyboard.asm"
%include "command.asm"
%include "strings.asm"

; ここで64KBまで0埋めする
times 65536-($-$$) db 0
```
</details>


<details>
<summary>strings.asm</summary>

```strings.asm

print_string:
    mov ah, 0x0E
.next:
    lodsb
    or al, al
    jz .done
    int 0x10
    jmp .next
.done:
    ret
    
; 小文字が含まれていれば大文字へ変換
; SI=対象文字列
to_upper:
    pusha
.loop:
    lodsb
    test al, al
    jz .done
    cmp al, 'a'
    jb .next
    cmp al, 'z'
    ja .next
    sub al, 0x20
    mov [si-1], al
.next:
    jmp .loop
.done:
    popa
    ret

; 文字列比較
; SI=文字列1, DI=文字列2
; 結果: ZF=1 if equal
strcmp:
    pusha
.compare:
    mov al, [si]
    mov bl, [di]
    cmp al, bl
    jne .done
    test al, al
    jz .done
    inc si
    inc di
    jmp .compare
.done:
    popa
    ret
```
</details>


<details>
<summary>keyboard.asm</summary>

```keyboard.asm
%define MAX_INPUT 64     ; 命令の最大入力文字数
input_buffer times MAX_INPUT+1 db 0

; キーボード入力関数
; 入力された結果はinput_bufferへ格納
read_input:
    pusha
    mov di, input_buffer ;入力バッファ
    mov cx, 0            ; 文字数を数える
    
.read_char:
    ; 一文字読み込み（結果はASCIIコードでALへ格納される）
    ; mov ah, 0x00
    ; int 0x16
    call get_key
    
    ; 無効キーは無視
    cmp al, 0
    je .read_char         ; 無効キーは無視
    
    ; エンターが押された場合の処理
    cmp al, 0x0D
    je .done_input
    
    ; 長さが超えていないか確認
    cmp cx, MAX_INPUT ; 入力文字数が上限に達していないか確認
    jae .read_char    ; 上限超えたら無視して次のキー入力へ
    
    ; 入力に問題が無い場合は入力バッファへ格納
    stosb    ; AL の文字を [ES:DI] に格納（バッファに保存）、DI++（次の位置へ）
    inc cx   ; 入力文字数＋１
    
    ; 入力された文字を画面へ表示
    mov ah, 0x0E
    int 0x10
    jmp .read_char
    
.done_input:
    ; 文字列を終了（終端文字を追加）
    mov al, 0
    stosb
    
    ; 改行
    mov ah, 0x0E
    mov al, 0x0D
    int 0x10
    mov al, 0x0A
    int 0x10
    
    popa
    ret
    
; キーボードからの入力を受け取り対応するASCIIコードをAL経由で返す
get_key:
    in al, 0x64       ; 0x64ポート経由で入力されているかを確認
    test al, 1        ; 0ビット目　1：有り　0：無し
    jz get_key        ; 押されていなければget_keyへ戻って初めから
    
    in al, 0x60       ; 押されたキーの番号を取得

    ; キーの番号最高位が　０：押された　１：離された
    test al, 0x80
    jnz get_key       ; 離された場合は入力と見做さない
    
    ; キーの番号からASCIIコードへ変換していく
    cmp al, 0x1E      ; 'a'
    je .a
    cmp al, 0x30      ; 'b'
    je .b
    cmp al, 0x2E      ; 'c'
    je .c
    cmp al, 0x20      ; 'd'
    je .d
    cmp al, 0x12      ; 'e'
    je .e
    cmp al, 0x21      ; 'f'
    je .f
    cmp al, 0x22      ; 'g'
    je .g
    cmp al, 0x23      ; 'h'
    je .h
    cmp al, 0x17      ; 'i'
    je .i
    cmp al, 0x24      ; 'j'
    je .j
    cmp al, 0x25      ; 'k'
    je .k
    cmp al, 0x26      ; 'l'
    je .l
    cmp al, 0x32      ; 'm'
    je .m
    cmp al, 0x31      ; 'n'
    je .n
    cmp al, 0x18      ; 'o'
    je .o
    cmp al, 0x19      ; 'p'
    je .p
    cmp al, 0x10      ; 'q'
    je .q
    cmp al, 0x13      ; 'r'
    je .r
    cmp al, 0x1F      ; 's'
    je .s
    cmp al, 0x14      ; 't'
    je .t
    cmp al, 0x16      ; 'u'
    je .u
    cmp al, 0x2F      ; 'v'
    je .v
    cmp al, 0x11      ; 'w'
    je .w
    cmp al, 0x2D      ; 'x'
    je .x
    cmp al, 0x15      ; 'y'
    je .y
    cmp al, 0x2C      ; 'z'
    je .z
    cmp al, 0x1C      ; Enter
    je .enter
    
    xor al, al        ; 上記以外のキーは無効（0を返す）
    ret
    
.a:
    mov al, 'a'
    ret
.b:
    mov al, 'b'
    ret
.c:
    mov al, 'c'
    ret
.d:
    mov al, 'd'
    ret
.e:
    mov al, 'e'
    ret
.f:
    mov al, 'f'
    ret
.g:
    mov al, 'g'
    ret
.h:
    mov al, 'h'
    ret
.i:
    mov al, 'i'
    ret
.j:
    mov al, 'j'
    ret
.k:
    mov al, 'k'
    ret
.l:
    mov al, 'l'
    ret
.m:
    mov al, 'm'
    ret
.n:
    mov al, 'n'
    ret
.o:
    mov al, 'o'
    ret
.p:
    mov al, 'p'
    ret
.q:
    mov al, 'q'
    ret
.r:
    mov al, 'r'
    ret
.s:
    mov al, 's'
    ret
.t:
    mov al, 't'
    ret
.u:
    mov al, 'u'
    ret
.v:
    mov al, 'v'
    ret
.w:
    mov al, 'w'
    ret
.x:
    mov al, 'x'
    ret
.y:
    mov al, 'y'
    ret
.z:
    mov al, 'z'
    ret
.enter:
    mov al, 0x0D      ; EnterのASCIIコード（CR）
    ret

    
times 510-($-$$) db 0
dw 0xAA55
```
</details>

<details>
<summary>command.asm</summary>

```command.asm
; 命令解析関数
parse_command:
    pusha
    
    ; 小文字→大文字変換(大文字小文字による差異を無くして後ろの文字列比較処理を簡単にする)
    mov si, input_buffer
    call to_upper
    
    ; 空でないか
    cmp byte [si], 0
    je .empty
    
    ; 命令毎に処理(それぞれの命令と入力を比較して一致すれば実行)

    ; help実行
    mov di, cmd_help
    call strcmp
    je .do_help

    ; reboot実行
    mov di, cmd_reboot
    call strcmp
    je .do_reboot
    
    ; 未定義命令
    mov si, unknown_cmd
    call print_string
    jmp .done
    
.do_help:
    mov si, help_text
    call print_string
    jmp .done
    
.do_reboot:
    ;int 0x19
    ;VBRは上書きしていないので再実行するだけの簡単な処理にした
    ;後からもっと細かい処理を実装する予定
    jmp 0x0000:0x7C00
    
.empty:
.done:
    popa
    ret

; データ置き場
cmd_dir db 'DIR', 0
cmd_help db 'HELP', 0
cmd_reboot db 'REBOOT', 0
    
unknown_cmd db 'Unknown command', 0x0D, 0x0A, 0

help_text db 'Available commands:', 0x0D, 0x0A
          db 'DIR    - Show files', 0x0D, 0x0A
          db 'HELP   - This help', 0x0D, 0x0A
          db 'REBOOT - Restart system', 0x0D, 0x0A, 0
```
</details>

## 実行結果
```
# コンパイル
nasm -f bin mbr.asm -o mbr.bin
nasm -f bin vbr.asm -o vbr.bin
nasm -f bin ​fat16_init.asm -o fat16_init.bin
nasm -f bin kernel.asm -o kernel.bin

# 1GBの仮想HDD生成
dd if=/dev/zero of=virtual_disk.img bs=1M count=1024 

# MBRを先頭512バイトへ書き込み
dd if=mbr.bin of=virtual_disk.img bs=512 count=1 conv=notrunc

# VBRを63セクタ目へ書き込み「第1パーティションの先頭512バイト」
dd if=vbr.bin of=virtual_disk.img bs=512 seek=63 conv=notrunc

# ルートディレクトリ及びFAT表を64バイト目〜「第1パーティションの2セクタ目」に書き込み
dd if=fat16_init.bin of=virtual_disk.img bs=512 seek=64 conv=notrunc

# カーネル部分
dd if=kernel.bin of=virtual_disk.img bs=512 seek=112 conv=notrunc

# 一時ファイル削除
rm -f mbr.bin
rm -f vbr.bin
rm -f fat16_init.bin
rm -f kernel.bin

# 起動する
qemu-system-i386 -hda virtual_disk.img -monitor stdio
```
今回実装したコマンドが動いていることが確認できます。
![图片.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4054974/c3f4eead-53a5-4330-b786-edbf4e0ab538.png)

## 次回の予定
次回は新しい機能は一切実装しません。
現在画面出力の部分に`int 0x10`を使用しているため、この部分を変更して直接VRAMへ書き込むようにする予定です。
又現在は一度入力したコマンドを消せないため、バックスペースで消せるようにします。