【低レイヤ入門】C言語で作る自作VMを少しOSらしくする: Welcomeプロンプトから外部バイナリを起動する | UNIX Cafe

* 当サイトでは、コンテンツの一部に広告を掲載しています。

System Note $ cat /proc/ai-disclosure

本記事の構成および論理分析には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 の固定文字列と比較し、複数文字コマンドとして分岐するところへ進みます。

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

この記事を書いた人

のいのアバター のい UNIX Cafe マスター

Macintosh Color Classicから始まった旅は、長いWindows時代を経て、Windows10のサポート終了をきっかけにUNIXの世界へ戻ってきました。UNIX Cafeでは、UNIX・Linux・そしてMacな世界を、むずかしい言葉を使わず、物語のように書いています。プログラミングは、アイデアをコンピューターに伝えるための言葉です。簡単な単語と文法を覚えれば、誰でもコマンドを使えます。ぜひ一度、やさしいプログラミングの世界をのぞいてみてください。

目次