MIPSの関数呼び出し規約を、gccを使って確認してみました。教科書によると、$4〜$7($a0〜$a3)が引数、$2〜$3($v0〜$v1)が戻り値として使われます。
元となるソース
違いを確認するために、引数が4個の関数と、引数が5個の関数を呼び出し、gccの出したアセンブラを見てみます。
int foo(void); int f4(int a, int b, int c, int d); int f5(int a, int b, int c, int d, int e); int foo(void) { int a; int b; a = f4(1,2,3,4); b = f5(5,6,7,8,9); return a + b; } int f4(int a, int b, int c, int d) { return a + b + c + d; } int f5(int a, int b, int c, int d, int e) { return a + b + c + d + e; }
引数の渡し方(引数が4つ以下)
呼び出し側では、引数を$4から$7にロードし、jal命令で関数を呼び出します。このときに、呼び出し側のアドレス(戻り先)が自動的に$31に保存されます。
li $4,1 # 0x1 li $5,2 # 0x2 li $6,3 # 0x3 li $7,4 # 0x4 sw $31,28($sp) jal f4 sw $16,24($sp)
呼び出された側では、$4〜$7を引数として使い、$2に戻り値を入れ、$31に保存されている戻り先に飛ぶことで、関数から戻ります。
addu $3,$4,$5 addu $2,$3,$6 j $31 addu $2,$2,$7
jの後にaddu命令がありますが、これが遅延スロットと呼ばれる物です。ジャンプの後にもう一命令実行しています。教科書の後の方で詳しくでてきますが、この例ではjの先にadduを計算し、nopを入れても同じです。
addu $3,$4,$5 addu $2,$3,$6 addu $2,$2,$7 j $31 nop
引数の渡し方(引数が5個以上)
呼び出し側では、5つ目以降の引数(今の例だと9)をスタックに積んで、関数を呼び出します。
move $16,$2 li $2,9 # 0x9 sw $2,16($sp) li $4,5 # 0x5 li $5,6 # 0x6 li $6,7 # 0x7 jal f5 li $7,8 # 0x8
呼び出された側では、5つ目の引数はスタックから取り出して使用します。
addu $8,$4,$5 addu $3,$8,$6 lw $2,16($sp) addu $4,$3,$7 j $31 addu $2,$4,$2
戻りアドレスの処理
関数の呼び出しにはjal(Jump and Link)命令を使用します。教科書から説明を引用します。
Unconditionally jump to the instruction at target. Save the address of the next instruction in register $ra.
(この命令は、オペランドに対して無条件ジャンプをします。同時に、次の命令のアドレスを$ra($31)レジスタに保存します。)
呼び出された側での$31の扱いは、その関数が他の関数を呼び出しているかどうかで変わります。ある関数が別の関数を呼び出している場合(non-leaf function)、関数の中で$31を保存する必要があります。逆に呼び出していない場合(leaf function)は、保存の必要がありません。
上の例で行くと関数foo()は、f4()、f5()を呼び出しているので、non-leaf functionになります。そのため、$31の保存と復元を行います。
f4()を呼び出す前にスタックに保存し、
sw $31,28($sp) jal f4 sw $16,24($sp)
foo()から戻るときに、スタックから$31へ戻り先を復元しています。
addu $2,$16,$2 lw $31,28($sp) lw $16,24($sp) j $31
レジスタによって、関数の中で破壊して良いレジスタ、破壊してはいけないレジスタが決められています。そのようなレジスタの詳細は、教科書のFigure2.18を見てください。
pcspimでの実行例
f4()が呼ばれた時のレジスタ値です。
$4〜$7($a0〜$a3)に引数が、$31に戻り番地が入っています。