Kanade Electronicskanaelecom
🔄

関数呼び出し規約

RK16 マイコン上で関数呼び出しがどう動くかを定義する ABI

ABI (Application Binary Interface) は、関数を呼び出す側 (caller) と呼ばれる側 (callee) のあいだの取り決めです。

  1. 引数・返り値の配置
  2. レジスタの保存責任
  3. スタックフレームの作り方

レジスタの役割

汎用レジスタ 10 本を 3 グループに分け、RISC-V 風の役割を割り当てます。

Addr名前用途保存責任
0x0006a0引数 / 返り値caller
0x0007a1引数 / 返り値caller
0x0008t0一時caller
0x0009t1一時caller
0x000At2一時caller
0x000Bt3一時caller
0x000Cs0保存callee
0x000Ds1保存callee
0x000Es2保存callee
0x000Fs3保存callee
  • a0-a1 (caller-save): 第 1, 2 引数および第 1, 2 返り値を運ぶ。呼出を跨いで保持されない
  • t0-t3 (caller-save): 一時計算用。呼出で破壊されうるので、跨いで使いたい値は caller が退避する
  • s0-s3 (callee-save): callee が prologue で退避・epilogue で復元する。caller から見れば呼出を跨いで値が保たれる

呼び出しを跨いで生存する変数は s に置く のが基本戦略です。これで spill が callee の prologue/epilogue 1 セットに集約されます。逆に呼出を跨がない短命な値は ta に置けば退避コードが発生しません。

 ra (0x0004) と sp (0x0005) も含めて整理すると:

  • ra: call 命令が PC+1 を書き込む。callee がさらに他関数を呼ぶ場合は prologue で退避する
  • sp: callee-save。関数を抜ける時点で呼出前の値に復元する

引数と返り値

  • 引数: 第 1, 2 引数は a0, a1 に置く。第 3 引数以降はスタック経由 (caller がフレームを伸ばして配置する)
  • 返り値: 16bit 単一値は a0。32bit ペアを返すなら a0, a1
  • void 関数では a0 の内容は未定義

スタックレイアウト

スタックは高位アドレスから低位アドレスへ伸びます。リセットベクタで loadi(sp, 0xffff) として最上位アドレスから始め、関数呼出のたびに subi(sp, sp, N) でフレームを伸ばします。

高位アドレス
+--------------------+
| caller's frame     |
+--------------------+ <- caller's sp (呼出直前)
| stack arg 2        |  caller が積む
| stack arg 3        |
|  : :               |
+--------------------+ <- sp on entry
| saved ra           |  callee が積む (他関数を呼ぶ場合のみ)
| saved s0           |  callee が積む (使う分のみ)
| saved s1           |
|  : :               |
| local variables    |
| spill slots        |
+--------------------+ <- sp in function body
低位アドレス

 sp 自体が callee-save なので、caller は sp の値を退避する必要がありません。

呼出シーケンス

caller : before

  1. 呼出後にも使いたい caller-save レジスタ (a0-a1, t0-t3) を自分のフレームに退避する
  2. 第 3 引数以降がある場合、subi(sp, sp, k) でスタックを伸ばし store で配置する
  3. 第 1, 2 引数を a0, a1 にセットする
  4. call(callee) で呼ぶ

 ステップ 1 は「呼出後にも値が必要」な場合のみです。再計算可能ならスキップできます。

callee : prologue

  1. 必要なフレームサイズ N を計算: 保存対象の ra (他関数を呼ぶ場合) + 使う s レジスタ数 + ローカル変数 + spill 領域
  2. subi(sp, sp, N) でフレームを確保
  3. 使う s レジスタを store で退避
  4. 他関数を呼ぶなら ra も退避
foo:
  subi(sp, sp, 4)    // N = 4
  store(ra, sp, 3)   // 後で call するので ra を保存
  store(s0, sp, 2)   // 使う s レジスタを保存
  store(s1, sp, 1)
  // sp+0 はローカル用

callee : 本体

 第 3 引数以降には sp + N + (引数番号 - 2) でアクセスできます。フレーム N word の上が caller のスタック引数領域です。

callee : epilogue

  1. 退避した s レジスタを復元
  2. ra を復元 (退避していれば)
  3. addi(sp, sp, N) でフレームを解放
  4. ret() で復帰
  load(s1, sp, 1)
  load(s0, sp, 2)
  load(ra, sp, 3)
  addi(sp, sp, 4)
  ret()

caller : after

  1. 返り値が必要なら a0 (および a1) から読み取る
  2. スタックに積んだ引数があるなら addi(sp, sp, k) で解放
  3. 退避した caller-save レジスタを復元

葉関数 (leaf function) の最適化

 他関数を呼ばない葉関数は ra の退避が不要です。さらに s レジスタを使わず a / t だけで済めば、フレーム確保自体が不要になります。

add_one:              // int add_one(int x) { return x + 1; }
  addi(a0, a0, 1)
  ret()