「手探りでCUI OS作成に挑む」連載
この記事は「手探りでCUI OS作成に挑む」連載の一部です。
全体の目次は「手探りでCUI OS作成に挑む」連載目次を御覧下さい。
やりたいこと
この記事は前回の続きです。
https://qiita.com/earthen94/items/414d632c6ddefa40191c
前回文字列を連続で出力し、文字列の出力とスクロールの処理には問題が無さそうなことを確認しました。
キーボード入力の処理を入れると、事象が発生したのでキーボード出力廻りを重点的に見ていきます。
自分の備忘録として実際に調べながら色々書き込んで入るのでOSの機能そのものに興味がある方は飛ばして下さい。
考えをまとめるため、
そして自分がどんな不具合に苦しみ、どうやって解決したか後から見返すために書いています。
不具合のあるソース
clear
rm -f os.img
nasm boot.asm -o boot.bin # コンパイル
nasm testcode.asm -o testcode.bin # コンパイル
cat boot.bin testcode.bin > os.img # 結合
qemu-system-i386 -hda os.img -monitor stdio
rm -f boot.bin
rm -f testcode.bin
testcode.asm
[bits 16]
[org 0x7E00]
start:
; セグメント初期化
xor ax, ax
mov ds, ax
mov ss, ax
mov sp, 0x7C00
; スタック設定
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
; command_loopへ戻りc:\>を表示して次の命令を待つ
jmp command_loop
jmp $ ; 無限ループ(停止)
; 起動画面に表示する文字列
message db "HEAD", 0x0D, 0x0A,
db "HEAD", 0x0D, 0x0A,
db "HEAD", 0x0D, 0x0A,
db "HEAD", 0x0D, 0x0A,
db "HEAD", 0x0D, 0x0A,
db "HEAD", 0x0D, 0x0A,
db "HEAD", 0x0D, 0x0A,
db "HEAD", 0x0D, 0x0A,0
prompt db 'C:\>', 0
%include "keyboard.asm"
%include "vga.asm"
%include "strings.asm"
times 5120-($-$$) db 0
dw 0xAA55
vga.asm
;------------------------------------------------------------
; VGAカーソル位置をI/Oポートから取得
; 結果は文字単位で cursor_pos に格納
; カーソル位置を取得しないとBIOS呼び出しを使って表示した文字が上にあった場合に
; 既に表示された文字の上から上書きしてしまいます
;------------------------------------------------------------
get_cursor_position:
; 下位バイト(カーソル位置のLSB)
mov dx, 0x3D4
mov al, 0x0F
out dx, al
inc dx ; DX = 0x3D5
in al, dx
mov bl, al ; BL = LSB
; 上位バイト(カーソル位置のMSB)
mov dx, 0x3D4
mov al, 0x0E
out dx, al
inc dx
in al, dx
mov bh, al ; BH = MSB
; BX = カーソル位置(文字単位)
mov [cursor_pos], bx
ret
;------------------------------------------------------------
; ハードウェアカーソルを更新(VGAポートに書き込み)
; 入力:BX = 新しいカーソル位置(文字単位)
;------------------------------------------------------------
update_hardware_cursor:
push ax
push dx
; 下位バイト(0x0F)を設定
mov dx, 0x3D4
mov al, 0x0F
out dx, al
inc dx ; DX = 0x3D5
mov al, bl ; 下位バイト(BXのLSB)
out dx, al
; 上位バイト(0x0E)を設定
dec dx ; DX = 0x3D4
mov al, 0x0E
out dx, al
inc dx ; DX = 0x3D5
mov al, bh ; 上位バイト(BXのMSB)
out dx, al
pop dx
pop ax
ret
;------------------------------------------------------------
; VRAMへ文字列表示(改行対応、BIOSなし)
;------------------------------------------------------------
print_string:
pusha
push si
; カーソル位置を取得(BIOSなし)
call get_cursor_position
; VRAMのセグメント B800h をESに設定
mov ax, 0xB800
mov es, ax
; カーソル位置を読み込んで、バイト単位に変換
mov di, [cursor_pos]
shl di, 1
.next_char:
lodsb ; AL ← [DS:SI]
or al, al ; 終端文字0なら終了
jz .done
cmp al, 0x0D ; CR(行頭に戻る)
je .carriage_return
cmp al, 0x0A ; LF(次の行)
je .line_feed
; 通常文字出力
mov [es:di], al
mov byte [es:di+1], 0x0A ; 緑文字
add di, 2
; スクロール判定(80×25×2 = 4000バイト)
cmp di, 4000
jl .next_char
call scroll_screen
; scroll後は最下行に戻す
; mov di, 80 * 24 * 2
mov di, 80 * 24
shl di, 1
jmp .next_char
.carriage_return:
mov ax, di
shr ax, 1
xor dx, dx
mov bx, 80
div bx ; AX = 行番号, DX = 列番号
mul bx ; 行頭へ
shl ax, 1
mov di, ax
jmp .next_char
.line_feed:
add di, 160 ; 次の行へ(80文字×2バイト)
; スクロール判定
cmp di, 4000
jl .next_char
call scroll_screen
mov di, 80 * 24 * 2
jmp .next_char
.done:
shr di, 1
mov [cursor_pos], di
mov bx, di ; BXにカーソル位置(文字単位)を設定
call update_hardware_cursor ; ハードウェアカーソル移動
pop si
popa
ret
;------------------------------------------------------------
; putchar_direct: ALの文字をVRAMに直接書き込む
;------------------------------------------------------------
putchar_direct:
pusha
; カーソル位置(文字単位)をDIに
mov di, [cursor_pos]
shl di, 1 ; DI *= 2(バイト単位に)
; セグメント ES に VRAM の 0xB800 を設定(ここでAXを退避せずAL破壊してどハマりした)
push ax
mov ax, 0xB800
mov es, ax
pop ax
; 文字と属性を書き込む
mov [es:di], al ; 文字
mov byte [es:di+1], 0x0A ; 属性(緑)
; カーソルを1文字進める(これがないと入力する度前の文字を上書きする)
add di, 2
shr di, 1
mov [cursor_pos], di
; カーソル表示も更新
mov bx, di
call update_hardware_cursor
popa
ret
;------------------------------------------------------------
; newline: カーソル位置を改行(CR+LF)に更新する関数
; cursor_pos: 文字単位のカーソル位置(0~80*25-1)
; 画面幅80文字、画面高さ25行固定
;------------------------------------------------------------
newline:
; 現在位置から行頭計算
mov ax, [cursor_pos]
xor dx, dx
mov bx, 80
div bx ; AX = 行番号, DX = 列番号
; 次の行へ
inc ax
cmp ax, 25
jb .no_scroll
; スクロールが必要な場合
call scroll_screen
jmp .update_cursor
.no_scroll:
; 新しい位置計算 (行番号 * 80)
mul bx
mov [cursor_pos], ax
.update_cursor:
mov bx, [cursor_pos]
call update_hardware_cursor
;------------------------------------------------------------
; 画面を1行スクロール(上に1行詰めて最下行を空白に)
;------------------------------------------------------------
scroll_screen:
pusha
push es
push ds
; ES = VRAM (0xB800)
mov ax, 0xB800
mov es, ax
mov ds, ax ; コピー元にも同じセグメント使う(安全のため)
; SI = 行1の先頭(2行目) = 1行160バイト(80文字×2byte 文字1byte 色1bite)
mov si, 160
; DI = 行0の先頭(1行目)
mov di, 0
; CX = 24行 × 80文字 = 1920文字(3840バイト)
mov cx, 80 * 24 ; (ここは25にしてはいけない)
.copy_loop:
mov ax, [ds:si]
mov [es:di], ax
add si, 2
add di, 2
loop .copy_loop
; 最下行(25行目 = 行24)の初期化
; DI は現在 3840バイト目(最下行の開始位置)
mov cx, 80
mov ax, 0x0720 ; 空白 + 属性(黒背景+灰色文字)
.clear_last_line:
mov [es:di], ax
add di, 2
loop .clear_last_line
; カーソル位置を1行上に(80文字 = 1行)
; sub word [cursor_pos], 80とすると画面の1行目に戻ってしまったので減算ではなくべた書き
mov word [cursor_pos], 80 * 24 ; 行番号24× 80列 = 1920文字
mov bx, 80 * 24
call update_hardware_cursor
pop ds
pop es
popa
ret
cursor_pos: dw 0
keyboard.asm
%define MAX_INPUT 64 ; 命令の最大入力文字数
input_buffer times MAX_INPUT+1 db 0
; キーボード入力関数
; 入力された結果はinput_bufferへ格納
read_input:
pusha
;BIOS依存(INT10)では動いていたがES=DSと明記と失敗してハマった
;(stosb は 必ず ES:DI を使う 命令)
push ds
pop es
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
call putchar_direct ; 表示する文字はALに入っている
jmp .read_char
.done_input:
; 文字列を終了(終端文字を追加)
mov al, 0
stosb
; 改行
; mov ah, 0x0E
; mov al, 0x0D
; int 0x10
; mov al, 0x0A
; int 0x10
call newline
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, 0x2C ; 'z'
je .z
cmp al, 0x1C ; Enter
je .enter
xor al, al ; 上記以外のキーは無効(0を返す)
ret
.a:
mov al, 'a'
ret
;...省略...
.z:
mov al, 'z'
ret
.enter:
mov al, 0x0D ; EnterのASCIIコード(CR)
ret
試行錯誤
初めに表示する文言を減らすと一回目にエンターを押した時に表示される空白の行数が多くなった。何故。。。
message db "HEAD", 0x0D, 0x0A,
db "HEAD", 0x0D, 0x0A,
db "HEAD", 0x0D, 0x0A,
db "HEAD", 0x0D, 0x0A,
db "HEAD", 0x0D, 0x0A,
db "HEAD", 0x0D, 0x0A,
db "HEAD", 0x0D, 0x0A,
db "HEAD", 0x0D, 0x0A,0
message db "HEAD", 0x0D, 0x0A,0
keyboard.asmの中で改行を消す(コメントアウト)と一切改行されないのでnewline
が怪しい。
.done_input:
; 文字列を終了(終端文字を追加)
mov al, 0
stosb
; 改行
; call newline
popa
ret
改行処理にnewline
関数を使わずprint_string
へ0x0D, 0x0A
を渡すようにするとうまく行った。
newline
中のカーソル処理に不具合があったのだとは思うが、もう疲れたしこの関数は使わないので調査はしない。
コードは短い方が良いのでこれからはprint_string
を使って改行する。
.done_input:
; 文字列を終了(終端文字を追加)
mov al, 0
stosb
mov si, newline
call print_string
popa
ret
newline db 0x0D, 0x0A,0