「手探りでCUI OS作成に挑む」連載
この記事は「手探りでCUI OS作成に挑む」連載の一部です。
全体の目次は「手探りでCUI OS作成に挑む」連載目次を御覧下さい。
前回まで
MBR→VBR→カーネルの順に起動するところまで実装していました。
MS-DOSのMBRの処理を追う
MBR・VBR自作でFAT16として認識させる
FAT16ファイルシステムを手書きで作る
MBR→VBR→カーネルの順に起動する雛形(自作OS)
今回やること
前に個別に実装していた処理を前回作ったカーネルに載せていきます。
C:\>
と表示しているだけでしたが、キーボードから文字入力を受け付けて、HELP
、REBOOT
コマンドの実行機能をつけました。
BIOS依存をなるべく減らしていく予定なのでキーボード入力は直接ポートを指定して値を取得するようにしています。
詳細は以下の記事の中で説明しています。
「シェル」の原型を実装
INT 16H風のキーボード割り込みを自作
実装コード
基本的には、以前の記事で個別に実装していた処理を、今回カーネル上に統合したものです。
細かい処理の方法については、以下に挙げる個別記事で詳しく解説していますので、そちらをご参照ください。
コードにはできる限り注釈を入れており、この記事単体でも流れが追えるよう心がけています。
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
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
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
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
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
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 ; 入力文字数+1
; 入力された文字を画面へ表示
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 ; 押されたキーの番号を取得
; キーの番号最高位が 0:押された 1:離された
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
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
実行結果
# コンパイル
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
次回の予定
次回は新しい機能は一切実装しません。
現在画面出力の部分にint 0x10
を使用しているため、この部分を変更して直接VRAMへ書き込むようにする予定です。
又現在は一度入力したコマンドを消せないため、バックスペースで消せるようにします。