本記事の構成および論理分析にはAI(人工知能)を使用しています。情報の正確性は、システム管理者(UNIXユーザー)による手動検証済みです。
【低レイヤ入門】C言語で作る自作VMを少しOSらしくする: 入力バッファの help / quit を文字列比較する | UNIX Cafe

前回は、小さな自作VM本体にhost側monitor modeを追加しました。
引数なしで ./handmade-vm を起動すると、Welcome to Handmade VM と > プロンプトが表示されます。そのプロンプトに programs/hello.bin のような外部バイナリのパスを入力すると、新しいVMへ読み込んで実行できるようにしました。
Welcome to Handmade VM
>programs/hello.bin
A
CPU halted.
>quit
Goodbye from Handmade VM前回のmonitorはまだVM上のOSではありません。ファイル名を読んでいるのは、VM上のプログラムではなくhost側のCコードです。
今回は、またVM上で動くプログラム側へ戻ります。以前作った line-buffer.bin では、Enterまでの入力をVM内メモリへ保存し、0終端文字列として表示できるようにしました。
Type a word, then Enter
>You typed: help
CPU halted.次に進みたいのは、保存した文字列をただ表示するだけではなく、help や quit というコマンドとして判定することです。
そこで今回は、入力バッファに入った文字列を、固定文字列 help / quit と1 byteずつ比較する word-command.bin を追加します。
Day 32: tools/write-word-command-bin.c / programs/word-command.bin今回の練習ノートとコードは、GitHubの handmade-vm-os に置いています。
今回の記事で扱う範囲
今回の記事で実装するのは、「Enterまで入力した文字列をVM内メモリへ保存し、その文字列を help / quit と比較して分岐するところ」までです。
VMには、まだ strcmp のような文字列比較命令はありません。そのため、今回は LDB で入力バッファから1 byteずつ読み、CMP と JNZ で1文字ずつ比較します。
help を入力すると、help用の文字列を表示します。
Command> Commands:
help: show help
quit: quit
CPU halted.quit を入力すると、終了メッセージを表示します。
Command> Goodbye from Handmade VM
CPU halted.どちらでもない文字列なら、unknown扱いにします。
Command> Unknown command
CPU halted.今回の段階では、まだコマンドループには戻りません。入力を1回読み、判定し、結果を表示して HALT するところまでを確認します。
host側のquit判定とは別物
ここで、前回の quit と今回の quit を分けておきます。
前回のmonitor modeにも quit 判定がありました。前回は、host側monitorの入力行を、C言語の strcmp で比較していました。
static bool is_quit_command(const char *line) {
return strcmp(line, "quit") == 0 || strcmp(line, "q") == 0 || strcmp(line, "exit") == 0;
}これは、VMの外側にあるCコードの話です。
今回の help / quit 判定は、VM上で動く word-command.bin の中で行います。host側の strcmp は使いません。
前回:
host側Cコード
fgetsで入力行を読む
strcmpで quit / q / exit を比較する
今回:
VM上のプログラム
SYSCALL 2で1 byteずつ読む
STBでVM内メモリへ保存する
LDBでVM内メモリから読み直す
CMPとJNZで1文字ずつ比較する前回は、VM本体の起動入口を整えました。今回は、そのVMの上で動くプログラムで、複数文字のコマンドを扱えるようにします。
仕様カード
word-command.bin は、新しいVM命令ではありません。既存の命令を組み合わせたサンプルプログラムです。
名前: word-command.bin
分類: sample program
目的: Enterまで入力した文字列を help / quit と比較し、複数文字コマンドとして分岐する
命令長: 4 byte固定
使う命令:
MOVI
SYSCALL 1
SYSCALL 2
CMP
LDB
STB
INC
JUMP
JZ
JNZ
HALT
読むレジスタ: R0, R1, R2
書くレジスタ: R0, R1, R2
読むメモリ: memory[0x500] から0終端までの入力バッファ
書くメモリ: memory[0x500] から最大63 byteと終端0 byte
PCの変化: fetch時に +4、JUMP/JZ/JNZで即値アドレスへ変更
条件フラグの変化: CMPでzero flagを更新する
成功条件: help ならhelp文字列を表示し、quit なら終了文字列を表示し、それ以外ならunknown文字列を表示する今回のポイントは、文字列比較命令を追加しないことです。LDB、CMP、JNZ という、すでにある命令だけで文字列比較の形を作ります。
入力バッファの場所を決める
今回は、入力バッファを memory[0x500] から始めます。
help と入力した場合、入力後のメモリは次のようになります。
0x500: h
0x501: e
0x502: l
0x503: p
0x504: 0x00quit と入力した場合は、次のようになります。
0x500: q
0x501: u
0x502: i
0x503: t
0x504: 0x00最後の 0x00 は、文字列の終わりを表します。C言語の文字列と同じように、0 byteが来たところで文字列が終わったと考えます。
この形にしておくと、あとから SYSCALL 1 で表示できます。今回のように、先頭から1 byteずつ読み直して比較することもできます。
まず1行を0終端文字列として保存する
入力の読み取りは、以前作った line-buffer.bin と同じ考え方です。
最初にプロンプト文字列を表示します。そのあと、入力バッファの先頭アドレス 0x500 を R1 に入れます。
MOVI R0, 0x300
SYSCALL 1
MOVI R1, 0x5000x300 には、プロンプト用の文字列 Command> を置きます。
次に、SYSCALL 2 で1 byte読みます。読んだ文字がLFまたはCRなら、入力終了として扱います。
SYSCALL 2
MOVI R2, 10
CMP R0, R2
JZ finish_input
MOVI R2, 13
CMP R0, R2
JZ finish_inputLFは 10、CRは 13 です。どちらもEnterとして扱います。
Enterではない文字なら、現在の R1 が指す場所へ保存します。
STB [R1], R0
INC R1
JUMP read_loopSTB [R1], R0 で、読んだ1文字を memory[R1] へ保存します。そのあと INC R1 で、次の保存先へ進みます。
入力が終わったら、Enterそのものは保存しません。代わりに、現在の R1 が指す場所へ 0x00 を書きます。
MOVI R0, 0
STB [R1], R0これで、入力バッファは0終端文字列になります。
help と1文字ずつ比較する
入力が終わったら、今度は memory[0x500] から文字を読み直します。そして、入力された文字列が help と一致するかを調べます。
VMには、まだ文字列比較命令はありません。そのため、比較は1文字ずつ行います。
MOVI R1, 0x500
LDB R0, [R1]
MOVI R2, 104
CMP R0, R2
JNZ compare_quit104 は、文字 h のASCIIコードです。
この命令列は、次のように読めます。
MOVI R1, 0x500 R1 = 入力バッファの先頭
LDB R0, [R1] R0 = memory[R1]
MOVI R2, 104 R2 = 'h'
CMP R0, R2 zero_flag = R0 == R2
JNZ compare_quit 一致しなければ quit の比較へ進む一致していれば、次の文字へ進みます。一致していなければ、その時点で help ではありません。その場合は、次の候補である quit の比較へ進みます。
同じ形で、e、l、p も比較します。
0x500: h
0x501: e
0x502: l
0x503: pここで大事なのは、文字列全体を一度に比較しているわけではないことです。1 byteずつ LDB で読み、レジスタに入れてから CMP しています。
最後の0x00まで比較する理由
help の4文字だけを比較すると、正しく判定できない場合があります。
例えば、入力が helpful だった場合です。
help:
h e l p 0x00
helpful:
h e l p f u l 0x00先頭4文字だけを見ると、どちらも h、e、l、p です。
しかし、コマンドとして扱いたいのは help と完全に一致した場合だけです。helpful は別の文字列なので、help として扱いません。
そこで、最後に 0x00 も比較します。
h
e
l
p
0x005文字目が 0x00 なら、入力はそこで終わっています。つまり help と完全一致しています。
一方、helpful の5文字目は f です。0x00 ではないため、help とは判定しません。
0終端文字列にしておくと、文字列の終わりまで比較できます。今回の小さなVMでも、この考え方はC言語の文字列と同じです。
helpでなければquitと比較する
help と一致しなかった場合は、次に quit と比較します。
比較の形は help と同じです。
q
u
i
t
0x00まず memory[0x500] が q かどうかを見ます。違えば、その時点で quit ではありません。
一致していれば、次に u、i、t を見ます。最後に 0x00 も見ます。
quit と完全一致した場合は、終了メッセージを表示します。
Command> Goodbye from Handmade VM
CPU halted.help でも quit でもない場合は、unknown扱いにします。
Command> Unknown command
CPU halted.word-command.binを生成するwriter
今回の命令列は、hello.asm や prompt-echo.asm より長くなります。
表示用の0終端文字列も、命令列の後ろに置きます。さらに、help ではなかった場合に quit の比較へ進む、というように、ジャンプ先もいくつか必要になります。
そのため、今回は手でバイナリを書くのではなく、tools/write-word-command-bin.c で programs/word-command.bin を生成します。
表示用の文字列と入力バッファのアドレスは、次のように決めています。
#define PROMPT_TEXT 0x300
#define HELP_TEXT 0x340
#define GOODBYE_TEXT 0x3A0
#define UNKNOWN_TEXT 0x3E0
#define INPUT_BUFFER 0x500
#define INPUT_LIMIT 0x53Fそれぞれの意味は、次の通りです。
0x300: "Command> \0"
0x340: "Commands:\nhelp: show help\nquit: quit\n\0"
0x3A0: "Goodbye from Handmade VM\n\0"
0x3E0: "Unknown command\n\0"
0x500: 入力バッファ文字列は、これまでと同じように write_string_at で指定アドレスへ書き込みます。
if (!write_string_at(file, PROMPT_TEXT, "Command> ")) {
perror(path);
fclose(file);
return 1;
}
if (!write_string_at(file, HELP_TEXT, "Commands:\nhelp: show help\nquit: quit\n")) {
perror(path);
fclose(file);
return 1;
}文字比較の命令列は、何度も同じ形になります。そのため、writer側では emit_compare_byte という小さな関数にしています。
static void emit_compare_byte(uint32_t *program, size_t *count, uint32_t offset, uint8_t expected) {
emit_movi(program, count, 1, INPUT_BUFFER + offset);
emit_ldb(program, count, 0, 1);
emit_movi(program, count, 2, expected);
emit_cmp(program, count, 0, 2);
}この関数は、入力バッファ内の指定した位置を読み、期待する文字と比較する命令列を出力します。
例えば、offset = 0、expected = 'h' なら、memory[0x500] を読み、'h' と比較する命令列になります。
monitorから手で起動して試す
前回、引数なしの ./handmade-vm でhost側monitorを起動できるようにしました。これは、毎回コマンドライン引数で programs/*.bin を指定するだけではなく、VMの入口から外部バイナリを選んで動かす感覚を確認するためでした。
今回の word-command.bin も、まずは自動テストではなく、monitorから手で起動してみます。ここでは、実際に端末で入力する流れをそのまま見ます。
まず、VM本体と外部バイナリを用意します。
makeすでに生成物が最新なら、Nothing to be done for `all'. と表示されることがあります。これはエラーではありません。
次に、引数なしでVMを起動します。
./handmade-vm起動すると、host側monitorの > プロンプトが表示されます。
Welcome to Handmade VM
>この > はhost側monitorのプロンプトです。ここでは、実行したい外部バイナリとして programs/word-command.bin を入力します。
>programs/word-command.binword-command.bin が起動すると、今度は Command> プロンプトが表示されます。
Command>この Command> は、VM上で動いている word-command.bin が表示しているプロンプトです。ここへ help と入力します。
Command> helphelp が入力バッファに保存され、固定文字列 help と一致すると、help用文字列が表示されます。
Commands:
help: show help
quit: quit
CPU halted.CPU halted. が表示されると、word-command.bin の実行は終わります。そのあと、VM本体はhost側monitorへ戻ります。
>最後に、monitor側の > プロンプトで quit と入力してmonitorを終了します。
>quit
Goodbye from Handmade VMここまでの流れをまとめると、端末では次のように見えます。
Welcome to Handmade VM
>programs/word-command.bin
Command> help
Commands:
help: show help
quit: quit
CPU halted.
>quit
Goodbye from Handmade VMこのログには、2種類のプロンプトが出てきます。
>はhost側monitorのプロンプトCommand>はVM上で動いているword-command.binのプロンプト
Command> help の help は、VM上のプログラムへの入力です。この文字列は SYSCALL 2 で1 byteずつ読まれ、VM内の memory[0x500] へ保存されます。そのあと、help という固定文字列と比較されます。
CPU halted. が表示されると、word-command.bin の実行は終わります。そのあと、VM本体はhost側monitorへ戻ります。最後の >quit はmonitorを終了するための入力です。
この手入力の確認を入れておくと、いま作っているものが単なるテスト用のバイナリではなく、VMのプロンプトから選んで動かせる小さなプログラムになっていることを体感できます。
残りの分岐も確認する
help の流れを確認できたので、残りの2つの分岐も短く確認します。やることは同じです。monitorの > で programs/word-command.bin を起動し、Command> に別の文字列を入力します。
まず、quit を入力します。
>programs/word-command.bin
Command> quit
Goodbye from Handmade VM
CPU halted.Command> quit は、VM上の word-command.bin への入力です。quit 文字列と一致したため、終了メッセージが表示されます。
次に、どちらにも一致しない例として hello を入力します。
>programs/word-command.bin
Command> hello
Unknown command
CPU halted.hello は help でも quit でもないため、unknown用の文字列が表示されます。
自動確認用のMakefileターゲット
手で動きを確認したあと、自動確認用に3つのターゲットも追加します。
test-word-command: $(VM) programs/word-command.bin
printf 'help\n' | ./$(VM) programs/word-command.bin
test-word-command-quit: $(VM) programs/word-command.bin
printf 'quit\n' | ./$(VM) programs/word-command.bin
test-word-command-unknown: $(VM) programs/word-command.bin
printf 'hello\n' | ./$(VM) programs/word-command.bin自動確認では標準入力へ文字列を渡すため、手入力した文字そのものは端末にechoされません。そのため、手で動かしたときの Command> help ではなく、Command> Commands: のように、プロンプトの直後に実行結果が続いて見えます。
今回分かったこと
今回は、VM上で動く word-command.bin を追加し、入力バッファに保存した文字列を help / quit と比較しました。
- Enterまでの入力を0終端文字列として保存できる
LDBで入力バッファから1 byteずつ読めるCMPとJNZで文字ごとの比較ができる- 最後の
0x00まで比較すると完全一致を判定できる help/quit/ unknown の複数文字コマンド分岐ができる
まだ、文字列比較命令や strcmp syscallは追加していません。今回は、既存の命令だけで文字列比較の形を作りました。
このままでも help / quit の判定はできます。ただし、入力バッファ処理や文字列比較処理には、同じ形の命令列が多く出てきます。
次回は、この重複した処理を少し整理し、入力バッファ処理や文字列比較処理をサブルーチン風に切り出すところへ進みます。
