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.

Registers in the Microsoft x64 ABI.
RegisterRolePreserved
RCX, RDX, R8, R9Integer, pointer and small struct arguments 1 to 4no
XMM0 to XMM3Floating-point arguments 1 to 4no
RAXInteger return valueno
XMM0Floating-point, vector and __int128 return valueno
R10, R11Scratchno
XMM4, XMM5Scratch; __vectorcall arguments 5 and 6no
RBX, RDI, RSI, R12 to R15Callee-savedyes
RBPCallee-saved; the frame pointer when a function uses oneyes
RSPStack pointeryes
XMM6 to XMM15Callee-saved, the lower 128 bits onlyyes
Upper halves of YMM0 to YMM15, ZMMScratchno
XMM16 to XMM31 (AVX-512)Scratchno
R16 to R29 (APX)Scratchno
R30, R31 (APX)Callee-savedyes
TMM0 to TMM7 (AMX)Scratchno
ST0 to ST7, MM0 to MM7Scratch; never used for argumentsno

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.

Six integer arguments: 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
        retq

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

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

Intel 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
        ret
Slots by position: a 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
        retq

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

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

How argument types are passed in the Microsoft x64 ABI.
C typeSizePassed asShown in
struct { int a, b; }8the value, in a general-purpose registerListing 3
struct { int a, b, c; }12a pointer to a copyListing 3
float, double4, 8the value, in an XMM registerListing 2
long double (MSVC, Clang)8the value, as a doubleListing 4
long double (MinGW-w64)16a pointer to a copyListing 4
__int128 (Clang, GCC)16a pointer to a copyListing 5
__m128 and other 16-byte vectors16a pointer to a copyListing 9
An 8-byte struct arrives in RCX, a 12-byte one as a pointer in RCX. 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
        retq

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

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

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

Microsoft 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
        retq

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

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

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

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

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

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

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

An 8-byte struct returns in RAX, built from ECX and EDX. The 12-byte struct returns through the memory at RCX, so the argument 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)
        retq

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

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

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

Callee-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
        retq

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

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

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

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

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

Two 16-byte vectors: by reference in RCX and RDX by default, by value in XMM0 and XMM1 with __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
        retq

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

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

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

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

Cygwin, 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.

The same function for Windows, Cygwin and UEFI: the registers are the same, and only Cygwin’s 64-bit 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
        retq

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

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

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

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

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

UEFI 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

  1. Microsoft Learn: x64 calling convention: commit f70d88cd5da7, 2026-09-24
  2. Microsoft Learn: Overview of x64 ABI conventions: commit f70d88cd5da7, 2026-09-24
  3. Microsoft Learn: x64 stack usage: commit f70d88cd5da7, 2026-09-24
  4. Microsoft Learn: __vectorcall: commit f70d88cd5da7, 2026-09-24
  5. GCC 14.2 manual: x86 Function Attributes: as published on 2026-09-25
  6. Wine 11.0: include/msvcrt/corecrt.h: commit db11d0fe6a16, 2026-01-13
  7. EDK II: BaseTools/Conf/tools_def.template: commit 2970e5699ba6, 2026-08-12
  8. Cygwin User's Guide: Building applications for 64 bit Cygwin: as published on 2026-09-25
  9. Clang 23.1.2: clang/lib/CodeGen/Targets/X86.cpp: commit 85ac56026243, 2026-09-20