2026-11-11

コンピューターシステムの理論と設計 ~第八章 仮想マシン(プログラム制御)~

引き続き、『コンピュータシステムの理論と実装』(通称 nand2tetris)を読み進めています。 今回は第8章で、第7章で作ったVM変換器に label・goto・if-goto や function・call・return といった プログラムの流れを制御する命令 を追加しました。その中でも一番頭を使った return の変換と、プログラムの最初に動く ブートストラップ について学んだことをまとめてみました。

関数呼び出しでスタックに積まれるもの

return の変換を理解するには、まず call のときにスタックがどうなっているかを押さえておく必要があります。 call で関数を呼ぶと、引数の後ろに 呼び出し元に戻るための情報 が5つ積まれ、その後ろに呼ばれた関数のローカル変数が並びます。

LCL は、関数のローカル変数がRAMの何番地から始まるかを覚えているメモです。保存した5つの値のすぐ後ろにローカル変数が並ぶので、LCL から1つずつ戻っていけば、保存しておいた値を取り出せます。 この LCL の値を、return の中では frame と呼んでいます。

保存している5つの値

保存している値がそれぞれ何なのか、次のコードで main が foo を呼んだ場合を例に、foo から見た形で整理してみます。

  • リターンアドレス: foo の処理が終わった後に、main のどこに戻るかを記録している
  • 呼び出し元の LCL: main のローカル変数は a・b・r で、その先頭の a の番地を記録している
    • return のときに LCL をこの番地に戻すことで、main に戻った後もローカル変数をまた読めるようにしている
  • 呼び出し元の ARG: ARG は関数の引数の先頭の番地で、呼び出し元の LCL と同じように main の引数の先頭の番地を記録している。用途も同じ
  • THIS: THIS は今扱っているオブジェクトの先頭の場所を指す。オブジェクトを操作したいときに、最初に THIS をオブジェクトの番地にセットして、THIS 経由でオブジェクトを操作する
  • THAT: THAT は今扱っている配列の要素の場所を指す。配列を読み書きするたびに THAT を要素の番地にセットして、THAT 経由で配列を操作する

ちなみに、図の ローカル変数0 ← LCL の LCL は、foo 実行中の LCL のことで、foo のローカル変数 x の番地を指しています。 LCL・ARG・THIS・THAT はRAMの1〜4番地に1つずつしかない入れ物なので、foo が自分用に書き換えると main の値が分からなくなってしまいます。そのため、呼ぶ前にスタックに保存しておき、return で元に戻しているわけです。

また、記録しているのはローカル変数の中身ではなく 先頭の番地だけ です。local i は RAM[LCL + i] を読むので、先頭の番地さえ戻せば、main のローカル変数がいくつあっても元どおり読めます。

return の変換

自分が書いた return の変換は次のとおりです。

やっていることを順番に整理すると、次のようになります。

  1. frame = LCL: 今の LCL(保存した値のすぐ後ろの番地)を R13 にメモする 今の順番(LCL を最後に戻す)ならなくても動くけど、LCLは手順5で書き変わるため、先に取っておくと戻す順番を気にしなくてよくなる。
  2. retAddr = *(frame - 5): 戻り先の行番号を R14 にメモする 引数が0の関数の場合、手順3で戻り値を置く場所(ARG)とリターンアドレスの場所(frame-5)が同じ番地になるので、先にメモしておくことで、戻り先が分からなくなることを防ぐ
  3. *ARG = pop(): スタックのいちばん上にある戻り値を、引数0があった場所に置く *ARGはARGの番地ではなく、番地の中の値 fooの計算で出た答え(戻り値)をmainが受け取る場所に移している。 ARGにfooの戻り値を移す理由は、ARGがfooのために使ったスタック領域の一番下で、4の処理でARGより上を全部すてることになるから。
  4. SP = ARG + 1: SP を戻り値のすぐ後ろに戻す これで、戻り値がスタックの一番上になり、次のスタック処理では前の処理で使ってもう使わなくなったスタックを上書きできる
  5. THAT・THIS・ARG・LCL を元に戻す: frame - 1 〜 frame - 4 から、呼び出し元の値を取り出す
  6. goto retAddr: 呼び出し元に戻る

3と4によって、呼び出し元から見ると「引数を積んで call したら、引数が消えて代わりに戻り値が1つ積まれている」という状態になります。第7章の add が「2つ取り出して、結果を1つ積む」のと同じ形になっているんですね。

なお、メモに R13・R14 を使っているのは、第7章のRAMの表にあったとおり、R13〜R15 が VM変換器が自由に使ってよいメモ だからです。

ブートストラップ

writeBootstrap は、VM変換器にフォルダ(複数の .vm ファイル)を渡したときに、出力するアセンブリの一番最初に入れる処理です。やっていることは次の2つです。

  • SP = 256: 第7章のRAMの表のとおり、スタックは256番地から始まるので、SP を256にしておく
  • call Sys.init: プログラムの入り口は Sys.init と決まっているので、まずそれを呼ぶ

JavaScriptやC言語で main 関数から実行が始まるのと同じように、VMでは Sys.init から始まるという約束になっています。 第5章で「電源を入れた直後は reset で0行目から実行される」と書きましたが、その0行目に置かれているのがこのブートストラップです。電源が入る → 0行目のブートストラップでスタックを準備する → Sys.init を呼ぶ、という流れでプログラムが動き始めるわけです。

まとめ

第8章では、VM変換器に関数呼び出しやブートストラップを追加しました。 return は一見ややこしいですが、call のときに積んだ5つの値を逆順に取り出して元に戻しているだけ、と分かると読みやすくなりました。 また、ブートストラップを通して、第5章の reset からVMの Sys.init までがつながったのが気持ちよかったです。

参考資料

  • nand2tetris Web IDE
  • nand2tetris 公式サイト

Thanks for the visit Nozo Blog