本記事の構成および論理分析にはAI(人工知能)を使用しています。情報の正確性は、システム管理者(UNIXユーザー)による手動検証済みです。
C言語でLC-3仮想マシンを自作する全12回まとめ: CPU、アセンブラ、デバッグ機能まで作る入門連載 | UNIX Cafe

LC-3VM:C言語で小さなCPUを動かす連載です
このページは、C言語でLC-3仮想マシンを自作する全12回の連載まとめ記事です。
この記事で作るもの:
- LC-3の
.objファイルを読み込み、命令を1つずつ実行するC言語製VM 2048.objやrogue.objをターミナル上で動かすTRAP、端末I/O、メモリマップドI/O- 実行中の状態を観察する
--trace、--profile、--step、--break、dump機能
LC-3は教育用の16bitコンピュータで、命令、レジスタ、メモリ、プログラムカウンタ、条件フラグ、TRAPといったCPUの基本要素を小さく学べる題材です。この連載では、そのLC-3用プログラムを実行する仮想マシンをC言語で作ります。
この連載では、LC-3用の.objファイルを読み込み、命令を1つずつ実行するVMをC言語で作ります。最初はHALTで停止するだけの小さなVMから始め、最終的には2048.objやrogue.objをターミナル上で動かします。
さらに後半では、LC-3VMの中を観察するために--trace、--profile、--step、--break、--dump-registers、--dump-memoryといった機能も追加します。
完成時には、次のようなコマンドでLC-3VMプログラムを実行したり、VM内部の状態を確認したりできます。
./lc3 programs/hello.obj
./lc3 --trace programs/hello.obj
./lc3 --profile programs/2048.obj
./lc3 --step programs/hello.obj
./lc3 --break x3002 programs/hello.obj
./lc3 --dump-registers programs/hello.obj
./lc3 --dump-memory x3000 16 programs/hello.obj高速な実用VMを作ることが目的ではありません。C言語の配列、bit演算、switch文、ファイル読み込み、端末制御を使って、CPUが命令を読み、レジスタを書き換え、メモリと入出力を扱う流れを近くで確認するための連載です。
GitHubではlc3-vm-cという名前で公開しています。
この連載で作るもの
全12回の流れは、大きく4つに分けられます。
第1回から第3回: VMを動かす
第4回から第5回: VMに渡す .obj を自分で作る
第6回から第10回: VMの中を観察する
第11回から第12回: 小さく検証し、公開できる形に整える最初の3回では、LC-3の命令を実行できるVM本体を作ります。次の2回では、VMに渡す.objファイルを自分で作るために、LC-3アセンブリと小さいアセンブラを扱います。
第6回以降では、命令の流れを見る--trace、命令ごとの実行回数を見る--profile、1命令ずつ進める--step、指定番地で止める--break、最後の状態を見る--dump系の機能を追加します。
まずどこから読むべきか
基本的には第1回から順番に読むと進めやすいです。ただし、目的がはっきりしている場合は、途中のブロックから読んでも構いません。
| 読者の目的 | おすすめの読み方 |
|---|---|
| C言語でVMを1つ完成させたい | 第1回から第3回を順番に読む |
| アセンブリと機械語の関係を知りたい | 第4回、第5回を読む |
| 自作VMの中身を観察したい | 第6回から第10回を読む |
| デバッグ機能の使い方を見たい | 第11回を読む |
| GitHubで公開する形を整えたい | 第12回を読む |
| 全体像をつかんでから手を動かしたい | この記事を読んでから第1回へ進む |
低レイヤーに興味はあるけれど、いきなり実CPUやOS自作へ進むのは少し重い、という方にもLC-3VMは扱いやすい題材です。命令数が少なく、メモリも16bitアドレス空間に収まるため、C言語の小さなプログラムとして全体像を追いやすくなっています。
全12回の記事一覧
ここから、各回で作るものを順番に紹介します。
各記事では、その回で完成したコードをGitHubのlessons/配下に置いています。連載を追いながら手を動かす場合は、記事本文とあわせて該当ディレクトリを見ると進めやすいです。
第1回: 16bit CPUの骨格を作る
第1回では、LC-3 VMの最小構成を作ります。メモリ、レジスタ、PCを用意し、.objファイルを読み込んで、PCが指す命令を1つ取り出します。最初はTRAP HALTで停止するだけの小さなVMです。
- 作るもの:
memory[65536]、reg[]、.objローダー、fetch-decode-executeループ、TRAP HALT - 学べること: VMが命令を読み、opcodeを取り出し、停止する流れ
- 第1回の完成コード: lessons/01-skeleton
第2回: 主要な命令セットを実装する
第2回では、ADD、AND、BR、LD、STなどの主要命令を実装し、VMをCPUらしく動かします。16bit命令の上位4bitをopcodeとして読み、残りのbitからレジスタ番号、即値、offsetを取り出します。
- 作るもの: 算術論理命令、分岐命令、load/store命令、
sign_extend()、update_flags() - 学べること: opcode、符号拡張、条件フラグ、PC相対アドレス
- 第2回の完成コード: lessons/02-instruction-set
第3回: TRAPと端末入力で2048とRogueを動かす
第3回では、LC-3プログラムとUNIX端末をつなぎます。GETC、OUT、PUTSなどのTRAPを実装し、メモリマップドI/Oとtermiosでキーボード入力を扱います。最後に2048.objとrogue.objを自作VM上で動かします。
- 作るもの: TRAP、メモリマップドI/O、端末制御、Ctrl-C時の復旧処理
- 学べること: VM内のプログラムとホストOS側の入出力をつなぐ考え方
- 第3回の完成コード: lessons/03-trap-terminal-games
第4回: LC-3アセンブリでHello Worldを書く
第4回では、VMに渡す.objファイルを作る側へ視点を移します。まずprintfで.objのbyte列を直接作り、そのあとLC-3アセンブリでHello Worldを書いてlc3asでアセンブルします。
- 作るもの: Hello Worldの
.obj、hello.asm - 学べること: VMとアセンブラの役割の違い、
.objの中身、LEA R0, HELLOが命令wordになる流れ - 第4回の作業ディレクトリ: lc3as-lab
第5回: 小さいLC-3アセンブラを作る
第5回では、Hello Worldが通る最小構成のアセンブラをC言語で作ります。ラベルを集めるPass 1と、命令wordを書き出すPass 2に分け、アセンブリから.objができる流れを確認します。
- 作るもの:
.ORIG、.END、.STRINGZ、LEA、PUTS、HALTに対応した小さいアセンブラ - 学べること: 2パスアセンブラ、ラベル解決、PC相対offset、big-endian書き出し
- 第5回のminias.cを見る
第6回: –traceで命令実行を1つずつ見る
第6回では、VMが命令を実行するたびに、PC、命令word、opcode、TRAP名、レジスタ、条件フラグを表示する--traceオプションを追加します。
- 作るもの:
trace_instruction()、trace_registers()、opcode_name()、trap_name()、cond_name() - 学べること: 命令の実行順、PCの進み方、レジスタの変化
- 第6回の完成コード: lessons/06-trace
第7回: –profileで命令の実行回数を数える
第7回では、プログラム全体でどの命令が何回実行されたかを見る--profileを作ります。Hello Worldのような小さいプログラムだけでなく、2048のような大きいプログラムの傾向も見ます。
- 作るもの: 実行命令数の合計、opcodeごとのカウンタ、profile結果の表示
- 学べること: 実行順を見るtraceと、実行回数を見るprofileの違い
- 第7回の完成コード: lessons/07-profile
第8回: –stepで1命令ずつ進める
第8回では、1命令実行するたびにVMを止め、Enterで次へ進める--stepを追加します。trace出力を見ながら、PCとレジスタの変化を落ち着いて確認できるようにします。
- 作るもの:
--stepオプション、1命令ごとの入力待ち、step時のtrace自動有効化 - 学べること: ステップ実行の基本、対話式デバッグの入口
- 第8回の完成コード: lessons/08-step
第9回: –breakでPCが指定アドレスと一致したら止める
第9回では、毎回止めるのではなく、--break x3002のように指定したアドレス値とPCの値が一致したときだけ停止できるようにします。見たい場所まで一気に進めて、そこで状態を確認します。
- 作るもの: ブレークポイント指定、アドレス文字列のパース、停止時の状態表示
- 学べること: PCとブレークポイントの比較、指定番地で止める仕組み
- 第9回の完成コード: lessons/09-breakpoint
第10回: –dump-registersと–dump-memoryで状態を見る
第10回では、プログラム終了後にレジスタ一覧と指定範囲のメモリ内容を表示します。traceをすべて追わなくても、最後に必要な状態だけを取り出せるようにします。
- 作るもの:
--dump-registers、--dump-memory x3000 16、レジスタ表示、メモリ範囲表示 - 学べること: 実行後のCPU状態、メモリ配置、debug用の表示関数の分け方
- 第10回の完成コード: lessons/10-dump
第11回: 小さいLC-3プログラムをデバッグする
第11回では、第10回までに作った観察機能を使い、小さいLC-3プログラムの動きを追います。trace、profile、break、dumpを組み合わせ、アセンブリ、機械語、レジスタ、メモリのつながりを確認します。
- 作るもの: 新しいVM機能は追加せず、デバッグ対象の小さいLC-3プログラムを使います
- 学べること:
LEAがなぜその番地を指すのか、BRpがなぜループするのか、命令回数とメモリ配置をどう読むか - 第11回の作業ディレクトリ: lessons/11-debug-example
第12回: GitHubで公開できる形に整える
第12回では、README、Makefile、.gitignore、サンプル取得スクリプト、lessons/ディレクトリなどを整えます。読者がcloneしてビルドし、同じように試せるリポジトリにします。
- 作るもの: README、Makefile、
.gitignore、programs/、scripts/、lessons/ - 学べること: 学習用リポジトリの整え方、ライセンスと出典の書き方、試しやすい公開形
- 第12回の作業ディレクトリ: lessons/12-release
この連載で分かるようになること
全12回を通して進めると、LC-3 VMを作るだけでなく、アセンブリ、機械語、デバッグ機能、公開用リポジトリの整え方まで一通り確認できます。
.objファイルを読み込み、メモリへ配置する流れが分かります- PCが指す命令をfetchし、opcodeごとに処理する流れが分かります
- レジスタ、条件フラグ、PC相対offsetの役割が分かります
TRAPを通じて、LC-3プログラムと端末入出力をつなぐ仕組みが分かります- アセンブリから機械語word列が作られる流れが分かります
- trace、profile、step、break、dumpのような観察機能をVMへ追加できます
- 作ったプログラムをGitHubで試しやすい形に整理できます
先に完成コードを見たい方へ
記事を読みながら実際に手を動かす場合は、第1回から順番に進めるのがおすすめです。先に全体像を見たい場合は、GitHubリポジトリのREADMEやlessons/配下を見ると、各回でどのようにVMが育っていくかを確認できます。
よくある疑問
LC-3VMとは何ですか?
LC-3は教育用の16bitコンピュータです。この連載で作るLC-3VMは、そのLC-3用プログラムをC言語で読み込み、命令、レジスタ、メモリ、プログラムカウンタ、条件フラグ、TRAPの動きを確認するための仮想マシンです。
C言語初心者でも読めますか?
配列、関数、switch文、ビット演算、ファイル読み書きが出てきます。完全なC言語入門ではありませんが、CPUやVMの仕組みをC言語で小さく試したい方には読み進めやすい構成です。
2048やRogueのゲーム本体もC言語で作るのですか?
ゲーム本体はLC-3用にアセンブル済みの.objを使います。この連載で作るVMは、ゲームのルールを知りません。LC-3の命令を正しく実行することで、結果としてゲームが動きます。
アセンブラも作る必要がありますか?
VMだけを作りたい場合は、第1回から第3回だけでも一区切りになります。第4回と第5回では、VMに渡す.objがどのように作られるのかを理解するために、小さいアセンブラを作ります。
デバッガを作る連載ですか?
本格的な対話式デバッガを作る連載ではありません。ただし、第6回から第10回で、trace、profile、step、break、dumpを追加するため、VMの内部状態をかなり追いやすくなります。
まとめ: まずは第1回から小さく始めます
この連載では、最初に小さなVMの骨格を作り、命令セット、TRAP、端末I/O、アセンブラ、デバッグ機能へ順番に広げていきます。
C言語の配列、bit演算、switch文だけでも、CPUが命令を読み、レジスタを書き換え、メモリと端末を動かす流れを近くで確認できます。
まずは第1回で、.objファイルを読み込み、TRAP HALTで停止する最小のLC-3 VMを作ります。
次に読む記事: C言語で始めるLC-3仮想マシン自作入門 第1回: 16bit CPUの骨格を作る
おまけ:マルチタスクと仮想メモリをコントで読む
マルチタスクや仮想メモリの仕組みを、散髪屋コントにたとえて整理した読み物です。「CPUの切り替え」や「ページテーブル」のイメージを、もう少し感覚的につかみたい方におすすめです。




















