32-bit Windows calling conventions (cdecl, stdcall, fastcall)
32-bit Windows is the one platform where several calling conventions are used side by side. They differ in which arguments go in registers, who removes the others from the stack, and how the name is decorated, and mixing them up crashes at run time.
32-bit programs for Windows, built with MSVC, clang-cl or MinGW-w64,
share one set of rules for data and registers, but a function may be
declared with one of five calling conventions: __cdecl, the default
of C and C++; __stdcall, the convention of the Windows API;
__fastcall; __thiscall, the default of C++ member functions; and
__vectorcall. They are Microsoft’s, but MinGW-w64 GCC and Clang
implement them too. System V i386, the 32-bit ABI of
Linux, corresponds to __cdecl, with differences in returning structs
and in stack alignment. 64-bit Windows has a single convention,
Microsoft x64.
The listings on this page come from Clang for i686-pc-windows-msvc,
which follows MSVC’s ABI, and from MinGW-w64 GCC, both at -O2. Both
are compiled for the Pentium 4 (-march=pentium4), since MSVC
generates SSE2 code by default; MinGW-w64 GCC would otherwise target
the Pentium Pro, and it still computes with the x87 unit unless told
otherwise. The buttons next to each listing switch between AT&T and
Intel syntax.
What all conventions share
- EAX, ECX and EDX are scratch; EBX, ESI, EDI and EBP are callee-saved, as in System V.
- Results return in EAX, 64-bit values in EDX:EAX, and
floatanddoubleon the x87 stack, in ST0. Unlike on Linux, a struct of 1, 2, 4 or 8 bytes returns in EAX or EDX:EAX too. - Arguments that are not in registers are pushed from right to left, each widened to a multiple of 4 bytes.
- The stack is aligned to 4 bytes only.
The five conventions
f(int, int); the number is the size of the arguments in bytes.| Convention | Register arguments | Who removes the stack arguments | Decorated name | Used by |
|---|---|---|---|---|
__cdecl | none | the caller | _f | C and C++ functions by default; all variadic functions |
__stdcall | none | the called function | _f@8 | the Windows API (WINAPI, CALLBACK) |
__fastcall | the first two 4-byte or smaller integers in ECX and EDX | the called function | @f@8 | code that asks for it |
__thiscall | this in ECX | the called function | none: C++ only | C++ member functions by default |
__vectorcall | integers as __fastcall; floating-point and vector values in XMM0 to XMM5 | the called function | f@@8 | vector code |
The difference that matters most is who removes the arguments. With
__cdecl, the caller adds to ESP after the call; with the others, the
function returns with ret n, which pops n bytes of arguments along
with the return address. A function called with the wrong convention
leaves ESP off by the size of its arguments, and the caller crashes
later. The decorated names make that mistake a link error instead:
_f@8 does not link with _f.
A variadic function cannot remove arguments it does not know about, so
variadic functions are always __cdecl, whatever their declaration
says.
__cdecl one returns with a plain ret, the __stdcall one removes its 12 bytes of arguments with ret $12, and the __fastcall one takes a and b in ECX and EDX and removes only c. In call_both, Clang adds 12 to ESP after the __cdecl call but not after the __stdcall one; MinGW-w64 GCC, which stores arguments into space it reserved once, makes up for the callee’s pop with subl $12.win32-conventions.c
/* The same function in three 32-bit Windows conventions: who removes
the arguments, which go in registers, and how the name is decorated. */
int __cdecl add_cdecl(int a, int b, int c)
{
return a + b + c;
}
int __stdcall add_stdcall(int a, int b, int c)
{
return a + b + c;
}
int __fastcall add_fastcall(int a, int b, int c)
{
return a + b + c;
}
extern int __cdecl ext_cdecl(int, int, int);
extern int __stdcall ext_stdcall(int, int, int);
int call_both(void)
{
return ext_cdecl(1, 2, 3) + ext_stdcall(4, 5, 6);
}
Clang 23.1.2 i686-pc-windows-msvc
AT&T syntax clang --target=i686-pc-windows-msvc -march=pentium4 -O2 -fno-asynchronous-unwind-tables -S win32-conventions.c
_add_cdecl:
movl 8(%esp), %eax
addl 4(%esp), %eax
addl 12(%esp), %eax
retl
_add_stdcall@12:
movl 8(%esp), %eax
addl 4(%esp), %eax
addl 12(%esp), %eax
retl $12
@add_fastcall@12:
leal (%ecx,%edx), %eax
addl 4(%esp), %eax
retl $4
_call_both:
pushl %esi
pushl $3
pushl $2
pushl $1
calll _ext_cdecl
addl $12, %esp
movl %eax, %esi
pushl $6
pushl $5
pushl $4
calll _ext_stdcall@12
addl %esi, %eax
popl %esi
retlIntel syntax clang --target=i686-pc-windows-msvc -march=pentium4 -O2 -fno-asynchronous-unwind-tables -masm=intel -S win32-conventions.c
_add_cdecl:
mov eax, dword ptr [esp + 8]
add eax, dword ptr [esp + 4]
add eax, dword ptr [esp + 12]
ret
_add_stdcall@12:
mov eax, dword ptr [esp + 8]
add eax, dword ptr [esp + 4]
add eax, dword ptr [esp + 12]
ret 12
@add_fastcall@12:
lea eax, [ecx + edx]
add eax, dword ptr [esp + 4]
ret 4
_call_both:
push esi
push 3
push 2
push 1
call _ext_cdecl
add esp, 12
mov esi, eax
push 6
push 5
push 4
call _ext_stdcall@12
add eax, esi
pop esi
retMinGW-w64 GCC 14.2.0 (Debian 14.2.0-19+27+b1) i686-w64-mingw32
AT&T syntax i686-w64-mingw32-gcc -march=pentium4 -mtune=generic -O2 -fno-asynchronous-unwind-tables -S win32-conventions.c
_add_cdecl:
movl 8(%esp), %eax
addl 4(%esp), %eax
addl 12(%esp), %eax
ret
_add_stdcall@12:
movl 8(%esp), %eax
addl 4(%esp), %eax
addl 12(%esp), %eax
ret $12
@add_fastcall@12:
leal (%ecx,%edx), %eax
addl 4(%esp), %eax
ret $4
_call_both:
pushl %ebx
subl $24, %esp
movl $3, 8(%esp)
movl $2, 4(%esp)
movl $1, (%esp)
call _ext_cdecl
movl $6, 8(%esp)
movl $5, 4(%esp)
movl %eax, %ebx
movl $4, (%esp)
call _ext_stdcall@12
subl $12, %esp
addl %ebx, %eax
addl $24, %esp
popl %ebx
retIntel syntax i686-w64-mingw32-gcc -march=pentium4 -mtune=generic -O2 -fno-asynchronous-unwind-tables -masm=intel -S win32-conventions.c
_add_cdecl:
mov eax, DWORD PTR [esp+8]
add eax, DWORD PTR [esp+4]
add eax, DWORD PTR [esp+12]
ret
_add_stdcall@12:
mov eax, DWORD PTR [esp+8]
add eax, DWORD PTR [esp+4]
add eax, DWORD PTR [esp+12]
ret 12
@add_fastcall@12:
lea eax, [ecx+edx]
add eax, DWORD PTR [esp+4]
ret 4
_call_both:
push ebx
sub esp, 24
mov DWORD PTR [esp+8], 3
mov DWORD PTR [esp+4], 2
mov DWORD PTR [esp], 1
call _ext_cdecl
mov DWORD PTR [esp+8], 6
mov DWORD PTR [esp+4], 5
mov ebx, eax
mov DWORD PTR [esp], 4
call _ext_stdcall@12
sub esp, 12
add eax, ebx
add esp, 24
pop ebx
retThe Windows API declares its functions WINAPI, a name for
__stdcall, and callbacks that Windows calls, such as a window
procedure, CALLBACK, which is __stdcall too. A callback declared
with the default __cdecl does not match the function pointer type
that Windows expects; forced in with a cast, it fails at run time.
thiscall
C++ member functions use __thiscall unless they are variadic: this
goes in ECX, the other arguments on the stack, and the member function
removes them. Clang’s MinGW-w64 target uses the same convention for
member functions, but mangles names the Itanium way of GCC rather than
MSVC’s.
this in ECX, k at 4(%esp), removed with ret $4. drop_two loads the object’s address into ECX before the call. Clang for MSVC and for MinGW-w64 differ only in the mangled names.win32-thiscall.cpp
// A C++ member function: this in ECX, the other arguments on the
// stack, removed by the member function itself.
struct Counter {
int n;
int add(int k);
int sub(int k);
};
int Counter::add(int k)
{
return n += k;
}
int drop_two(Counter *c)
{
return c->sub(2);
}
Clang 23.1.2 i686-pc-windows-msvc
AT&T syntax clang --target=i686-pc-windows-msvc -march=pentium4 -O2 -fno-asynchronous-unwind-tables -S win32-thiscall.cpp
"?add@Counter@@QAEHH@Z":
movl (%ecx), %eax
addl 4(%esp), %eax
movl %eax, (%ecx)
retl $4
"?drop_two@@YAHPAUCounter@@@Z":
movl 4(%esp), %ecx
pushl $2
calll "?sub@Counter@@QAEHH@Z"
retlIntel syntax clang --target=i686-pc-windows-msvc -march=pentium4 -O2 -fno-asynchronous-unwind-tables -masm=intel -S win32-thiscall.cpp
"?add@Counter@@QAEHH@Z":
mov eax, dword ptr [ecx]
add eax, dword ptr [esp + 4]
mov dword ptr [ecx], eax
ret 4
"?drop_two@@YAHPAUCounter@@@Z":
mov ecx, dword ptr [esp + 4]
push 2
call "?sub@Counter@@QAEHH@Z"
retClang 23.1.2 i686-w64-windows-gnu
AT&T syntax clang --target=i686-w64-windows-gnu -march=pentium4 -O2 -fno-asynchronous-unwind-tables -S win32-thiscall.cpp
__ZN7Counter3addEi:
movl (%ecx), %eax
addl 4(%esp), %eax
movl %eax, (%ecx)
retl $4
__Z8drop_twoP7Counter:
movl 4(%esp), %ecx
pushl $2
calll __ZN7Counter3subEi
retlIntel syntax clang --target=i686-w64-windows-gnu -march=pentium4 -O2 -fno-asynchronous-unwind-tables -masm=intel -S win32-thiscall.cpp
__ZN7Counter3addEi:
mov eax, dword ptr [ecx]
add eax, dword ptr [esp + 4]
mov dword ptr [ecx], eax
ret 4
__Z8drop_twoP7Counter:
mov ecx, dword ptr [esp + 4]
push 2
call __ZN7Counter3subEi
retvectorcall
__vectorcall passes the first two integer arguments like
__fastcall, in ECX and EDX, and the first six floating-point or vector
arguments in XMM0 to XMM5, by position among the vector arguments. The
function removes its stack arguments, and its name gets @@ and the
size of its arguments. double values return in XMM0 rather than ST0.
x and y arrive in XMM0 and XMM1, n in ECX, and the result returns in XMM0. The name is scale@@20: two double values and an int.win32-vectorcall.c
/* __vectorcall: integers in ECX and EDX as with __fastcall, and
floating-point and vector values in XMM0 to XMM5. */
double __vectorcall scale(double x, int n, double y)
{
return x * n + y;
}
Clang 23.1.2 i686-pc-windows-msvc
AT&T syntax clang --target=i686-pc-windows-msvc -march=pentium4 -O2 -fno-asynchronous-unwind-tables -S win32-vectorcall.c
scale@@20:
cvtsi2sd %ecx, %xmm2
mulsd %xmm2, %xmm0
addsd %xmm1, %xmm0
retlIntel syntax clang --target=i686-pc-windows-msvc -march=pentium4 -O2 -fno-asynchronous-unwind-tables -masm=intel -S win32-vectorcall.c
scale@@20:
cvtsi2sd xmm2, ecx
mulsd xmm0, xmm2
addsd xmm0, xmm1
retReturning structs
MSVC returns a struct of 1, 2, 4 or 8 bytes in EAX or EDX:EAX, as the
BSDs do, and larger ones through memory: the caller passes the address
as a hidden first argument on the stack, and the function returns it in
EAX. Unlike on Linux, a __cdecl function leaves that hidden argument
for the caller to remove.
make_pair returns its 8-byte struct in EDX:EAX. make_triple writes 12 bytes through the pointer at 4(%esp) and returns with a plain ret.win32-struct-return.c
/* MSVC returns a struct of 8 bytes in EDX:EAX; a larger one through a
hidden pointer, which the caller removes afterwards. */
struct pair { int a, b; };
struct triple { int a, b, c; };
struct pair make_pair(int a, int b)
{
struct pair p = { a, b };
return p;
}
struct triple make_triple(int x)
{
struct triple t = { x, x, x };
return t;
}
Clang 23.1.2 i686-pc-windows-msvc
AT&T syntax clang --target=i686-pc-windows-msvc -march=pentium4 -O2 -fno-asynchronous-unwind-tables -S win32-struct-return.c
_make_pair:
movl 4(%esp), %eax
movl 8(%esp), %edx
retl
_make_triple:
movl 4(%esp), %eax
movl 8(%esp), %ecx
movl %ecx, (%eax)
movl %ecx, 4(%eax)
movl %ecx, 8(%eax)
retlIntel syntax clang --target=i686-pc-windows-msvc -march=pentium4 -O2 -fno-asynchronous-unwind-tables -masm=intel -S win32-struct-return.c
_make_pair:
mov eax, dword ptr [esp + 4]
mov edx, dword ptr [esp + 8]
ret
_make_triple:
mov eax, dword ptr [esp + 4]
mov ecx, dword ptr [esp + 8]
mov dword ptr [eax], ecx
mov dword ptr [eax + 4], ecx
mov dword ptr [eax + 8], ecx
retMinGW-w64 GCC 14.2.0 (Debian 14.2.0-19+27+b1) i686-w64-mingw32
AT&T syntax i686-w64-mingw32-gcc -march=pentium4 -mtune=generic -O2 -fno-asynchronous-unwind-tables -S win32-struct-return.c
_make_pair:
movl 4(%esp), %eax
movl 8(%esp), %edx
ret
_make_triple:
movd 8(%esp), %xmm0
movl 4(%esp), %eax
movdqu %xmm0, %xmm1
movd %xmm0, 8(%eax)
punpckldq %xmm0, %xmm1
movq %xmm1, (%eax)
retIntel syntax i686-w64-mingw32-gcc -march=pentium4 -mtune=generic -O2 -fno-asynchronous-unwind-tables -masm=intel -S win32-struct-return.c
_make_pair:
mov eax, DWORD PTR [esp+4]
mov edx, DWORD PTR [esp+8]
ret
_make_triple:
movd xmm0, DWORD PTR [esp+8]
mov eax, DWORD PTR [esp+4]
movdqu xmm1, xmm0
movd DWORD PTR [eax+8], xmm0
punpckldq xmm1, xmm0
movq QWORD PTR [eax], xmm1
retThe stack
32-bit Windows compilers assume only 4-byte alignment on entry to a function, as the original i386 ABI did, where Linux has moved to 16. A function that needs more for its own data, such as a local array aligned for SSE, aligns its frame itself: it saves EBP, rounds ESP down, and addresses its arguments through EBP.
andl $-16 and read the argument x at 8(%ebp). GCC for Linux, which can count on 16-byte alignment on entry, only subtracts a suitable amount and finds the array at 12(%esp).win32-align.c
/* The stack is only 4-byte aligned on 32-bit Windows: a function that
needs more for its own data aligns ESP itself. */
extern void use(double *d);
void aligned_local(double x)
{
double d[2] __attribute__((aligned(16))) = { x, x };
use(d);
}
Clang 23.1.2 i686-pc-windows-msvc
AT&T syntax clang --target=i686-pc-windows-msvc -march=pentium4 -O2 -fno-asynchronous-unwind-tables -S win32-align.c
_aligned_local:
pushl %ebp
movl %esp, %ebp
andl $-16, %esp
subl $32, %esp
movsd 8(%ebp), %xmm0
movl %esp, %eax
movsd %xmm0, (%esp)
movsd %xmm0, 8(%esp)
pushl %eax
calll _use
addl $4, %esp
movl %ebp, %esp
popl %ebp
retlIntel syntax clang --target=i686-pc-windows-msvc -march=pentium4 -O2 -fno-asynchronous-unwind-tables -masm=intel -S win32-align.c
_aligned_local:
push ebp
mov ebp, esp
and esp, -16
sub esp, 32
movsd xmm0, qword ptr [ebp + 8]
mov eax, esp
movsd qword ptr [esp], xmm0
movsd qword ptr [esp + 8], xmm0
push eax
call _use
add esp, 4
mov esp, ebp
pop ebp
retMinGW-w64 GCC 14.2.0 (Debian 14.2.0-19+27+b1) i686-w64-mingw32
AT&T syntax i686-w64-mingw32-gcc -march=pentium4 -mtune=generic -O2 -fno-asynchronous-unwind-tables -S win32-align.c
_aligned_local:
pushl %ebp
movl %esp, %ebp
andl $-16, %esp
subl $32, %esp
movsd 8(%ebp), %xmm0
leal 16(%esp), %eax
movl %eax, (%esp)
unpcklpd %xmm0, %xmm0
movups %xmm0, 16(%esp)
call _use
leave
retIntel syntax i686-w64-mingw32-gcc -march=pentium4 -mtune=generic -O2 -fno-asynchronous-unwind-tables -masm=intel -S win32-align.c
_aligned_local:
push ebp
mov ebp, esp
and esp, -16
sub esp, 32
movsd xmm0, QWORD PTR [ebp+8]
lea eax, [esp+16]
mov DWORD PTR [esp], eax
unpcklpd xmm0, xmm0
movups XMMWORD PTR [esp+16], xmm0
call _use
leave
retGCC 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 win32-align.c
aligned_local:
pushl %ebx
call __x86.get_pc_thunk.bx
addl $_GLOBAL_OFFSET_TABLE_, %ebx
subl $36, %esp
fldl 44(%esp)
leal 12(%esp), %eax
fstl 12(%esp)
fstpl 20(%esp)
pushl %eax
call use@PLT
addl $40, %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 win32-align.c
aligned_local:
push ebx
call __x86.get_pc_thunk.bx
add ebx, OFFSET FLAT:_GLOBAL_OFFSET_TABLE_
sub esp, 36
fld QWORD PTR 44[esp]
lea eax, 12[esp]
fst QWORD PTR 12[esp]
fstp QWORD PTR 20[esp]
push eax
call use@PLT
add esp, 40
pop ebx
ret
__x86.get_pc_thunk.bx:
mov ebx, DWORD PTR [esp]
retThe ABI overview gives the sizes of the C
types on 32-bit Windows: double and long long are aligned to 8
bytes in structs, where Linux aligns them to 4, and long double is
the same as double for MSVC but the 12-byte x87 type for MinGW-w64.
Sources
- Microsoft Learn: Argument passing and naming conventions: commit
f70d88cd5da7, 2026-09-24 - Microsoft Learn: __cdecl: commit
f70d88cd5da7, 2026-09-24 - Microsoft Learn: __stdcall: commit
f70d88cd5da7, 2026-09-24 - Microsoft Learn: __fastcall: commit
f70d88cd5da7, 2026-09-24 - Microsoft Learn: __thiscall: commit
f70d88cd5da7, 2026-09-24 - Microsoft Learn: __vectorcall: commit
f70d88cd5da7, 2026-09-24 - Microsoft Learn: Decorated names: commit
f70d88cd5da7, 2026-09-24 - Microsoft Learn: /arch (x86): commit
f70d88cd5da7, 2026-09-24 - Microsoft Learn: Windows Data Types: commit
e103fa4e8810, 2026-09-15 - GCC 14.2 manual: x86 Function Attributes: as published on 2026-09-25
- Clang 23.1.2: clang/lib/CodeGen/Targets/X86.cpp: commit
85ac56026243, 2026-09-20