「手探りでCUI OS作成に挑む」実装篇　「シェル」の原型を実装

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

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

## 実装箇所
前回は、ブートローダーを実装して第2セクタを読み込むところまで作りました。
今回はそこから先、簡易的なCUI（コマンドライン）OSの土台となる「カーネル部分」を作っていきます。

主な実装
- C:\> の表示しキーボードからの入力受付
- 以下の3つのコマンドに対応
dir：仮のファイル一覧を表示（※ファイルシステムは未実装）
help：使用可能なコマンド一覧を表示
reboot：システムを再起動（BIOSの int 0x19 呼び出し）

まだ最低限の機能だけですが、「OSっぽい見た目で命令を打てる」ところまで到達しました。

### 動作コマンド
```
ubuntu@ubuntu:~/kaihatsu/test2$ nasm -f bin boot.asm -o boot.bin
ubuntu@ubuntu:~/kaihatsu/test2$ nasm -f bin kernel.asm -o kernel.bin
ubuntu@ubuntu:~/kaihatsu/test2$ cat boot.bin kernel.bin > os-image.bin
ubuntu@ubuntu:~/kaihatsu/test2$ qemu-system-i386 -fda os-image.bin
```

### 実際に動かした様子
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4054974/6a8eb69b-1c59-4d85-b0ba-3814d38f3370.png)


## コード
ブートローダーは昨日と何も変わりません。
kernel.asmの方を実装しました。

<details>
<summary>boot.asm</summary>

```boot.asm
org 0x7C00

start:
    ; セグメント初期化
    xor ax, ax
    mov ds, ax
    mov es, ax

    ; スタック設定
    mov ss, ax
    mov sp, 0x7C00

    ; ディスクから第2セクタ（LBA=1）を 0x8000 に読み込む
    mov ah, 0x02        ; INT 13h サービス 2: 読み込み
    mov al, 1           ; 読み込むセクタ数 = 1
    mov ch, 0           ; シリンダ = 0
    mov cl, 2           ; セクタ番号 = 2（1始まり）
    mov dh, 0           ; ヘッド = 0
    mov dl, 0           ; ドライブ番号（通常フロッピーは 0x00）
    mov bx, 0x7E00      ; 読み込み先アドレス
    int 0x13
    jc disk_error       ; エラー時に無限ループ

    ; 読み込んだコードへジャンプ（第2セクタ）
    jmp 0x0000:0x7E00

disk_error:
    jmp $ ; 無限ループ（エラー時に停止）

times 510-($-$$) db 0
dw 0xAA55

```
</details>

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

```kernel.asm
org 0x7E00

%define MAX_INPUT 64     ; 命令の最大入力文字数

start:
    ; レジスタ初期化
    xor ax, ax
    mov ds, ax
    mov es, ax
    
    ; スタック設定
    mov ss, ax
    mov sp, 0x7C00
    
    ; 画面クリア
    mov ax, 0x0003
    int 0x10
    
    ; 起動時の文言表示
    mov si, welcome_msg
    call print_string
    
    ; 命令を待ち受ける無限ループ
command_loop:
    ; c:\>を表示
    mov si, prompt
    call print_string
    
    ; キーボードから入力されるのを待つ(入力が終わるまで返ってこない)
    call read_input
    
    ; 入力された命令によって処理をする
    call parse_command
    
    ; command_loopへ戻りc:\>を表示して次の命令を待つ
    jmp command_loop

; キーボード入力関数
; 入力された結果はinput_bufferへ格納
read_input:
    pusha
    mov di, input_buffer ;入力バッファ
    mov cx, 0            ; 文字数を数える
    
.read_char:
    ; 一文字読み込み（結果はASCIIコードでALへ格納される）
    mov ah, 0x00
    int 0x16
    
    ; エンターが押された場合の処理
    cmp al, 0x0D
    je .done_input
    
    ; バックスペースが押された場合の処理
    cmp al, 0x08
    je .backspace
    
    ; 長さが超えていないか確認
    cmp cx, MAX_INPUT ; 入力文字数が上限に達していないか確認
    jae .read_char    ; 上限超えたら無視して次のキー入力へ
    
    ; 入力に問題が無い場合は入力バッファへ格納
    stosb    ; AL の文字を [ES:DI] に格納（バッファに保存）、DI++（次の位置へ）
    inc cx   ; 入力文字数＋１
    
    ; 入力された文字を画面へ表示
    mov ah, 0x0E
    int 0x10
    jmp .read_char
    
.backspace:
    ; バックスペース
    test cx, cx
    jz .read_char      ; 何も入力されていない場合のバックアップは無視(消す対象が無い)
    
    ; カーソルを左に動かす
    mov ah, 0x0E
    mov al, 0x08
    int 0x10           ; カーソルを左に移動
    mov al, ' '
    int 0x10           ; 空白で上書きする
    mov al, 0x08
    int 0x10           ; カーソルを左に移動
    
    ; 入力バッファを更新
    dec di
    dec cx
    jmp .read_char
    
.done_input:
    ; 文字列を終了（終端文字を追加）
    mov al, 0
    stosb
    
    ; 改行
    mov ah, 0x0E
    mov al, 0x0D
    int 0x10
    mov al, 0x0A
    int 0x10
    
    popa
    ret

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

    ; 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_dir:
    call show_dir
    jmp .done
    
.do_help:
    mov si, help_text
    call print_string
    jmp .done
    
.do_reboot:
    int 0x19
    
.empty:
.done:
    popa
    ret

; dir ファイル一覧表示関数
show_dir:
    pusha
    mov si, dir_header
    call print_string
    
    mov si, file_list
    call print_string
    
    popa
    ret

; 文字列比較
; SI=文字列1, DI=文字列2
; 結果: ZF=1 if equal
strcmp:
    pusha
.compare:
    mov al, [si]
    mov bl, [di]
    cmp al, bl
    jne .not_equal
    test al, al
    jz .equal
    inc si
    inc di
    jmp .compare
.equal:
    popa
    cmp al, al      ; ZF=1
    ret
.not_equal:
    popa
    cmp al, bl      ; ZF=0
    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

; 文字列表示
print_string:
    pusha
    mov ah, 0x0E
    mov bh, 0
.print_loop:
    lodsb
    test al, al
    jz .done
    int 0x10
    jmp .print_loop
.done:
    popa
    ret

; 各種文字列をここに保存
welcome_msg db 'Simple CLI Shell', 0x0D, 0x0A
            db 'Type HELP for commands', 0x0D, 0x0A, 0

prompt db 'C:\> ', 0

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

dir_header db 'Directory Listing:', 0x0D, 0x0A
           db '-----------------', 0x0D, 0x0A, 0

; ファイルシステムを実装していない為、一旦これらの文字列を固定で表示する
file_list db 'KERNEL.BIN', 0x0D, 0x0A
          db 'COMMAND.COM', 0x0D, 0x0A
          db 'CONFIG.SYS', 0x0D, 0x0A, 0

input_buffer times MAX_INPUT+1 db 0

times 1024-($-$$) db 0
```
</details>

`print_string`、`to_upper`は他の記事で個別に解説していますので目次からご参照下さい。

## 今後の予定
ソースをファイル分割、よく使う処理を汎用関数にする、命令の入力履歴を保存、ファイルシステム(FAT12)実装、COMファイル実行機能実装