2026-11-10
引き続き、『コンピュータシステムの理論と実装』(通称 nand2tetris)を読み進めています。
今回は第7章で、push local 2 や add のような VM(仮想マシン)の命令 を、第6章までで扱ってきたHackのアセンブリに変換する VM変換器 を作ってみて学んだことをまとめてみました。
VMの命令は、スタック(値を上に積んでいき、上から取り出す入れ物)を使って計算します。 たとえば次の2つは、どちらもスタックとメモリの間で値をやり取りする命令です。
push local 2: local の2番目の値を、スタックに積むpop temp 3: スタックのいちばん上の値を取り出して、temp(一時メモ)の3番にしまうJavaScriptで書くと、次のようなイメージです。
add や gt のような計算の命令は、スタックの上から値を取り出して計算し、結果をまたスタックに積みます。
VMでは、RAMのどこを何に使うかが次のように決まっています。
| RAM | 使い道 |
|---|---|
| 0 | SP(スタックのいちばん上の次の番地) |
| 1 | LCL(local の先頭番地) |
| 2 | ARG(argument の先頭番地) |
| 3 | THIS(this の先頭番地) |
| 4 | THAT(that の先頭番地) |
| 5〜12 | temp(temp i は RAM[5 + i]) |
| 13〜15 | R13〜R15(VM変換器が自由に使ってよいメモ) |
| 16〜255 | static 変数(アセンブラが16から順に割り当てる) |
| 256〜2047 | スタック |
スタックが256番地から始まるのは、0〜255番地の場所とぶつからないようにするためです。
第6章のアセンブラで SP・LCL・ARG・THIS・THAT を定義済みシンボルとして0〜4番地に登録していましたが、それはこのVMの決まりのためだったんですね。
@SP と RAM[SP] の違い自分が最初に混乱したのがここです。第4章のロッカーの例えで整理すると、次のようになります。
@SP: ロッカーの番号(0番)SP(RAM[0] の中身): ロッカーの中に入っている、別のロッカーの番号RAM[SP]: その「中に入っていた番号」のロッカーの中身つまり SP 自体はスタックの値ではなく、スタックのどこを指しているかという番地 です。アセンブリでスタックの値を触るときは、次のように2段階でたどる必要があります。
VMでは、eq・gt・lt のような比較の結果を、true は -1、false は 0 で表します。1ではなく-1にしているのは、not で true と false を正しくひっくり返すためです。
-1(1111111111111111)を not すると 0000000000000000 になり、ちゃんと false(0)になる1(0000000000000001)を not すると 1111111111111110 になり、-2 になってしまい false(0)にならないnot はビットを全部反転させる命令なので、全ビットが1の -1 を true にしておくと、反転させたときにちょうど 0 になるわけです。
比較命令(eq・gt・lt)は、Hackのアセンブリに if がないので、ジャンプを使って変換します。
自分のVM変換器では、比較命令ごとに次のようなアセンブリを出力しています(gt の場合)。
流れは次のとおりです。
y、x の順に取り出して、D = x - y を計算するD の値が条件を満たせば (TRUE0) にジャンプして -1(true)を、満たさなければ 0(false)を RAM[SP] に入れるSP を1つ進めるM=-1 は RAM[A](スタックのいちばん上)に true を、M=0 は false を入れています。
ジャンプの部分は eq なら JEQ、lt なら JLT に変わります。また、ラベル名の 0 の部分は比較命令が出てくるたびに1ずつ増やしていて、同じラベルが2回定義されないようにしています。
D;JGT の判定はどこでやっているのかD;JGT と書くだけで「D が0より大きいか」が判定できるのが不思議だったのですが、これは第5章で作ったCPUがやっていることでした。
D を計算して、zr(0か)と ng(負か)を出す
comp が D のとき、ALUの x には D、y には A(ここでは @TRUE0 の行番号)が入るy は使わないので、zy で y を0にし、ny でビットを反転させて -1(全ビット1)にするx & y を計算すると、x の値だけが残るjjj の3ビットと zr・ng から「飛ぶかどうか」を決める
PC = A(TRUE0 の行番号)になる@TRUE0 で A にジャンプ先を入れておく理由も、第5章の PC(in= aOut, ...) を思い出すと納得できました。
第7章では、スタックを使うVMの命令をHackのアセンブリに変換するVM変換器を作りました。
SP が「値」ではなく「番地」を持っていることや、true を -1 にしている理由など、一見ただの決まりごとに見える部分にも、ちゃんとハードウェア側の理由があるのが面白いなと感じました。
特に比較命令の変換では、第4章のアセンブリ・第5章のCPU・第6章のアセンブラの知識が全部つながってきた感じがします。