System V ABI

System V ABI
ABI は、Application Binary Interface の略です。
アプリケーションプログラムとシステムの間での、バイナリインターフェイスを意味します。

データ型のサイズや、関数呼び出しの規約、ELF などのフォーマットで使われる詳細などが定義されています。

この規則に沿うようにバイナリを出力すれば、異なる言語間でも、関数などを同じように扱うことができます。
種類
ABI は、アーキテクチャや OS によって、いくつか種類があります。

Windows x64 ABIWindows x64
「windows abi」で検索してください。
System V AMD64 ABIUnix 系 x64 (Linux, MacOS X など)
https://gitlab.com/x86-psABIs/x86-64-ABI

Windows と System V では、引数や戻り値として使用されるレジスタなどが異なるので、注意してください。

ここでは、System V AMD64 ABI のみの説明を行います。
データ型のサイズ
C 言語のデータ型と、x64 上で扱われるデータサイズの一覧です。

C の型sizeアライメントAMD64
_Bool11boolean
0 で false、それ以外は true
char
signed char
11符号付き 1byte
unsigned char11符号なし 1byte
signed short22符号付き 2byte
unsigned short22符号なし 2byte
signed int
enum
44符号付き 4byte
unsigned int44符号なし 4byte
signed long (LP64)88符号付き 8byte
unsigned long (LP64)88符号なし 8byte
signed long (ILP32)44符号付き 4byte
unsigned long (ILP32)44符号なし 4byte
signed long long88符号付き 8byte
unsigned long long88符号なし 8byte
__int128
signed __int128
1616符号付き 16byte
unsigned __int1281616符号なし 16byte
ポインタ
any-type * (LP64)
any-type (*)() (LP64)
88符号なし 8byte
any-type * (ILP32)
any-type (*)() (ILP32)
44符号なし 4byte
浮動小数点数
_Float162216bit (IEEE-754)
float44単精度 (IEEE-754)
double88倍精度 (IEEE-754)
__float80
long double
161680-bit 拡張 (IEEE-754)
__float128
long double
1616128-bit 拡張 (IEEE-754)
固定浮動小数点数
_Decimal324432bit BID (IEEE-754R)
_Decimal648864bit BID (IEEE-754R)
_Decimal1281616128bit BID (IEEE-754R)
パック
__m6488MMX, 3DNow!
__m1281616SSE
__m2563232AVX
__m5126464AVX-512

long とポインタ型は、x86 では 32bit、x64 では 64bit になります。
ただし、Windows x64 では、long は 32bit になります。

SSE/AVX などの命令は除いて、アライメント (バイト境界) が揃っていないアドレス位置からのメモリアクセスは可能ですが、速度は落ちます。
構造体と共用体
構造体 (struct) と共用体 (union) については、先頭のメンバから順に、メモリの上から下に配置していく形になります。

また、各メンバは、それぞれの型ごとに、適切な位置にアライメントする必要があります。
例えば、int は4バイト境界、long は8バイト境界にする必要があります。

先頭のメンバの位置を 0 として、各メンバの位置が、そのデータ型に応じたアライメント数の、倍数の位置になるようにする必要があります。
位置がアライメントに合わない場合は、そのメンバの前に、余白のバイトが追加されます (内容は未定義)。

また、構造体/共用体の全体のサイズは、すべてのメンバの、最大のアライメント数の倍数である必要があり、サイズが合わない場合は、終端に余白が追加されます。

なお、構造体/共用体のデータを、実際にメモリ上に格納する時は、先頭位置のアドレスが、メンバ内の最大のアライメント数に合うように、メモリ位置を調整する必要があります。
例1
typedef struct {
    char a;
    int b;
    short c;
    long d;
} data;

size: 24 byte

---------------
 0  1  2  3  4  5  6  7  8  9 10 11 12 13 14 15
aa -- -- -- bb bb bb bb cc cc -- -- -- -- -- --

16 17 18 19 20 21 22 23
dd dd dd dd dd dd dd dd
--------------

  • int b の位置は、4バイト境界にする必要があります。
    char a の直後の位置は 1 のため、3バイトの余白が追加されて、4 の位置に配置されます。
  • short c は、現在位置が 8 なので、そのまま続けて配置します。
  • long d は、現在位置が 10 なので、8 の倍数にするため、16 の位置に配置します。
    よって、6バイトの余白を追加します。
例2
typedef struct {
    long a;
    char b;
    //7byte 余白
} data;

sizeof(data): 16 byte

各メンバのアライメントは問題ありませんが、全体サイズは 9 byte です。
最大のアライメントは 8 (long) であるため、全体サイズは 8 の倍数にする必要があります。
よって、終端に7バイトの余白を追加して、16 byte になります。
関数呼び出し
関数内で退避するレジスタ
RBX, RBP, R12〜R15 のレジスタの値は、関数の呼び出し元に属されているとみなすため、関数が戻る際に、これらのレジスタの値は、呼び出された直後と同じ値になっている必要があります。

そのため、関数内で上記のレジスタを使用して、値を変更する場合は、関数の開始時 (ローカル変数の確保後) に PUSH 命令で値を保存し、関数から戻る時に POP 命令で値を戻す必要があります。

ほかのレジスタについては、関数内で好きなように変更しても構いません。
RFLAGS
RFLAGS レジスタの方向フラグ (DF) は、関数を呼ぶ直前と、関数から戻る時は、常に 0 になっている必要があります。
つまり、関数が呼び出された時、DF は常に 0 になっていることが保証されます。

関数内で DF フラグを変更した場合は、0 に戻す必要があります。
MMX/x87
関数に入った時は、x87 モードであるとみなされます。
よって、関数が呼び出された時は、x87 レジスタが通常通り使用できる状態であることが必要になります。

具体的には、関数内で MMX レジスタを使用する場合、関数から戻るか、別の (規約に従う) 関数を呼び出す前に、EMMS 命令を実行して、x87 レジスタがすべて使用できる状態にする必要があります。

x87 コントロールワードレジスタの、丸めや例外マスクの設定は、関数内で変更される場合、呼び出し元の状態に戻すようにします。
関数のスタックフレーム
スタックフレームは、以下のように形成されます。

---------
 (ローカル変数領域)
--------- RBP
 RBP (呼び出し元の RBP)
--------- RBP + 8
 RIP (戻る位置)
--------- RBP + 16
↑この位置を揃える
 引数1,2...
 <余白>
---------
引数領域の境界合わせ
引数領域の先頭位置のアドレスは、16 byte 境界に揃える必要があります。

ただし、__m256 または __m512 の引数がスタック内で渡される場合は、32 byte または 64 byte 境界となります。
(スタックに渡される引数のうち、すべての型の最大アライメントが適用されます)

つまり、引数をスタックに格納した後、CALL 命令を呼び出す直前の時点で、RSP のアドレスは常に 16 の倍数になっている必要があります。

CALL 命令を呼び出す直前の RSP 値は、引数の先頭の値のアドレスとなります。
この位置を揃えるということは、各引数のアライメントを揃えるということです。

CALL 命令で関数が呼び出された直後は、RIP の値がスタックに入っているので、-8 byte。
そこから PUSH RBP を行うと、-8 byte で、RSP は再び 16 byte 境界になります。

その後、ローカル変数領域を割り当てた後は、RBX などの退避する必要があるレジスタを PUSH します。

また、関数内で、規約に従う関数を呼び出す場合は、CALL 命令直前の RSP を 16 byte 境界にしておく必要があるため、現在の RSP アドレスがどの境界にあるかを把握しておく必要があります。
関数の引数
引数を渡す手順
関数に渡す引数の値は、第1引数から順に、値のタイプに応じて、レジスタまたはスタックに格納していきます。

PUSH 命令を使って、値をスタックに格納する場合は、スタックに格納する一番最後の引数から順に格納していきます。
(メモリ上では、先頭から順に並ぶ形になります)

  1. 全体を2つまでの数値として扱えない構造体データ (細かい分類あり)、または、アライメントされていないメンバが含まれる構造体などは、スタックに値を格納します。
  2. 値が整数またはポインタの場合、RDI, RSI, RDX, RCX, R8, R9 の順で、使用可能な次のレジスタに引数をセットします。
  3. 浮動小数点数 (float, double) の場合、XMM0〜XMM7 (128bit レジスタ) の順で、使用可能な次のレジスタに引数をセットします。
    なお、x64 では、浮動小数点数は、SSE 命令を使って演算されます。
  4. レジスタに格納できない引数は、スタックに値を格納します (一つの値は 8 byte 単位)。

複数の引数でタイプが混在している場合は、先頭から順に、それぞれのタイプごとに、値をレジスタに割り当てていきます。
割り当てられるレジスタがなくなった場合は、スタックに格納します。

void func(int a, double b, short c, float d, int e[2]);

RDI  : a
XMM0 : b
RSI  : c
XMM1 : d
RDX  : e (ポインタ)
スタックへの格納の仕方
スタックに引数を格納する必要がある場合は、PUSH 命令を使わずに、MOV 命令で値をセットした方が、格納しやすくなります。

理由は、引数領域先頭のアライメントを 16 byte に合わせる必要があるのと、先頭の引数から順に格納するほうが扱いやすいからです。

まずは、引数を格納する前に、RSP から引数領域の全体サイズ分を減算しておきます。
この場合、減算後の RSP のアドレスが、16 byte (または必要なアライメント数) の境界に合うように、サイズを調整する必要があります。
これで、スタックに引数領域を確保することができ、アライメントに必要な余白も調整することができます。

その後、MOV 命令を使って、[RSP + N] の位置に、各引数の値を格納していきます (引数先頭のオフセットは 0 です)。
値のサイズが 64bit 以下でも、一つの値は常に 8 byte 単位になるので、注意してください。

MOV 命令で値を格納する場合は、64bit に符号拡張する必要はなく、任意のサイズで格納することができます。
引数を読み込む関数側では、引数の実際の定義サイズで値が扱われるため、64bit 中の余分なバイトの値を気にする必要はありません。

sub rsp, 16         ; 引数領域の確保
mov dword [rsp], 1  ; 第 N 引数   (8byte単位、32bit 数値)
mov byte [rsp+8], 2 ; 第 N+1 引数 (8byte単位、8bit 数値)

call func
add rsp, 16         ; 引数領域を削除

-------- RSP (<- 境界合わせの対象)
01 00 00 00 ?? ?? ?? ??
-------- RSP+8
02 ?? ?? ?? ?? ?? ?? ??
--------
境界合わせのためのパディング (必要なら)
--------
構造体
引数に構造体の値を直接指定する場合は、少々ややこしくなります。

基本的に、構造体全体のデータを、メモリ上の 8 byte 単位で分割し、そこに含まれるメンバのタイプを、一つのタイプに分類して、結果として、一つの 8 byte 整数、または浮動小数点数として、通常の引数と同じように扱います。

構造体全体を、2つの 8 byte 値で扱うことが出来る場合、(使用可能なレジスタがあれば) レジスタに格納されます。
2つの 8 byte 値で扱えない場合は、構造体全体がスタックに格納されます。

構造体に関しては、色々と細かい分類がありますが、とりあえず簡単な例だけ説明しておきます。

例1
以下の場合、構造体のサイズは、余白を含めて 8 byte です。
メンバはすべて整数なので、構造体全体を一つの 64bit 整数値として扱い、RDI レジスタに値が格納されます。

//sizeof(data) = 8
typedef struct
{
    char a,b; //aa bb -- --
    int c;
} data;

void func(data d)
{
    //d -> RDI (0xCCCCCCCC_????BBAA)
}

引数を読み込む側では、一度 RDI の値をメモリに格納してから、各メンバの値を参照する形になります。

例2
//sizeof(data) = 16
typedef struct
{
    int a,b,c,d;
} data;

RDI = 0xBBBBBBBB_AAAAAAAA
RSI = 0xDDDDDDDD_CCCCCCCC

------------
//sizeof(data) = 16
typedef struct
{
    int a;
    //4byte余白
    double b;
} data;

RDI = a
XMM0 = b

------------
//sizeof(data) = 20
typedef struct
{
    int a,b,c,d,e;
} data;

2 つの 8 byte で扱えないため、全体がスタックに格納される
可変数引数
可変数引数を扱う関数の場合は、AL レジスタが隠し引数として使用され、引数で使用される XMM レジスタの数を指定します。

AL の値は、XMM レジスタの数と正確に一致する必要はありませんが、使用される XMM レジスタの数の上限であり、0〜8 の範囲内である必要があります。
※RAX の残りのビット部分は未定義。

全体の引数の数を正確に判断することはできないので、可変数引数を扱う関数の方で、引数の終端を判断する方法を決めておく必要があります。
(引数の値が 0 の場合に終了するなど)
関数の戻り値
関数の戻り値は、以下のように、関数が戻った時の、特定のレジスタに格納されている値を使います。

  1. 値が整数またはポインタの場合、RAX と RDX を使います。
  2. 浮動小数点数 (float, double など) の場合、XMM0 と XMM1 を使います。

レジスタが2つあるのは、128 bit 値を2つの 64 bit に分けて渡す場合や、戻り値が構造体の場合などに、2つの 8 byte 値として、値を返す時のためです。
この場合、引数に渡す時と同じように、構造体のデータを分類します。

通常の一つの戻り値であれば、RAX か XMM0 を使います。

なお、2つの 8 byte では扱えない、大きなデータを返す必要がある場合、呼び出し元は、戻り値用のスペースを確保し、そのアドレスを RDI に渡します。
これは第1引数として扱われますが、C 言語などでは、これを非表示の引数とします。
関数が戻る時、RAX には、呼び出し元によって、RDI に渡されたアドレスが返されます。