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

Registers in the System V i386 ABI.
RegisterRolePreserved
EAXReturn value; the address of a returned structno
EDXUpper half of a 64-bit return valueno
ECXScratchno
EBXCallee-saved; the GOT address for calls through the PLTyes
ESI, EDICallee-savedyes
EBPCallee-saved; the frame pointer when a function uses oneyes
ESPStack pointeryes
ST0Return value of type float, double or long doubleno
ST1 to ST7, MM0 to MM7Scratch; the x87 unit must be in x87 mode at callsno
XMM0 to XMM2The first three __m128 arguments; XMM0 returns __m128no
XMM3 to XMM7, K0 to K7Scratchno
GSReserved for the system: the thread pointer on Linuxno

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
        ret

Intel 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]
        ret

Clang 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
        retl

Intel 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
        ret

Passing 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.

Three 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
        ret

Intel 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]
        ret

Clang 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
        retl

Intel 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
        ret
Two long 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
        ret

Intel 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]
        ret

Clang 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
        retl

Intel 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]
        ret

Returning values

Where the i386 psABI returns values.
C typeReturned in
char, short, int, long, pointersEAX; the bits beyond the type’s size are undefined
long longEDX:EAX, the high half in EDX
float, double, long doubleST0, on top of the x87 register stack
_Complex floatEAX (real part) and EDX (imaginary part)
__m64; __m128; _Float16MM0; 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
        ret

Intel 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]
        ret

Clang 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
        retl

Intel 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
        ret

Returning 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.

Returning a struct of 8 bytes. On Linux, 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
        ret

Intel 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]
        ret

Clang 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
        retl

Intel 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
        ret

Clang 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
        retl

Intel 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
        ret

The 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.

The layout of an i386 stack frame, from high addresses to low.
AddressContentsFrame
8(%ebp) and upArguments, the first at the lowest addresscaller
4(%ebp)Return address, pushed by callcallee
0(%ebp)Saved EBP of the caller, if the function uses a frame pointercallee
belowSaved registers and local variablescallee
0(%esp)Arguments of the next call, 16-byte alignedcallee

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
        ret

Intel 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]
        ret

Clang 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
        retl

Intel 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
        ret

regparm 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.

The kernel’s options: arguments in EAX, EDX and ECX, and small structs returned in EAX and EDX, so 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:
        ret

Intel 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:
        ret

Clang 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:
        retl

Intel 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:
        ret

Clang 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

  1. System V Application Binary Interface, Intel386 Architecture Processor Supplement, version 1.2: commit 20ec676cd56d, 2025-08-24
  2. System V Application Binary Interface, Intel386 Architecture Processor Supplement, Fourth Edition: as published on 2026-09-25
  3. GCC 14.2 manual: x86 Function Attributes: as published on 2026-09-25
  4. Linux 7.2: arch/x86/Makefile: commit 8d3ae59288f1, 2026-08-16
  5. Clang 23.1.2: clang/lib/CodeGen/Targets/X86.cpp: commit 85ac56026243, 2026-09-20
  6. Wine 11.0: include/msvcrt/corecrt.h: commit db11d0fe6a16, 2026-01-13