【コンピュータシステムの理論と実装】プログラム制御おさらいメモ
- 手続き型言語における関数
- オブジェクト思考言語におけるメソッド
名前は違えども、高水準言語は自分の好きな処理を一つの塊として定義できる。これをルーチンと呼ぶ。
関数の中で実行される関数のことをサブルーチンと呼ぶ。
サブルーチンの呼び出しは高水準言語では簡単に行えるが、それを低レイヤで実現しようとすると結構だるい。
手順としては、
- 親ルーチン(サブルーチンの呼び出し側)からサブルーチンへ引数を渡す
- サブルーチンの呼び出し前に、親ルーチンの状態を保存する
- サブルーチンにて、ローカル変数のためのメモリ空間を確保する
- サブルーチンへjump
- サブルーチン実行
- 親ルーチンへ値を返す(return)
- サブルーチンのローカル変数のためのメモリ空間を再利用できるようにする
- 親ルーチンの状態を復帰させる
- サブルーチンの次の場所に実行を移す
プログラムの処理のために必要な準備を整える作業をハウスキーピング。
実は、サブルーチンの呼び出しもスタックを用いて解決できる。
VM言語からアセンブリを生成するために関数呼び出しのコマンドを用意する必要があり、これら3つのコマンドを用意することで、サブルーチンの呼び出し処理を実現できる。
- function {f} n: n個のローカル変数を持つfという関数を定義する
- call {f} m: fという関数を呼ぶ。m個の引数は呼び出し側によってスタックにプッシュ済みであるとする
- return: 親ルーチンへリターンする
親ルーチン側では何を意識するか?
関数の呼び出し側でやっていることをもう少し詳しく辿る。
呼び出し側でやっていることは
- 親ルーチン(サブルーチンの呼び出し側)からサブルーチンへ引数を渡す
- サブルーチンの呼び出し前に、親ルーチンの状態を保存する
- 親ルーチンの状態を復帰させる
- サブルーチンの次の場所に実行を移す
の4つである
サブルーチンへ引数を渡す
1のサブルーチンに引数を渡すためには、アセンブリ上でどのような挙動をすれば良いのかというと、必要な個数分の引数をスタックにpushすることで実現できる。
親ルーチンの状態を保存する
2の親ルーチンの状態を保存するためには、アセンブリ上でどのような挙動をすれば良いのか?
まず、親ルーチン上では以下のような状態(メモリセグメント)が存在する。
- リターンアドレス: 親ルーチンの実行位置がどこだったか
- LCL(local): 現在実行されている関数(=サブルーチン)のローカル変数を格納するためのベースアドレス。
- ARG(argument): 現在実行中の関数の引数を格納するためのベースアドレス
- THIS, THAT: 汎用セグメント。異なるヒープ領域に対応するように作られている。プログラミングのさまざまなニーズに応える。
ヒープ領域ってなんなん?
動的に確保可能なメモリ領域
スタックはFILO(先入れ後出し)の順序があるが、ヒープ領域には順番がないのでどの領域を解放するかはソフトウェア側で自由に決められる。
これら4つの状態をどうやって保存するかというと、全てスタックにpushすることで実現できる。
サブルーチンを呼び出す
2はcallコマンドによって関数を呼び出す。そうすると、サブルーチン実行前にpushした引数はスタックから消えていて、戻り値がスタックの最上位に来ている状態になる。
return後
親ルーチンのメモリ空間であるargument, local, static, this, thatはサブルーチン呼び出し前と同じ状態。
サブルーチン側では何を意識するか?
呼び出し側では、以下のフェースが担当領域。
3. サブルーチンにて、ローカル変数のためのメモリ空間を確保する
4. サブルーチンへjump
5. サブルーチン実行
6. 親ルーチンへ値を返す(return)
7. サブルーチンのローカル変数のためのメモリ空間を再利用できるようにする
引数がセットされる
サブルーチンが呼び出されると、argumentセグメントが、呼び出し時にセットされた実際の引数の値に初期化される。
また、ローカル変数のセグメント(LCL)が用意され、0に初期化される。
this, that, pointer, tempセグメントは最初は未定義。
戻り値をpush
関数の処理が全て完了したら、戻り値をスタックにpushする。