System V x86-64 ABI and calling convention
The calling convention of almost every x86-64 system except Windows. Which registers carry what, how structs are split into registers, and what the stack looks like, with compiled code for every rule.
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.
| Register | Role | Preserved |
|---|---|---|
| RDI, RSI, RDX, RCX, R8, R9 | Integer and pointer arguments 1 to 6, in this order | no |
| RAX | Return value; with a variadic call, AL gives the number of vector registers used | no |
| RDX | Also the second eightbyte of a return value | no |
| RBX, R12, R13, R14 | Callee-saved | yes |
| RBP | Callee-saved; the frame pointer when a function uses one | yes |
| R15 | Callee-saved; optionally the base of the GOT | yes |
| RSP | Stack pointer | yes |
| R10 | Static chain pointer of nested functions | no |
| R11 | Scratch, free for PLT stubs to use | no |
| R16 to R31 (APX) | Scratch | no |
| XMM0 to XMM7 | Floating-point and vector arguments 1 to 8 | no |
| XMM0, XMM1 | Floating-point and vector return values | no |
| XMM8 to XMM31 | Scratch | no |
| YMM, ZMM | The same registers, wider, for __m256 and __m512 | no |
| K0 to K7 | Scratch (AVX-512 masks) | no |
| ST0, ST1 | Return long double (ST0) and _Complex long double (both) | no |
| ST2 to ST7, MM0 to MM7 | Scratch; the CPU must be in x87 mode at calls | no |
| TMM0 to TMM7 | Scratch (AMX tiles) | no |
| FS | Thread pointer; see thread-local storage | yes |
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.
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
retIntel 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
retClang 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
retqIntel 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
retPassing 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.
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
retIntel 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
retClang 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
retqIntel 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
reti, 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 1082130432Intel 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 1082130432An 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.
| Class | Meaning | Passed in |
|---|---|---|
| INTEGER | Fits a general-purpose register: integers, pointers, _Bool | next of RDI, RSI, RDX, RCX, R8, R9 |
| SSE | Fits a vector register: float, double, _Float16, __m64 | next of XMM0 to XMM7 |
| SSEUP | The upper part of a vector whose low eightbyte is SSE | the rest of the same register |
| X87, X87UP | The mantissa and the exponent of a long double | the stack |
| COMPLEX_X87 | _Complex long double | the stack |
| MEMORY | Too large, or not fit for registers | the stack |
| NO_CLASS | Padding and empty structs | nothing |
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:
- A value larger than 64 bytes, or one with unaligned fields, has class MEMORY.
- 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.
- 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.
- 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.
| C type | Size | Eightbytes | Passed in | Shown in |
|---|---|---|---|---|
struct pair { long a, b; } | 16 | INTEGER, INTEGER | two general-purpose registers | Listing 4 |
struct point { double x, y; } | 16 | SSE, SSE | two XMM registers | Listing 4 |
struct mixed { int a, b; double d; } | 16 | INTEGER, SSE | a general-purpose and an XMM register | Listing 4 |
struct tagged { int tag; float value; } | 8 | INTEGER | one general-purpose register | Listing 5 |
struct vec3 { float x, y, z; } | 12 | SSE, SSE | two XMM registers: x and y in the first | Listing 5 |
struct triple { long a, b, c; } | 24 | MEMORY | the stack | Listing 6 |
__int128 | 16 | INTEGER, INTEGER | two general-purpose registers, or the stack | Listing 7 |
long double | 16 | X87, X87UP | the stack | Listing 9 |
union { long double d; void *p; } | 16 | INTEGER, X87UP: MEMORY | the stack | Listing 17 |
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
retIntel 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
retIn mixed_sum, the two ints share RDI: a is in the low half, and
b needs a shift by 32 bits.
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
retIntel 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
retsysv-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
retIntel 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
retClang 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
retqIntel 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
retIntel 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
retClang 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
retqIntel 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]
retReturning 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
doubleand alongreturns in XMM0 and RAX. - An X87 value,
long double, returns in ST0, and a_Complex long doublein 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.
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)
retIntel 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
retClang 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)
retqIntel 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
retlong 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)
retIntel 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]
retThe 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.
| Address | Contents | Frame |
|---|---|---|
16(%rbp) and up | Arguments passed in memory, the first at the lowest address | caller |
8(%rbp) | Return address, pushed by call | callee |
0(%rbp) | Saved RBP of the caller, if the function uses a frame pointer | callee |
| below | Saved registers and local variables | callee |
0(%rsp) | Outgoing arguments of the next call, 16-byte aligned | callee |
| 128 bytes below RSP | The red zone | callee |
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.
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
retIntel 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
retIn 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.
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
retIntel 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]
retGCC 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
retIntel 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
retCode 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.
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@PLTIntel 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@PLTClang 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.
va_list, a 24-byte structure. Because va_list is an array, a function that receives one gets a pointer to it.| Offset | Field | Meaning |
|---|---|---|
| 0 | unsigned int gp_offset | Offset of the next general-purpose register in the save area; 48 once they are used up |
| 4 | unsigned int fp_offset | Offset of the next vector register in the save area; 176 once they are used up |
| 8 | void *overflow_arg_area | Address of the next argument passed on the stack |
| 16 | void *reg_save_area | Address 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
retIntel 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
retClang 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
retqIntel 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
retva_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
retIntel 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]
retClang 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
retqIntel 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]
retVector 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.
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@PLTIntel 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@PLTClang 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@PLTIntel 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@PLTClang 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_shortIntel 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_shortmacOS
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 anenumwith 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 compileswidenwithout any extension. - Vectors of 8 bytes or less
- Vectors smaller than 8 bytes are INTEGER. Of the 8-byte vectors, a
vector of one
doubleis 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.
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@PLTIntel syntax gcc-14 -O2 -fno-asynchronous-unwind-tables -masm=intel -S macos-vector64.c
pass_v1di:
movq xmm0, rdi
jmp take_v1di@PLTClang 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@PLTIntel 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@PLTClang 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_v1diIntel 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_v1diClang 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_v1diIntel 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_v1dilong doublenext 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.
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
retIntel syntax gcc-14 -O2 -fno-asynchronous-unwind-tables -masm=intel -S macos-x87-union.c
odd_p:
mov rax, QWORD PTR 8[rsp]
retClang 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
retqIntel 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]
retClang 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
retqIntel 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
retApple also documents conventions for code outside plain C:
- the initialization functions of C++
thread_localvariables (_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
__weakfields 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.
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@PLTIntel 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@PLTClang 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@PLTIntel 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@PLTGCC 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@PLTIntel 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@PLTThe 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
- System V Application Binary Interface, AMD64 Architecture Processor Supplement, version 1.0: commit
e1ce098331da, 2025-03-12 - Apple Developer: Writing 64-bit Intel code for Apple Platforms: as published on 2026-09-25
- Clang 23.1.2: clang/lib/CodeGen/Targets/X86.cpp: commit
85ac56026243, 2026-09-20 - LLVM 23.1.2: llvm/lib/Target/X86/X86CallingConv.td: commit
85ac56026243, 2026-09-20 - GCC 14.2.0: gcc/config/i386/i386.cc: commit
04696df09633, 2024-08-01 - EDK II: BaseTools/Conf/tools_def.template: commit
2970e5699ba6, 2026-08-12