コンピュータシステムの
理論と実装
8章 バーチャルマシン#2 プログラム制御
アジェンダ
• 前回の復習
• プログラムフロー
• サブルーチン
• 実装
前回の復習
• バーチャルマシンを導入
– Hackプラットフォーム上で動作するVM実装を開
発
• VM言語からHack機械語へ変換するVM変換器を作成
した
今回やること
• 前回作成したVM変換器に機能を追加し、VM
実装を完成させる
– プログラムフローと関数呼び出し機能
プログラムフロー
• プログラムは通常、ひとつずつ順序通りに命
令が実行される
• 上記のフローは、分岐コマンドによって変更さ
れる
– e.g. ループによる反復処理
低水準のプログラミングにおける分岐
• 「goto destination」コマンドを用いて、任意の
場所にある命令を実行することで分岐できる
– 「destination」の指定には、
• 物理アドレスの直接指定
• シンボルラベルを用いて指定
がある
条件付き分岐
• 「if-goto destination」コマンドを用いて、与えられ
た条件がtrueの場合のみ移動する命令を実行さ
せられる
– ブール条件の導入には、スタックの最上位にある値
を条件とするのが最も自然
• 0でなければ指定された場所へ移動、0なら次のコマンドを
実行
プログラムフロー制御
if (cond)
s1
else
s2
...
while (cond)
s1
...
~ (cond)を計算するVMコード
if-goto L1
s1を計算するVMコード
goto L2
label L1
s2を計算するVMコード
...
label L1
~ (cond)を計算するVMコード
if-goto L2
s1を計算するVMコード
goto s1
label L2
...
高水準言語におけるフロー制御 疑似VMコード
「label」
「goto」
「if-goto」
「スタックの最上位に格納され
るブール値」
を使って、プログラムのフロー
制御を実現できる
サブルーチン
• ビルトインのコマンドに加えて、プログラマが定義する高水準の命令のこと
• 手続き型言語では
– 「サブルーチン」
– 「プロシージャ」
– 「関数」
• オブジェクト指向言語では
– 「メソッド」
と呼ばれるのが一般的
サブルーチン
• e.g. ビルトインのaddとサブルーチンpowerの例
– 引数をとり結果を返すという点では、同じ見た目
• 違いは「call」を用いるか否かという点だけ
// x+2
push x
push 2
add
...
// x^3
push x
push 3
call power
...
// (x^3+2)^y
push x
push 3
call power
push 2
add
push y
call power
...
// 累乗(power)関数
// 第1引数が第2引数だけ累乗
// される。power(2, 3)は
// 2の3乗を計算する。
function power
// (コードは省略)
push result
return
サブルーチン
• powerのようなサブルーチンは、一時記憶に
置かれるローカル変数を用いるのが普通
– ローカル変数は、サブルーチンを開始してからリ
ターンコマンドが実行されるまでの間、メモリ中に
保持される必要がある
ネスト化されたサブルーチン
• あるサブルーチンの中で別のサブルーチンを呼び、その別
のサブルーチンがさらに別の・・・
• 再帰処理の場合は、各処理は他と独立に実行され、専用の
ローカル変数と引数を保持しなければならない・・・
ネスト化された構造のためのメモリ管理を実現するには?
ネスト化されたサブルーチン
• 「call-returnロジック」は階層的な性質を持つ
サブルーチンの呼び出しチェーンは任意の深さまで再
帰的に繰り返すことができるが、
ある瞬間においてはそのチェーンの最後尾を実行して
いるだけ
– LIFO -> スタックのデータ構造と適合する
サブルーチン呼び出しとスタックの状態
start a
start b
start c
start d
end d
end c
start d
end d
end b
start c
start d
end d
end c
end a
subroutine a:
call b
call c
...
subroutine b:
call c
call d
...
subroutine c:
call d
...
subroutine d:
...
スタックの状態
bフレーム
cフレーム
dフレーム
aフレーム
dフレーム
cフレーム
dフレーム
コード フロー
まとめ:call xxx の実装
1. 呼び出し側のフレームをスタック上に格納
2. 呼び出された側(xxx)のローカル変数のための
メモリ領域を確保
3. シンボル名xxxをメモリアドレスへ変換して、そ
のアドレスへ移動
まとめ:return の実装
1. 「call xxx」に出くわす度、callコマンドの次のコマ
ンドのアドレスをスタックにプッシュ
2. (xxxを実行)
3. returnコマンドに出くわしたときに、スタックから
リターンアドレスをポップして、そのアドレスへ
gotoで移動する
VM仕様(第2部)
• 7章のVM仕様を拡張し、
– 「プログラムフロー」
– 「関数呼び出し」
の2つのコマンドを追加する
プログラムフローコマンド (p.174)
• フローコマンドは以下の3種
– label xxx
– goto xxx
– if-goto xxx
label xxx
関数のコードにおいて現在の位置をラベル付けする。
プログラムの他の場所から移動する場合、その目的となりうる場所は
ラベル付けされた場所に限られる。
ラベルのスコープは、それが定義された関数内である。
ここでxxxというラベル名には任意の文字列—アルファベット、数字、
アンダースコア(_)、ドット(.)、コロン(:)—を用いることができる。
ただし、数字から始まる文字列は除く。
goto xxx
無条件の移動命令を行う。
xxxでラベル付けされた場所からプログラムの実行を開始する。
移動先は同じ関数内に限られる。
if-goto xxx
条件付きの移動を行う。
スタックの最上位の値をポップし、
* その値が0でなければ、xxxでラベル付けされた場所からプログラ
ムの実行を開始する。
* その値が0であれば、プログラムの次のコマンドが実行される。
移動先は同じ関数内に限られる。
関数呼び出しコマンド
• 関数呼び出しコマンドは以下の3種
– function f n
• n個のローカル変数を持つ f という名前の関数を定義する
– call f m
• f という関数を呼ぶ。ここで、m個の引数は、呼び出し側によってスタックに
プッシュ済みであるとする
– return
• 呼び出し元へリターンする
関数呼び出しプロトコル
• 関数を呼び出す側の視点
1. 必要な個数分の引数をスタックにプッシュ
2. callコマンドを用いて関数の呼び出し
3. 関数がリターンされた後
• 呼び出し側がプッシュした引数はスタックから取り除かれ、戻り値がスタックの最上
位に格納されている
• 呼び出し側のメモリセグメントであるargument 、local、static、this、that、pointerは関
数を呼ぶ前と同じ(tempセグメントは未定義)
関数呼び出しプロトコル
• 関数を呼び出される側の視点
1. 関数の実行が開始すると、
• argumentセグメントが呼び出し側から渡された実際の引数の値に初期化される
• local変数のセグメントが割り当てられ、0に初期化
• staticセグメントは、そのVMファイルのstaticセグメントに属する
• ワーキングスタックは空
• this、that、pointer、tempセグメントは最初は未定義
2. リターンされる前に、呼び出された関数はスタック上に値をプッシュしなければならない
初期化
• VM実装の実行が開始されると、「Sys.init」と呼ばれる引数
のないVM関数を実行するのが慣例
– 「Sys.init」関数はユーザープログラムの「main」関数を呼ぶ
• 「VMにはブートストラップ用のコードが含まれ、自動的に
Sys.initを呼び出す」
– 「Sys.init」は12章で実装する模様
テストプログラムと初期化
• 以下の3つのテストプログラムは、スタートアップコードが実装されていない前提に
なっている
– BasicLoop
– FibonacciSeries
– SimpleFunction
• 以下の3つのプログラムは、VM実装としてスタートアップコードを実装すること
– FibonacciElement
– NestedCall
– StaticTest
Hackプラットフォームの標準VMマッピング
グローバルスタックの構造 (p.178)
引数0
引数1
・・・
引数n-1
リターンアドレス
保存されたLCL
保存されたARG
保存されたTHIS
保存されたTHAT
ローカル変数0
ローカル変数1
・・・
ローカル変数k-1
呼び出しチェーン上の
すべての関数フレーム
ARG ->
LCL ->
SP ->
現在の関数のためにプッシュされた引
数
呼び出し側の関数の状態を保存する。
現在の関数から呼び出し側の関数へリ
ターンするときに、リターンアドレスが使
用される。
また保持されているセグメントの情報が
復元される。
現在の関数のローカル変数
現在の関数のワーキングスタック
関数呼び出しコマンドのVM実装 (p.179)
VMコマンド VM実装によって生成される(疑似)コード
call f n
(n個の引数がスタックにプッシュされた後に
関数fが呼ばれる)
push return-address // (以下のラベル宣言を用いる)
push LCL // 関数の呼び出し側のLCLを格納する
push ARG // 関数の呼び出し側のARGを格納する
push THIS // 関数の呼び出し側のTHISを格納する
push THAT // 関数の呼び出し側のTHATを格納する
ARG = SP-n-5 // ARGを別の場所に移す(n=引数の数)
LCL = SP // LCLを別の場所に移す
goto f // 制御を移す
(return-address) // リターンアドレスのためのラベルを宣言する
function f k
(k個のローカル変数を持つ関数fを宣言す
る)
(f) // 関数の開始位置のためのラベルを宣言する
repeat k times: // k=ローカル変数の個数
push 0 // すべてを0で初期化する
return
(関数からのリターン)
FRAME = LCL // FRAMEは一時変数
RET = * (FRAME-5) // 一時変数に保存されているリターンアドレスを取得する
*ARG = POP() // 関数の呼び出し側のために、関数の戻り値を別の場所へ移す
SP = ARG+1 // 呼び出し側のSPを戻す
THAT = * (FRAME-1) // 呼び出し側のTHATを戻す
THIS = * (FRAME-2) // 呼び出し側のTHISを戻す
ARG = * (FRAME-3) // 呼び出し側のARGを戻す
LCL = * (FRAME-4) // 呼び出し側のLCLを戻す
goto RET // リターンアドレスへ移動する(呼び出し側のコードへ戻る)
アセンブリ言語のシンボル (p.180)
シンボル 使用法
SP、LCL、ARG、THIS、THAT SPはスタックの最上位の場所を指す。残りのシンボルについては、仮想セグメントであるlocal、argument、
this、thatのベースアドレスを指す
R13-R15 どのような目的でも使うことができる
Xxx.j Xxx.vmというVMファイルに存在するスタティック変数jはアセンブリのシンボルにおいてXxx.jに変換され
る。その後に続くアセンブリ処理では、そのシンボル変数は、HackアセンブラによってRAM領域に割り当
てられる
functionName$label Xxx.vmというVMファイルに存在するラベルコマンドがあれば、VM実装はグローバル(プログラム全体)で
ユニークな"f$b"というシンボルを生成する。
ここでfは関数名であり、bはVM関数コード内のラベル記号である。
「goto b」や「if-goto b」などのVMコマンドが対象の言語に変換されると、ラベルの指定には「b」のかわり
に「f$b」を使わなければならない
(functionName) fというVM関数を定義すると、「f」というシンボルが生成され、そのシンボルはその関数の開始位置を指
す
return-address VM関数呼び出しは、変換コードにユニークなシンボルを挿入し、このユニークなシンボルはリターンアド
レスとして、つまり関数呼び出しの次のコマンドがあるメモリ位置として機能する
ブートストラップ(初期化)
• VM変換後の.asmファイルは以下の規則に従
う
– VMスタックはRAM[256]から先へ対応付けする
– 最初に実行するVM関数はSys.initである
• 上記を解決するために、次のコードをブートス
トラップコードとして実行する
SP = 256 // スタックポインタを0x100に初期化
call Sys.init // (変換されたコードの) Sys.initを実行
テストプログラム
• プログラムフローコマンドのテスト
– BasicLoop
– Fibonacci
• 関数呼び出しコマンドのテスト
– SimpleFunction
– FibonacciElement
– StaticTest
実装
ユニットテスト
• プログラムフローコマンドが実装出来たら、
CPU Emulatorで
– BasicLoop
– FibonacciSeries
のユニットテストが通るか確認
実装: プログラムフローコマンド
(xxx)label xxx
.vm .asm
@xxx
0;JMP
goto xxx
@SP
AM=M-1
D=M
@xxx
D;JNE
if xxx
ユニットテスト
• 「function」と「return」が実装出来たら、
CPU Emulatorで
– SimpleFunction
のユニットテストが通るか確認
実装: 関数呼び出しコマンド
(xxx)
// C_PUSH constant 0
@0
D=A
@SP
A=M
M=D
@SP
M=M+1
// ... ↑C_PUSH constant 0 をn回
function xxx n
.vm .asm
実装: 関数呼び出しコマンド
// FRAME =LCL
@LCL
D=M
@R13
M=D
// RET = *(FRAME-5)
@5
A=D-A
D=M
@R14
M=D
// 続く・・・
return
.vm .asm // *ARG = POP()
@ARG
D=M
@R15
M=D
@SP
AM=M-1
D=M
@R15
A=M
M=D
// 続く・・・
// SP = ARG + 1
@ARG
D=M
@SP
M=D+1
// THAT = *(FRAME-1)
@R13
AM=M-1
D=M
@THAT
M=D
// THIS = *(FRAM-2) ...
// (略)
ユニットテスト
• 「call」の実装ができたら、CPU Emulatorで
– FibonacciElement
– NestedCall
– StaticsTest
のユニットテストが通るか確認
実装: ブートストラップ
@256
D=A
@SP
M=D
// call Sys.init 0
(略)
SP=256
call Sys.init
ブートストラップ .asm
実装: 関数呼び出しコマンド
// push return-address
@RET_ADDRESS_{count}
D=A
@SP
A=M
M=D
@SP
M=M+1
// push LCL
@LCL
D=M
@SP
call functionName numArgs
.vm .asm A=M
M=D
@SP
M=M+1
// push ARG ...
...
(略)
...
// ARG = SP-n-5
@SP
D=M
@5
D=D-A
// 実際は整数
@{numArgs}
D=D-A
@ARG
M=D
// LCL = SP
@SP
D=M
@LCL
M=D
// goto f
(略)
...
終わり
• お疲れ様でした
• JavaScript(Node.js)での実装サンプルは以下
にあります
– https://github.com/khashii/nand2tetris/tree/mast
er/projects/08/VMtranslator

08