【低レイヤ入門】C言語で自作VMに外部バイナリローダーを追加して hello.bin を実行する | UNIX Cafe

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

System Note $ cat /proc/ai-disclosure

本記事の構成および論理分析にはAI(人工知能)を使用しています。情報の正確性は、システム管理者(UNIXユーザー)による手動検証済みです。

【低レイヤ入門】C言語で自作VMに外部バイナリローダーを追加して hello.bin を実行する | UNIX Cafe

自作VM開発記 4

今回は、自作VMに外部ファイルを読み込ませ、文字のAを表示して終了するところまで進めます。VM(仮想マシン)は、ここではC言語で作った小さなコンピューターの仕組みです。例えば「数値を保存する」「文字を表示する」という命令を読み、Cの処理でその動きを再現します。

前回までに、計算や、条件によって実行する場所を変える処理を用意しました。ただし、動かしたい命令はVM本体の notes/vm.c に直接書いていたため、表示する文字を変えるだけでもVM本体を作り直す必要がありました。

そこで、命令を hello.bin という別のファイルへ移します。バイナリファイルとは、ここではVM用の命令を数値の並びとして保存したファイルです。その中身をVMのメモリへ読み込む処理を「ローダー」と呼びます。例えば hello.bin の先頭の数値をメモリの先頭へコピーし、そこから命令を実行できるようにします。

記事では、まず完成したツールで動きを確かめ、その後にローダーとファイル生成の仕組みを読みます。最後に、命令を数値で直接書く方法から、MOVI R0, 65 のような名前で書く方法へ進みます。

Day 21: 外部ファイルをメモリへ読むローダー
Day 22: 固定の命令値からhello.binを生成するツール
Day 23: 命令名で書いたテキストを.binへ変換するツール

今回の練習ノートとコードは、GitHubの handmade-vm-os に置いています。本文のCコードは要点の抜粋です。実行するときは、リポジトリ内の各ソースファイルを使います。

目次

1. まずhello.binを生成して実行する

最初に確認するのは、ファイルから読み込んだプログラムでAを表示し、VMが停止することです。ファイル名は hello.bin ですが、この段階で表示するのはHelloという文字列ではなく、Aの1文字です。成功すると次の2行が表示されます。

A
CPU halted.

以下のコマンドは、notes、tools、programs があるリポジトリの最上位ディレクトリで、PCのターミナルに入力します。Cコンパイラの cc が使える環境を前提にしています。コンパイルとは、CのソースコードをPCで起動できる実行ファイルへ変換することです。

まず、固定の命令を書き出すツールをコンパイルし、programs/hello.bin を生成します。-o の後ろは実行ファイルの保存先です。ここでは、一時ファイル用の /tmp ディレクトリへ置きます。

cc tools/write-hello-bin.c -o /tmp/write-hello-bin
/tmp/write-hello-bin
wrote programs/hello.bin

次に、VM本体をコンパイルし、生成したファイル名を渡して起動します。

cc notes/vm.c -o /tmp/handmade-vm
/tmp/handmade-vm programs/hello.bin
A
CPU halted.

1行目のAがプログラムの出力です。2行目の CPU halted. は、停止命令を実行したことをVMが知らせる表示です。実際のPCが停止するわけではなく、このVMでの命令実行が終わり、ターミナルへ戻ります。

ファイルを開けないというエラーになった場合は、先ほどの生成ツールを実行できたか、現在のディレクトリに programs/hello.bin があるかを確認します。programs/hello.bin は、コマンドを実行した場所から見たファイルの位置です。

2. VM本体と、VMで動かすプログラムを分ける

ここで、PCで動くものとVMが読むものを整理します。この記事では、VMを動かしている実際のPC側を「ホスト」と呼びます。先ほど起動した /tmp/write-hello-bin と /tmp/handmade-vm は、どちらもホスト側の実行ファイルです。

ホスト側の生成ツール
  /tmp/write-hello-bin
       ↓ 命令をファイルへ保存
  programs/hello.bin
       ↓ VMが読み込む
  /tmp/handmade-vm
       ↓ VM用の命令を解釈して実行
  Aを表示して停止

hello.bin は、この自作VMが決めた命令形式で書かれています。そのため、PCのターミナルからファイル自体を直接起動するのではなく、/tmp/handmade-vm に渡します。

従来は load_test_program() というC関数で、命令をVMのメモリへ書き込んでいました。メモリは命令やデータを置く場所で、このVMでは memory という配列で表しています。例えば memory[0] は先頭の保存場所です。この位置を表す番号を「アドレス」と呼びます。

同じAを表示するプログラムをCから配置するなら、次のように書きます。write_inst() の第2引数が置く場所、第3引数が命令を表す数値です。先頭の 0x は16進数の印で、例えば 0x41 は10進数の65です。各命令値の意味は次の節で確認します。

write_inst(vm, 0x00000000, 0x40000041);  // 0番地: R0へ65を入れる
write_inst(vm, 0x00000004, 0x60000000);  // 4番地: 文字を表示する
write_inst(vm, 0x00000008, 0x01000000);  // 8番地: 停止する

この配置方法では、命令値を変えるたびに notes/vm.c の編集と再コンパイルが必要です。外部ファイルを読めるようにすると、VM本体の実行ファイルをそのまま使い、渡す .bin を差し替えて別のプログラムを動かせます。今回変えるのは、この「実行前に命令を用意する方法」です。

3. Aを表示する3命令を読む

まず、ファイルに入れた命令の意味を確かめます。アセンブリとは、命令を数値の代わりに名前で書く表記です。例えば「R0へ65を入れる」を MOVI R0, 65 と書きます。今回のプログラムは次の3行です。

MOVI R0, 65
SYSCALL 0
HALT

R0 はレジスタの1つです。レジスタは、VMが計算や入出力に使う値を一時的に持つ小さな保存場所で、このVMには R0 から R7 までの8個があります。MOVI R0, 65 を実行すると、R0の中身が65になります。C実装では regs[0] = 65 に相当します。

65を使うのは、文字に番号を割り当てるASCIIという規則で、Aの文字コードが65だからです。R0に入っているのは数値ですが、文字として出力するとAになります。例えば66ならBです。

SYSCALL は、VMからホスト側に用意した入出力処理などを呼び出す命令です。この呼び出しをシステムコールと呼びます。今回の SYSCALL 0 は「R0の値を1文字として表示し、改行する」という、このVM独自の約束です。末尾の0は処理を選ぶ番号で、表示する文字の値はR0から受け取ります。

HALT はVMの実行を止める命令です。ファイルの末尾に達したことを停止の合図にするのではなく、プログラム自身がこの命令で終わりを伝えます。

MOVI R0, 65 → R0の中身が65になる
SYSCALL 0   → R0の65を文字Aとして表示し、改行する
HALT        → CPU halted.を表示して実行を止める

4. 3命令が12バイトになる理由

VMが直接読むのは、MOVI という文字ではなく、命令を表す数値です。このVMでは1命令を32ビットで表します。ビットは0か1を表す単位で、8ビットをまとめたものが1バイトです。したがって、32ビットの命令1個は4バイト、3命令なら12バイトになります。

16進数では1桁が4ビットなので、2桁で1バイト、8桁で1命令を表せます。今回の対応は次の通りです。アドレスが0、4、8と進むのは、1命令が4バイトを使うためです。

アドレス  アセンブリ     32ビットの命令値  保存する4バイト
0         MOVI R0, 65    0x40000041        40 00 00 41
4         SYSCALL 0      0x60000000        60 00 00 00
8         HALT           0x01000000        01 00 00 00

このように、大きい桁のバイトから順に保存する並びを「ビッグエンディアン」と呼びます。0x40000041 を左から2桁ずつ読むと 40 00 00 41 になり、ファイル上の並びも同じです。数値の見た目と保存順を対応させやすいため、このVMではこの順序にしています。

逆に小さい桁のバイトから保存する方式もありますが、ここで必要なのは、書く側と読む側が同じ順序を使うことです。PC自身が数値をどの順で保存するかに任せず、ツールとVMの両方で4バイトの順序を決めます。

生成したファイルを、内容を16進数で表示する xxd コマンドで確認します。表示を1バイトずつ区切るため、-g 1 を付けます。

xxd -g 1 programs/hello.bin
00000000: 40 00 00 41 60 00 00 00 01 00 00 00              @..A`.......

左端の 00000000: は、その行がファイルの先頭から何バイト目かを示します。中央の12個の値がファイルの中身です。右端は同じ値を表示可能な文字に置き換えた補助表示なので、ここでは中央の値を上の表と照合します。

エディターで 40 00 00 41 と入力して保存しても、このバイナリにはなりません。その場合は4や0という文字が保存されるためです。今回のツールは、0x40 という値そのものを1バイトとして書き込みます。

5. ファイルをメモリの先頭へ読み込む

ローダーの役割は、先ほど確認した12バイトを、順番を変えずにVMのメモリへコピーすることです。例えばファイルの先頭4バイトが 40 00 00 41 なら、読み込み後は次の配置になります。

ファイルの位置       VMのメモリ
先頭から0バイト目 → memory[0] = 0x40
先頭から1バイト目 → memory[1] = 0x00
先頭から2バイト目 → memory[2] = 0x00
先頭から3バイト目 → memory[3] = 0x41
                 …
先頭から11バイト目 → memory[11] = 0x00

このVMのメモリは、Cの uint8_t memory[MEMORY_SIZE] という配列です。uint8_t は0〜255を保存できる8ビットの整数型で、配列の1要素が1バイトに対応します。容量は 1024 × 1024 バイトです。

名前: 外部バイナリローダー
担当: ホスト側で、命令実行の前にファイルを読む
入力: hello.binなどのファイル名
配置先: memory[0]から、読み込んだバイト数分
最大サイズ: 1,048,576バイト(1024 × 1024)
実行開始位置: メモリの0番地
成功の確認: hello.binでAとCPU halted.が表示される

ローダーは、ファイルの内容を命令ごとに翻訳しません。すでにVM用の数値になったデータを、そのまま配置します。また、この処理は新しいVM命令ではなく、ホスト側のC関数として実行します。

実装は notes/vm.c の load_program_file() です。引数の vm は読み込み先のVMを指し、path は programs/hello.bin などのファイル名です。成功なら true、失敗なら false を呼び出し元へ返します。

static bool load_program_file(VM *vm, const char *path) {
    FILE *file = fopen(path, "rb");

    if (file == NULL) {
        perror(path);
        return false;
    }

    size_t bytes_read = fread(vm->memory, 1, MEMORY_SIZE, file);

    if (ferror(file)) {
        perror(path);
        fclose(file);
        return false;
    }

    if (bytes_read == MEMORY_SIZE && fgetc(file) != EOF) {
        fprintf(stderr, "program too large: %s\n", path);
        fclose(file);
        return false;
    }

    fclose(file);
    return true;
}

fopen(path, "rb") は、読み込み用の r と、バイナリとして扱う b を指定してファイルを開きます。改行などをテキスト向けに変換せず、保存されたバイトを読みたいからです。開けなかった場合の NULL を確認し、perror() で原因を表示します。

中心になるのは次の1行です。vm->memory は、vm が指すVMのメモリ配列を表しています。

size_t bytes_read = fread(vm->memory, 1, MEMORY_SIZE, file);

fread() の引数は「保存先」「1個分のサイズ」「最大個数」「読むファイル」の順です。今回は1バイトずつ、最大 MEMORY_SIZE 個を読みます。そのため、戻り値の bytes_read は実際に読めたバイト数になり、hello.bin なら12です。size_t は、このようなサイズや個数を表すためのCの整数型です。

ferror(file) は読み込みエラーの確認です。また、メモリ容量と同じだけ読めたときは、fgetc() でさらに1バイト読もうとします。まだデータがあれば容量を超えているため、program too large と表示して失敗にします。EOF は、読み取る文字がないことを示す値です。

最後の fclose() でファイルを閉じます。この関数が成功しても、ファイルの内容が正しいプログラムかどうかまで検査したわけではありません。命令の意味を調べるのは、次に見るVMの実行処理です。

6. 読み込んだ命令を0番地から実行する

メモリへ配置した後は、VMに「最初にどこを読むか」を指定します。その位置を持つのが PC(プログラムカウンタ)です。例えば PC=0 なら、次の命令をメモリの0番地から読みます。ファイルを先頭へ置いたので、開始時のPCも0にします。

前回までに用意した実行処理は、次の3段階を繰り返します。今回は最初の MOVI R0, 65 を例に、各段階の役割を確認します。

fetch(命令の取り出し)
  memory[0]〜memory[3]の40 00 00 41を読む
  → 命令値0x40000041を組み立て、PCを4へ進める

decode(命令の分解)
  0x40000041から、処理の種類・レジスタ番号・数値を取り出す
  → MOVI、R0、65だと分かる

execute(命令の実行)
  → R0へ65を保存する

命令値の中で、用途ごとに区切った部分を「フィールド」と呼びます。MOVI では、処理の大分類が type、その分類内の操作が op、保存先のレジスタ番号が rd です。命令へ直接書き込んだ数値は「即値」と呼び、imm で表します。この例の即値は65です。

MOVI R0, 65
    ↓ 命令値は0x40000041
    ↓ decodeで分解
type = 4    MOVIが属する分類
op   = 0    MOVIの操作番号
rd   = 0    保存先はR0
imm  = 65   保存する数値
    ↓ executeで実行
regs[rd] = imm;
    ↓ 具体的な値に置き換える
regs[0] = 65;

rd=0 の0はレジスタの番号です。regs[0] は、そのレジスタの中身です。番号が0でも、そこへ保存する値は65になる、という区別を押さえておきます。

続く命令も同じ流れで実行します。今回のプログラムには実行位置を飛ばす命令がないため、4バイトずつ順番に進みます。

取り出す前のPC  取り出す命令    取り出した後のPC  実行結果
0               MOVI R0, 65    4                 R0が65になる
4               SYSCALL 0      8                 Aと改行を表示
8               HALT           12                VMの実行を停止

HALT の取り出しでもPCは12へ進みますが、停止した後は次の命令を読みません。これまでCから直接置いていた命令も、今回ファイルから読み込んだ命令も、メモリ上で同じバイト列なら同じ処理になります。

7. 起動時に外部ファイルと内蔵テストを選ぶ

次に、ローダーをいつ呼ぶかを確認します。/tmp/handmade-vm programs/hello.bin と起動したとき、実行ファイル名の後ろに渡した programs/hello.bin を「コマンドライン引数」と呼びます。Cの main() は、argc で引数の個数、argv で各文字列を受け取ります。

起動コマンド:
  /tmp/handmade-vm programs/hello.bin

argc    = 2
argv[0] = "/tmp/handmade-vm"
argv[1] = "programs/hello.bin"

個数には実行ファイル名自身も含まれます。Day 21では、argc==2 なら外部ファイルを読み、実行ファイル名だけの argc==1 なら従来の内蔵テストを使う形にしました。読み込みに失敗したときは、そのまま実行へ進まず終了します。

以下は、その起動処理を示す抜粋です。最初の VM vm = {0} でメモリやレジスタなどを0にし、実行前の状態をそろえます。running は実行を続けるかどうかを示す値で、true なら命令の読み取りを始められます。

SP(スタックポインタ)は、一時保存に使うメモリ領域の現在位置です。例えば、別の処理を呼び出した後の戻り先を保存するときに使います。このVMではメモリ末尾の次の位置 0x00100000 から始め、保存するときに小さいアドレス側へ進めます。今回の3命令では使いませんが、VMの初期状態として設定します。

VM vm = {0};

if (argc > 2) {
    fprintf(stderr, "usage: %s [program.bin]\n", argv[0]);
    return 1;
}

vm.pc = 0x00000000;
vm.sp = 0x00100000;
vm.running = true;

if (argc == 2) {
    if (!load_program_file(&vm, argv[1])) {
        return 1;
    }
} else {
    load_test_program(&vm);
}

run(&vm);

run(&vm) が、先ほどの fetch、decode、execute を繰り返します。外部ファイルを読む場合も内蔵テストの場合も、命令をメモリへ用意し終えてから同じ関数へ進みます。

なお、リポジトリの現行版は後の回の機能も含むため、引数なしで起動すると、実行するファイル名を受け付けるmonitorという入力画面になります。現行版で内蔵テストを実行する指定は --self-test です。この記事の /tmp/handmade-vm programs/hello.bin という実行方法は共通して使えます。

8. 固定の命令値をファイルへ書き出す

ここからは、冒頭で使った tools/write-hello-bin.c の中身を読みます。このツールの目的は、同じ12バイトの hello.bin を何度でも生成できるようにすることです。入力する命令はCコード内に固定し、まずは「決めた数値を正しい順で保存する」部分を確認します。

32ビットの整数型 uint32_t で受け取った値を、4つのバイトに分けて書き込みます。例えば 0x40000041 なら、期待する出力は 40 00 00 41 です。これを担当するのが write_u32_be() です。名前の u32 は符号なし32ビット整数、be はビッグエンディアンを表しています。

static void write_u32_be(FILE *file, uint32_t value) {
    fputc((value >> 24) & 0xFF, file);
    fputc((value >> 16) & 0xFF, file);
    fputc((value >> 8) & 0xFF, file);
    fputc(value & 0xFF, file);
}

>> は、値のビットを右へずらす演算です。24ビットは3バイト分なので、0x40000041 >> 24 とすると、左端の 40 が右端まで移動します。& 0xFF は右端の8ビットだけを残す操作で、必要な1バイトを取り出せます。

value = 0x40000041

(value >> 24) & 0xFF → 0x40
(value >> 16) & 0xFF → 0x00
(value >>  8) & 0xFF → 0x00
 value        & 0xFF → 0x41

fputc() は、取り出した値を1バイトとしてファイルへ書き込みます。main() 側では、出力ファイルを fopen(path, "wb") で開き、次の3回の呼び出しを行います。w は書き込み用で、すでに同名ファイルがあれば内容を置き換えます。

write_u32_be(file, 0x40000041);  // MOVI R0, 65
write_u32_be(file, 0x60000000);  // SYSCALL 0
write_u32_be(file, 0x01000000);  // HALT

VMが命令を読む側では、この逆を行います。最初のバイトを左へ24ビット、次を16ビット、次を8ビット移動させ、| というビット単位のOR演算で合わせます。今回はそれぞれ別の位置を使うため、4つの部分を1つの32ビット値に組み立てられます。

static uint32_t read_u32_be(const uint8_t *memory, uint32_t address) {
    return ((uint32_t)memory[address] << 24) |
           ((uint32_t)memory[address + 1] << 16) |
           ((uint32_t)memory[address + 2] << 8) |
           ((uint32_t)memory[address + 3]);
}

(uint32_t) は、読み取った1バイトを32ビットの整数型として扱う指定です。大きい桁へ移す前に型をそろえています。先頭の命令なら、計算は次のようになります。

0x40 << 24 → 0x40000000
0x00 << 16 → 0x00000000
0x00 <<  8 → 0x00000000
0x41       → 0x00000041
                  ↓ ORで合わせる
             0x40000041

書き出した4バイトから元の命令値に戻りました。ローダーはバイト列をそのままコピーし、fetch が read_u32_be() を使って命令値へ戻す、という役割分担です。

9. 命令名からバイナリを生成する

固定の生成ツールでは、0x40000041 という命令値を人が用意していました。次は、MOVI R0, 65 と書けば同じ値へ変換できるようにします。この変換を行う道具が「アセンブラ」です。命令名で書いたテキストを入力し、VM用のバイナリを出力します。

Day 23で追加したアセンブラは tools/small-asm.c です。まずは MOVI、SYSCALL、HALT の3種類を対象にしました。現行版には後の回で追加した命令もありますが、この節では最初の3種類に絞って仕組みを追います。

入力ファイル programs/hello.asm の内容は次の通りです。こちらは文字を読むファイルなので、エディターで3行を入力して保存できます。

MOVI R0, 65
SYSCALL 0
HALT

先に動作を確かめます。アセンブラをコンパイルし、入力ファイルと出力先を順番に指定します。固定の生成ツールとの比較用に、出力先は /tmp/hello-small-asm.bin にします。

cc tools/small-asm.c -o /tmp/small-asm
/tmp/small-asm programs/hello.asm /tmp/hello-small-asm.bin
assembled 3 instructions to /tmp/hello-small-asm.bin

この表示は3命令を変換できたことを示します。内容も一致するか、2つのファイルをバイト単位で比較する cmp コマンドで確認します。違いがなければ何も表示されず、ターミナルへ戻ります。

cmp programs/hello.bin /tmp/hello-small-asm.bin

最後に、生成したファイルをVMへ渡します。期待する結果は、固定の生成ツールを使ったときと同じ2行です。

/tmp/handmade-vm /tmp/hello-small-asm.bin
A
CPU halted.

10. MOVIの文字列を命令値へ変換する

では、アセンブラが MOVI R0, 65 をどう変換するかを見ます。まず1行を命令名と、その命令へ渡す値に分けます。命令に渡す値を「オペランド」と呼び、この例では R0 と 65 の2つです。

入力: MOVI R0, 65
          ↓ 分ける
命令名: MOVI
第1オペランド: R0 → レジスタ番号0
第2オペランド: 65 → 即値65
          ↓ 決められた位置へ数値を入れる
命令値: 0x40000041

assemble_line() は、コメントや空白を処理し、命令名に応じてオペランドを調べます。parse_register() は R0〜R7 かを確認して番号を取り出し、parse_imm20() は数値を読みます。例えば R8 は存在しないレジスタなので、変換を失敗にします。

即値には20ビットを使うため、このアセンブラで受け付ける範囲は0〜0xFFFFF(1,048,575)です。例えば65は範囲内です。文字列から数値を取り出した後は、前の節で見たフィールドの位置へ配置します。

MOVIの32ビット命令

ビット位置  31〜28   27〜24   23〜20   19〜0
用途        type     op       rd       imm
今回の値    4        0        0        65(0x41)

16進数      4        0        0        00041
            └───────── 0x40000041 ─────────┘

ビット位置は右端を0として数えます。type と op を置いた 0x40000000 に、レジスタ番号と即値を合わせるCコードは次の通りです。*inst は、呼び出し元から渡された保存先へ命令値を書き込む表記です。

*inst = 0x40000000 | ((uint32_t)reg << 20) | imm;

reg << 20 は、レジスタ番号を右端から20ビット左へ移し、rd の位置へ置きます。R0の場合は番号が0なので変化が見えませんが、R1なら 1 << 20 が 0x00100000 になります。

MOVI R0, 65:
  0x40000000 | 0x00000000 | 0x00000041
  = 0x40000041

MOVI R1, 65:
  0x40000000 | 0x00100000 | 0x00000041
  = 0x40100041

アセンブラは「処理の種類・レジスタ番号・数値を合わせて命令値を作る」側です。VMの decode は、その命令値から同じ情報を取り出す側です。この2つが同じ配置の約束を使うことで、アセンブリで書いた内容がVMへ伝わります。

11. SYSCALLとHALTも同じ形式へ変換する

SYSCALL 0 は、処理の分類が type=6、操作番号が op=0 です。システムコールの番号は即値として入れるため、先頭部分の 0x60000000 に imm を合わせます。

*inst = 0x60000000 | imm;

今回の即値は0なので、結果は 0x60000000 です。R0という文字を命令に書かなくても、「番号0の処理はR0を読む」という約束をVM側が持っているため、Aの文字コードを受け取れます。

HALT は追加のオペランドを取りません。このVMでは、命令値全体が 0x01000000 なら停止する仕様なので、その値をそのまま生成します。

*inst = 0x01000000;

こうして得た命令値を、固定の生成ツールと同じ write_u32_be() で4バイトずつ保存します。アセンブラがAを表示するわけではありません。ファイルを生成するところまでがアセンブラの仕事で、Aを表示するのは、そのファイルを読み込んで命令を実行するVMです。

Day 23の段階では、命令の位置に名前を付けるラベルや、文字列データの配置などは扱いません。例えば loop: のような名前で戻り先を指定する機能は、後で必要になったときに追加します。まずは3命令が同じ12バイトへ変わり、同じ結果で動くことを確かめます。

今回分かったこと

今回は、VM本体のCコードに埋め込んでいた命令を、外部ファイルから読み込めるようにしました。ファイルを生成する方法は、固定の数値を書き出すツールから、命令名を読むアセンブラへ進みました。全体の流れをつなぐと、次のようになります。

hello.asmにMOVI / SYSCALL / HALTを書く
    ↓ small-asmが数値へ変換
.binに12バイトの命令列を保存
    ↓ ローダーがそのままコピー
memory[0]から12バイトを配置
    ↓ PC=0からfetch → decode → execute
Aを表示してHALTで停止

ローダーが担当するのはファイルからメモリへのコピー、アセンブラが担当するのは命令名から数値への変換です。VMはメモリに置かれた命令を順番に実行します。この役割を分けたことで、VM本体を再コンパイルせず、読み込ませるプログラムを差し替えられるようになりました。

次に進むこと

次回は、キーボードから1文字を受け取るシステムコールを追加します。今回はR0へ65を直接入れてAを表示しましたが、次は入力した文字のコードをR0へ入れます。外部ファイルでプログラムを用意する今回の流れを使い、文字の表示から入力へ進みます。

おまけ:マルチタスクと仮想メモリをコントで読む

マルチタスクや仮想メモリの仕組みを、散髪屋コントにたとえて整理した読み物です。「CPUの切り替え」や「ページテーブル」のイメージを、もう少し感覚的につかみたい方におすすめです。

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

この記事を書いた人

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

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

目次