UP | HOME

手続き呼出し規約とは - 2023年度 システムプログラミング

手続き呼出し規約とは

はじめに

手続き呼出し規約については,教科書では,2.8節「コンピュータ・ハードウェア内での手続きのサポート」と B.6節「手続き呼出し規約」で詳しく説明されています. コンピュータハードウェア内での手続きのサポートのために手続き間で以下の方法を定めておく必要があります.

  1. 引数の渡し方,戻り値の受け取り方
  2. 呼出し元への戻り方
  3. 手続き内でのレジスタの保存

ここでは,これらについての取り決めについて概要とその意義を説明します.

引数の渡し方と戻り値の受け取り方

C言語からの関数呼出しを

a = func(1, 2, 3, 4, 5, ...);

とした場合,MIPSのコンパイラは, 1$a02$a1 に… 4$a3 にという具合に $a? を使って引数を渡します. $a?$a0〜$a3 の 4つしかないので, 5つ目の引数 (ここでは 5) は,スタック(後述) に保存して渡すことになっています.

戻り値は,32ビットまでの値 (long 型まで) なら $v0 に入れて返します.32ビットを越える場合は, $v0, $v1 に上位下位を分割代入して返します.

呼出し元への戻り方

これまで jal を用いた手続きからの復帰方法については学習しました. おおよそ以下の通りです.

  • 呼出し側は, jal 命令を用いて,戻って来る場所を $ra に入れてから呼出す
  • 手続き側(呼び出される側) は, $ra を利用して j $ra で戻る

このため,呼び出された手続きが更に別の手続きを呼ぶ場合, $ra を保存しておかなくてはならないことも学びました. $ra を一旦 $s0 などのレジスタに保存してから手続きを呼び出す手法です.

しかし,手続き呼出しが何段にもなった場合, 限りあるレジスタでその復帰先を保存する方式には限界があります. そもそも, $ra 以外のレジスタ ($s0$t0 など) も手続き呼出し間で保存しておかなくてはならないことにも気付くでしょう. つまり, $ra を一旦 $s0 に保存しておく方式は,破綻することが明らかです.

もう一度手続き呼出しの流れを整理してみます.

(a) 呼出し元 START

(b) $a0 〜 $a3, スタックに引数セット

(c) jal func で func を呼出し
    ($ra が上書きで壊される)       → (d) func スタート

                                      (e)  $a0〜$a3,スタックの引数に従って
                                           処理を実行

                                            :  各種レジスタが壊れる(A)

                                      (f)  $v0, $v1 に戻り値をセット

(h) 戻り値の $v0 (及び $v1)        ← (g) j $ra で戻る
    を用いて続きの処理

(i) 呼出し元に j $ra で戻る??

という処理になります.問題点を整理します.

  1. (A)の領域で, $ra を含めたレジスタが壊れるので, 呼出し元,手続き内のどちらかが保存しておく必要がある. つまり,レジスタ保存の責任に関する取り決めが必要です.
  2. 呼出しが多段になった場合, $ra を含めたレジスタを保存する場所が必要である. これを解決するためには,メモリを使わざるを得ません.

つまり,呼出し元へ正しく戻るためには, レジスタを正しく保存,復元することと同じ問題であると分かります.

レジスタ使用規約

これまでの議論で分かるように, レジスタ保存の責任に関する取り決めが必要です. 具体的には,手続き内で値を書換えてしまってもよいレジスタと, 手続きの呼出し前後で値を変えてはいけない(使用するなら保存して復元が必要) レジスタを決めておくということです. MIPS では,以下のように決められています.

手続き内で壊してよい
$a0〜$a3,$t0〜$t9,$v0〜$v1
手続き内で壊してはいけない
$s0〜$s7, $sp, $ra

これをプログラミングの観点からまとめると,以下のようになります.

レジスタ 規約
$a0〜$a3 手続き内で壊してよい.逆にいえば,他を呼出すと壊されるので必要ならば自分で保存 (引数として使わなくても)
$v0〜$v1 同上
$t0〜$t9 同上
$s0〜$s7 手続き内で使用する場合は,最初に保存して,最後に復元
$sp, $fp 同上
$ra 手続き内で他の手続きを呼出す場合 (jal する場合) は保存.通常は,常に最初に保存,最後に復元するのが無難.

これらを守らないと,C言語で書いた関数との連携ができないわけです.

レジスタの保存と復元 – スタックの利用

手続き呼出しが多段になった場合を考えると, レジスタの一時的な保存と復元のためにメモリを使用せざるを得ないということが分かります.

また,レジスタ保存以外にも,手続き内だけで一時的にメモリを使用することがあります. 例えば,手続き内で利用する配列や変数のための領域です. C言語では,関数内で宣言される変数がこれに対応します.

こうした手続き内のみで一時的に必要なメモリは,スタックセグメントと呼ばれる領域に確保します. スタックセグメントが実際にメモリ中のどのアドレスに配置されているかは,意識する必要はありませんが, 詳細は,教科書B.5節「主記憶領域の使用法」にあります.

さて,スタック(stack)は,言葉の通り物を積上げるように領域を確保, 解放するデータ構造です. そのため,スタックをどこまで使ったか(どのアドレスまで使用データが積上がっているか) を表すポインタが1つ必要です. MIPS では,レジスタ $sp を用いて,これをスタックポインタと呼びます.

スタックは,値が減少する方向に伸張するので, スタック領域を手続き内で利用するために 32バイト確保するには,以下のようにします.

subu $sp, $sp, 32  # 32バイト確保

こうすることで, 0($sp) 〜 31($sp) までの 32バイトを手続き内で一時的に利用できます. また,手続き内で使い終わったら, $sp を元に戻して,未使用に(解放)します.

addu $sp, $sp, 32

手続きが多段に呼び出された場合でも, スタックが多段に積み上がることでデータを確保できることが分かると思います.

スタックを用いた値の退避と復元例

以下は,実際にスタックを確保している様子です.

satck-frame1.png
Figure 1: スタックを確保している様子

スタックの確保,解放を手続きの最初と最後で行うことで, 「新たな確保領域」を一時的に使用します. 実際の手続き内でのプログラムを以下に示します.

 1: subu $sp, $sp, 32       # スタックを32バイト確保
 2: sw   $ra, 20($sp)       # $sp + 20 のアドレスにもともとの
 3:                         # 呼出し元アドレスを保存
 4:                         # X区間で jal しなければ,不要
 5: 
 6: sw   $fp, 16($sp)       # $sp + 16 のアドレスに $fp を保存
 7: addu $fp, $sp, 28       # 新しく $fp を設定
 8: 
 9: # (この間で必要な処理) (X区間)
10: 
11: lw   $ra, 20($sp)       # 保存しておいた $ra を復元
12: lw   $fp, 16($sp)       # 保存しておいた $fp を復元
13: addu $sp, $sp, 32       # スタックを開放
14: j    $ra                # 呼出し元に戻る

もちろん, $fp, $ra 以外にも保存すべきレジスタがある場合は, 24($sp) ($ra の保存場所の直後)などに保存しておいて,最後に復元します. X区間(9行目)直前のスタックの様子は,以下の通りです.

$sp offset内容 領域名
: - 既に使用中の領域
旧$sp→ +32〜+35 - 既に使用中の領域
新$fp→ +28〜+31レジスタ保存用1新たな確保領域
+24〜+27レジスタ保存用2新たな確保領域
+20〜+23保存した $ra 新たな確保領域
+16〜+19保存した $fp 新たな確保領域
+12〜+15 - 新たな確保領域
+08〜+11 - 新たな確保領域
+04〜+07 - 新たな確保領域
新$sp→ +00〜+03 - 新たな確保領域
-04〜-01 - 未使用領域
: - 未使用領域

4バイト単位で考えて, 新$fp新$sp が指すアドレスの間が利用できる領域であると分かります. この区間をスタックフレームと呼びます. $fp はフレームポインタと呼ばれるように,フレームの領域を計算しやすくするために使用されますが, 必須ではありません.32バイト確保したというのを覚えておけば, $sp からの相対位置だけでフレームを管理できるからです. $fp を使用するコンパイラとそうでないコンパイラがあります.

X区間で壊す可能性があるレジスタについては, 「レジスタ保存用1, 2」の部分に保存することができます. $ra, $fp 以外にレジスタの保存が不要な場合は, この領域分だけ,スタックフレームを短かくできます. また,+00 から +15 までの領域は, ムダに開けてあるように見えます. この領域の用途については,後のCコンパイラとの連携で説明します.

また,手続き内では,24バイト以上,8バイト単位でスタックを確保して使用するという規約があるので, それを守っています. この例では, +00 から +31 までの32バイトを確保しています.

Author: Yoshinari Nomura

Emacs 27.1 (Org mode 9.3)