Microsoft Visual C++ - x64

The VC++ x64_86 implementation relies on the System V ABI that Microsoft uses for it’s calling convention, particularl for it’s Variable Argument functions. It lays out much of it’s details in two documents:

As the storage in registers in fickle, we explicitly rely on 2 things to keep work going:

  1. an intrinsic from <intrin.h> called _AddressofReturnAddress(); and,

  2. the fact that we can use assembler to access registers that get spilled to the stack.

The second is not necessarily guaranteed: highly-optimized Variable Argument function calls do not spill values to the stack. But in general, registers documented such as rcx and rdx are frequently used when any amount of work is done inside of these functions, and that causes the stack to immediately “re-home” the values in those registers to (8-byte aligned) places just above the address of the return address (e.g., what we will use as our stack pointer to walk the stack for arguments).

Note

💡 Observed (But Not Documented)

In testing, any usage of ztdc_va_list and ztdc_va_start within a Variable Argument function triggered the register rehoming. This allowed us to get at the arguments that were previously in registers that were both in practice and in documentation too widely reused, too volatile, and too hot to reliably extract.

Note that simply walking the stack is not 100% effective, even with stack re-homing: floating point arguments are not typically re-homed in the Microsoft System V ABI, and therefore must be accessed directly in their registers xmm0 through xmm3 for the corresponding to the first 4 arguments.

  • For the first 4 arguments:
    • Non-floating point types including integers, aggregates (std::is_aggregate_v and all C types) with sizeof(Type) <= 8; rcx, rdx, r8, and r9. Re-homed to locations rsp + 8, rsp + 16, rsp + 24, rsp + 32, rsp representing the address from _AddressofReturnAddress().

    • Floating point types, float and half, and all __mNN types up to __m64: xmm0, xmm1, xmm2, xmm3. Not re-homed to anywhere on the stack reliably.

    • All other values are turned into pointers, and those pointer values are stored in the rcx, rdx, r8, and r9 (and re-homed).

  • For each argument after:
    • Stored on the stack from rsp + 40 onwards, regardless of whether or not any registers are re-homed. The way they are stored follows the above: direct values for all types that are sizeof(T) <= 8, and pointers to said values for anything else.

Warning

There seem to be alignment issues on Windows that are not clearly explained in the documentation. rsp and the “rehomed” space may not be aligned properly, despite the documentation stating that non-leaf (framed) functions must be aligned properly. It is hard to get it to keep the data in the right place and occasionally seems to produce data pointers in the rehomed and and other stack pointer places that are not where they are expected to be.

A cheap check for knowing if you have walked off the edge of the stack is testing if the pointer value for any stack values is greater than the address of the ztdc_va_list list. This can be done as an assert (which can be turned off in Release builds by-default).

float types are automatically promoted to double, and so if a person requests float it must be converted to double first and then explicitly downcast within the platform’s implementation of ztdc_va_next.