ABI (Application Binary Interface) は、関数を呼び出す側 (caller) と呼ばれる側 (callee) のあいだの取り決めです。
- 引数・返り値の配置
- レジスタの保存責任
- スタックフレームの作り方
レジスタの役割
汎用レジスタ 10 本を 3 グループに分け、RISC-V 風の役割を割り当てます。
| Addr | 名前 | 用途 | 保存責任 |
|---|---|---|---|
0x0006 | a0 | 引数 / 返り値 | caller |
0x0007 | a1 | 引数 / 返り値 | caller |
0x0008 | t0 | 一時 | caller |
0x0009 | t1 | 一時 | caller |
0x000A | t2 | 一時 | caller |
0x000B | t3 | 一時 | caller |
0x000C | s0 | 保存 | callee |
0x000D | s1 | 保存 | callee |
0x000E | s2 | 保存 | callee |
0x000F | s3 | 保存 | 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 セットに集約されます。逆に呼出を跨がない短命な値は t か a に置けば退避コードが発生しません。
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
- 呼出後にも使いたい caller-save レジスタ (
a0-a1,t0-t3) を自分のフレームに退避する - 第 3 引数以降がある場合、
subi(sp, sp, k)でスタックを伸ばしstoreで配置する - 第 1, 2 引数を
a0,a1にセットする call(callee)で呼ぶ
ステップ 1 は「呼出後にも値が必要」な場合のみです。再計算可能ならスキップできます。
callee : prologue
- 必要なフレームサイズ
Nを計算: 保存対象のra(他関数を呼ぶ場合) + 使うsレジスタ数 + ローカル変数 + spill 領域 subi(sp, sp, N)でフレームを確保- 使う
sレジスタをstoreで退避 - 他関数を呼ぶなら
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
- 退避した
sレジスタを復元 raを復元 (退避していれば)addi(sp, sp, N)でフレームを解放ret()で復帰
load(s1, sp, 1)
load(s0, sp, 2)
load(ra, sp, 3)
addi(sp, sp, 4)
ret()
caller : after
- 返り値が必要なら
a0(およびa1) から読み取る - スタックに積んだ引数があるなら
addi(sp, sp, k)で解放 - 退避した caller-save レジスタを復元
葉関数 (leaf function) の最適化
他関数を呼ばない葉関数は ra の退避が不要です。さらに s レジスタを使わず a / t だけで済めば、フレーム確保自体が不要になります。
add_one: // int add_one(int x) { return x + 1; }
addi(a0, a0, 1)
ret()