2026-11-10

コンピューターシステムの理論と設計 ~第七章 仮想マシン(スタック算術)~

引き続き、『コンピュータシステムの理論と実装』(通称 nand2tetris)を読み進めています。 今回は第7章で、push local 2 や add のような VM(仮想マシン)の命令 を、第6章までで扱ってきたHackのアセンブリに変換する VM変換器 を作ってみて学んだことをまとめてみました。

VMの命令

VMの命令は、スタック(値を上に積んでいき、上から取り出す入れ物)を使って計算します。 たとえば次の2つは、どちらもスタックとメモリの間で値をやり取りする命令です。

  • push local 2: local の2番目の値を、スタックに積む
  • pop temp 3: スタックのいちばん上の値を取り出して、temp(一時メモ)の3番にしまう

JavaScriptで書くと、次のようなイメージです。

add や gt のような計算の命令は、スタックの上から値を取り出して計算し、結果をまたスタックに積みます。

RAMの使い方

VMでは、RAMのどこを何に使うかが次のように決まっています。

RAM使い道
0SP(スタックのいちばん上の次の番地)
1LCL(local の先頭番地)
2ARG(argument の先頭番地)
3THIS(this の先頭番地)
4THAT(that の先頭番地)
5〜12temp(temp i は RAM[5 + i])
13〜15R13〜R15(VM変換器が自由に使ってよいメモ)
16〜255static 変数(アセンブラが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段階でたどる必要があります。

trueは1ではなく-1

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 の場合)。

流れは次のとおりです。

  1. スタックから y、x の順に取り出して、D = x - y を計算する
  2. D の値が条件を満たせば (TRUE0) にジャンプして -1(true)を、満たさなければ 0(false)を RAM[SP] に入れる
  3. 最後に 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がやっていることでした。

  1. ALUが D を計算して、zr(0か)と ng(負か)を出す
    • comp が D のとき、ALUの x には D、y には A(ここでは @TRUE0 の行番号)が入る
    • ただし y は使わないので、zy で y を0にし、ny でビットを反転させて -1(全ビット1)にする
    • そのうえで x & y を計算すると、x の値だけが残る
  2. ジャンプ判定の回路が、jjj の3ビットと zr・ng から「飛ぶかどうか」を決める
    • 飛ぶなら PC = A(TRUE0 の行番号)になる

@TRUE0 で A にジャンプ先を入れておく理由も、第5章の PC(in= aOut, ...) を思い出すと納得できました。

まとめ

第7章では、スタックを使うVMの命令をHackのアセンブリに変換するVM変換器を作りました。 SP が「値」ではなく「番地」を持っていることや、true を -1 にしている理由など、一見ただの決まりごとに見える部分にも、ちゃんとハードウェア側の理由があるのが面白いなと感じました。 特に比較命令の変換では、第4章のアセンブリ・第5章のCPU・第6章のアセンブラの知識が全部つながってきた感じがします。

参考資料

  • nand2tetris Web IDE
  • nand2tetris 公式サイト

Thanks for the visit Nozo Blog