System V x86-64 ABI は、System V ABI の AMD64 補足である x86-64 psABI で定められています。Linux、Android、FreeBSD、NetBSD、OpenBSD、DragonFly BSD、illumos と Solaris、Haiku がすべてこれを使い、Intel プロセッサーの macOS も、macOS の節に挙げる少数の相違点を除いてこれを使います。Linux x32 は、ポインターを 32 ビットにした同じ ABI です (後述)。Windows は別の規約である Microsoft x64 を使います。ABI の概要には、どのプラットフォームがどの ABI を使うかと、それぞれでの C の型のサイズがあります。

このページのリストは、Linux 向けの GCC と Clang を -O2 で使ったものと、ページが比較する箇所では macOS、FreeBSD、x32 向けの Clang を使ったものです。各リストの横のボタンで AT&T 構文と Intel 構文を切り替えられます。

レジスター

関数は、スタックポインターと 6 つの汎用レジスターを除くどのレジスターを変更しても構いません。RBX、RBP、R12 から R15 は呼び出し元のもので、これらを使う関数は保存して復元します。ベクトルレジスターはすべてスクラッチ (scratch) で、8 つの x87 レジスターも同様です。表 1 に各レジスターの役割を示します。

System V x86-64 ABI のレジスター
レジスター役割保存される
RDI、RSI、RDX、RCX、R8、R9整数とポインターの引数 1 から 6 (この順)いいえ
RAX戻り値。可変長引数の呼び出しでは、AL が使われたベクトルレジスターの数を示すいいえ
RDX戻り値の 2 番目の eightbyte にも使ういいえ
RBX、R12、R13、R14呼び出し先保存はい
RBP呼び出し先保存。関数がフレームポインターを使う場合はフレームポインターはい
R15呼び出し先保存。任意で GOT のベースはい
RSPスタックポインターはい
R10入れ子関数の静的チェーンポインターいいえ
R11スクラッチ。PLT スタブが自由に使えるいいえ
R16 から R31 (APX)スクラッチいいえ
XMM0 から XMM7浮動小数点とベクトルの引数 1 から 8いいえ
XMM0、XMM1浮動小数点とベクトルの戻り値いいえ
XMM8 から XMM31スクラッチいいえ
YMM、ZMM同じレジスターの幅を広げたもの。__m256 と __m512 用いいえ
K0 から K7スクラッチ (AVX-512 のマスク)いいえ
ST0、ST1long double (ST0) と _Complex long double (両方) を返すいいえ
ST2 から ST7、MM0 から MM7スクラッチ。呼び出し時に CPU は x87 モードでなければならないいいえ
TMM0 から TMM7スクラッチ (AMX のタイル)いいえ
FSスレッドポインター。スレッドローカルストレージを参照はい

レジスターのほかに、関数は方向フラグをクリアしたままにし、丸めモードなど MXCSR の制御ビットと x87 制御ワードを受け取ったときのまま保たなければなりません。ステータスフラグは保存されません。MMX レジスターを使う関数は、戻る前または別の関数を呼び出す前に EMMS を実行しなければなりません。

リスト 1 は、呼び出しをまたいで引数を RBX に保持します。RBX は呼び出し先保存なので f はそれを変更せずに返しますが、keep は先に呼び出し元の RBX の値を保存しなければなりません。このプッシュは、スタックフレームで説明するように、呼び出しのためにスタックのアラインメントを保つ役目も果たします。

呼び出しを越えて残る必要がある値は、呼び出し先保存レジスターに置かれます。

sysv-callee-saved.c

/* x must survive the call to f, so it goes into RBX, which f has to
   preserve; RBX itself is saved and restored around it. */
extern long f(long);

long keep(long x)
{
	return f(x) + x;
}

GCC 14.2.0 (Debian 14.2.0-19) x86_64-linux-gnu

AT&T 構文 gcc-14 -O2 -fno-asynchronous-unwind-tables -S sysv-callee-saved.c

keep:
        pushq   %rbx
        movq    %rdi, %rbx
        call    f@PLT
        addq    %rbx, %rax
        popq    %rbx
        ret

Intel 構文 gcc-14 -O2 -fno-asynchronous-unwind-tables -masm=intel -S sysv-callee-saved.c

keep:
        push    rbx
        mov     rbx, rdi
        call    f@PLT
        add     rax, rbx
        pop     rbx
        ret

Clang 23.1.2 x86_64-linux-gnu

AT&T 構文 clang --target=x86_64-linux-gnu -O2 -fno-asynchronous-unwind-tables -S sysv-callee-saved.c

keep:
        pushq   %rbx
        movq    %rdi, %rbx
        callq   f@PLT
        addq    %rbx, %rax
        popq    %rbx
        retq

Intel 構文 clang --target=x86_64-linux-gnu -O2 -fno-asynchronous-unwind-tables -masm=intel -S sysv-callee-saved.c

keep:
        push    rbx
        mov     rbx, rdi
        call    f@PLT
        add     rax, rbx
        pop     rbx
        ret

引数の渡し方

引数は左から右へ割り当てられます。整数とポインターは RDI、RSI、RDX、RCX、R8、R9 のうち次の空きレジスターを使い、float、double、ベクトルは XMM0 から XMM7 のうち次のものを使います。2 つの並びは独立しているので、関数は 6 つの整数と 8 つの浮動小数点値をレジスターで受け取れます。収まらないものはスタックに右から左へプッシュされるので、最初のスタック引数は戻りアドレスのすぐ上に来ます。

8 つの整数引数。sum8 は 7 番目と 8 番目を 8(%rsp) と 16(%rsp) に見つけ、call8 は呼び出しの前にそれらをプッシュします。

sysv-int-args.c

/* Eight integer arguments: the first six arrive in registers, the
   last two on the stack. */
long sum8(long a, long b, long c, long d, long e, long f, long g, long h)
{
	return a + b + c + d + e + f + g + h;
}

extern long take8(long, long, long, long, long, long, long, long);

long call8(void)
{
	return take8(1, 2, 3, 4, 5, 6, 7, 8);
}

GCC 14.2.0 (Debian 14.2.0-19) x86_64-linux-gnu

AT&T 構文 gcc-14 -O2 -fno-asynchronous-unwind-tables -S sysv-int-args.c

sum8:
        addq    %rsi, %rdi
        addq    %rdx, %rdi
        addq    %rcx, %rdi
        addq    %r8, %rdi
        leaq    (%rdi,%r9), %rax
        addq    8(%rsp), %rax
        addq    16(%rsp), %rax
        ret

call8:
        subq    $8, %rsp
        movl    $6, %r9d
        movl    $5, %r8d
        movl    $4, %ecx
        pushq   $8
        movl    $3, %edx
        movl    $2, %esi
        movl    $1, %edi
        pushq   $7
        call    take8@PLT
        addq    $24, %rsp
        ret

Intel 構文 gcc-14 -O2 -fno-asynchronous-unwind-tables -masm=intel -S sysv-int-args.c

sum8:
        add     rdi, rsi
        add     rdi, rdx
        add     rdi, rcx
        add     rdi, r8
        lea     rax, [rdi+r9]
        add     rax, QWORD PTR 8[rsp]
        add     rax, QWORD PTR 16[rsp]
        ret

call8:
        sub     rsp, 8
        mov     r9d, 6
        mov     r8d, 5
        mov     ecx, 4
        push    8
        mov     edx, 3
        mov     esi, 2
        mov     edi, 1
        push    7
        call    take8@PLT
        add     rsp, 24
        ret

Clang 23.1.2 x86_64-linux-gnu

AT&T 構文 clang --target=x86_64-linux-gnu -O2 -fno-asynchronous-unwind-tables -S sysv-int-args.c

sum8:
        addq    %rdi, %rsi
        leaq    (%rdx,%rcx), %rax
        addq    %rsi, %rax
        addq    %r8, %rax
        addq    %r9, %rax
        addq    8(%rsp), %rax
        addq    16(%rsp), %rax
        retq

call8:
        pushq   %rax
        movl    $1, %edi
        movl    $2, %esi
        movl    $3, %edx
        movl    $4, %ecx
        movl    $5, %r8d
        movl    $6, %r9d
        pushq   $8
        pushq   $7
        callq   take8@PLT
        addq    $16, %rsp
        popq    %rcx
        retq

Intel 構文 clang --target=x86_64-linux-gnu -O2 -fno-asynchronous-unwind-tables -masm=intel -S sysv-int-args.c

sum8:
        add     rsi, rdi
        lea     rax, [rdx + rcx]
        add     rax, rsi
        add     rax, r8
        add     rax, r9
        add     rax, qword ptr [rsp + 8]
        add     rax, qword ptr [rsp + 16]
        ret

call8:
        push    rax
        mov     edi, 1
        mov     esi, 2
        mov     edx, 3
        mov     ecx, 4
        mov     r8d, 5
        mov     r9d, 6
        push    8
        push    7
        call    take8@PLT
        add     rsp, 16
        pop     rcx
        ret
整数と浮動小数点の引数は別々に数えます。i、l、p は RDI、RSI、RDX に、x、f、y は XMM0、XMM1、XMM2 に入ります。

sysv-mixed-args.c

/* Integer and floating-point arguments take registers from two
   separate sequences. */
extern double mix(int i, double x, long l, float f, char *p, double y);

double call_mix(char *p)
{
	return mix(1, 2.0, 3, 4.0f, p, 5.0);
}

GCC 14.2.0 (Debian 14.2.0-19) x86_64-linux-gnu

AT&T 構文 gcc-14 -O2 -fno-asynchronous-unwind-tables -S sysv-mixed-args.c

call_mix:
        movsd   .LC0(%rip), %xmm2
        movss   .LC2(%rip), %xmm1
        movq    %rdi, %rdx
        movl    $3, %esi
        movsd   .LC1(%rip), %xmm0
        movl    $1, %edi
        jmp     mix@PLT

.LC0:
        .long   0
        .long   1075052544

.LC1:
        .long   0
        .long   1073741824

.LC2:
        .long   1082130432

Intel 構文 gcc-14 -O2 -fno-asynchronous-unwind-tables -masm=intel -S sysv-mixed-args.c

call_mix:
        movsd   xmm2, QWORD PTR .LC0[rip]
        movss   xmm1, DWORD PTR .LC2[rip]
        mov     rdx, rdi
        mov     esi, 3
        movsd   xmm0, QWORD PTR .LC1[rip]
        mov     edi, 1
        jmp     mix@PLT

.LC0:
        .long   0
        .long   1075052544

.LC1:
        .long   0
        .long   1073741824

.LC2:
        .long   1082130432

64 ビットより小さい整数はレジスターの下位ビットを占め、psABI は残りのビットを規定していません。そのため受け取る側は、より広い値が必要なら自分で値を拡張しなければなりません。リスト 18 では、x86-64 の load が int のインデックスをアドレスに使う前に movslq で符号拡張しています。この点で GCC と Clang がどう違うかは小さな整数で示します。_Bool は例外で、その最下位 8 ビットは 0 か 1 を保持します。

分類

単純な整数値や浮動小数点値でないものは、まずクラスに分類されます。値の 8 バイトずつの各部分である eightbyte のクラスが、その部分をどこに置くかを決めます。

psABI のクラス。INTEGER と SSE のように 2 種類のレジスターのクラスを持つ値は、それぞれから 1 つずつ使います。
クラス意味渡す場所
INTEGER汎用レジスターに収まるもの: 整数、ポインター、_BoolRDI、RSI、RDX、RCX、R8、R9 のうち次のもの
SSEベクトルレジスターに収まるもの: float、double、_Float16、__m64XMM0 から XMM7 のうち次のもの
SSEUP下位の eightbyte が SSE であるベクトルの上位部分同じレジスターの残りの部分
X87、X87UPlong double の仮数と指数スタック
COMPLEX_X87_Complex long doubleスタック
MEMORY大きすぎるもの、またはレジスターに適さないものスタック
NO_CLASSパディングと空の構造体なし

__m128 や __float128 は SSE の eightbyte 1 つと SSEUP 1 つなので、XMM レジスター 1 つを占めます。__m256 は SSE と 3 つの SSEUP で、プロセッサーに YMM レジスターがあれば 1 つを占めます。__int128 は 2 つの long 値からなる構造体として扱われますが、スタック上では 16 バイトにアラインされる点が異なります。

構造体、共用体、配列は、そのフィールドによって分類されます。

  1. 64 バイトより大きい値、またはアラインされていないフィールドを持つ値のクラスは MEMORY です。
  2. ユーザー提供のコピーコンストラクターやデストラクターを持つものなど、呼び出しの観点でトリビアルでない (non-trivial for the purpose of calls) C++ オブジェクトは、コピーへのポインターとして渡され、そのクラスは INTEGER です。
  3. それ以外では、各 eightbyte は NO_CLASS から始まり、各フィールドが自分の占める eightbyte に自分のクラスを併合します。同じクラス同士ならそのままです。NO_CLASS はもう一方のクラスに譲ります。MEMORY が優先され、次に INTEGER が優先されます。X87、X87UP、COMPLEX_X87 は結果を MEMORY にします。それ以外は SSE です。
  4. 最後に後処理があります。どれかの eightbyte が MEMORY なら値全体が MEMORY です。X87UP が X87 の後に続かない場合、値全体が MEMORY です。2 つより多い eightbyte からなる値は、SSE の後に SSEUP だけが続くもの以外は MEMORY です。前に SSE がない SSEUP は SSE になります。

実質的に、16 バイトより大きいものは、単独または構造体の中の __m256 か __m512 のベクトル 1 つである場合を除き、MEMORY になります。その後、eightbyte が順に割り当てられます。そのすべてに足りるだけのレジスターが残っていなければ、値全体がスタックに置かれ、レジスターは次の引数のために空いたままになります。

分類の実例 (それぞれリストで確認済み)
C の型サイズeightbyte渡す場所示すリスト
struct pair { long a, b; }16INTEGER、INTEGER2 つの汎用レジスターリスト 4
struct point { double x, y; }16SSE、SSE2 つの XMM レジスターリスト 4
struct mixed { int a, b; double d; }16INTEGER、SSE汎用レジスター 1 つと XMM レジスター 1 つリスト 4
struct tagged { int tag; float value; }8INTEGER1 つの汎用レジスターリスト 5
struct vec3 { float x, y, z; }12SSE、SSE2 つの XMM レジスター。x と y は 1 つ目に入るリスト 5
struct triple { long a, b, c; }24MEMORYスタックリスト 6
__int12816INTEGER、INTEGER2 つの汎用レジスター、またはスタックリスト 7
long double16X87、X87UPスタックリスト 9
union { long double d; void *p; }16INTEGER、X87UP: MEMORYスタックリスト 17
2 つの eightbyte からなる構造体はレジスターで渡されます。struct pair は RDI と RSI に、struct point は XMM0 と XMM1 に入り、struct mixed は 2 つの int が RDI に、double が XMM0 に入ります。

sysv-struct-regs.c

/* Structs of up to 16 bytes are split into eightbytes, and each
   eightbyte goes to a general-purpose or a vector register. */
struct pair { long a, b; };		/* INTEGER, INTEGER */
struct point { double x, y; };		/* SSE, SSE */
struct mixed { int a, b; double d; };	/* INTEGER, SSE */

long pair_sum(struct pair p)
{
	return p.a + p.b;
}

double point_dot(struct point p)
{
	return p.x * p.y;
}

double mixed_sum(struct mixed m)
{
	return m.a + m.b + m.d;
}

GCC 14.2.0 (Debian 14.2.0-19) x86_64-linux-gnu

AT&T 構文 gcc-14 -O2 -fno-asynchronous-unwind-tables -S sysv-struct-regs.c

pair_sum:
        leaq    (%rdi,%rsi), %rax
        ret

point_dot:
        mulsd   %xmm1, %xmm0
        ret

mixed_sum:
        movq    %rdi, %rax
        movapd  %xmm0, %xmm1
        pxor    %xmm0, %xmm0
        shrq    $32, %rax
        addl    %edi, %eax
        cvtsi2sdl       %eax, %xmm0
        addsd   %xmm1, %xmm0
        ret

Intel 構文 gcc-14 -O2 -fno-asynchronous-unwind-tables -masm=intel -S sysv-struct-regs.c

pair_sum:
        lea     rax, [rdi+rsi]
        ret

point_dot:
        mulsd   xmm0, xmm1
        ret

mixed_sum:
        mov     rax, rdi
        movapd  xmm1, xmm0
        pxor    xmm0, xmm0
        shr     rax, 32
        add     eax, edi
        cvtsi2sd        xmm0, eax
        addsd   xmm0, xmm1
        ret

mixed_sum では、2 つの int が RDI を共有します。a は下位半分にあり、b には 32 ビットのシフトが必要です。

eightbyte を共有するフィールドはクラスを併合します。struct tagged の int と float は INTEGER の eightbyte になるので、float は RDI の上位半分で渡されます。struct vec3 の z は 2 番目の eightbyte に単独で入り、その eightbyte は XMM1 で渡されます。

sysv-struct-merge.c

/* Fields that share an eightbyte merge their classes: an int and a
   float make one INTEGER eightbyte, two floats one SSE eightbyte. */
struct tagged { int tag; float value; };	/* INTEGER */
struct vec3 { float x, y, z; };			/* SSE, SSE */

float tagged_value(struct tagged t)
{
	return t.value;
}

float vec3_z(struct vec3 v)
{
	return v.z;
}

GCC 14.2.0 (Debian 14.2.0-19) x86_64-linux-gnu

AT&T 構文 gcc-14 -O2 -fno-asynchronous-unwind-tables -S sysv-struct-merge.c

tagged_value:
        shrq    $32, %rdi
        movd    %edi, %xmm0
        ret

vec3_z:
        movaps  %xmm1, %xmm0
        ret

Intel 構文 gcc-14 -O2 -fno-asynchronous-unwind-tables -masm=intel -S sysv-struct-merge.c

tagged_value:
        shr     rdi, 32
        movd    xmm0, edi
        ret

vec3_z:
        movaps  xmm0, xmm1
        ret
24 バイトの構造体は MEMORY です。呼び出し元はそれを、呼び出しに渡す引数の領域の最下部にコピーし、呼び出し先は自分の戻りアドレスの上からそれを読みます。

sysv-struct-memory.c

/* A struct of more than 16 bytes has class MEMORY: the caller copies
   it to the stack. */
struct triple { long a, b, c; };

long triple_sum(struct triple t)
{
	return t.a + t.b + t.c;
}

extern long take_triple(struct triple t);

long pass_triple(long x)
{
	struct triple t = { x, 2 * x, 3 * x };
	return take_triple(t);
}

GCC 14.2.0 (Debian 14.2.0-19) x86_64-linux-gnu

AT&T 構文 gcc-14 -O2 -fno-asynchronous-unwind-tables -S sysv-struct-memory.c

triple_sum:
        movq    16(%rsp), %rax
        addq    8(%rsp), %rax
        addq    24(%rsp), %rax
        ret

pass_triple:
        subq    $72, %rsp
        leaq    (%rdi,%rdi), %rax
        movq    %rax, 40(%rsp)
        addq    %rdi, %rax
        movq    %rdi, 32(%rsp)
        movdqa  32(%rsp), %xmm0
        movq    %rax, 16(%rsp)
        movups  %xmm0, (%rsp)
        call    take_triple@PLT
        addq    $72, %rsp
        ret

Intel 構文 gcc-14 -O2 -fno-asynchronous-unwind-tables -masm=intel -S sysv-struct-memory.c

triple_sum:
        mov     rax, QWORD PTR 16[rsp]
        add     rax, QWORD PTR 8[rsp]
        add     rax, QWORD PTR 24[rsp]
        ret

pass_triple:
        sub     rsp, 72
        lea     rax, [rdi+rdi]
        mov     QWORD PTR 40[rsp], rax
        add     rax, rdi
        mov     QWORD PTR 32[rsp], rdi
        movdqa  xmm0, XMMWORD PTR 32[rsp]
        mov     QWORD PTR 16[rsp], rax
        movups  XMMWORD PTR [rsp], xmm0
        call    take_triple@PLT
        add     rsp, 72
        ret

Clang 23.1.2 x86_64-linux-gnu

AT&T 構文 clang --target=x86_64-linux-gnu -O2 -fno-asynchronous-unwind-tables -S sysv-struct-memory.c

triple_sum:
        movq    16(%rsp), %rax
        addq    8(%rsp), %rax
        addq    24(%rsp), %rax
        retq

pass_triple:
        subq    $56, %rsp
        movq    %rdi, 32(%rsp)
        leaq    (%rdi,%rdi), %rax
        movq    %rax, 40(%rsp)
        leaq    (%rdi,%rdi,2), %rax
        movq    %rax, 48(%rsp)
        movq    %rax, 16(%rsp)
        movups  32(%rsp), %xmm0
        movups  %xmm0, (%rsp)
        callq   take_triple@PLT
        addq    $56, %rsp
        retq

Intel 構文 clang --target=x86_64-linux-gnu -O2 -fno-asynchronous-unwind-tables -masm=intel -S sysv-struct-memory.c

triple_sum:
        mov     rax, qword ptr [rsp + 16]
        add     rax, qword ptr [rsp + 8]
        add     rax, qword ptr [rsp + 24]
        ret

pass_triple:
        sub     rsp, 56
        mov     qword ptr [rsp + 32], rdi
        lea     rax, [rdi + rdi]
        mov     qword ptr [rsp + 40], rax
        lea     rax, [rdi + 2*rdi]
        mov     qword ptr [rsp + 48], rax
        mov     qword ptr [rsp + 16], rax
        movups  xmm0, xmmword ptr [rsp + 32]
        movups  xmmword ptr [rsp], xmm0
        call    take_triple@PLT
        add     rsp, 56
        ret
__int128 はレジスターの組で、下位半分から先に渡されます。late128 では x に R9 しか残っていないので、x はスタックに置かれ、g はそれでも R9 を受け取ります。

sysv-int128.c

/* __int128 takes two general-purpose registers. When only one is
   left, it goes to the stack whole, and the next argument still gets
   the register. */
__int128 add128(__int128 a, __int128 b)
{
	return a + b;
}

long late128(long a, long b, long c, long d, long e, __int128 x, long g)
{
	return (long)x + g;
}

GCC 14.2.0 (Debian 14.2.0-19) x86_64-linux-gnu

AT&T 構文 gcc-14 -O2 -fno-asynchronous-unwind-tables -S sysv-int128.c

add128:
        movq    %rdx, %rax
        movq    %rcx, %rdx
        addq    %rdi, %rax
        adcq    %rsi, %rdx
        ret

late128:
        movq    8(%rsp), %rax
        addq    %r9, %rax
        ret

Intel 構文 gcc-14 -O2 -fno-asynchronous-unwind-tables -masm=intel -S sysv-int128.c

add128:
        mov     rax, rdx
        mov     rdx, rcx
        add     rax, rdi
        adc     rdx, rsi
        ret

late128:
        mov     rax, QWORD PTR 8[rsp]
        add     rax, r9
        ret

Clang 23.1.2 x86_64-linux-gnu

AT&T 構文 clang --target=x86_64-linux-gnu -O2 -fno-asynchronous-unwind-tables -S sysv-int128.c

add128:
        movq    %rdi, %rax
        addq    %rdx, %rax
        adcq    %rcx, %rsi
        movq    %rsi, %rdx
        retq

late128:
        movq    %r9, %rax
        addq    8(%rsp), %rax
        retq

Intel 構文 clang --target=x86_64-linux-gnu -O2 -fno-asynchronous-unwind-tables -masm=intel -S sysv-int128.c

add128:
        mov     rax, rdi
        add     rax, rdx
        adc     rsi, rcx
        mov     rdx, rsi
        ret

late128:
        mov     rax, r9
        add     rax, qword ptr [rsp + 8]
        ret

値の返し方

戻り値は引数と同じように分類され、そのうえで次のようになります。

  • INTEGER の eightbyte は RAX、次に RDX で返り、SSE の eightbyte は XMM0、次に XMM1 で返ります。double と long からなる構造体は XMM0 と RAX で返ります。
  • X87 の値である long double は ST0 で返り、_Complex long double は ST0 と ST1 で返ります。
  • MEMORY の値は、呼び出し元が用意したメモリを通して返されます。呼び出し元はそのアドレスを最初の隠し引数として RDI で渡し、本来の引数はレジスター 1 つ分後ろにずれます。関数は同じアドレスを RAX で返します。
RAX と RDX、XMM0 と RAX、メモリを通して返される構造体。ret_triple は結果のアドレスを RDI で受け取るので、引数 x は RSI で渡されます。

sysv-returns.c

/* Return values are classified like arguments. */
struct pair { long a, b; };
struct dl { double d; long l; };
struct triple { long a, b, c; };

struct pair ret_pair(long a, long b)
{
	return (struct pair){ a, b };
}

struct dl ret_dl(double d, long l)
{
	return (struct dl){ d, l };
}

struct triple ret_triple(long x)
{
	return (struct triple){ x, x, x };
}

GCC 14.2.0 (Debian 14.2.0-19) x86_64-linux-gnu

AT&T 構文 gcc-14 -O2 -fno-asynchronous-unwind-tables -S sysv-returns.c

ret_pair:
        movq    %rdi, %rax
        movq    %rsi, %rdx
        ret

ret_dl:
        movq    %rdi, %rax
        ret

ret_triple:
        movq    %rsi, %xmm0
        movq    %rsi, 16(%rdi)
        movq    %rdi, %rax
        movdqa  %xmm0, %xmm1
        punpcklqdq      %xmm1, %xmm1
        movups  %xmm1, (%rdi)
        ret

Intel 構文 gcc-14 -O2 -fno-asynchronous-unwind-tables -masm=intel -S sysv-returns.c

ret_pair:
        mov     rax, rdi
        mov     rdx, rsi
        ret

ret_dl:
        mov     rax, rdi
        ret

ret_triple:
        movq    xmm0, rsi
        mov     QWORD PTR 16[rdi], rsi
        mov     rax, rdi
        movdqa  xmm1, xmm0
        punpcklqdq      xmm1, xmm1
        movups  XMMWORD PTR [rdi], xmm1
        ret

Clang 23.1.2 x86_64-linux-gnu

AT&T 構文 clang --target=x86_64-linux-gnu -O2 -fno-asynchronous-unwind-tables -S sysv-returns.c

ret_pair:
        movq    %rsi, %rdx
        movq    %rdi, %rax
        retq

ret_dl:
        movq    %rdi, %rax
        retq

ret_triple:
        movq    %rdi, %rax
        movq    %rsi, (%rdi)
        movq    %rsi, 8(%rdi)
        movq    %rsi, 16(%rdi)
        retq

Intel 構文 clang --target=x86_64-linux-gnu -O2 -fno-asynchronous-unwind-tables -masm=intel -S sysv-returns.c

ret_pair:
        mov     rdx, rsi
        mov     rax, rdi
        ret

ret_dl:
        mov     rax, rdi
        ret

ret_triple:
        mov     rax, rdi
        mov     qword ptr [rdi], rsi
        mov     qword ptr [rdi + 8], rsi
        mov     qword ptr [rdi + 16], rsi
        ret

long double は例外的です。この型の引数は常にスタックで渡されますが、結果は x87 のレジスタースタックで返ります。リスト 9 は、そうした引数 2 つを 16 バイト離れた位置でスタックから読み、そこで掛け合わせます。XMM レジスターと x87 スタックの間で値を移す命令はないので、double の変換にはメモリ、ここではレッドゾーンを経由する必要があります。

スタック上の long double 引数と、ST0 の結果

sysv-long-double.c

/* long double travels in memory and returns on the x87 stack. */
long double ld_mul(long double a, long double b)
{
	return a * b;
}

long double ld_from(double d)
{
	return d;
}

GCC 14.2.0 (Debian 14.2.0-19) x86_64-linux-gnu

AT&T 構文 gcc-14 -O2 -fno-asynchronous-unwind-tables -S sysv-long-double.c

ld_mul:
        fldt    8(%rsp)
        fldt    24(%rsp)
        fmulp   %st, %st(1)
        ret

ld_from:
        movsd   %xmm0, -16(%rsp)
        fldl    -16(%rsp)
        ret

Intel 構文 gcc-14 -O2 -fno-asynchronous-unwind-tables -masm=intel -S sysv-long-double.c

ld_mul:
        fld     TBYTE PTR 8[rsp]
        fld     TBYTE PTR 24[rsp]
        fmulp   st(1), st
        ret

ld_from:
        movsd   QWORD PTR -16[rsp], xmm0
        fld     QWORD PTR -16[rsp]
        ret

スタックフレーム

スタックは下へ伸びます。call 命令の時点で RSP は 16 の倍数でなければならないので、関数に入った時点、つまり戻りアドレスがプッシュされた後では RSP + 8 が 16 の倍数です。__m256 または __m512 の引数をスタックで渡す場合、この境界は 32 バイトまたは 64 バイトになります。この規則がなければ、スタック上のデータに対するアラインされた SSE のロードとストアがフォルトを起こすおそれがあります。

スタックフレームのレイアウト (高いアドレスから低いアドレスへ)
アドレス内容フレーム
16(%rbp) 以上メモリで渡される引数。最初のものが最も低いアドレス呼び出し元
8(%rbp)call がプッシュした戻りアドレス呼び出し先
0(%rbp)関数がフレームポインターを使う場合、保存した呼び出し元の RBP呼び出し先
その下保存したレジスターとローカル変数呼び出し先
0(%rsp)次の呼び出しに渡す引数。16 バイトにアラインされる呼び出し先
RSP より下の 128 バイトレッドゾーン呼び出し先

フレームポインターは任意です。最適化を有効にすると、Linux 向けの GCC と Clang は RSP を通してフレームにアクセスし、RBP を空けておきます。ただし、可変長配列や alloca のようにフレームのサイズが実行時に変わる場合は別です。

可変長配列。RBP がフレームの位置を示し、配列のサイズは 16 の倍数に切り上げられるので、呼び出しのために RSP のアラインメントが保たれます。

sysv-frame.c

/* A variable-length array needs a frame pointer, and its size is
   rounded up to keep RSP 16-byte aligned for the call. */
extern void use(char *buf, long n);

void frame(long n)
{
	char buf[n];

	use(buf, n);
}

GCC 14.2.0 (Debian 14.2.0-19) x86_64-linux-gnu

AT&T 構文 gcc-14 -O2 -fno-asynchronous-unwind-tables -S sysv-frame.c

frame:
        pushq   %rbp
        leaq    15(%rdi), %rax
        movq    %rdi, %rsi
        andq    $-16, %rax
        movq    %rsp, %rbp
        subq    %rax, %rsp
        movq    %rsp, %rdi
        call    use@PLT
        leave
        ret

Intel 構文 gcc-14 -O2 -fno-asynchronous-unwind-tables -masm=intel -S sysv-frame.c

frame:
        push    rbp
        lea     rax, 15[rdi]
        mov     rsi, rdi
        and     rax, -16
        mov     rbp, rsp
        sub     rsp, rax
        mov     rdi, rsp
        call    use@PLT
        leave
        ret

リスト 2 では、call8 は 2 つのスタック引数のために 16 バイトを必要とします。関数に入った時点で RSP は 16 の倍数より 8 バイトずれているので、GCC はまず 8 を引き、次に 8 バイトの引数を 2 つプッシュし、呼び出しは 16 の倍数の位置で行われます。呼び出しの後、addq $24 が 3 つすべてを取り除きます。リスト 1 では、RBX のプッシュが同じ役目を果たします。

レッドゾーン

RSP より下の 128 バイトがレッドゾーン (red zone) です。シグナルハンドラーと割り込みハンドラーはこの領域を変更してはならないので、関数は呼び出しをまたいで必要としないデータを、RSP を動かさずにそこに置けます。リーフ関数はフレーム全体をそこに置けます。

レッドゾーンがあると、leaf は配列を RSP より下に格納します。-mno-red-zone では、先に RSP を下げ、最後に元に戻さなければなりません。

sysv-red-zone.c

/* A leaf function can keep its locals below RSP, in the red zone. */
int leaf(int x)
{
	volatile int tmp[4];

	tmp[0] = x;
	tmp[1] = x * 3;
	tmp[2] = x * 5;
	tmp[3] = x * 7;
	return tmp[x & 3];
}

GCC 14.2.0 (Debian 14.2.0-19) x86_64-linux-gnu

AT&T 構文 gcc-14 -O2 -fno-asynchronous-unwind-tables -S sysv-red-zone.c

leaf:
        leal    (%rdi,%rdi,2), %eax
        movl    %edi, -24(%rsp)
        movl    %eax, -20(%rsp)
        leal    (%rdi,%rdi,4), %eax
        movl    %eax, -16(%rsp)
        leal    0(,%rdi,8), %eax
        subl    %edi, %eax
        andl    $3, %edi
        movl    %eax, -12(%rsp)
        movl    -24(%rsp,%rdi,4), %eax
        ret

Intel 構文 gcc-14 -O2 -fno-asynchronous-unwind-tables -masm=intel -S sysv-red-zone.c

leaf:
        lea     eax, [rdi+rdi*2]
        mov     DWORD PTR -24[rsp], edi
        mov     DWORD PTR -20[rsp], eax
        lea     eax, [rdi+rdi*4]
        mov     DWORD PTR -16[rsp], eax
        lea     eax, 0[0+rdi*8]
        sub     eax, edi
        and     edi, 3
        mov     DWORD PTR -12[rsp], eax
        mov     eax, DWORD PTR -24[rsp+rdi*4]
        ret

GCC 14.2.0 (Debian 14.2.0-19) x86_64-linux-gnu

AT&T 構文 gcc-14 -O2 -fno-asynchronous-unwind-tables -mno-red-zone -S sysv-red-zone.c

leaf:
        subq    $24, %rsp
        leal    (%rdi,%rdi,2), %eax
        movl    %edi, (%rsp)
        movl    %eax, 4(%rsp)
        leal    (%rdi,%rdi,4), %eax
        movl    %eax, 8(%rsp)
        leal    0(,%rdi,8), %eax
        subl    %edi, %eax
        andl    $3, %edi
        movl    %eax, 12(%rsp)
        movl    (%rsp,%rdi,4), %eax
        addq    $24, %rsp
        ret

Intel 構文 gcc-14 -O2 -fno-asynchronous-unwind-tables -mno-red-zone -masm=intel -S sysv-red-zone.c

leaf:
        sub     rsp, 24
        lea     eax, [rdi+rdi*2]
        mov     DWORD PTR [rsp], edi
        mov     DWORD PTR 4[rsp], eax
        lea     eax, [rdi+rdi*4]
        mov     DWORD PTR 8[rsp], eax
        lea     eax, 0[0+rdi*8]
        sub     eax, edi
        and     edi, 3
        mov     DWORD PTR 12[rsp], eax
        mov     eax, DWORD PTR [rsp+rdi*4]
        add     rsp, 24
        ret

オペレーティングシステムのカーネルやファームウェアのように、割り込みが同じスタックにプッシュする場所で動くコードは、レッドゾーンに頼ってはならず、-mno-red-zone でコンパイルされます。たとえば EDK II は x64 の UEFI ファームウェアをそのようにコンパイルします。

可変長引数関数

printf のような可変長引数関数の呼び出しは、ほかの呼び出しと同じレジスターを使い、さらに隠し引数を 1 つ加えます。AL には、引数を運ぶベクトルレジスターの数の上限が 0 から 8 で入ります。RAX の残りの部分は未定義です。プロトタイプのない関数の呼び出しでも AL を設定します。その関数が可変長引数関数かもしれないからです。

AL はベクトルレジスターの数です。2 つの int では 0、1 つの double では 1 です。GCC は EAX 全体に書き込み、Clang は AL だけに書き込みます。

sysv-varargs-call.c

/* Calling a variadic function: AL says how many vector registers
   carry arguments. */
extern int log_msg(const char *fmt, ...);

int call_ints(void)
{
	return log_msg("%d %d", 1, 2);
}

int call_double(double x)
{
	return log_msg("%f %d", x, 3);
}

GCC 14.2.0 (Debian 14.2.0-19) x86_64-linux-gnu

AT&T 構文 gcc-14 -O2 -fno-asynchronous-unwind-tables -S sysv-varargs-call.c

.LC0:
        .string "%d %d"

call_ints:
        movl    $2, %edx
        movl    $1, %esi
        leaq    .LC0(%rip), %rdi
        xorl    %eax, %eax
        jmp     log_msg@PLT

.LC1:
        .string "%f %d"

call_double:
        movl    $3, %esi
        leaq    .LC1(%rip), %rdi
        movl    $1, %eax
        jmp     log_msg@PLT

Intel 構文 gcc-14 -O2 -fno-asynchronous-unwind-tables -masm=intel -S sysv-varargs-call.c

.LC0:
        .string "%d %d"

call_ints:
        mov     edx, 2
        mov     esi, 1
        lea     rdi, .LC0[rip]
        xor     eax, eax
        jmp     log_msg@PLT

.LC1:
        .string "%f %d"

call_double:
        mov     esi, 3
        lea     rdi, .LC1[rip]
        mov     eax, 1
        jmp     log_msg@PLT

Clang 23.1.2 x86_64-linux-gnu

AT&T 構文 clang --target=x86_64-linux-gnu -O2 -fno-asynchronous-unwind-tables -S sysv-varargs-call.c

call_ints:
        leaq    .L.str(%rip), %rdi
        movl    $1, %esi
        movl    $2, %edx
        xorl    %eax, %eax
        jmp     log_msg@PLT

call_double:
        leaq    .L.str.1(%rip), %rdi
        movl    $3, %esi
        movb    $1, %al
        jmp     log_msg@PLT

.L.str:
        .asciz  "%d %d"

.L.str.1:
        .asciz  "%f %d"

Intel 構文 clang --target=x86_64-linux-gnu -O2 -fno-asynchronous-unwind-tables -masm=intel -S sysv-varargs-call.c

call_ints:
        lea     rdi, [rip + .L.str]
        mov     esi, 1
        mov     edx, 2
        xor     eax, eax
        jmp     log_msg@PLT

call_double:
        lea     rdi, [rip + .L.str.1]
        mov     esi, 3
        mov     al, 1
        jmp     log_msg@PLT

.L.str:
        .asciz  "%d %d"

.L.str.1:
        .asciz  "%f %d"

可変長引数関数は名前のない引数がどこにあるかを知ることができないので、そのプロローグは引数レジスターを 176 バイトのレジスター保存領域に保存します。RDI から R9 をオフセット 0 から 40 に、続いて XMM0 から XMM7 をオフセット 48 から 160 に、それぞれ 16 バイトずつ置きます。AL が 0 のときは、ベクトルレジスターの保存を飛ばします。va_list は、次の引数がどこにあるかを記録する構造体 1 つからなる配列です。

24 バイトの構造体である va_list のフィールド。va_list は配列なので、それを受け取る関数はそれへのポインターを受け取ります。
オフセットフィールド意味
0unsigned int gp_offset保存領域内の次の汎用レジスターのオフセット。使い切ると 48
4unsigned int fp_offset保存領域内の次のベクトルレジスターのオフセット。使い切ると 176
8void *overflow_arg_areaスタックで渡された次の引数のアドレス
16void *reg_save_areaレジスター保存領域のアドレス
log_msg の va_start。RSI から R9 と、AL が 0 でなければ XMM0 から XMM7 が、32(%rsp) のレジスター保存領域に置かれます。fmt が RDI を使ったので、gp_offset は 8 から、fp_offset は 48 から始まります。Clang は両方を 0x3000000008 の 1 つの movabsq で格納します。

sysv-varargs.c

/* va_start: the prologue saves the argument registers in the register
   save area, and the va_list records where the next argument is. */
#include <stdarg.h>

extern int vlog(const char *fmt, va_list ap);

int log_msg(const char *fmt, ...)
{
	va_list ap;
	int r;

	va_start(ap, fmt);
	r = vlog(fmt, ap);
	va_end(ap);
	return r;
}

GCC 14.2.0 (Debian 14.2.0-19) x86_64-linux-gnu

AT&T 構文 gcc-14 -O2 -fno-asynchronous-unwind-tables -S sysv-varargs.c

log_msg:
        subq    $216, %rsp
        movq    %rsi, 40(%rsp)
        movq    %rdx, 48(%rsp)
        movq    %rcx, 56(%rsp)
        movq    %r8, 64(%rsp)
        movq    %r9, 72(%rsp)
        testb   %al, %al
        je      .L3
        movaps  %xmm0, 80(%rsp)
        movaps  %xmm1, 96(%rsp)
        movaps  %xmm2, 112(%rsp)
        movaps  %xmm3, 128(%rsp)
        movaps  %xmm4, 144(%rsp)
        movaps  %xmm5, 160(%rsp)
        movaps  %xmm6, 176(%rsp)
        movaps  %xmm7, 192(%rsp)
.L3:
        leaq    224(%rsp), %rax
        leaq    8(%rsp), %rsi
        movl    $8, 8(%rsp)
        movq    %rax, 16(%rsp)
        leaq    32(%rsp), %rax
        movl    $48, 12(%rsp)
        movq    %rax, 24(%rsp)
        call    vlog@PLT
        addq    $216, %rsp
        ret

Intel 構文 gcc-14 -O2 -fno-asynchronous-unwind-tables -masm=intel -S sysv-varargs.c

log_msg:
        sub     rsp, 216
        mov     QWORD PTR 40[rsp], rsi
        mov     QWORD PTR 48[rsp], rdx
        mov     QWORD PTR 56[rsp], rcx
        mov     QWORD PTR 64[rsp], r8
        mov     QWORD PTR 72[rsp], r9
        test    al, al
        je      .L3
        movaps  XMMWORD PTR 80[rsp], xmm0
        movaps  XMMWORD PTR 96[rsp], xmm1
        movaps  XMMWORD PTR 112[rsp], xmm2
        movaps  XMMWORD PTR 128[rsp], xmm3
        movaps  XMMWORD PTR 144[rsp], xmm4
        movaps  XMMWORD PTR 160[rsp], xmm5
        movaps  XMMWORD PTR 176[rsp], xmm6
        movaps  XMMWORD PTR 192[rsp], xmm7
.L3:
        lea     rax, 224[rsp]
        lea     rsi, 8[rsp]
        mov     DWORD PTR 8[rsp], 8
        mov     QWORD PTR 16[rsp], rax
        lea     rax, 32[rsp]
        mov     DWORD PTR 12[rsp], 48
        mov     QWORD PTR 24[rsp], rax
        call    vlog@PLT
        add     rsp, 216
        ret

Clang 23.1.2 x86_64-linux-gnu

AT&T 構文 clang --target=x86_64-linux-gnu -O2 -fno-asynchronous-unwind-tables -S sysv-varargs.c

log_msg:
        subq    $216, %rsp
        movq    %rsi, 40(%rsp)
        movq    %rdx, 48(%rsp)
        movq    %rcx, 56(%rsp)
        movq    %r8, 64(%rsp)
        movq    %r9, 72(%rsp)
        testb   %al, %al
        je      .LBB0_2
        movaps  %xmm0, 80(%rsp)
        movaps  %xmm1, 96(%rsp)
        movaps  %xmm2, 112(%rsp)
        movaps  %xmm3, 128(%rsp)
        movaps  %xmm4, 144(%rsp)
        movaps  %xmm5, 160(%rsp)
        movaps  %xmm6, 176(%rsp)
        movaps  %xmm7, 192(%rsp)
.LBB0_2:
        leaq    32(%rsp), %rax
        movq    %rax, 16(%rsp)
        leaq    224(%rsp), %rax
        movq    %rax, 8(%rsp)
        movabsq $206158430216, %rax
        movq    %rax, (%rsp)
        movq    %rsp, %rsi
        callq   vlog@PLT
        addq    $216, %rsp
        retq

Intel 構文 clang --target=x86_64-linux-gnu -O2 -fno-asynchronous-unwind-tables -masm=intel -S sysv-varargs.c

log_msg:
        sub     rsp, 216
        mov     qword ptr [rsp + 40], rsi
        mov     qword ptr [rsp + 48], rdx
        mov     qword ptr [rsp + 56], rcx
        mov     qword ptr [rsp + 64], r8
        mov     qword ptr [rsp + 72], r9
        test    al, al
        je      .LBB0_2
        movaps  xmmword ptr [rsp + 80], xmm0
        movaps  xmmword ptr [rsp + 96], xmm1
        movaps  xmmword ptr [rsp + 112], xmm2
        movaps  xmmword ptr [rsp + 128], xmm3
        movaps  xmmword ptr [rsp + 144], xmm4
        movaps  xmmword ptr [rsp + 160], xmm5
        movaps  xmmword ptr [rsp + 176], xmm6
        movaps  xmmword ptr [rsp + 192], xmm7
.LBB0_2:
        lea     rax, [rsp + 32]
        mov     qword ptr [rsp + 16], rax
        lea     rax, [rsp + 224]
        mov     qword ptr [rsp + 8], rax
        movabs  rax, 206158430216
        mov     qword ptr [rsp], rax
        mov     rsi, rsp
        call    vlog@PLT
        add     rsp, 216
        ret
va_arg(ap, int)。gp_offset がもう 1 つレジスターを許す間はレジスター保存領域から、その後は overflow_arg_area から 8 バイトずつ読みます。

sysv-va-arg.c

/* va_arg: take the next int from the register save area while
   gp_offset is below 48, then from the stack. */
#include <stdarg.h>

int next_int(va_list ap)
{
	return va_arg(ap, int);
}

GCC 14.2.0 (Debian 14.2.0-19) x86_64-linux-gnu

AT&T 構文 gcc-14 -O2 -fno-asynchronous-unwind-tables -S sysv-va-arg.c

next_int:
        movl    (%rdi), %eax
        cmpl    $47, %eax
        ja      .L2
        movl    %eax, %edx
        addl    $8, %eax
        addq    16(%rdi), %rdx
        movl    %eax, (%rdi)
        movl    (%rdx), %eax
        ret
.L2:
        movq    8(%rdi), %rdx
        leaq    8(%rdx), %rax
        movq    %rax, 8(%rdi)
        movl    (%rdx), %eax
        ret

Intel 構文 gcc-14 -O2 -fno-asynchronous-unwind-tables -masm=intel -S sysv-va-arg.c

next_int:
        mov     eax, DWORD PTR [rdi]
        cmp     eax, 47
        ja      .L2
        mov     edx, eax
        add     eax, 8
        add     rdx, QWORD PTR 16[rdi]
        mov     DWORD PTR [rdi], eax
        mov     eax, DWORD PTR [rdx]
        ret
.L2:
        mov     rdx, QWORD PTR 8[rdi]
        lea     rax, 8[rdx]
        mov     QWORD PTR 8[rdi], rax
        mov     eax, DWORD PTR [rdx]
        ret

Clang 23.1.2 x86_64-linux-gnu

AT&T 構文 clang --target=x86_64-linux-gnu -O2 -fno-asynchronous-unwind-tables -S sysv-va-arg.c

next_int:
        movl    (%rdi), %ecx
        cmpq    $40, %rcx
        ja      .LBB0_2
        movq    %rcx, %rax
        addq    16(%rdi), %rax
        addl    $8, %ecx
        movl    %ecx, (%rdi)
        movl    (%rax), %eax
        retq
.LBB0_2:
        movq    8(%rdi), %rax
        leaq    8(%rax), %rcx
        movq    %rcx, 8(%rdi)
        movl    (%rax), %eax
        retq

Intel 構文 clang --target=x86_64-linux-gnu -O2 -fno-asynchronous-unwind-tables -masm=intel -S sysv-va-arg.c

next_int:
        mov     ecx, dword ptr [rdi]
        cmp     rcx, 40
        ja      .LBB0_2
        mov     rax, rcx
        add     rax, qword ptr [rdi + 16]
        add     ecx, 8
        mov     dword ptr [rdi], ecx
        mov     eax, dword ptr [rax]
        ret
.LBB0_2:
        mov     rax, qword ptr [rdi + 8]
        lea     rcx, [rax + 8]
        mov     qword ptr [rdi + 8], rcx
        mov     eax, dword ptr [rax]
        ret

32 バイトと 64 バイトのベクトル引数は、名前のない引数であれば常にスタックで渡され、それらを可変長引数関数に渡すにはプロトタイプが必要です。

小さな整数

リスト 15 は short を関数に渡し、int に広げて返します。どちらのコンパイラーも呼び出しの前に引数を 32 ビットに符号拡張しますが、呼び出された関数の中で再び拡張するのは GCC だけです。Clang は RDI の下位 32 ビットをそのまま使います。psABI は呼び出し元に拡張を求めていないので、Clang がコンパイルした関数は、GCC と Clang の呼び出し元が提供はするものの psABI が約束していないことに依存しています。macOS では、Apple は呼び出し元による拡張を ABI の規則にしています。これは次の節で説明します。

short の引数。GCC は呼び出し元と呼び出し先の両方で拡張し、Clang は呼び出し元だけで拡張します。

sysv-small-int.c

/* A short argument: who extends it to 32 bits? */
int widen(short s)
{
	return s;
}

extern int take_short(short s);

int pass_short(int x)
{
	return take_short(x);
}

GCC 14.2.0 (Debian 14.2.0-19) x86_64-linux-gnu

AT&T 構文 gcc-14 -O2 -fno-asynchronous-unwind-tables -S sysv-small-int.c

widen:
        movswl  %di, %eax
        ret

pass_short:
        movswl  %di, %edi
        jmp     take_short@PLT

Intel 構文 gcc-14 -O2 -fno-asynchronous-unwind-tables -masm=intel -S sysv-small-int.c

widen:
        movsx   eax, di
        ret

pass_short:
        movsx   edi, di
        jmp     take_short@PLT

Clang 23.1.2 x86_64-linux-gnu

AT&T 構文 clang --target=x86_64-linux-gnu -O2 -fno-asynchronous-unwind-tables -S sysv-small-int.c

widen:
        movl    %edi, %eax
        retq

pass_short:
        movswl  %di, %edi
        jmp     take_short@PLT

Intel 構文 clang --target=x86_64-linux-gnu -O2 -fno-asynchronous-unwind-tables -masm=intel -S sysv-small-int.c

widen:
        mov     eax, edi
        ret

pass_short:
        movsx   edi, di
        jmp     take_short@PLT

Clang 23.1.2 x86_64-apple-macos11

AT&T 構文 clang --target=x86_64-apple-macos11 -O2 -fno-asynchronous-unwind-tables -S sysv-small-int.c

_widen:
        pushq   %rbp
        movq    %rsp, %rbp
        movl    %edi, %eax
        popq    %rbp
        retq

_pass_short:
        pushq   %rbp
        movq    %rsp, %rbp
        movswl  %di, %edi
        popq    %rbp
        jmp     _take_short

Intel 構文 clang --target=x86_64-apple-macos11 -O2 -fno-asynchronous-unwind-tables -masm=intel -S sysv-small-int.c

_widen:
        push    rbp
        mov     rbp, rsp
        mov     eax, edi
        pop     rbp
        ret

_pass_short:
        push    rbp
        mov     rbp, rsp
        movsx   edi, di
        pop     rbp
        jmp     _take_short

macOS

Intel プロセッサーの macOS は、LP64 データモデルで psABI に従います。ただし、Apple のコンパイラーが昔から異なる動作をしてきた点は例外です。Apple は次の相違点を文書化しています。

小さな整数の引数
呼び出し元は、int より小さい引数を、より小さい基になる型を持つ enum も含めて 32 ビットに拡張し、呼び出し先はそれに依存して構いません。戻り値とスタック上の引数についても同じです。リスト 15 にその効果が表れています。macOS 向けの Clang は widen を拡張なしでコンパイルします。
8 バイト以下のベクトル
8 バイトより小さいベクトルは INTEGER です。8 バイトのベクトルのうち、double 1 つのベクトルは MEMORY、64 ビット整数 1 つのベクトルは INTEGER、それ以外はすべて SSE です。psABI は __m64 のように 8 バイトのベクトルをすべて SSE にしています。64 ビット整数 1 つのベクトルについては、Clang は FreeBSD でも Apple の規則を使います。
long long 1 つのベクトル。Linux は XMM0 で渡し、macOS と FreeBSD は x がすでにある RDI で渡します。Clang はこの 2 つのターゲットではどちらもフレームポインターを保持するので、RBP のプッシュとポップがあります。

macos-vector64.c

/* An 8-byte vector of one 64-bit integer, like MMX's __m64. */
typedef long long v1di __attribute__((vector_size(8)));

extern void take_v1di(v1di v);

void pass_v1di(long long x)
{
	take_v1di((v1di){ x });
}

GCC 14.2.0 (Debian 14.2.0-19) x86_64-linux-gnu

AT&T 構文 gcc-14 -O2 -fno-asynchronous-unwind-tables -S macos-vector64.c

pass_v1di:
        movq    %rdi, %xmm0
        jmp     take_v1di@PLT

Intel 構文 gcc-14 -O2 -fno-asynchronous-unwind-tables -masm=intel -S macos-vector64.c

pass_v1di:
        movq    xmm0, rdi
        jmp     take_v1di@PLT

Clang 23.1.2 x86_64-linux-gnu

AT&T 構文 clang --target=x86_64-linux-gnu -O2 -fno-asynchronous-unwind-tables -S macos-vector64.c

pass_v1di:
        movq    %rdi, %xmm0
        jmp     take_v1di@PLT

Intel 構文 clang --target=x86_64-linux-gnu -O2 -fno-asynchronous-unwind-tables -masm=intel -S macos-vector64.c

pass_v1di:
        movq    xmm0, rdi
        jmp     take_v1di@PLT

Clang 23.1.2 x86_64-apple-macos11

AT&T 構文 clang --target=x86_64-apple-macos11 -O2 -fno-asynchronous-unwind-tables -S macos-vector64.c

_pass_v1di:
        pushq   %rbp
        movq    %rsp, %rbp
        popq    %rbp
        jmp     _take_v1di

Intel 構文 clang --target=x86_64-apple-macos11 -O2 -fno-asynchronous-unwind-tables -masm=intel -S macos-vector64.c

_pass_v1di:
        push    rbp
        mov     rbp, rsp
        pop     rbp
        jmp     _take_v1di

Clang 23.1.2 x86_64-unknown-freebsd14

AT&T 構文 clang --target=x86_64-unknown-freebsd14 -O2 -fno-asynchronous-unwind-tables -S macos-vector64.c

pass_v1di:
        pushq   %rbp
        movq    %rsp, %rbp
        popq    %rbp
        jmp     take_v1di

Intel 構文 clang --target=x86_64-unknown-freebsd14 -O2 -fno-asynchronous-unwind-tables -masm=intel -S macos-vector64.c

pass_v1di:
        push    rbp
        mov     rbp, rsp
        pop     rbp
        jmp     take_v1di
ほかのフィールドと並ぶ long double
X87UP が X87 の後に続かないときに値を MEMORY にする後処理は行われません。代わりに、そのような X87UP の eightbyte は SSE になります。そのため リスト 17 の共用体は、スタックではなく RDI と XMM0 で渡されます。
long double とポインターの共用体。Linux 向けの GCC と Clang はそれをスタックから読み、macOS 向けの Clang は p を RDI に見つけます。

macos-x87-union.c

/* A union whose second eightbyte is only the x87 exponent. */
typedef union { long double d; void *p; } odd;

void *odd_p(odd u)
{
	return u.p;
}

GCC 14.2.0 (Debian 14.2.0-19) x86_64-linux-gnu

AT&T 構文 gcc-14 -O2 -fno-asynchronous-unwind-tables -S macos-x87-union.c

odd_p:
        movq    8(%rsp), %rax
        ret

Intel 構文 gcc-14 -O2 -fno-asynchronous-unwind-tables -masm=intel -S macos-x87-union.c

odd_p:
        mov     rax, QWORD PTR 8[rsp]
        ret

Clang 23.1.2 x86_64-linux-gnu

AT&T 構文 clang --target=x86_64-linux-gnu -O2 -fno-asynchronous-unwind-tables -S macos-x87-union.c

odd_p:
        movq    8(%rsp), %rax
        retq

Intel 構文 clang --target=x86_64-linux-gnu -O2 -fno-asynchronous-unwind-tables -masm=intel -S macos-x87-union.c

odd_p:
        mov     rax, qword ptr [rsp + 8]
        ret

Clang 23.1.2 x86_64-apple-macos11

AT&T 構文 clang --target=x86_64-apple-macos11 -O2 -fno-asynchronous-unwind-tables -S macos-x87-union.c

_odd_p:
        pushq   %rbp
        movq    %rsp, %rbp
        movq    %rdi, %rax
        popq    %rbp
        retq

Intel 構文 clang --target=x86_64-apple-macos11 -O2 -fno-asynchronous-unwind-tables -masm=intel -S macos-x87-union.c

_odd_p:
        push    rbp
        mov     rbp, rsp
        mov     rax, rdi
        pop     rbp
        ret

Apple は、単純な C 以外のコードについての規約も文書化しています。

  • C++ の thread_local 変数の初期化関数 (_ZTH... と _ZTW...) は、RCX、RDX、RSI、R8、R9、R10、R11 も保存します。
  • Swift は、非同期関数のフレームに R14 を使うなど、独自の目的にレジスターを使います。
  • Clang の拡張である任意の要素数のベクトルは、サイズとして次の 2 のべき乗を取ります。ARC の __weak フィールドを持つ構造体は、メモリで渡され、メモリで返されます。

macOS のリストは、ABI が求めていない点でも異なります。シンボルはアンダースコアで始まり、Clang はこのターゲットではデフォルトでフレームポインターを保持するので、すべての関数に push rbp と mov rbp, rsp が加わります。Apple は macOS Catalina 以降、32 ビットの Intel コードをサポートしていません。

Linux x32

x32 は、プログラムを 64 ビットモードで、16 個の汎用レジスターすべてとこのページの呼び出し規約を使って実行しますが、データモデルは ILP32 です。long とポインターは 32 ビットで、プログラムはアドレス空間の下位 4 GB に置かれます。psABI は規則を 1 つ加えています。レジスターで渡される、または返されるポインターは、ビット 32 から 63 がクリアされています。

x32 のポインターと、比較のための同じコードの x86-64 版。32 ビットレジスターへの書き込みは上位半分をクリアするので、leal と addl はすでに正しい形のポインターを生成します。それでも GCC は pass_ptr で RDI の上位半分を再びクリアします。load はアドレスに 32 ビットレジスターを使います。

x32-pointers.c

/* Pointers and long have 32 bits in x32, in 64-bit registers. */
long *next(long *p)
{
	return p + 1;
}

long load(long *p, int i)
{
	return p[i];
}

extern void take_ptr(void *p);

void pass_ptr(char *base, int off)
{
	take_ptr(base + off);
}

GCC 14.2.0 (Debian 14.2.0-19) x86_64-linux-gnux32

AT&T 構文 gcc-14 -mx32 -O2 -fno-asynchronous-unwind-tables -S x32-pointers.c

next:
        leal    4(%rdi), %eax
        ret

load:
        movl    (%edi,%esi,4), %eax
        ret

pass_ptr:
        addl    %esi, %edi
        movl    %edi, %edi
        jmp     take_ptr@PLT

Intel 構文 gcc-14 -mx32 -O2 -fno-asynchronous-unwind-tables -masm=intel -S x32-pointers.c

next:
        lea     eax, 4[rdi]
        ret

load:
        mov     eax, DWORD PTR [edi+esi*4]
        ret

pass_ptr:
        add     edi, esi
        mov     edi, edi
        jmp     take_ptr@PLT

Clang 23.1.2 x86_64-linux-gnux32

AT&T 構文 clang --target=x86_64-linux-gnux32 -O2 -fno-asynchronous-unwind-tables -S x32-pointers.c

next:
        leal    4(%rdi), %eax
        retq

load:
        movl    (%edi,%esi,4), %eax
        retq

pass_ptr:
        addl    %esi, %edi
        jmp     take_ptr@PLT

Intel 構文 clang --target=x86_64-linux-gnux32 -O2 -fno-asynchronous-unwind-tables -masm=intel -S x32-pointers.c

next:
        lea     eax, [rdi + 4]
        ret

load:
        mov     eax, dword ptr [edi + 4*esi]
        ret

pass_ptr:
        add     edi, esi
        jmp     take_ptr@PLT

GCC 14.2.0 (Debian 14.2.0-19) x86_64-linux-gnu

AT&T 構文 gcc-14 -O2 -fno-asynchronous-unwind-tables -S x32-pointers.c

next:
        leaq    8(%rdi), %rax
        ret

load:
        movslq  %esi, %rsi
        movq    (%rdi,%rsi,8), %rax
        ret

pass_ptr:
        movslq  %esi, %rsi
        addq    %rsi, %rdi
        jmp     take_ptr@PLT

Intel 構文 gcc-14 -O2 -fno-asynchronous-unwind-tables -masm=intel -S x32-pointers.c

next:
        lea     rax, 8[rdi]
        ret

load:
        movsx   rsi, esi
        mov     rax, QWORD PTR [rdi+rsi*8]
        ret

pass_ptr:
        movsx   rsi, esi
        add     rdi, rsi
        jmp     take_ptr@PLT

ほかの規則は、型が小さくなったことから導かれます。long long や double は依然としてレジスター 1 つを使い、long とポインターはレジスター 1 つのうち 32 ビットを使います。va_list は 16 バイトに縮み、long double と __int128 は 16 バイトのままです。サイズは ABI の概要に挙げています。

出典

  1. System V Application Binary Interface, AMD64 Architecture Processor Supplement, version 1.0: コミット e1ce098331da、2025-03-12
  2. Apple Developer: Writing 64-bit Intel code for Apple Platforms: 2026-09-25 の公開版
  3. Clang 23.1.2: clang/lib/CodeGen/Targets/X86.cpp: コミット 85ac56026243、2026-09-20
  4. LLVM 23.1.2: llvm/lib/Target/X86/X86CallingConv.td: コミット 85ac56026243、2026-09-20
  5. GCC 14.2.0: gcc/config/i386/i386.cc: コミット 04696df09633、2024-08-01
  6. EDK II: BaseTools/Conf/tools_def.template: コミット 2970e5699ba6、2026-08-12