本記事の構成および論理分析にはAI(人工知能)を使用しています。情報の正確性は、システム管理者(UNIXユーザー)による手動検証済みです。
【低レイヤ入門】C言語で作る自作VMを少しOSらしくする: Welcomeプロンプトから外部バイナリを起動する | UNIX Cafe

前回は、小さな自作VM上で、Enterまでの入力をVM内メモリに保存する line-buffer.bin を追加しました。
line-buffer.bin では、SYSCALL 2 で1 byteずつ入力を読み、STB [R1], R0 で memory[0x180] から順番に保存しました。最後に 0x00 を書くことで、入力された help を0終端文字列として扱えるようにしました。
Type a word, then Enter
>You typed: help
CPU halted.ここまで来ると、次は help や quit の文字列比較へ進みたくなります。ただ、その前にVM本体の起動方法を少し見直します。
これまで、外部バイナリを実行するときは、次のようにコマンドライン引数へファイル名を渡していました。
./handmade-vm programs/line-buffer.binこれは分かりやすい形ですが、せっかく Welcome to Handmade VM や > プロンプトを作ってきたので、引数なしでVMを起動したときにも、少しOSらしい入口を表示したくなります。
そこで今回は、host側のCコードに小さなmonitor modeを追加します。
Day 31: notes/vm.c monitor mode今回の練習ノートとコードは、GitHubの handmade-vm-os に置いています。
今回の記事で扱う範囲
今回の記事で実装するのは、「引数なしで ./handmade-vm を起動したときに、Welcome表示と > プロンプトを出し、入力された外部バイナリを実行できるhost側monitor」です。
手動で実行すると、次のような流れになります。
Welcome to Handmade VM
>programs/hello.bin
A
CPU halted.
>programs/line-buffer.bin
Type a word, then Enter
>help
You typed: help
CPU halted.
>quit
Goodbye from Handmade VMここで大事なのは、このmonitorがまだVM上のOSではないことです。host側のCコードが、入力されたファイル名を読み、hostのファイルとして programs/hello.bin や programs/line-buffer.bin を開き、新しいVMの memory[0] へ読み込んで実行します。
つまり、今回のmonitorは「VM上のOSがファイルを読んでいる」ものではありません。まだhost側の起動ランチャーです。ただし、この形にしておくと、これまで作ってきた外部バイナリをVMのプロンプトから選んで動かす感覚を確認できます。
今回のゴール
まずは自動確認用のターゲットを見ます。
make test-monitorこのターゲットでは、monitorへ次の入力をまとめて渡します。
programs/hello.bin
programs/line-buffer.bin
help
quit実行結果は次のようになります。
Welcome to Handmade VM
>A
CPU halted.
>Type a word, then Enter
>You typed: help
CPU halted.
>Goodbye from Handmade VMこの出力では、入力したパスが表示されていない点が少し分かりにくいかもしれません。make test-monitor では、キーボードから手で打つ代わりに、上の4行を標準入力へまとめて渡しています。この方法では、渡した文字列そのものは端末に表示されません。
そのため、1回目のプロンプトでは >programs/hello.bin とは見えず、> の直後に hello.bin の出力である A が続いて、>A のように見えます。
line-buffer.bin が読む help も同じです。プログラムは標準入力から help を読んでいますが、画面には >help とは表示されません。その結果、line-buffer.bin の > の直後に、結果表示の You typed: help が続いて見えます。
手動で入力する場合は、プロンプトの右側に自分が入力したパスが見えます。
>programs/hello.bin
A
CPU halted.仕様カード
今回のmonitor modeは、新しいVM命令ではありません。VM本体の起動時処理です。
名前: monitor mode
分類: VM起動機能
目的: 引数なしでVMを起動したとき、host側のmonitorから実行する外部バイナリを選べるようにする
命令長: なし。命令ではなくVM本体の起動時処理
bit配置: なし
読むレジスタ: なし
書くレジスタ: なし
読むメモリ: なし
書くメモリ: 選択した program.bin を実行するたびに、新しいVMの memory[0] から読み込む
PCの変化: 外部バイナリ実行ごとに PC = 0x00000000 から開始する
条件フラグの変化: 外部バイナリ実行ごとに初期化する
エラー時: ファイルを開けない場合はmonitorへ戻る
成功条件: ./handmade-vm で Welcome と > を表示し、入力した .bin を実行し、HALT後にmonitorへ戻る今回のポイントは、VMを実行するたびに新しい VM を初期化することです。前に動かしたプログラムの memory や regs が、次のプログラムへ残らないようにします。
まだOSではない
今回のmonitorは、見た目だけを見ると少しOSやシェルのように見えます。
Welcome to Handmade VM
>programs/hello.binしかし、ここでファイル名を読んでいるのはVM上のプログラムではありません。host側の notes/vm.c です。
host側monitor:
ファイル名を読む
program.bin を開く
VMのmemoryへ読み込む
VMを実行する
VM上のプログラム:
memory[0] からfetchされる命令列として動くこの区別は大事です。今回の段階では、VM上のOSがhostファイルを開いているわけではありません。VMの外側にあるCコードが、実行する外部バイナリを選んでいます。
それでも、このmonitorには意味があります。これまで作ってきた hello.bin、boot-message.bin、line-buffer.bin を、毎回シェルから引数指定するのではなく、VMの起動プロンプトから選んで動かせるからです。
VM初期化を関数にする
monitorから複数の外部バイナリを実行するには、実行のたびにVMを初期化する必要があります。そのため、これまで main に直接書いていた初期化を init_vm にまとめます。
static void init_vm(VM *vm) {
memset(vm, 0, sizeof(*vm));
vm->pc = 0x00000000;
vm->sp = 0x00100000;
vm->running = true;
}memset で VM 全体を0にし、PC、SP、running を初期値に戻します。
これにより、1つ目のプログラムが使った memory や regs が、2つ目のプログラムへ残らないようになります。
外部バイナリを1回実行する関数
次に、外部バイナリを1回実行する処理を run_program_file にまとめます。
static bool run_program_file(const char *path) {
VM vm;
init_vm(&vm);
if (!load_program_file(&vm, path)) {
return false;
}
run(&vm);
return true;
}この関数は、新しい VM を用意し、指定されたパスのバイナリを memory[0] へ読み込み、run で実行します。
ここでも、ファイルを開いているのはhost側です。VM上のプログラムがファイルシステムを操作しているわけではありません。
monitor loopを作る
monitor本体は、標準入力から1行読み、空行なら何もせず、終了コマンドなら終了し、それ以外なら外部バイナリのパスとして実行します。
static void run_monitor(void) {
char line[256];
printf("Welcome to Handmade VM\n");
while (true) {
printf(">");
fflush(stdout);
if (fgets(line, sizeof(line), stdin) == NULL) {
putchar('\n');
break;
}
strip_line_end(line);
if (line[0] == '\0') {
continue;
}
if (is_quit_command(line)) {
printf("Goodbye from Handmade VM\n");
break;
}
run_program_file(line);
}
}printf(">") のあとに fflush(stdout) を呼んでいるのは、改行なしのプロンプトをすぐ画面に表示したいからです。
fgets で読んだ行には、最後に \n が残ります。そのままだとファイル名の一部として扱われてしまうので、strip_line_end で改行を取り除きます。
終了コマンドを決める
monitorでは、次の3つを終了コマンドとして扱います。
quit
q
exit実装では、strcmp で入力行を比較します。
static bool is_quit_command(const char *line) {
return strcmp(line, "quit") == 0 || strcmp(line, "q") == 0 || strcmp(line, "exit") == 0;
}ここで比較しているのは、host側monitorの入力行です。前回作ったVM内入力バッファの文字列比較とは別物です。
内蔵テストは–self-testへ移す
これまで、引数なしで ./handmade-vm を起動すると、内蔵テストプログラムが実行されていました。
A
VM flow complete.
CPU halted.今回から、引数なし起動はmonitor modeに使います。そのため、内蔵テストは --self-test へ移します。
./handmade-vm --self-testこれで、学習用の統合VM確認は残したまま、引数なし起動をmonitorの入口として使えます。
mainの分岐
main では、引数の有無で動きを分けます。
if (argc == 2) {
init_vm(&vm);
if (strcmp(argv[1], "--self-test") == 0) {
load_test_program(&vm);
} else if (!load_program_file(&vm, argv[1])) {
return 1;
}
run(&vm);
} else {
run_monitor();
}./handmade-vmならmonitor mode./handmade-vm programs/hello.binなら従来通り1回実行./handmade-vm --self-testなら内蔵テスト
既存の外部バイナリ実行方法を壊さず、引数なし起動だけを新しい入口に変えます。
今回分かったこと
今回は、VM本体にhost側monitor modeを追加しました。
- 引数なしの
./handmade-vmでmonitorを起動できる - monitorは
Welcome to Handmade VMと>を表示する - 入力されたパスをhost側で開き、新しいVMへ読み込んで実行する
- 外部バイナリ実行ごとに
VMを初期化する quit、q、exitでmonitorを終了する- 内蔵テストは
--self-testに移した
今回のmonitorは、まだVM上のOSではありません。host側のCコードが外部バイナリを選び、VMへ読み込む入口です。
次回は、またVM上のプログラム側へ戻ります。line-buffer.bin で保存した入力バッファを、help や quit の固定文字列と比較し、複数文字コマンドとして分岐するところへ進みます。
