呼び出し規約
呼び出し規約とは、プログラム内の関数が別の関数を呼び出す方法を規定したものです。コンパイラはこれを自動的に処理しますが、関数をアセンブラ言語で記述している場合は、そのパラメータの位置や特定方法、呼び出されたプログラム位置に戻る方法、結果を返す方法がわかっている必要があります。
また、アセンブラレベルのルーチンがどのレジスタを保存する必要があるかを知ることも重要です。プログラムが保存するレジスタが多すぎると、非効率になる場合があります。保存するレジスタが少なすぎると、不正なプログラムになる可能性があります。
ここでは、コンパイラで使用される呼び出し規約について説明します。内容は以下のとおりです。
最後に、実際の呼び出し規約の例を示します。
コンパイラで使用される呼び出し規約はAEABIの一部であるAAPCS (Procedure Call Standard for the Arm Architecture) に準拠します。AEABIへの準拠を参照してください。ここではAAPCSについては、概略のみ説明します。たとえば、VFP呼び出し規約を使用する際の浮動小数点コプロセッサレジスタの使用については、省略します。
関数の宣言
Cでは、コンパイラで関数の呼び出し方法を特定できるように、関数は規則に沿って宣言する必要があります。宣言の例を次に示します。
int MyFunction(int first, char * second);この宣言は、整数と文字へのポインタの2 つのパラメータを関数で指定することを示します。この関数は、整数を返します。
通常は、コンパイラが関数について特定できるのはこれだけです。したがって、この情報から呼び出し規約を推定できる必要があります。
C++ソースコードでのCリンケージの使用
C++では、関数はCまたはC++のいずれかのリンケージを持つことができます。 アセンブラルーチンをC++から呼び出すには、C++関数にCリンケージを持たせるのが最も簡単です。
C リンケージを持つ関数の宣言例を示します。
extern "C"
{
int F(int);
}多くの場合は、ヘッダファイルをCとC++で共有するのが実用的です。C と C++ の両方で C リンケージを持つ関数を宣言する例を示します。
#ifdef __cplusplus
extern "C"
{
#endif
int F(int);
#ifdef __cplusplus
}
#endif保護レジスタとスクラッチレジスタ
通常のArm CPUのレジスタは、以下で説明する3種類に分類されます。
スクラッチレジスタ
スクラッチレジスタの内容は、任意の関数により破壊される可能性があります。関数が別の関数を呼び出した後もレジスタの値を必要とする場合は、呼び出し中はその値をスタックなどで保存する必要があります。
32ビットモードの場合、レジスタR0からR3、およびR12は、関数でスクラッチレジスタとして使用されます。64ビットモードの場合、スクラッチレジスタとして使用されるレジスタはX0からX15です。
注記
ベニアの命令が自動的に挿入されたため、アセンブラ関数を呼び出す場合、32ビットモードの場合はR12、64ビットモードの場合はX16とX17もスクラッチレジスタです。
保護レジスタ
保存レジスタは関数呼び出しを跨いで保持されます(スクラッチレジスタは保持されません)。呼び出された関数は保護レジスタを他の用途で使用できますが、使用前に値を保存し、関数終了時に値を復元する必要があります。
32ビットモードの場合、レジスタR4からR11は、保護レジスタです。これらは、呼び出し先関数で保持されます。64ビットモードの場合、レジスタX18からX30は保護レジスタです。
32ビットモードの専用レジスタ
32ビットモードのレジスタの場合、考慮すべき特別な要件があります。
64ビットモードの専用レジスタ
64ビットモードのレジスタは、考慮すべき特別な要件があります。
関数の入口
これらの基本的な方法のいずれかを使用して、パラメータを関数に渡すことができます。
レジスタ内
スタック上
メモリ経由で迂回して引き渡すよりもレジスタを使用する方が大幅に効率的です。そのため、呼び出し規約では可能な限りレジスタを使用するように規定されています。パラメータの引渡しに使用できるレジスタ数は非常に少ないため、レジスタが不足する場合は、残りのパラメータはスタックに引き渡されます。これらの規則には次の例外が適用されます。
パラメータを受け入れて値を返すソフトウェア割り込み関数を除いて、割り込み関数にはどんなパラメータも指定できません。
ソフトウェア割り込み関数は、通常の関数と同じ様にはスタックを使用できません。
SVC命令が実行されると、プロセッサはスーパーバイザモードに切り替わり、スーパーバイザスタックが使用されるので、 引数は、アプリケーションが割り込み発生前にスーパーバイザモードで実行していない場合、スタックに渡すことはできません。
隠しパラメータ
関数の宣言や定義で明示されるパラメータに加えて、隠しパラメータが存在する場合があります。
関数より返された構造体が32ビットを超えている場合、その構造体が格納されるメモリ位置は、追加パラメータとして渡されます。これは、常に最初のパラメータとして扱われることに注意してください。
関数が非静的C++メンバ関数の場合、thisポインタが最初のパラメータとして渡されます(ただし、1つのみの場合、配置されるのは構造体ポインタのリターン後)。詳細については、Cからのアセンブラルーチンの呼び出しを参照してください。
32ビットモードのレジスタパラメータ
パラメータの引渡しに利用可能な32ビットモードのレジスタ
パラメータ | レジスタでの引渡し |
|---|---|
32ビット以下のスカラ値と浮動小数点値、および単精度(32ビット)浮動小数点値 | 最初の空きレジスタを使用して渡されます: |
| 最初に使用できるレジスタペアで渡されます: |
レジスタをパラメータに割り当てることは簡単なプロセスです。パラメータを左から右の順に調べ、最初のパラメータを空きレジスタに代入します。空きレジスタがない場合は、パラメータはスタックを逆方向で使用して引き渡されます。
32ビット未満のパラメータを持つ関数が呼び出される場合、未使用ビットの値が一貫するように、値は符号拡張またはゼロ拡張されます。値が符号拡張またはゼロ拡張されるかどうかは、そのタイプsigned または unsigned により異なります。
64ビットモードのレジスタパラメータ
パラメータの引渡しに利用可能な64ビットモードのレジスタ:
パラメータ | レジスタでの引渡し |
|---|---|
整数、ポインタ、小さい構造体(最大8バイト) | 最初の空きレジスタを使用して渡されます: |
小さい構造体(9–16バイト) | 最初の空きレジスタペアを使用して渡されます: |
浮動小数点数値 | 最初の空きレジスタを使用して渡されます: |
均一の構造(浮動小数点またはベクタ型の1–4要素) | 最初の空きレジスタを使用して渡されます: |
大きい構造体 | ポインタは最初の空きレジスタを使用して渡されます: |
レジスタをパラメータに割り当てることは簡単なプロセスです。パラメータを左から右の順に調べ、最初のパラメータを空きレジスタに代入します。空きレジスタがない場合は、パラメータはスタックを逆方向で使用して引き渡されます。
64ビットモードの場合、パラメータのサイズと統一されたビットのみが許可されます。したがって、呼び出された関数は通常、サイズが 32 ビットより小さいパラメータを符号拡張またはゼロ拡張します。
スタックパラメータとレイアウト
スタックパラメータは、メモリのスタックポインタが指す位置を開始位置として格納されています。スタックポインタ以下(下位メモリ方向)には、呼出し先関数が使用可能な空きエリアがあります。最初のスタックパラメータは、スタックポインタが指す位置に格納されています。それ以降のスタックパラメータは、スタック上の4で割り切れる次の位置に順に格納されます。呼び出された関数がリターンした後スタックをクリーンするのは、呼び出し元で行うべきです。
次の図は、パラメータがスタック上に格納される様子を示します。

関数の終了
関数は、呼び出し元の関数やプログラムに値を返すことができます。または、関数のリターン型がvoidの場合もあります。
関数のリターン値がある場合は、スカラ(整数、ポインタなど)、浮動小数点数、構造体のいずれかになります。
リターン値の32ビットモードに使用するレジスタ
リターン値の32ビットモードで使用できるレジスタは、R0およびR0:R1です。
リターン値 | レジスタでの引渡し |
|---|---|
32ビット以下のスカラ値と構造体リターン値、および単精度(32ビット)浮動小数点リターン値 |
|
32ビット超の構造体リターン値のメモリアドレス |
|
|
|
リターン値が32ビット未満の場合、値は32ビットに符号拡張またはゼロ-拡張されます。
64ビットモードで値を返すために使用されるレジスタ
リターン値 | レジスタでの引渡し |
|---|---|
整数、ポインタ、小さい構造体(最大8バイト) |
|
小さい構造体(9–16バイト) |
|
浮動小数点数値 |
|
均一の構造(浮動小数点またはベクタ型の1–4要素) |
|
大きい構造体 |
|
リターン値のサイズと一致するリターン値のビットのみにアクセスできます。
関数終了時のスタックのレイアウト
呼び出された関数がリターンした後スタックをクリーンするのは、呼び出し元で行うべきです。
32ビットモード—リターンアドレスの処理
アセンブラ言語で記述した関数は、レジスタLRによりポイントされるアドレスまでジャンプすることで、終了時に呼び出し元に戻ります。
関数の入口で、非スカラレジスタおよびLRレジスタは、1つの命令でプッシュできます。関数の出口では、これらすべてのレジスタは1つの命令でポップできます。リターンアドレスは、PCに直接ポップできます。
以下の例は、この動作を示しています。
name call
section .text:CODE
extern func
push {r4-r6,lr} ; Preserve stack alignment 8
bl func
; Do something here.
pop {r4-r6,pc} ; return
end64ビットモード—リターンアドレスの処理
アセンブラ言語で記述した関数は、レジスタLRによりポイントされるアドレスまでジャンプすることで、終了時に呼び出し元に戻ります。
関数の入口で、非スクラッチレジスタおよびLRレジスタをスタックにプッシュできます。関数の終了では、これらすべてのレジスタはスタックにリストアする必要があります。
以下の例は、この動作を示しています。
name call
section .text:CODE
extern func
strp x9, lr, [sp, #16]! ; Preserve stack alignment 16
bl func
; Do something here.
ldrp x9, x7, [sp, #16]
ret
end例
以下では、宣言の例や対応する呼び出し規約を紹介します。後の例ほど複雑になっています。
例1
以下の関数が宣言されているとします。
int add1(int);
32ビットモードの場合、この関数は、1つのパラメータをレジスタR0を使用して引き渡し、リターン値をレジスタR0を使用して呼出し元に返します。
以下のアセンブラルーチンは、この宣言に適合します。このルーチンは、パラメータの値よりも1つ大きな値を返します。
name return
section .text:CODE
add r0, r0, #1
bx lr
end64ビットモードの場合、この関数は、1つのパラメータをレジスタX0を使用して引き渡し、リターン値をレジスタX0を使用して呼出し元に返します。宣言と互換性のある関連のアセンブラルーチンは以下のようになります。
name return
section .text:CODE
add x0, x0, #1
ret
end例2
この例は、構造体がスタックを使用して引き渡される方法を説明しています。以下が宣言されているとします。
struct MyStruct
{
short a;
short b;
short c;
short d;
short e;
};
int MyFunction(struct MyStruct x, int y);32ビットモードの場合、構造体メンバa、b、c、およびdの値は、レジスタR0-R3に渡されます。最後の構造体メンバeおよび整数パラメータyはスタックで渡されます。呼び出し元関数は、スタックの上位8バイトを確保し、2つのスタックパラメータの内容をその位置にコピーします。リターン値は、R0レジスタを使用して呼び出し元に返されます。
64ビットモードの場合、xの値は、X0とX1に渡され、yはX2に渡されます。リターン値はX0に渡されます。
例 3
次の関数は、structMyStruct型の構造体を返します。
struct MyStruct
{
int mA[20];
};
struct MyStruct MyFunction(int x);リターン値のメモリ位置を割り当てて、それにポインタを最初の隠しパラメータとして引き渡すのは、呼び出し元の関数の役割です。32ビットモードの場合、リターン値が格納されるべき位置へのポインタは、R0内で引き渡されます。パラメータxはR1で引き渡されます。64ビットモードの場合、リターン値が格納されるべき位置へのポインタは、X8内で引き渡されます。パラメータxはX0に渡されます。
関数が構造体へのポインタを返すよう宣言されているとします。
struct MyStruct *MyFunction(int x);
この場合、リターン値はスカラであり、隠しパラメータはありません。32 ビットモードの場合、パラメータxはR0に渡され、リターン値はR0で返されます。64ビットモードの場合、パラメータxはX0に渡され、リターン値はX0で返されます。