(自作CUIOS)VGA　VRAM直書き出力のデバッグ2

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

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

## やりたいこと
この記事は前回の続きです。
https://qiita.com/earthen94/items/414d632c6ddefa40191c

前回文字列を連続で出力し、文字列の出力とスクロールの処理には問題が無さそうなことを確認しました。
キーボード入力の処理を入れると、事象が発生したのでキーボード出力廻りを重点的に見ていきます。
![图片.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4054974/a9b290e3-e0c6-4f64-816f-c13b4aca5741.png)

自分の備忘録として実際に調べながら色々書き込んで入るので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
```

<details>
<summary>testcode.asm</summary>

```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

```
</details>

<details>
<summary>vga.asm</summary>

```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
```
</details>


<details>
<summary>keyboard.asm</summary>

```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   ; 入力文字数＋１
    
    ; 入力された文字を画面へ表示
    ; 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       ; 押されたキーの番号を取得

    ; キーの番号最高位が　０：押された　１：離された
    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
```
</details>

## 試行錯誤

初めに表示する文言を減らすと一回目にエンターを押した時に表示される空白の行数が多くなった。何故。。。
```testcode.bin
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
```
![图片.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4054974/e499eded-55ef-4e3e-a97a-8113b09c8828.png)

```testcode.bin
message db "HEAD", 0x0D, 0x0A,0
```
![图片.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4054974/7b3a6cf9-ede3-4566-8035-254c7cb90885.png)

keyboard.asmの中で改行を消す(コメントアウト)と一切改行されないので`newline`が怪しい。
```keyboard.asm
.done_input:
    ; 文字列を終了（終端文字を追加）
    mov al, 0
    stosb
    
    ; 改行
    ; call newline
    
    popa
    ret
```
![图片.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4054974/93374328-d163-4466-8a37-13bf17cfb53b.png)

改行処理に`newline`関数を使わず`print_string`へ`0x0D, 0x0A`を渡すようにするとうまく行った。
`newline`中のカーソル処理に不具合があったのだとは思うが、もう疲れたしこの関数は使わないので調査はしない。
コードは短い方が良いのでこれからは`print_string`を使って改行する。
```keyboard.asm
.done_input:
    ; 文字列を終了（終端文字を追加）
    mov al, 0
    stosb

    mov si, newline
    call print_string
    
    popa
    ret

newline db 0x0D, 0x0A,0
```
![图片.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4054974/61d3b249-6c26-4c0f-93ee-4072bd8bc968.png)

## 紙に書いた分
![default.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4054974/73c18726-94c4-44df-8c65-7734451e4683.jpeg)
![default(1).jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4054974/30d89cc4-54e5-4ed3-8b11-31378dadb82e.jpeg)