System V i386 ABI and cdecl calling convention
The 32-bit calling convention of Linux and the BSDs, often called cdecl. Every argument goes on the stack, results come back in EAX, EDX or the x87 stack, and the details of structs and alignment differ between systems.
The System V i386 ABI is defined by the i386 psABI, the Intel386 supplement to the System V ABI. 32-bit Linux uses it, and so do Android on 32-bit x86 and the 32-bit BSDs, with a few differences between systems that the sections below point out. 32-bit Windows uses other conventions, described on the Windows x86 page. Compared with x86-64, almost everything is different: there are eight general-purpose registers, and arguments are not passed in them.
The listings on this page come from GCC and Clang for
i386-linux-gnu, both at -O2 and for the i686 processor
(-march=i686), which is what Debian’s GCC generates by default:
floating-point arithmetic then uses the x87 unit. Clang otherwise
targets the Pentium 4 and computes with SSE2, which changes the code
but not the calling convention. Both compilers generate
position-independent code by default on Linux, which explains the
__x86.get_pc_thunk.bx calls (below).
The buttons next to each listing switch between AT&T and Intel syntax.
Registers
| Register | Role | Preserved |
|---|---|---|
| EAX | Return value; the address of a returned struct | no |
| EDX | Upper half of a 64-bit return value | no |
| ECX | Scratch | no |
| EBX | Callee-saved; the GOT address for calls through the PLT | yes |
| ESI, EDI | Callee-saved | yes |
| EBP | Callee-saved; the frame pointer when a function uses one | yes |
| ESP | Stack pointer | yes |
| ST0 | Return value of type float, double or long double | no |
| ST1 to ST7, MM0 to MM7 | Scratch; the x87 unit must be in x87 mode at calls | no |
| XMM0 to XMM2 | The first three __m128 arguments; XMM0 returns __m128 | no |
| XMM3 to XMM7, K0 to K7 | Scratch | no |
| GS | Reserved for the system: the thread pointer on Linux | no |
As on x86-64, the direction flag is clear at calls and returns, and the control bits of MXCSR and the x87 control word are callee-saved.
x must survive the call to f, so it moves to ESI, which keep saves first; EBX is saved too, since it holds the GOT address for the call.i386-callee-saved.c
/* EBX, ESI, EDI and EBP belong to the caller. */
extern int f(int);
int keep(int x)
{
return f(x) + x;
}
GCC 14.2.0 (Debian 14.2.0-19) i386-linux-gnu
AT&T syntax gcc-14 -m32 -march=i686 -mtune=generic -O2 -fno-asynchronous-unwind-tables -S i386-callee-saved.c
keep:
pushl %esi
pushl %ebx
call __x86.get_pc_thunk.bx
addl $_GLOBAL_OFFSET_TABLE_, %ebx
subl $16, %esp
movl 28(%esp), %esi
pushl %esi
call f@PLT
addl $20, %esp
addl %esi, %eax
popl %ebx
popl %esi
ret
__x86.get_pc_thunk.bx:
movl (%esp), %ebx
retIntel syntax gcc-14 -m32 -march=i686 -mtune=generic -O2 -fno-asynchronous-unwind-tables -masm=intel -S i386-callee-saved.c
keep:
push esi
push ebx
call __x86.get_pc_thunk.bx
add ebx, OFFSET FLAT:_GLOBAL_OFFSET_TABLE_
sub esp, 16
mov esi, DWORD PTR 28[esp]
push esi
call f@PLT
add esp, 20
add eax, esi
pop ebx
pop esi
ret
__x86.get_pc_thunk.bx:
mov ebx, DWORD PTR [esp]
retClang 23.1.2 i686-linux-gnu
AT&T syntax clang --target=i686-linux-gnu -march=i686 -O2 -fno-asynchronous-unwind-tables -S i386-callee-saved.c
keep:
pushl %ebx
pushl %esi
pushl %eax
calll .L0$pb
.L0$pb:
popl %ebx
.Ltmp0:
addl $_GLOBAL_OFFSET_TABLE_+(.Ltmp0-.L0$pb), %ebx
movl 16(%esp), %esi
movl %esi, (%esp)
calll f@PLT
addl %esi, %eax
addl $4, %esp
popl %esi
popl %ebx
retlIntel syntax clang --target=i686-linux-gnu -march=i686 -O2 -fno-asynchronous-unwind-tables -masm=intel -S i386-callee-saved.c
keep:
push ebx
push esi
push eax
call .L0$pb
.L0$pb:
pop ebx
.Ltmp0:
add ebx, offset _GLOBAL_OFFSET_TABLE_+(.Ltmp0-.L0$pb)
mov esi, dword ptr [esp + 16]
mov dword ptr [esp], esi
call f@PLT
add eax, esi
add esp, 4
pop esi
pop ebx
retPassing arguments
All arguments are passed on the stack, pushed from right to left, so
the first one ends up right above the return address, at 4(%esp) on
entry. Each takes a multiple of 4 bytes: a char or short gets a
whole 4-byte slot, a long long or double two, and a struct is
copied onto the stack by value, padded to a multiple of 4 bytes. The
only exceptions are vector types: the first three __m64 arguments go
in MM0 to MM2 and the first three __m128 in XMM0 to XMM2.
int arguments at 4(%esp), 8(%esp) and 12(%esp); call3 pushes them in reverse order and removes them after the call. The caller removes the arguments, which is what “cdecl” usually means.i386-args.c
/* Every argument on the stack, first argument at the lowest address,
right above the return address. */
int sum3(int a, int b, int c)
{
return a + b + c;
}
extern int take3(int, int, int);
int call3(void)
{
return take3(1, 2, 3) + 1;
}
GCC 14.2.0 (Debian 14.2.0-19) i386-linux-gnu
AT&T syntax gcc-14 -m32 -march=i686 -mtune=generic -O2 -fno-asynchronous-unwind-tables -S i386-args.c
sum3:
movl 8(%esp), %eax
addl 4(%esp), %eax
addl 12(%esp), %eax
ret
call3:
pushl %ebx
call __x86.get_pc_thunk.bx
addl $_GLOBAL_OFFSET_TABLE_, %ebx
subl $12, %esp
pushl $3
pushl $2
pushl $1
call take3@PLT
addl $24, %esp
addl $1, %eax
popl %ebx
ret
__x86.get_pc_thunk.bx:
movl (%esp), %ebx
retIntel syntax gcc-14 -m32 -march=i686 -mtune=generic -O2 -fno-asynchronous-unwind-tables -masm=intel -S i386-args.c
sum3:
mov eax, DWORD PTR 8[esp]
add eax, DWORD PTR 4[esp]
add eax, DWORD PTR 12[esp]
ret
call3:
push ebx
call __x86.get_pc_thunk.bx
add ebx, OFFSET FLAT:_GLOBAL_OFFSET_TABLE_
sub esp, 12
push 3
push 2
push 1
call take3@PLT
add esp, 24
add eax, 1
pop ebx
ret
__x86.get_pc_thunk.bx:
mov ebx, DWORD PTR [esp]
retClang 23.1.2 i686-linux-gnu
AT&T syntax clang --target=i686-linux-gnu -march=i686 -O2 -fno-asynchronous-unwind-tables -S i386-args.c
sum3:
movl 8(%esp), %eax
addl 4(%esp), %eax
addl 12(%esp), %eax
retl
call3:
pushl %ebx
subl $8, %esp
calll .L1$pb
.L1$pb:
popl %ebx
.Ltmp0:
addl $_GLOBAL_OFFSET_TABLE_+(.Ltmp0-.L1$pb), %ebx
subl $4, %esp
pushl $3
pushl $2
pushl $1
calll take3@PLT
addl $16, %esp
incl %eax
addl $8, %esp
popl %ebx
retlIntel syntax clang --target=i686-linux-gnu -march=i686 -O2 -fno-asynchronous-unwind-tables -masm=intel -S i386-args.c
sum3:
mov eax, dword ptr [esp + 8]
add eax, dword ptr [esp + 4]
add eax, dword ptr [esp + 12]
ret
call3:
push ebx
sub esp, 8
call .L1$pb
.L1$pb:
pop ebx
.Ltmp0:
add ebx, offset _GLOBAL_OFFSET_TABLE_+(.Ltmp0-.L1$pb)
sub esp, 4
push 3
push 2
push 1
call take3@PLT
add esp, 16
inc eax
add esp, 8
pop ebx
retlong long arguments take two slots each, low half first; the result returns in EDX:EAX.i386-long-long.c
/* 64-bit values take two stack slots, and return in EDX:EAX. */
long long add64(long long a, long long b)
{
return a + b;
}
GCC 14.2.0 (Debian 14.2.0-19) i386-linux-gnu
AT&T syntax gcc-14 -m32 -march=i686 -mtune=generic -O2 -fno-asynchronous-unwind-tables -S i386-long-long.c
add64:
movl 12(%esp), %eax
movl 16(%esp), %edx
addl 4(%esp), %eax
adcl 8(%esp), %edx
retIntel syntax gcc-14 -m32 -march=i686 -mtune=generic -O2 -fno-asynchronous-unwind-tables -masm=intel -S i386-long-long.c
add64:
mov eax, DWORD PTR 12[esp]
mov edx, DWORD PTR 16[esp]
add eax, DWORD PTR 4[esp]
adc edx, DWORD PTR 8[esp]
retClang 23.1.2 i686-linux-gnu
AT&T syntax clang --target=i686-linux-gnu -march=i686 -O2 -fno-asynchronous-unwind-tables -S i386-long-long.c
add64:
movl 12(%esp), %eax
movl 16(%esp), %edx
addl 4(%esp), %eax
adcl 8(%esp), %edx
retlIntel syntax clang --target=i686-linux-gnu -march=i686 -O2 -fno-asynchronous-unwind-tables -masm=intel -S i386-long-long.c
add64:
mov eax, dword ptr [esp + 12]
mov edx, dword ptr [esp + 16]
add eax, dword ptr [esp + 4]
adc edx, dword ptr [esp + 8]
retReturning values
| C type | Returned in |
|---|---|
char, short, int, long, pointers | EAX; the bits beyond the type’s size are undefined |
long long | EDX:EAX, the high half in EDX |
float, double, long double | ST0, on top of the x87 register stack |
_Complex float | EAX (real part) and EDX (imaginary part) |
__m64; __m128; _Float16 | MM0; XMM0; XMM0 |
| Structs and unions (Linux) | Memory that the caller provides |
A floating-point result arrives on the x87 register stack, and the caller must pop it even if it does not use the value; otherwise the stack would fill up.
half returns in ST0. drop ignores the result of sqr, but still pops it with fstp %st(0).i386-float.c
/* float and double are passed on the stack and return in ST0, which
the caller must pop even if it ignores the value. */
double half(double x)
{
return x / 2;
}
extern double sqr(double x);
void drop(double x)
{
sqr(x);
}
GCC 14.2.0 (Debian 14.2.0-19) i386-linux-gnu
AT&T syntax gcc-14 -m32 -march=i686 -mtune=generic -O2 -fno-asynchronous-unwind-tables -S i386-float.c
half:
call __x86.get_pc_thunk.ax
addl $_GLOBAL_OFFSET_TABLE_, %eax
flds .LC0@GOTOFF(%eax)
fmull 4(%esp)
ret
drop:
pushl %ebx
call __x86.get_pc_thunk.bx
addl $_GLOBAL_OFFSET_TABLE_, %ebx
subl $16, %esp
pushl 28(%esp)
pushl 28(%esp)
call sqr@PLT
fstp %st(0)
addl $24, %esp
popl %ebx
ret
.LC0:
.long 1056964608
__x86.get_pc_thunk.ax:
movl (%esp), %eax
ret
__x86.get_pc_thunk.bx:
movl (%esp), %ebx
retIntel syntax gcc-14 -m32 -march=i686 -mtune=generic -O2 -fno-asynchronous-unwind-tables -masm=intel -S i386-float.c
half:
call __x86.get_pc_thunk.ax
add eax, OFFSET FLAT:_GLOBAL_OFFSET_TABLE_
fld DWORD PTR .LC0@GOTOFF[eax]
fmul QWORD PTR 4[esp]
ret
drop:
push ebx
call __x86.get_pc_thunk.bx
add ebx, OFFSET FLAT:_GLOBAL_OFFSET_TABLE_
sub esp, 16
push DWORD PTR 28[esp]
push DWORD PTR 28[esp]
call sqr@PLT
fstp st(0)
add esp, 24
pop ebx
ret
.LC0:
.long 1056964608
__x86.get_pc_thunk.ax:
mov eax, DWORD PTR [esp]
ret
__x86.get_pc_thunk.bx:
mov ebx, DWORD PTR [esp]
retClang 23.1.2 i686-linux-gnu
AT&T syntax clang --target=i686-linux-gnu -march=i686 -O2 -fno-asynchronous-unwind-tables -S i386-float.c
.LCPI0_0:
.long 0x3f000000
half:
calll .L0$pb
.L0$pb:
popl %eax
.Ltmp0:
addl $_GLOBAL_OFFSET_TABLE_+(.Ltmp0-.L0$pb), %eax
fldl 4(%esp)
fmuls .LCPI0_0@GOTOFF(%eax)
retl
drop:
pushl %ebx
subl $8, %esp
calll .L1$pb
.L1$pb:
popl %ebx
.Ltmp1:
addl $_GLOBAL_OFFSET_TABLE_+(.Ltmp1-.L1$pb), %ebx
fldl 16(%esp)
fstpl (%esp)
calll sqr@PLT
fstp %st(0)
addl $8, %esp
popl %ebx
retlIntel syntax clang --target=i686-linux-gnu -march=i686 -O2 -fno-asynchronous-unwind-tables -masm=intel -S i386-float.c
.LCPI0_0:
.long 0x3f000000
half:
call .L0$pb
.L0$pb:
pop eax
.Ltmp0:
add eax, offset _GLOBAL_OFFSET_TABLE_+(.Ltmp0-.L0$pb)
fld qword ptr [esp + 4]
fmul dword ptr [eax + .LCPI0_0@GOTOFF]
ret
drop:
push ebx
sub esp, 8
call .L1$pb
.L1$pb:
pop ebx
.Ltmp1:
add ebx, offset _GLOBAL_OFFSET_TABLE_+(.Ltmp1-.L1$pb)
fld qword ptr [esp + 16]
fstp qword ptr [esp]
call sqr@PLT
fstp st(0)
add esp, 8
pop ebx
retReturning structs
The psABI returns every struct and union through memory: the caller
passes the address of the result as a hidden first argument on the
stack, and the called function pops that argument itself, with
ret $4, and returns the address in EAX. It is the one case in which
a System V i386 function removes an argument from the stack.
FreeBSD, OpenBSD and DragonFly BSD, and 32-bit macOS, return small
structs in registers instead: a struct of 1, 2, 4 or 8 bytes comes back
in EAX, or in EAX and EDX. GCC’s option -freg-struct-return does the
same, and -fpcc-struct-return the opposite.
make_pair writes it through the pointer at 4(%esp) and pops that pointer with ret $4, and use_pair passes the address of a local. On FreeBSD it returns in EAX and EDX.i386-struct-return.c
/* Returning a struct: Linux passes a hidden pointer, which the callee
pops; FreeBSD returns a struct of 8 bytes in EAX and EDX. */
struct pair { int a, b; };
struct pair make_pair(int a, int b)
{
struct pair p = { a, b };
return p;
}
extern struct pair get_pair(void);
int use_pair(void)
{
return get_pair().b;
}
GCC 14.2.0 (Debian 14.2.0-19) i386-linux-gnu
AT&T syntax gcc-14 -m32 -march=i686 -mtune=generic -O2 -fno-asynchronous-unwind-tables -S i386-struct-return.c
make_pair:
movl 4(%esp), %eax
movl 8(%esp), %edx
movl %edx, (%eax)
movl 12(%esp), %edx
movl %edx, 4(%eax)
ret $4
use_pair:
pushl %ebx
call __x86.get_pc_thunk.bx
addl $_GLOBAL_OFFSET_TABLE_, %ebx
subl $24, %esp
leal 8(%esp), %eax
subl $12, %esp
pushl %eax
call get_pair@PLT
movl 24(%esp), %eax
addl $36, %esp
popl %ebx
ret
__x86.get_pc_thunk.bx:
movl (%esp), %ebx
retIntel syntax gcc-14 -m32 -march=i686 -mtune=generic -O2 -fno-asynchronous-unwind-tables -masm=intel -S i386-struct-return.c
make_pair:
mov eax, DWORD PTR 4[esp]
mov edx, DWORD PTR 8[esp]
mov DWORD PTR [eax], edx
mov edx, DWORD PTR 12[esp]
mov DWORD PTR 4[eax], edx
ret 4
use_pair:
push ebx
call __x86.get_pc_thunk.bx
add ebx, OFFSET FLAT:_GLOBAL_OFFSET_TABLE_
sub esp, 24
lea eax, 8[esp]
sub esp, 12
push eax
call get_pair@PLT
mov eax, DWORD PTR 24[esp]
add esp, 36
pop ebx
ret
__x86.get_pc_thunk.bx:
mov ebx, DWORD PTR [esp]
retClang 23.1.2 i686-linux-gnu
AT&T syntax clang --target=i686-linux-gnu -march=i686 -O2 -fno-asynchronous-unwind-tables -S i386-struct-return.c
make_pair:
movl 4(%esp), %eax
movl 12(%esp), %ecx
movl 8(%esp), %edx
movl %edx, (%eax)
movl %ecx, 4(%eax)
retl $4
use_pair:
pushl %ebx
subl $24, %esp
calll .L1$pb
.L1$pb:
popl %ebx
.Ltmp0:
addl $_GLOBAL_OFFSET_TABLE_+(.Ltmp0-.L1$pb), %ebx
leal 16(%esp), %eax
movl %eax, (%esp)
calll get_pair@PLT
subl $4, %esp
movl 20(%esp), %eax
addl $24, %esp
popl %ebx
retlIntel syntax clang --target=i686-linux-gnu -march=i686 -O2 -fno-asynchronous-unwind-tables -masm=intel -S i386-struct-return.c
make_pair:
mov eax, dword ptr [esp + 4]
mov ecx, dword ptr [esp + 12]
mov edx, dword ptr [esp + 8]
mov dword ptr [eax], edx
mov dword ptr [eax + 4], ecx
ret 4
use_pair:
push ebx
sub esp, 24
call .L1$pb
.L1$pb:
pop ebx
.Ltmp0:
add ebx, offset _GLOBAL_OFFSET_TABLE_+(.Ltmp0-.L1$pb)
lea eax, [esp + 16]
mov dword ptr [esp], eax
call get_pair@PLT
sub esp, 4
mov eax, dword ptr [esp + 20]
add esp, 24
pop ebx
retClang 23.1.2 i686-unknown-freebsd14
AT&T syntax clang --target=i686-unknown-freebsd14 -march=i686 -O2 -fno-asynchronous-unwind-tables -S i386-struct-return.c
make_pair:
pushl %ebp
movl %esp, %ebp
movl 8(%ebp), %eax
movl 12(%ebp), %edx
popl %ebp
retl
use_pair:
pushl %ebp
movl %esp, %ebp
calll get_pair
movl %edx, %eax
popl %ebp
retlIntel syntax clang --target=i686-unknown-freebsd14 -march=i686 -O2 -fno-asynchronous-unwind-tables -masm=intel -S i386-struct-return.c
make_pair:
push ebp
mov ebp, esp
mov eax, dword ptr [ebp + 8]
mov edx, dword ptr [ebp + 12]
pop ebp
ret
use_pair:
push ebp
mov ebp, esp
call get_pair
mov eax, edx
pop ebp
retThe stack
The stack grows down, and on entry to a function ESP + 4, the address
just above the return address, is a multiple of 16; a function that
calls another therefore keeps ESP a multiple of 16 at the call. The
boundary is 32 or 64 bytes when a __m256 or __m512 argument is
passed on the stack.
| Address | Contents | Frame |
|---|---|---|
8(%ebp) and up | Arguments, the first at the lowest address | caller |
4(%ebp) | Return address, pushed by call | callee |
0(%ebp) | Saved EBP of the caller, if the function uses a frame pointer | callee |
| below | Saved registers and local variables | callee |
0(%esp) | Arguments of the next call, 16-byte aligned | callee |
There is no red zone: nothing below ESP is safe from signal handlers.
The 16-byte alignment is newer than the ABI. The original Intel386
supplement of SCO and AT&T required only 4-byte alignment, and 32-bit
Windows compilers still assume no more (see
Windows x86). GCC has aligned to 16
bytes by default for a long time, so that SSE data on the stack can be
aligned, and the current psABI makes it the rule. A function that may
be called with a stack aligned to 4 bytes only, such as a Windows API
function that Wine implements, gets GCC’s force_align_arg_pointer
attribute, which realigns the stack on entry.
In Listing 2, GCC’s call3 is entered with ESP
12 bytes above a multiple of 16. The push of EBX and subl $12 take 16
bytes, which keeps it there, and the three 4-byte arguments bring ESP
down to the multiple for the call.
EBX and position-independent code
i386 has no instruction-pointer-relative addressing for data, so
position-independent code finds its global offset table (GOT) by
loading its own address with a call: __x86.get_pc_thunk.bx returns
the return address, to which the code adds the offset of the GOT. A
call through the procedure linkage table (PLT) expects the GOT address
in EBX, which is why functions that call others set up EBX, and why
EBX, being callee-saved, is pushed first. Most Linux distributions
build their programs as position-independent executables, so this code
is everywhere.
Variadic functions
A variadic function finds its unnamed arguments on the stack like all
others, right above the named ones, so va_list is a plain pointer
that va_arg moves through the arguments, 4 bytes or more at a time.
There is no hidden argument like x86-64’s AL.
va_start makes the va_list point at 20(%esp), the first argument after fmt, which log_msg passes on to vlog.i386-varargs.c
/* Variadic arguments are on the stack like all others, so va_list is
a pointer to the next one. */
#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) i386-linux-gnu
AT&T syntax gcc-14 -m32 -march=i686 -mtune=generic -O2 -fno-asynchronous-unwind-tables -S i386-varargs.c
log_msg:
pushl %ebx
call __x86.get_pc_thunk.bx
addl $_GLOBAL_OFFSET_TABLE_, %ebx
subl $8, %esp
leal 20(%esp), %eax
subl $8, %esp
pushl %eax
pushl 28(%esp)
call vlog@PLT
addl $24, %esp
popl %ebx
ret
__x86.get_pc_thunk.bx:
movl (%esp), %ebx
retIntel syntax gcc-14 -m32 -march=i686 -mtune=generic -O2 -fno-asynchronous-unwind-tables -masm=intel -S i386-varargs.c
log_msg:
push ebx
call __x86.get_pc_thunk.bx
add ebx, OFFSET FLAT:_GLOBAL_OFFSET_TABLE_
sub esp, 8
lea eax, 20[esp]
sub esp, 8
push eax
push DWORD PTR 28[esp]
call vlog@PLT
add esp, 24
pop ebx
ret
__x86.get_pc_thunk.bx:
mov ebx, DWORD PTR [esp]
retClang 23.1.2 i686-linux-gnu
AT&T syntax clang --target=i686-linux-gnu -march=i686 -O2 -fno-asynchronous-unwind-tables -S i386-varargs.c
log_msg:
pushl %ebx
subl $8, %esp
calll .L0$pb
.L0$pb:
popl %ebx
.Ltmp0:
addl $_GLOBAL_OFFSET_TABLE_+(.Ltmp0-.L0$pb), %ebx
movl 16(%esp), %eax
leal 20(%esp), %ecx
movl %ecx, 4(%esp)
subl $8, %esp
pushl %ecx
pushl %eax
calll vlog@PLT
addl $24, %esp
popl %ebx
retlIntel syntax clang --target=i686-linux-gnu -march=i686 -O2 -fno-asynchronous-unwind-tables -masm=intel -S i386-varargs.c
log_msg:
push ebx
sub esp, 8
call .L0$pb
.L0$pb:
pop ebx
.Ltmp0:
add ebx, offset _GLOBAL_OFFSET_TABLE_+(.Ltmp0-.L0$pb)
mov eax, dword ptr [esp + 16]
lea ecx, [esp + 20]
mov dword ptr [esp + 4], ecx
sub esp, 8
push ecx
push eax
call vlog@PLT
add esp, 24
pop ebx
retregparm and the Linux kernel
GCC’s regparm(n) attribute passes the first n integer arguments,
up to three, in EAX, EDX and ECX instead of on the stack; variadic
functions still take all of theirs on the stack. The option
-mregparm=3 applies it to every function. The 32-bit Linux kernel is
built that way, with -freg-struct-return and without
position-independent code.
make_pair finds its result already in place and only returns.i386-regparm.c
/* Compiled like the 32-bit Linux kernel, with -mregparm=3: the first
three integer arguments in EAX, EDX and ECX. With
-freg-struct-return, a struct of 8 bytes returns in EAX and EDX. */
int sum3(int a, int b, int c)
{
return a + b + c;
}
extern int take3(int, int, int);
int call3(void)
{
return take3(1, 2, 3) + 1;
}
struct pair { int a, b; };
struct pair make_pair(int a, int b)
{
struct pair p = { a, b };
return p;
}
GCC 14.2.0 (Debian 14.2.0-19) i386-linux-gnu
AT&T syntax gcc-14 -m32 -march=i686 -mtune=generic -O2 -fno-asynchronous-unwind-tables -mregparm=3 -freg-struct-return -fno-pic -S i386-regparm.c
sum3:
addl %edx, %eax
addl %ecx, %eax
ret
call3:
subl $12, %esp
movl $3, %ecx
movl $2, %edx
movl $1, %eax
call take3
addl $12, %esp
addl $1, %eax
ret
make_pair:
retIntel syntax gcc-14 -m32 -march=i686 -mtune=generic -O2 -fno-asynchronous-unwind-tables -mregparm=3 -freg-struct-return -fno-pic -masm=intel -S i386-regparm.c
sum3:
add eax, edx
add eax, ecx
ret
call3:
sub esp, 12
mov ecx, 3
mov edx, 2
mov eax, 1
call take3
add esp, 12
add eax, 1
ret
make_pair:
retClang 23.1.2 i686-linux-gnu
AT&T syntax clang --target=i686-linux-gnu -march=i686 -O2 -fno-asynchronous-unwind-tables -mregparm=3 -freg-struct-return -fno-pic -S i386-regparm.c
sum3:
addl %edx, %eax
addl %ecx, %eax
retl
call3:
subl $12, %esp
movl $1, %eax
movl $2, %edx
movl $3, %ecx
calll take3
incl %eax
addl $12, %esp
retl
make_pair:
retlIntel syntax clang --target=i686-linux-gnu -march=i686 -O2 -fno-asynchronous-unwind-tables -mregparm=3 -freg-struct-return -fno-pic -masm=intel -S i386-regparm.c
sum3:
add eax, edx
add eax, ecx
ret
call3:
sub esp, 12
mov eax, 1
mov edx, 2
mov ecx, 3
call take3
inc eax
add esp, 12
ret
make_pair:
retClang supports regparm too. A related GCC attribute, sseregparm,
passes up to three floating-point arguments in XMM registers.
Data types
On i386, long double has 12 bytes, while double and long long are
aligned to only 4 bytes inside structs; 32-bit Windows aligns them to 8.
Android on x86 makes long double the same as double. The
ABI overview has the sizes for every
platform.
Sources
- System V Application Binary Interface, Intel386 Architecture Processor Supplement, version 1.2: commit
20ec676cd56d, 2025-08-24 - System V Application Binary Interface, Intel386 Architecture Processor Supplement, Fourth Edition: as published on 2026-09-25
- GCC 14.2 manual: x86 Function Attributes: as published on 2026-09-25
- Linux 7.2: arch/x86/Makefile: commit
8d3ae59288f1, 2026-08-16 - Clang 23.1.2: clang/lib/CodeGen/Targets/X86.cpp: commit
85ac56026243, 2026-09-20 - Wine 11.0: include/msvcrt/corecrt.h: commit
db11d0fe6a16, 2026-01-13