「手探りでCUI OS作成に挑む」連載
この記事は「手探りでCUI OS作成に挑む」連載の一部です。
全体の目次は「手探りでCUI OS作成に挑む」連載目次を御覧下さい。
現象
キーボードから入力したコマンドはinput_buffer
に一時保存し、エンターが押されると予め登録していたコマンド(help
等)等と比較し、一致すればそれらの処理を開始する仕組みとなっている。
どんなコマンドを入力しても、登録しているコマンドと一致しないため、エンターを押す度にinput_buffer
の中身を画面へ出力するようにした結果、初めの1文字しか保存されていないことが分かった。
検証用の為、最小限に絞ったコード
testcode.asm
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
mov si, input_buffer; ; ; ; ;
call print_string ; ; ; ; ;
mov si, newline
call print_string ; ; ; ; ;
; command_loopへ戻りc:\>を表示して次の命令を待つ
jmp command_loop
jmp $ ; 無限ループ(停止)
; 起動画面に表示する文字列
message db "HEAD", 0x0D, 0x0A,0
prompt db 'C:\>', 0
%include "keyboard.asm"
%include "vga.asm"
times 5120-($-$$) db 0
dw 0xAA55
keyboard.asm
keyboard.asm
%define MAX_INPUT 64 ; 命令の最大入力文字数
input_buffer times MAX_INPUT+1 db 0
newline db 0x0D, 0x0A,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
mov si, newline
call print_string
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
vga.asm
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
;------------------------------------------------------------
; 画面を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の中でget_key
を呼び出し、[ES:DI]経由でinput_buffer
へ1文字ずつ保存している。
元々はES=0x0000としていたはずが、putchar_direct
の内部でESをVRAMの0xB800へ設定し、その後復元していなかった為最初の一回しか正常に保存ができていなかった。
keyboard.asm
.read_char:
; 一文字読み込み(結果はASCIIコードでALへ格納される)
; mov ah, 0x00
; int 0x16
call get_key
... 省略 ...
; 入力に問題が無い場合は入力バッファへ格納
stosb ; AL の文字を [ES:DI] に格納(バッファに保存)、DI++(次の位置へ)
inc cx ; 入力文字数+1
call putchar_direct ; 表示する文字はALに入っている
jmp .read_char
vga.asm
putchar_direct:
pusha
push es ; 今回追加
; カーソル位置(文字単位)を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
... 省略 ...
pop es ; 今回追加
popa
ret