MBR→VBR→カーネルの順に起動する雛形（自作OS）

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

## 前回まで
前回までにMBR及びVBRを実装し、FAT16の要であるルートディレクトリ及びFAT表を実装しました。

詳細は以下の記事をご参照下さい。
[MBR・VBR自作でFAT16として認識させる](https://qiita.com/earthen94/items/958fbd1a52031123bc0b)
[FAT16ファイルシステムを手書きで作る](https://qiita.com/earthen94/items/d227367cd6cb84f86e70)

## 今回の目的
今回はkernel.binの中に処理を書き、VBRから起動するところを実装します。
MBR及びVBRはファイルとして認識されませんが、MSDOSのようにカーネル部分はファイルとして認識されるように設計します。

**MBR**：パーティションの設定及びVBRの読み込み
**VBR**：FAT16が使う設定値をまとめたBPBの登録及びkernel.binの起動
**kernel.bin**：OSの中核機能は主にこの中で実装していく

## 実装部分

<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...", 0
msg_disk_error db "Disk read error!", 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>

前回までは`"FAT16 VBR Loaded!"`と文字列を表示するだけで何もしませんでしたが、
112セクタ目から64KBバイト分のkernel.binをメモリ0x8000番地へ読み込み、起動するコードを追加しました。
```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>
<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>

今回はMBR→VBR→カーネルの順に起動する処理に重点を置いて説明をしたかった為、
`c:\>`と表示し何か押されたら改行して又`c:\>`と表示するだけのOSの雛形を書きました。
```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
    
    ; キーボードから入力されるのを待つ(入力が終わるまで返ってこない)
    mov ah, 0x00
    int 0x16 
    
    ; command_loopへ戻りc:\>を表示して次の命令を待つ
    jmp command_loop

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

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

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

## 動作確認

```
# コンパイル
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

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

# 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

# >>>>>>>>今回追加>>>>>>>>
# kernel.binのコンパイル
nasm -f bin kernel.asm -o kernel.bin

# 112セクタ目〜に書き込み
dd if=kernel.bin of=virtual_disk.img bs=512 seek=112 conv=notrunc
# <<<<<<<<今回追加<<<<<<<<

# 起動する
qemu-system-i386 -hda virtual_disk.img -monitor stdio
```


QEMUで実行したところMBR→VBR→カーネルの順に起動し、最後にエンターを押した分だけ`c:\>`と表示されていることが確認出来ます。
![图片.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4054974/dbf6afbd-d7d5-45bf-9422-a43c03000515.png)

## 課題
XP上でこの仮想HDDをEドライブとして読み込みバイナリエディタでkernel.binを開こうとすると読み込みに失敗します。原因があるとすればVBR中のBPB、FAT表、ルートディレクトリに誤りがありkernel.binを書き込んだ場所と矛盾が生じているのかもしれません。
このままでも起動は出来ますがカーネルを1つのファイルとして実装する以上、開けないと意味が無いので今後修正する予定です。
![图片.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4054974/e2a84c47-f189-4899-b513-96e8461ef7cf.png)
![图片.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4054974/f7f82fea-fb8c-4ae0-9ddb-21a4be60c870.png)

## 今後の展望
今後は32ビットモード（プロテクトモード）へ切り替える予定ですが、その場合BIOS呼び出しが直接使えなくなるため、基本的な入出力処理を自力で実装していきます。具体的にはキーボード入力、画面表示、ディスク読み込みなどのドライバ機能を作成し、BIOSに依存しない環境を整えます。

また、COMファイル（EXEファイルの原型で、単純な実行形式）の実行機能をOSに実装する予定です。これにより、自作OS上で簡単なアプリの動作が可能になります。

COMファイルの実行環境が整った後は、アセンブリ言語を駆使して三角関数を用いた3Dモデル描写アプリの作成を計画しています。
(最近ゲーム実況をよく見ているのですが、立体の描写の仕組みが気になりすぎて、内容が全然頭に入ってこないので、原理から研究して自分で実装することにしました……)