Microsoft x64 ABI and calling convention
The calling convention of 64-bit Windows and of the firmware that boots it. Four register slots filled by position, 32 bytes of shadow space, and everything larger than 8 bytes passed by reference, with compiled code for every rule.
Microsoft’s x64 convention is used by every program on 64-bit Windows,
whether it was built with MSVC, clang-cl or MinGW-w64, by UEFI
firmware on x64 machines, and by Cygwin and MSYS2’s MSYS environment,
which keep the convention but give long 64 bits. It is simpler than
System V x86-64, which Linux and macOS use: four
argument slots filled by position, and nothing larger than 8 bytes in a
register.
The listings on this page come from Clang for the
x86_64-pc-windows-msvc target, which follows MSVC’s ABI and is the
compiler behind clang-cl, and from MinGW-w64 GCC, both at -O2. The
buttons next to each listing switch between AT&T and Intel syntax.
Registers
The first four arguments go in RCX, RDX, R8 and R9, or in XMM0 to XMM3 for floating-point values. A function may change RAX, RCX, RDX, R8 to R11 and XMM0 to XMM5; it must preserve RBX, RBP, RDI, RSI, RSP, R12 to R15 and XMM6 to XMM15. That RDI, RSI and six XMM registers are callee-saved is the difference from System V that matters most when the two meet.
| Register | Role | Preserved |
|---|---|---|
| RCX, RDX, R8, R9 | Integer, pointer and small struct arguments 1 to 4 | no |
| XMM0 to XMM3 | Floating-point arguments 1 to 4 | no |
| RAX | Integer return value | no |
| XMM0 | Floating-point, vector and __int128 return value | no |
| R10, R11 | Scratch | no |
| XMM4, XMM5 | Scratch; __vectorcall arguments 5 and 6 | no |
| RBX, RDI, RSI, R12 to R15 | Callee-saved | yes |
| RBP | Callee-saved; the frame pointer when a function uses one | yes |
| RSP | Stack pointer | yes |
| XMM6 to XMM15 | Callee-saved, the lower 128 bits only | yes |
| Upper halves of YMM0 to YMM15, ZMM | Scratch | no |
| XMM16 to XMM31 (AVX-512) | Scratch | no |
| R16 to R29 (APX) | Scratch | no |
| R30, R31 (APX) | Callee-saved | yes |
| TMM0 to TMM7 (AMX) | Scratch | no |
| ST0 to ST7, MM0 to MM7 | Scratch; never used for arguments | no |
As in System V, a function leaves the direction flag clear and keeps the control bits of MXCSR and the x87 control word; their status bits are scratch.
Passing arguments
Each of the first four arguments has a slot, and its position alone
decides the register: the second argument goes in RDX if it is an
integer and in XMM1 if it is a double, whatever the first one was.
The other register of the slot stays unused. Arguments from the fifth
on go on the stack, 8 bytes each, from right to left.
Above the return address, the caller always reserves 32 bytes of
shadow space, also called home space, one 8-byte home for each register
argument, even when the called function takes fewer than four
arguments. The called function owns this space: it may store its
register arguments there, or anything else. The fifth argument
therefore starts at 40(%rsp) on entry to a function, after the return
address and the shadow space.
sum6 finds the fifth and sixth at 40(%rsp) and 48(%rsp), above the shadow space. call6 reserves 56 bytes: 32 of shadow space, 16 for the two stack arguments, and 8 to keep RSP aligned to 16 at the call.win-int-args.c
/* Six integer arguments: four in registers, two on the stack above
the 32-byte shadow space. */
long long sum6(long long a, long long b, long long c, long long d, long long e, long long f)
{
return a + b + c + d + e + f;
}
extern long long take6(long long, long long, long long, long long, long long, long long);
long long call6(void)
{
return take6(1, 2, 3, 4, 5, 6) + 1;
}
Clang 23.1.2 x86_64-pc-windows-msvc
AT&T syntax clang --target=x86_64-pc-windows-msvc -O2 -fno-asynchronous-unwind-tables -S win-int-args.c
sum6:
addq %rdx, %rcx
leaq (%r8,%r9), %rax
addq %rcx, %rax
addq 40(%rsp), %rax
addq 48(%rsp), %rax
retq
call6:
subq $56, %rsp
movq $6, 40(%rsp)
movq $5, 32(%rsp)
movl $1, %ecx
movl $2, %edx
movl $3, %r8d
movl $4, %r9d
callq take6
incq %rax
addq $56, %rsp
retqIntel syntax clang --target=x86_64-pc-windows-msvc -O2 -fno-asynchronous-unwind-tables -masm=intel -S win-int-args.c
sum6:
add rcx, rdx
lea rax, [r8 + r9]
add rax, rcx
add rax, qword ptr [rsp + 40]
add rax, qword ptr [rsp + 48]
ret
call6:
sub rsp, 56
mov qword ptr [rsp + 40], 6
mov qword ptr [rsp + 32], 5
mov ecx, 1
mov edx, 2
mov r8d, 3
mov r9d, 4
call take6
inc rax
add rsp, 56
retMinGW-w64 GCC 14.2.0 (Debian 14.2.0-19+27+b1) x86_64-w64-mingw32
AT&T syntax x86_64-w64-mingw32-gcc -O2 -fno-asynchronous-unwind-tables -S win-int-args.c
sum6:
addq %rdx, %rcx
addq %r8, %rcx
leaq (%rcx,%r9), %rax
addq 40(%rsp), %rax
addq 48(%rsp), %rax
ret
call6:
subq $56, %rsp
movl $4, %r9d
movl $3, %r8d
movl $2, %edx
movq $6, 40(%rsp)
movl $1, %ecx
movq $5, 32(%rsp)
call take6
addq $56, %rsp
addq $1, %rax
retIntel syntax x86_64-w64-mingw32-gcc -O2 -fno-asynchronous-unwind-tables -masm=intel -S win-int-args.c
sum6:
add rcx, rdx
add rcx, r8
lea rax, [rcx+r9]
add rax, QWORD PTR 40[rsp]
add rax, QWORD PTR 48[rsp]
ret
call6:
sub rsp, 56
mov r9d, 4
mov r8d, 3
mov edx, 2
mov QWORD PTR 40[rsp], 6
mov ecx, 1
mov QWORD PTR 32[rsp], 5
call take6
add rsp, 56
add rax, 1
reta in ECX, b in XMM1, c in R8D and d in XMM3; RDX, XMM0, R9 and XMM2 stay unused. e and f go on the stack at 32(%rsp) and 40(%rsp).win-mixed-args.c
/* The position decides the register: the second argument goes to
RDX or XMM1, whatever came before it. */
extern double mix(int a, double b, int c, float d, char *e, double f);
double call_mix(char *p)
{
return mix(1, 2.0, 3, 4.0f, p, 5.0) * 2;
}
Clang 23.1.2 x86_64-pc-windows-msvc
AT&T syntax clang --target=x86_64-pc-windows-msvc -O2 -fno-asynchronous-unwind-tables -S win-mixed-args.c
__real@4000000000000000:
.quad 0x4000000000000000
__real@40800000:
.long 0x40800000
call_mix:
subq $56, %rsp
movabsq $4617315517961601024, %rax
movq %rax, 40(%rsp)
movq %rcx, 32(%rsp)
movsd __real@4000000000000000(%rip), %xmm1
movss __real@40800000(%rip), %xmm3
movl $1, %ecx
movl $3, %r8d
callq mix
addsd %xmm0, %xmm0
addq $56, %rsp
retqIntel syntax clang --target=x86_64-pc-windows-msvc -O2 -fno-asynchronous-unwind-tables -masm=intel -S win-mixed-args.c
__real@4000000000000000:
.quad 0x4000000000000000
__real@40800000:
.long 0x40800000
call_mix:
sub rsp, 56
movabs rax, 4617315517961601024
mov qword ptr [rsp + 40], rax
mov qword ptr [rsp + 32], rcx
movsd xmm1, qword ptr [rip + __real@4000000000000000]
movss xmm3, dword ptr [rip + __real@40800000]
mov ecx, 1
mov r8d, 3
call mix
addsd xmm0, xmm0
add rsp, 56
retThe callee may not assume anything about the upper bits of a register
that holds a smaller argument: an int in ECX leaves the upper half of
RCX undefined.
Structs, vectors and other types
A value travels in a register only if it has 1, 2, 4 or 8 bytes. Any
other struct or union, a __m128 vector, and any value of 16 bytes or
more is passed by reference: the caller makes a copy and passes its
address in the argument’s slot.
| C type | Size | Passed as | Shown in |
|---|---|---|---|
struct { int a, b; } | 8 | the value, in a general-purpose register | Listing 3 |
struct { int a, b, c; } | 12 | a pointer to a copy | Listing 3 |
float, double | 4, 8 | the value, in an XMM register | Listing 2 |
long double (MSVC, Clang) | 8 | the value, as a double | Listing 4 |
long double (MinGW-w64) | 16 | a pointer to a copy | Listing 4 |
__int128 (Clang, GCC) | 16 | a pointer to a copy | Listing 5 |
__m128 and other 16-byte vectors | 16 | a pointer to a copy | Listing 9 |
pass_triple builds the copy in its own frame and passes its address.win-struct-args.c
/* Structs of 1, 2, 4 or 8 bytes travel like integers; any other size
is passed as a pointer to a copy the caller makes. */
struct pair32 { int a, b; }; /* 8 bytes: in a register */
struct triple32 { int a, b, c; }; /* 12 bytes: by reference */
int pair_sum(struct pair32 p)
{
return p.a + p.b;
}
int triple_sum(struct triple32 t)
{
return t.a + t.b + t.c;
}
extern int take_triple(struct triple32 t);
int pass_triple(int x)
{
struct triple32 t = { x, 2 * x, 3 * x };
return take_triple(t) + 1;
}
Clang 23.1.2 x86_64-pc-windows-msvc
AT&T syntax clang --target=x86_64-pc-windows-msvc -O2 -fno-asynchronous-unwind-tables -S win-struct-args.c
pair_sum:
movq %rcx, %rax
shrq $32, %rax
addl %ecx, %eax
retq
triple_sum:
movl 4(%rcx), %eax
addl (%rcx), %eax
addl 8(%rcx), %eax
retq
pass_triple:
subq $56, %rsp
leal (%rcx,%rcx), %eax
leal (%rcx,%rcx,2), %edx
movl %ecx, 44(%rsp)
movl %eax, 48(%rsp)
movl %edx, 52(%rsp)
leaq 44(%rsp), %rcx
callq take_triple
incl %eax
addq $56, %rsp
retqIntel syntax clang --target=x86_64-pc-windows-msvc -O2 -fno-asynchronous-unwind-tables -masm=intel -S win-struct-args.c
pair_sum:
mov rax, rcx
shr rax, 32
add eax, ecx
ret
triple_sum:
mov eax, dword ptr [rcx + 4]
add eax, dword ptr [rcx]
add eax, dword ptr [rcx + 8]
ret
pass_triple:
sub rsp, 56
lea eax, [rcx + rcx]
lea edx, [rcx + 2*rcx]
mov dword ptr [rsp + 44], ecx
mov dword ptr [rsp + 48], eax
mov dword ptr [rsp + 52], edx
lea rcx, [rsp + 44]
call take_triple
inc eax
add rsp, 56
retMinGW-w64 GCC 14.2.0 (Debian 14.2.0-19+27+b1) x86_64-w64-mingw32
AT&T syntax x86_64-w64-mingw32-gcc -O2 -fno-asynchronous-unwind-tables -S win-struct-args.c
pair_sum:
movq %rcx, %rax
shrq $32, %rax
addl %ecx, %eax
ret
triple_sum:
movl 4(%rcx), %eax
addl (%rcx), %eax
addl 8(%rcx), %eax
ret
pass_triple:
pushq %rbp
leal (%rcx,%rcx), %eax
movq %rsp, %rbp
andq $-16, %rsp
subq $64, %rsp
movl %ecx, 48(%rsp)
movl %eax, 52(%rsp)
movq 48(%rsp), %rdx
addl %ecx, %eax
leaq 32(%rsp), %rcx
movl %eax, 40(%rsp)
movq %rdx, 32(%rsp)
call take_triple
leave
addl $1, %eax
retIntel syntax x86_64-w64-mingw32-gcc -O2 -fno-asynchronous-unwind-tables -masm=intel -S win-struct-args.c
pair_sum:
mov rax, rcx
shr rax, 32
add eax, ecx
ret
triple_sum:
mov eax, DWORD PTR 4[rcx]
add eax, DWORD PTR [rcx]
add eax, DWORD PTR 8[rcx]
ret
pass_triple:
push rbp
lea eax, [rcx+rcx]
mov rbp, rsp
and rsp, -16
sub rsp, 64
mov DWORD PTR 48[rsp], ecx
mov DWORD PTR 52[rsp], eax
mov rdx, QWORD PTR 48[rsp]
add eax, ecx
lea rcx, 32[rsp]
mov DWORD PTR 40[rsp], eax
mov QWORD PTR 32[rsp], rdx
call take_triple
leave
add eax, 1
retMicrosoft asks for the caller’s copy to be 16-byte aligned. MinGW-w64
GCC aligns it, rounding down RSP with andq $-16; Clang places the
12-byte copy in Listing 3 at 44(%rsp),
which is only 4-byte aligned. A called function should not rely on more
alignment than the type itself has.
long double shows the two Windows toolchains apart. MSVC makes it the
same type as double, and so does Clang for the MSVC target. MinGW-w64
keeps the x87 80-bit format of GCC, which has 16 bytes and so is passed
and returned by reference.
long double is a double in XMM0 for the MSVC target. MinGW-w64 GCC receives a pointer to the value in RDX and stores the result through the pointer in RCX, which it returns in RAX.win-long-double.c
/* long double is double for MSVC, and the 80-bit x87 type for
MinGW-w64, which passes it by reference. */
long double ld_twice(long double x)
{
return x * 2;
}
Clang 23.1.2 x86_64-pc-windows-msvc
AT&T syntax clang --target=x86_64-pc-windows-msvc -O2 -fno-asynchronous-unwind-tables -S win-long-double.c
ld_twice:
addsd %xmm0, %xmm0
retqIntel syntax clang --target=x86_64-pc-windows-msvc -O2 -fno-asynchronous-unwind-tables -masm=intel -S win-long-double.c
ld_twice:
addsd xmm0, xmm0
retMinGW-w64 GCC 14.2.0 (Debian 14.2.0-19+27+b1) x86_64-w64-mingw32
AT&T syntax x86_64-w64-mingw32-gcc -O2 -fno-asynchronous-unwind-tables -S win-long-double.c
ld_twice:
fldt (%rdx)
pushq %rbp
movq %rcx, %rax
fadd %st(0), %st
movq %rsp, %rbp
andq $-16, %rsp
fstpt (%rcx)
leave
retIntel syntax x86_64-w64-mingw32-gcc -O2 -fno-asynchronous-unwind-tables -masm=intel -S win-long-double.c
ld_twice:
fld TBYTE PTR [rdx]
push rbp
mov rax, rcx
fadd st, st(0)
mov rbp, rsp
and rsp, -16
fstp TBYTE PTR [rcx]
leave
ret__int128, which only Clang and GCC offer on Windows: both arguments by reference, and the result in XMM0.win-int128.c
/* __int128 (Clang and GCC only): by reference as an argument. */
__int128 add128(__int128 a, __int128 b)
{
return a + b;
}
Clang 23.1.2 x86_64-pc-windows-msvc
AT&T syntax clang --target=x86_64-pc-windows-msvc -O2 -fno-asynchronous-unwind-tables -S win-int128.c
add128:
movq (%rdx), %rax
addq (%rcx), %rax
movq 8(%rdx), %rdx
adcq 8(%rcx), %rdx
movq %rax, %xmm0
movq %rdx, %xmm1
punpcklqdq %xmm1, %xmm0
retqIntel syntax clang --target=x86_64-pc-windows-msvc -O2 -fno-asynchronous-unwind-tables -masm=intel -S win-int128.c
add128:
mov rax, qword ptr [rdx]
add rax, qword ptr [rcx]
mov rdx, qword ptr [rdx + 8]
adc rdx, qword ptr [rcx + 8]
movq xmm0, rax
movq xmm1, rdx
punpcklqdq xmm0, xmm1
retMinGW-w64 GCC 14.2.0 (Debian 14.2.0-19+27+b1) x86_64-w64-mingw32
AT&T syntax x86_64-w64-mingw32-gcc -O2 -fno-asynchronous-unwind-tables -S win-int128.c
add128:
pushq %rbp
movq %rsp, %rbp
andq $-16, %rsp
subq $16, %rsp
movq (%rdx), %rax
movq 8(%rdx), %rdx
addq (%rcx), %rax
adcq 8(%rcx), %rdx
movq %rax, (%rsp)
movq %rdx, 8(%rsp)
movdqu (%rsp), %xmm0
leave
retIntel syntax x86_64-w64-mingw32-gcc -O2 -fno-asynchronous-unwind-tables -masm=intel -S win-int128.c
add128:
push rbp
mov rbp, rsp
and rsp, -16
sub rsp, 16
mov rax, QWORD PTR [rdx]
mov rdx, QWORD PTR 8[rdx]
add rax, QWORD PTR [rcx]
adc rdx, QWORD PTR 8[rcx]
mov QWORD PTR [rsp], rax
mov QWORD PTR 8[rsp], rdx
movdqu xmm0, XMMWORD PTR [rsp]
leave
retReturning values
Integers, pointers and structs of 1, 2, 4 or 8 bytes return in RAX;
float, double and 16-byte vectors in XMM0. For anything else the
caller provides the memory: it passes the address in RCX, as a hidden
first argument that moves the real arguments one slot along, and the
function returns the same address in RAX. In C++, a type with a
user-defined constructor, destructor or copy assignment always returns
through memory, whatever its size.
x arrives in EDX.win-returns.c
/* A struct of 8 bytes returns in RAX; a larger one through memory the
caller provides, whose address arrives in RCX. */
struct pair32 { int a, b; };
struct triple32 { int a, b, c; };
struct pair32 ret_pair(int a, int b)
{
return (struct pair32){ a, b };
}
struct triple32 ret_triple(int x)
{
return (struct triple32){ x, x, x };
}
Clang 23.1.2 x86_64-pc-windows-msvc
AT&T syntax clang --target=x86_64-pc-windows-msvc -O2 -fno-asynchronous-unwind-tables -S win-returns.c
ret_pair:
shlq $32, %rdx
movl %ecx, %eax
orq %rdx, %rax
retq
ret_triple:
movq %rcx, %rax
movl %edx, (%rcx)
movl %edx, 4(%rcx)
movl %edx, 8(%rcx)
retqIntel syntax clang --target=x86_64-pc-windows-msvc -O2 -fno-asynchronous-unwind-tables -masm=intel -S win-returns.c
ret_pair:
shl rdx, 32
mov eax, ecx
or rax, rdx
ret
ret_triple:
mov rax, rcx
mov dword ptr [rcx], edx
mov dword ptr [rcx + 4], edx
mov dword ptr [rcx + 8], edx
retMinGW-w64 GCC 14.2.0 (Debian 14.2.0-19+27+b1) x86_64-w64-mingw32
AT&T syntax x86_64-w64-mingw32-gcc -O2 -fno-asynchronous-unwind-tables -S win-returns.c
ret_pair:
salq $32, %rdx
movl %ecx, %eax
orq %rdx, %rax
ret
ret_triple:
movd %edx, %xmm0
movl %edx, 8(%rcx)
movq %rcx, %rax
pshufd $0xe0, %xmm0, %xmm1
movq %xmm1, (%rcx)
retIntel syntax x86_64-w64-mingw32-gcc -O2 -fno-asynchronous-unwind-tables -masm=intel -S win-returns.c
ret_pair:
sal rdx, 32
mov eax, ecx
or rax, rdx
ret
ret_triple:
movd xmm0, edx
mov DWORD PTR 8[rcx], edx
mov rax, rcx
pshufd xmm1, xmm0, 0xe0
movq QWORD PTR [rcx], xmm1
retCallee-saved registers
RSI, RDI and XMM6 to XMM15 are the registers that Windows code keeps across calls and System V code does not. A value that must survive a call can stay in one of them, and a function that uses one saves it first; for XMM6 to XMM15 only the lower 128 bits count.
x must survive the call to f, so it moves to XMM6, a callee-saved register, which keep saves in its frame first.win-callee-saved.c
/* RSI, RDI and XMM6 to XMM15 are callee-saved on Windows, unlike in
System V: a double that must survive a call can stay in XMM6. */
extern double f(double);
double keep(double x)
{
return f(x) + x;
}
Clang 23.1.2 x86_64-pc-windows-msvc
AT&T syntax clang --target=x86_64-pc-windows-msvc -O2 -fno-asynchronous-unwind-tables -S win-callee-saved.c
keep:
subq $56, %rsp
movaps %xmm6, 32(%rsp)
movapd %xmm0, %xmm6
callq f
addsd %xmm6, %xmm0
movaps 32(%rsp), %xmm6
addq $56, %rsp
retqIntel syntax clang --target=x86_64-pc-windows-msvc -O2 -fno-asynchronous-unwind-tables -masm=intel -S win-callee-saved.c
keep:
sub rsp, 56
movaps xmmword ptr [rsp + 32], xmm6
movapd xmm6, xmm0
call f
addsd xmm0, xmm6
movaps xmm6, xmmword ptr [rsp + 32]
add rsp, 56
retMinGW-w64 GCC 14.2.0 (Debian 14.2.0-19+27+b1) x86_64-w64-mingw32
AT&T syntax x86_64-w64-mingw32-gcc -O2 -fno-asynchronous-unwind-tables -S win-callee-saved.c
keep:
subq $56, %rsp
movups %xmm6, 32(%rsp)
movupd %xmm0, %xmm6
call f
addsd %xmm6, %xmm0
movups 32(%rsp), %xmm6
addq $56, %rsp
retIntel syntax x86_64-w64-mingw32-gcc -O2 -fno-asynchronous-unwind-tables -masm=intel -S win-callee-saved.c
keep:
sub rsp, 56
movups XMMWORD PTR 32[rsp], xmm6
movupd xmm6, xmm0
call f
addsd xmm0, xmm6
movups xmm6, XMMWORD PTR 32[rsp]
add rsp, 56
retThe stack
RSP is a multiple of 16 at every call, so it is 8 past a multiple of 16
on entry to a function. Between the return address and the local
variables, each function that calls others reserves an outgoing
parameter area at the bottom of its frame: the shadow space and room
for the stack arguments of the largest call it makes. A function
allocates it once in its prolog, rather than pushing arguments for each
call, as call6 does in Listing 1.
There is no red zone. Memory below RSP may be overwritten at any time, by the operating system, an interrupt or a debugger, so a function moves RSP before it stores anything on the stack. System V’s leaf functions keep data below RSP; on Windows they use the shadow space their caller provided.
Microsoft also restricts how a function may change RSP and save
registers in its prolog and epilog, since the unwind data in .pdata
and .xdata must describe them; the page on
unwinding explains that data.
Variadic functions
A variadic function is called like any other, with one addition: a floating-point argument in one of the four register slots is passed in both its XMM register and the matching general-purpose register, since the callee cannot know its type. The same goes for calls to functions without a prototype.
The shadow space makes the rest simple. A variadic function stores RDX,
R8 and R9 into their homes, which lie right below the stack arguments,
so all its arguments form one array of 8-byte slots. va_list is a
plain char * that walks that array, and va_arg takes 8 bytes at a
time.
log_msg stores RDX, R8 and R9 in their homes at 56(%rsp) and up, and va_list points at the first of them. call_double passes x in both XMM1 and RDX.win-varargs.c
/* A variadic function stores RDX, R8 and R9 in the shadow space, right
below the stack arguments, and va_list is a plain pointer into it. A
variadic call puts a double in both XMM1 and RDX. */
#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;
}
int call_double(double x)
{
return log_msg("%f", x);
}
Clang 23.1.2 x86_64-pc-windows-msvc
AT&T syntax clang --target=x86_64-pc-windows-msvc -O2 -fno-asynchronous-unwind-tables -S win-varargs.c
log_msg:
subq $40, %rsp
movq %rdx, 56(%rsp)
movq %r8, 64(%rsp)
movq %r9, 72(%rsp)
leaq 56(%rsp), %rdx
movq %rdx, 32(%rsp)
callq vlog
addq $40, %rsp
retq
call_double:
subq $40, %rsp
leaq "??_C@_02NJPGOMH@?$CFf?$AA@"(%rip), %rcx
movdqa %xmm0, %xmm1
movq %xmm0, %rdx
callq log_msg
addq $40, %rsp
retq
"??_C@_02NJPGOMH@?$CFf?$AA@":
.asciz "%f"Intel syntax clang --target=x86_64-pc-windows-msvc -O2 -fno-asynchronous-unwind-tables -masm=intel -S win-varargs.c
log_msg:
sub rsp, 40
mov qword ptr [rsp + 56], rdx
mov qword ptr [rsp + 64], r8
mov qword ptr [rsp + 72], r9
lea rdx, [rsp + 56]
mov qword ptr [rsp + 32], rdx
call vlog
add rsp, 40
ret
call_double:
sub rsp, 40
lea rcx, [rip + "??_C@_02NJPGOMH@?$CFf?$AA@"]
movdqa xmm1, xmm0
movq rdx, xmm0
call log_msg
add rsp, 40
ret
"??_C@_02NJPGOMH@?$CFf?$AA@":
.asciz "%f"MinGW-w64 GCC 14.2.0 (Debian 14.2.0-19+27+b1) x86_64-w64-mingw32
AT&T syntax x86_64-w64-mingw32-gcc -O2 -fno-asynchronous-unwind-tables -S win-varargs.c
log_msg:
subq $56, %rsp
movq %rdx, 72(%rsp)
leaq 72(%rsp), %rdx
movq %r8, 80(%rsp)
movq %r9, 88(%rsp)
movq %rdx, 40(%rsp)
call vlog
addq $56, %rsp
ret
.LC0:
.ascii "%f\0"
call_double:
movq %xmm0, %rdx
movupd %xmm0, %xmm1
leaq .LC0(%rip), %rcx
jmp log_msgIntel syntax x86_64-w64-mingw32-gcc -O2 -fno-asynchronous-unwind-tables -masm=intel -S win-varargs.c
log_msg:
sub rsp, 56
mov QWORD PTR 72[rsp], rdx
lea rdx, 72[rsp]
mov QWORD PTR 80[rsp], r8
mov QWORD PTR 88[rsp], r9
mov QWORD PTR 40[rsp], rdx
call vlog
add rsp, 56
ret
.LC0:
.ascii "%f\0"
call_double:
movq rdx, xmm0
movupd xmm1, xmm0
lea rcx, .LC0[rip]
jmp log_msg__vectorcall
__vectorcall extends the convention for vector code. Floating-point
and vector arguments among the first six are passed by value in XMM0 to
XMM5, or YMM0 to YMM5 for __m256, and homogeneous aggregates of up to
four vectors can use the free vector registers. Integers keep their
four slots. The name of a __vectorcall function is decorated with @@
and the size of its arguments in bytes.
__vectorcall, whose name becomes add_vectorcall@@32.win-vectorcall.c
/* Vectors of 16 bytes are passed by reference by default; __vectorcall
passes them in XMM registers. */
typedef float v4sf __attribute__((vector_size(16)));
v4sf add_default(v4sf a, v4sf b)
{
return a + b;
}
v4sf __vectorcall add_vectorcall(v4sf a, v4sf b)
{
return a + b;
}
Clang 23.1.2 x86_64-pc-windows-msvc
AT&T syntax clang --target=x86_64-pc-windows-msvc -O2 -fno-asynchronous-unwind-tables -S win-vectorcall.c
add_default:
movaps (%rcx), %xmm0
addps (%rdx), %xmm0
retq
add_vectorcall@@32:
addps %xmm1, %xmm0
retqIntel syntax clang --target=x86_64-pc-windows-msvc -O2 -fno-asynchronous-unwind-tables -masm=intel -S win-vectorcall.c
add_default:
movaps xmm0, xmmword ptr [rcx]
addps xmm0, xmmword ptr [rdx]
ret
add_vectorcall@@32:
addps xmm0, xmm1
retCalling between Windows and System V code
GCC and Clang have two function attributes, ms_abi and sysv_abi,
that choose the convention of one function, whatever the target.
Wine uses ms_abi to run Windows programs on Linux: its headers define
__stdcall, and so every Windows API function it implements, as
ms_abi on x86-64. EDK II builds x64 UEFI firmware with GCC and Clang
the same way.
A function called the Windows way that calls ordinary System V code must keep the Windows promises: RSI, RDI and XMM6 to XMM15 are callee-saved for its caller, but scratch for the functions it calls.
ms_abi function on Linux: its arguments arrive in RCX and RDX and move to RDI and RSI for the System V call, and it saves RSI, RDI and XMM6 to XMM15, 176 bytes, around that call.win-ms-abi.c
/* GCC's ms_abi attribute on Linux: this function receives its
arguments the Windows way, then calls a System V function, which may
change RSI, RDI and XMM6 to XMM15; the Windows caller expects them
back, so they are saved and restored. */
extern long sysv_callee(long a, long b);
__attribute__((ms_abi)) long from_windows(long a, long b)
{
return sysv_callee(a, b) + 1;
}
GCC 14.2.0 (Debian 14.2.0-19) x86_64-linux-gnu
AT&T syntax gcc-14 -O2 -fno-asynchronous-unwind-tables -S win-ms-abi.c
from_windows:
pushq %rdi
movq %rcx, %rdi
pushq %rsi
movq %rdx, %rsi
subq $168, %rsp
movaps %xmm6, (%rsp)
movaps %xmm7, 16(%rsp)
movaps %xmm8, 32(%rsp)
movaps %xmm9, 48(%rsp)
movaps %xmm10, 64(%rsp)
movaps %xmm11, 80(%rsp)
movaps %xmm12, 96(%rsp)
movaps %xmm13, 112(%rsp)
movaps %xmm14, 128(%rsp)
movaps %xmm15, 144(%rsp)
call sysv_callee@PLT
movaps (%rsp), %xmm6
movaps 16(%rsp), %xmm7
movaps 32(%rsp), %xmm8
movaps 48(%rsp), %xmm9
addq $1, %rax
movaps 64(%rsp), %xmm10
movaps 80(%rsp), %xmm11
movaps 96(%rsp), %xmm12
movaps 112(%rsp), %xmm13
movaps 128(%rsp), %xmm14
movaps 144(%rsp), %xmm15
addq $168, %rsp
popq %rsi
popq %rdi
retIntel syntax gcc-14 -O2 -fno-asynchronous-unwind-tables -masm=intel -S win-ms-abi.c
from_windows:
push rdi
mov rdi, rcx
push rsi
mov rsi, rdx
sub rsp, 168
movaps XMMWORD PTR [rsp], xmm6
movaps XMMWORD PTR 16[rsp], xmm7
movaps XMMWORD PTR 32[rsp], xmm8
movaps XMMWORD PTR 48[rsp], xmm9
movaps XMMWORD PTR 64[rsp], xmm10
movaps XMMWORD PTR 80[rsp], xmm11
movaps XMMWORD PTR 96[rsp], xmm12
movaps XMMWORD PTR 112[rsp], xmm13
movaps XMMWORD PTR 128[rsp], xmm14
movaps XMMWORD PTR 144[rsp], xmm15
call sysv_callee@PLT
movaps xmm6, XMMWORD PTR [rsp]
movaps xmm7, XMMWORD PTR 16[rsp]
movaps xmm8, XMMWORD PTR 32[rsp]
movaps xmm9, XMMWORD PTR 48[rsp]
add rax, 1
movaps xmm10, XMMWORD PTR 64[rsp]
movaps xmm11, XMMWORD PTR 80[rsp]
movaps xmm12, XMMWORD PTR 96[rsp]
movaps xmm13, XMMWORD PTR 112[rsp]
movaps xmm14, XMMWORD PTR 128[rsp]
movaps xmm15, XMMWORD PTR 144[rsp]
add rsp, 168
pop rsi
pop rdi
retCygwin, MSYS2 and UEFI
Cygwin and MSYS2’s MSYS environment, which is built on it, use this
calling convention with the LP64 data model of Linux: long has 64
bits. In Listing 11, the diff of long values
works on the full RCX and RDX.
long changes the code. Clang keeps a frame pointer for the UEFI target.every-abi.c
/* Two functions, compiled for every ABI: where do the arguments
arrive, and where does the result go? */
long diff(long a, long b)
{
return a - b;
}
double axpy(double a, double x, double y)
{
return a * x + y;
}
Clang 23.1.2 x86_64-pc-windows-msvc
AT&T syntax clang --target=x86_64-pc-windows-msvc -O2 -fno-asynchronous-unwind-tables -S every-abi.c
diff:
movl %ecx, %eax
subl %edx, %eax
retq
axpy:
mulsd %xmm1, %xmm0
addsd %xmm2, %xmm0
retqIntel syntax clang --target=x86_64-pc-windows-msvc -O2 -fno-asynchronous-unwind-tables -masm=intel -S every-abi.c
diff:
mov eax, ecx
sub eax, edx
ret
axpy:
mulsd xmm0, xmm1
addsd xmm0, xmm2
retClang 23.1.2 x86_64-pc-cygwin
AT&T syntax clang --target=x86_64-pc-cygwin -O2 -fno-asynchronous-unwind-tables -S every-abi.c
diff:
movq %rcx, %rax
subq %rdx, %rax
retq
axpy:
mulsd %xmm1, %xmm0
addsd %xmm2, %xmm0
retqIntel syntax clang --target=x86_64-pc-cygwin -O2 -fno-asynchronous-unwind-tables -masm=intel -S every-abi.c
diff:
mov rax, rcx
sub rax, rdx
ret
axpy:
mulsd xmm0, xmm1
addsd xmm0, xmm2
retClang 23.1.2 x86_64-unknown-uefi
AT&T syntax clang --target=x86_64-unknown-uefi -O2 -fno-asynchronous-unwind-tables -S every-abi.c
diff:
pushq %rbp
movq %rsp, %rbp
movl %ecx, %eax
subl %edx, %eax
popq %rbp
retq
axpy:
pushq %rbp
movq %rsp, %rbp
mulsd %xmm1, %xmm0
addsd %xmm2, %xmm0
popq %rbp
retqIntel syntax clang --target=x86_64-unknown-uefi -O2 -fno-asynchronous-unwind-tables -masm=intel -S every-abi.c
diff:
push rbp
mov rbp, rsp
mov eax, ecx
sub eax, edx
pop rbp
ret
axpy:
push rbp
mov rbp, rsp
mulsd xmm0, xmm1
addsd xmm0, xmm2
pop rbp
retUEFI firmware on x64 calls its boot and runtime services with this convention and the LLP64 data model. The ABI overview gives the sizes of the C types for all of these platforms.
Sources
- Microsoft Learn: x64 calling convention: commit
f70d88cd5da7, 2026-09-24 - Microsoft Learn: Overview of x64 ABI conventions: commit
f70d88cd5da7, 2026-09-24 - Microsoft Learn: x64 stack usage: commit
f70d88cd5da7, 2026-09-24 - Microsoft Learn: __vectorcall: commit
f70d88cd5da7, 2026-09-24 - GCC 14.2 manual: x86 Function Attributes: as published on 2026-09-25
- Wine 11.0: include/msvcrt/corecrt.h: commit
db11d0fe6a16, 2026-01-13 - EDK II: BaseTools/Conf/tools_def.template: commit
2970e5699ba6, 2026-08-12 - Cygwin User's Guide: Building applications for 64 bit Cygwin: as published on 2026-09-25
- Clang 23.1.2: clang/lib/CodeGen/Targets/X86.cpp: commit
85ac56026243, 2026-09-20