The System V x86-64 ABI is defined by the x86-64 psABI, the AMD64 supplement to the System V ABI. Linux, Android, FreeBSD, NetBSD, OpenBSD, DragonFly BSD, illumos and Solaris, and Haiku all use it, and so does macOS on Intel processors, with the few differences that its section lists. Linux x32 is the same ABI with 32-bit pointers (below). Windows uses a different convention, Microsoft x64; the ABI overview maps platforms to ABIs and gives the sizes of the C types on each.

The listings on this page come from GCC and Clang for Linux at -O2, and from Clang for macOS, FreeBSD and x32 where the page compares them. The buttons next to each listing switch between AT&T and Intel syntax.

Registers

A function may change any register except the stack pointer and six general-purpose registers: RBX, RBP and R12 to R15 belong to the caller, and a function that uses them saves and restores them. Every vector register is scratch, and so are the eight x87 registers. Table 1 gives the role of each register.

Registers in the System V x86-64 ABI.
RegisterRolePreserved
RDI, RSI, RDX, RCX, R8, R9Integer and pointer arguments 1 to 6, in this orderno
RAXReturn value; with a variadic call, AL gives the number of vector registers usedno
RDXAlso the second eightbyte of a return valueno
RBX, R12, R13, R14Callee-savedyes
RBPCallee-saved; the frame pointer when a function uses oneyes
R15Callee-saved; optionally the base of the GOTyes
RSPStack pointeryes
R10Static chain pointer of nested functionsno
R11Scratch, free for PLT stubs to useno
R16 to R31 (APX)Scratchno
XMM0 to XMM7Floating-point and vector arguments 1 to 8no
XMM0, XMM1Floating-point and vector return valuesno
XMM8 to XMM31Scratchno
YMM, ZMMThe same registers, wider, for __m256 and __m512no
K0 to K7Scratch (AVX-512 masks)no
ST0, ST1Return long double (ST0) and _Complex long double (both)no
ST2 to ST7, MM0 to MM7Scratch; the CPU must be in x87 mode at callsno
TMM0 to TMM7Scratch (AMX tiles)no
FSThread pointer; see thread-local storageyes

Beyond the registers, a function must leave the direction flag clear and keep the control bits of MXCSR and the x87 control word, such as the rounding mode, as it found them; the status flags are not preserved. A function that uses MMX registers must execute EMMS before it returns or calls another.

Listing 1 keeps its argument in RBX across a call. RBX is callee-saved, so f returns it unchanged, but keep must save the caller’s value of RBX first. The push also keeps the stack aligned for the call, as the stack frame explains.

A value that must survive a call lives in a callee-saved register.

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 syntax 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 syntax 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 syntax 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 syntax 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

Passing arguments

Arguments are assigned from left to right. Integers and pointers take the next free register of RDI, RSI, RDX, RCX, R8 and R9; float, double and vectors take the next of XMM0 to XMM7. The two sequences are independent, so a function can receive six integers and eight floating-point values in registers. Whatever does not fit goes on the stack, pushed from right to left, so that the first stack argument sits right above the return address.

Eight integer arguments: sum8 finds the seventh and eighth at 8(%rsp) and 16(%rsp), and call8 pushes them before the call.

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 syntax 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 syntax 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 syntax 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 syntax 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
Integer and floating-point arguments count separately: i, l and p go to RDI, RSI and RDX, and x, f and y to XMM0, XMM1 and 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 syntax 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 syntax 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

An integer smaller than 64 bits occupies the low bits of its register, and the psABI leaves the other bits unspecified, so the receiving side must extend the value if it needs a wider one. In Listing 18, the x86-64 load sign-extends its int index with movslq before it uses it in an address. Small integers shows where GCC and Clang differ on this. A _Bool is the exception: its lowest 8 bits hold 0 or 1.

Classification

Everything that is not a plain integer or floating-point value is sorted into classes first. The class of each eightbyte, each 8-byte piece of the value, decides where that piece goes.

The classes of the psABI. A value in two classes of registers, such as INTEGER and SSE, uses one of each.
ClassMeaningPassed in
INTEGERFits a general-purpose register: integers, pointers, _Boolnext of RDI, RSI, RDX, RCX, R8, R9
SSEFits a vector register: float, double, _Float16, __m64next of XMM0 to XMM7
SSEUPThe upper part of a vector whose low eightbyte is SSEthe rest of the same register
X87, X87UPThe mantissa and the exponent of a long doublethe stack
COMPLEX_X87_Complex long doublethe stack
MEMORYToo large, or not fit for registersthe stack
NO_CLASSPadding and empty structsnothing

A __m128 or __float128 is one SSE eightbyte and one SSEUP, and so fills one XMM register; a __m256 is SSE and three SSEUP and fills a YMM register, if the processor has them. __int128 counts as a struct of two long values, except that it is 16-byte aligned on the stack.

Structs, unions and arrays are classified by their fields:

  1. A value larger than 64 bytes, or one with unaligned fields, has class MEMORY.
  2. A C++ object that is not trivial for the purpose of calls, such as one with a user-provided copy constructor or destructor, is passed as a pointer to a copy, which is INTEGER.
  3. Otherwise each eightbyte starts as NO_CLASS, and every field merges its class into the eightbytes it occupies. Two equal classes stay; NO_CLASS gives way to the other class; MEMORY wins, then INTEGER; X87, X87UP and COMPLEX_X87 turn the result into MEMORY; and what remains is SSE.
  4. A cleanup follows: if any eightbyte is MEMORY, the whole value is; if X87UP does not follow X87, the whole value is MEMORY; a value of more than two eightbytes is MEMORY unless it is SSE followed only by SSEUP; and SSEUP without SSE before it becomes SSE.

In effect, anything larger than 16 bytes is MEMORY, unless it is one __m256 or __m512 vector, alone or in a struct. Then the eightbytes are assigned in order. If there are not enough registers left for all of them, the whole value goes on the stack, and the registers stay free for the next arguments.

Worked examples of the classification, each confirmed by a listing.
C typeSizeEightbytesPassed inShown in
struct pair { long a, b; }16INTEGER, INTEGERtwo general-purpose registersListing 4
struct point { double x, y; }16SSE, SSEtwo XMM registersListing 4
struct mixed { int a, b; double d; }16INTEGER, SSEa general-purpose and an XMM registerListing 4
struct tagged { int tag; float value; }8INTEGERone general-purpose registerListing 5
struct vec3 { float x, y, z; }12SSE, SSEtwo XMM registers: x and y in the firstListing 5
struct triple { long a, b, c; }24MEMORYthe stackListing 6
__int12816INTEGER, INTEGERtwo general-purpose registers, or the stackListing 7
long double16X87, X87UPthe stackListing 9
union { long double d; void *p; }16INTEGER, X87UP: MEMORYthe stackListing 17
Structs of two eightbytes in registers: struct pair in RDI and RSI, struct point in XMM0 and XMM1, and struct mixed with both ints in RDI and the double in 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 syntax 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 syntax 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

In mixed_sum, the two ints share RDI: a is in the low half, and b needs a shift by 32 bits.

Fields that share an eightbyte merge their classes. The int and the float of struct tagged make an INTEGER eightbyte, so the float arrives in the upper half of RDI; z of struct vec3 is alone in the second eightbyte, which is 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 syntax 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 syntax 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
A struct of 24 bytes is MEMORY: the caller copies it to the bottom of its outgoing argument area, and the callee reads it above its return address.

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 syntax 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 syntax 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 syntax 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 syntax 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 in pairs of registers, low half first. In late128, only R9 is left for x, so x goes to the stack and g still gets 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 syntax 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 syntax 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 syntax 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 syntax 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

Returning values

The return value is classified like an argument, then:

  • INTEGER eightbytes return in RAX and then RDX, and SSE eightbytes in XMM0 and then XMM1. A struct of a double and a long returns in XMM0 and RAX.
  • An X87 value, long double, returns in ST0, and a _Complex long double in ST0 and ST1.
  • A MEMORY value is returned through memory that the caller provides. The caller passes its address in RDI, as a hidden first argument that moves the real arguments one register along, and the function returns the same address in RAX.
Structs returned in RAX and RDX, in XMM0 and RAX, and through memory: ret_triple gets the address of the result in RDI, so its argument x arrives in 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 syntax 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 syntax 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 syntax 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 syntax 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 is the odd one out: arguments of this type always go on the stack, while the result comes back on the x87 register stack. Listing 9 reads two of them from the stack, 16 bytes apart, and multiplies them there. Converting a double takes a trip through memory, here the red zone, since no instruction moves a value between an XMM register and the x87 stack.

long double arguments on the stack and the result in 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 syntax 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 syntax 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

The stack frame

The stack grows down. At a call instruction, RSP must be a multiple of 16, so on entry to a function, after the return address was pushed, RSP + 8 is. The boundary is 32 or 64 bytes when a __m256 or __m512 argument is passed on the stack. Without this rule, aligned SSE loads and stores of stack data could fault.

The layout of a stack frame, from high addresses to low.
AddressContentsFrame
16(%rbp) and upArguments passed in memory, the first at the lowest addresscaller
8(%rbp)Return address, pushed by callcallee
0(%rbp)Saved RBP of the caller, if the function uses a frame pointercallee
belowSaved registers and local variablescallee
0(%rsp)Outgoing arguments of the next call, 16-byte alignedcallee
128 bytes below RSPThe red zonecallee

A frame pointer is optional. With optimization, GCC and Clang for Linux address the frame through RSP and leave RBP free, unless the frame changes size at run time, as with a variable-length array or alloca.

A variable-length array: RBP marks the frame, and the array’s size is rounded up to a multiple of 16 so that RSP stays aligned for the call.

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 syntax 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 syntax 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

In Listing 2, call8 needs 16 bytes for the two stack arguments. On entry RSP is 8 bytes past a multiple of 16, so GCC first subtracts 8, then pushes two 8-byte arguments, and the call happens on a multiple of 16; after it, addq $24 removes all three. In Listing 1, the push of RBX does the same job.

The red zone

The 128 bytes below RSP are the red zone. Signal and interrupt handlers must not change them, so a function may keep data there that it does not need across a call, without moving RSP. A leaf function can keep its whole frame there.

With the red zone, leaf stores its array below RSP. With -mno-red-zone, it must move RSP down first and back up at the end.

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 syntax 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 syntax 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 syntax 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 syntax 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

Code that runs where an interrupt pushes onto the same stack, such as an operating system kernel or firmware, must not rely on the red zone and is compiled with -mno-red-zone; EDK II, for example, compiles x64 UEFI firmware that way.

Variadic functions

A call to a variadic function, such as printf, uses the same registers as any other call, plus one hidden argument: AL holds an upper bound on the number of vector registers that carry arguments, from 0 to 8. The rest of RAX is undefined. Calls to functions without a prototype set AL too, since the function might be variadic.

AL counts the vector registers: 0 for two ints, 1 for one double. GCC writes all of EAX, Clang only 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 syntax 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 syntax 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 syntax 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 syntax 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"

The variadic function cannot know where its unnamed arguments are, so its prologue saves the argument registers into a 176-byte register save area: RDI to R9 at offsets 0 to 40, then XMM0 to XMM7 at offsets 48 to 160, 16 bytes each. It skips the vector registers when AL is 0. va_list is an array of one structure that records where the next argument is.

The fields of va_list, a 24-byte structure. Because va_list is an array, a function that receives one gets a pointer to it.
OffsetFieldMeaning
0unsigned int gp_offsetOffset of the next general-purpose register in the save area; 48 once they are used up
4unsigned int fp_offsetOffset of the next vector register in the save area; 176 once they are used up
8void *overflow_arg_areaAddress of the next argument passed on the stack
16void *reg_save_areaAddress of the register save area
va_start in log_msg: RSI to R9 and, if AL is not zero, XMM0 to XMM7 go to the register save area at 32(%rsp). fmt took RDI, so gp_offset starts at 8; fp_offset starts at 48. Clang stores both in one movabsq of 0x3000000008.

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 syntax 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 syntax 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 syntax 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 syntax 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): from the register save area while gp_offset allows another register, then from overflow_arg_area, 8 bytes at a time.

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 syntax 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 syntax 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 syntax 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 syntax 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

Vector arguments of 32 and 64 bytes are always passed on the stack when they are unnamed, and passing them to a variadic function needs a prototype.

Small integers

Listing 15 passes a short to a function and returns it widened to an int. Both compilers sign-extend the argument to 32 bits before the call, but only GCC does it again in the called function: Clang takes the lower 32 bits of RDI as they are. The psABI does not require the caller to extend, so a function compiled by Clang relies on something that GCC and Clang callers provide but the psABI does not promise. For macOS, Apple makes the caller’s extension a rule of the ABI, described in the next section.

A short argument: GCC extends it in the caller and again in the callee; Clang only in the caller.

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 syntax 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 syntax 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 syntax 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 syntax 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 syntax 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 syntax 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

macOS on Intel processors follows the psABI with the LP64 data model, except where Apple’s compiler has always behaved differently. Apple documents these differences:

Small integer arguments
The caller extends an argument smaller than int, including an enum with a smaller underlying type, to 32 bits, and the callee may rely on it. The same holds for return values and for arguments on the stack. Listing 15 shows the effect: Clang for macOS compiles widen without any extension.
Vectors of 8 bytes or less
Vectors smaller than 8 bytes are INTEGER. Of the 8-byte vectors, a vector of one double is MEMORY, a vector of one 64-bit integer is INTEGER, and all others are SSE. The psABI makes every 8-byte vector, like __m64, SSE. For vectors of one 64-bit integer, Clang uses Apple’s rule on FreeBSD too.
A vector of one long long: Linux passes it in XMM0, macOS and FreeBSD in RDI, where x already is. Clang keeps a frame pointer for both of these targets, hence the push and pop of 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 syntax gcc-14 -O2 -fno-asynchronous-unwind-tables -S macos-vector64.c

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

Intel syntax 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 syntax 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 syntax 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 syntax 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 syntax 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 syntax 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 syntax 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 next to other fields
The cleanup step that makes a value MEMORY when X87UP does not follow X87 does not happen. Instead, such an X87UP eightbyte becomes SSE. The union of Listing 17 therefore travels in RDI and XMM0 instead of on the stack.
A union of a long double and a pointer: GCC and Clang for Linux read it from the stack, Clang for macOS finds p in 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 syntax gcc-14 -O2 -fno-asynchronous-unwind-tables -S macos-x87-union.c

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

Intel syntax 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 syntax clang --target=x86_64-linux-gnu -O2 -fno-asynchronous-unwind-tables -S macos-x87-union.c

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

Intel syntax 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 syntax 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 syntax 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 also documents conventions for code outside plain C:

  • the initialization functions of C++ thread_local variables (_ZTH... and _ZTW...) preserve RCX, RDX, RSI, R8, R9, R10 and R11 as well;
  • Swift uses registers for its own purposes, such as R14 for the frame of an asynchronous function;
  • vectors of any number of elements, a Clang extension, take the next power of two as their size; structures with ARC __weak fields are passed and returned in memory.

The listings for macOS also differ in ways the ABI does not require: symbols start with an underscore, and Clang keeps a frame pointer by default for this target, which adds push rbp and mov rbp, rsp to every function. Apple has not supported 32-bit Intel code since macOS Catalina.

Linux x32

x32 runs programs in 64-bit mode, with all sixteen general-purpose registers and the calling convention of this page, but with the ILP32 data model: long and pointers have 32 bits, and a program lives in the low 4 GB of the address space. The psABI adds one rule: a pointer passed or returned in a register has bits 32 to 63 cleared.

Pointers in x32, and the same code for x86-64 for comparison. Writing a 32-bit register clears the upper half, so leal and addl already give well-formed pointers; GCC still clears the upper half of RDI again in pass_ptr. load uses 32-bit registers in its address.

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 syntax 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 syntax 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 syntax 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 syntax 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 syntax 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 syntax 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

The other rules follow from the smaller types. A long long or a double still takes one register, while long and pointers take 32 bits of one. va_list shrinks to 16 bytes, and long double and __int128 keep their 16 bytes; the ABI overview lists the sizes.

Sources

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