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:
an intrinsic from
<intrin.h>
called_AddressofReturnAddress()
; and,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) withsizeof(Type) <= 8
;rcx
,rdx
,r8
, andr9
. Re-homed to locationsrsp + 8
,rsp + 16
,rsp + 24
,rsp + 32
,rsp
representing the address from_AddressofReturnAddress()
.Floating point types,
float
andhalf
, 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
, andr9
(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 aresizeof(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
.