A while back I wrote about bad va_list assumptions. Recap: AMD64 passes some arguments through registers, and GNU’s va_list structure changes to accomodate that. Such changes mean you need to use va_copy instead of relying on x86 assumptions.
Microsoft does not have va_copy, so I was unsure how their x64 compiler solved the problem. I had three guesses: 1) va_list could be copied through assignment, 2) all variadic functions required every parameter to be on the stack, or 3) something else.
It turned out to be something else. Microsoft takes a rather strange approach. The caller reserves space on the stack for all of the registers who have arguments being passed. Then it moves the data into the respective registers but doesn’t touch the stack. The variadic callee then moves these register values into the stack above the frame, that is, where the other variadic parameters are.
For example, here is how logmessage() gets called:
000000013F7F1060 sub rsp,28h logmessage("%s %s %s\n", "a", "b", "c"); 000000013F7F1064 lea r9,[string "c" (13F7F21B0h)] 000000013F7F106B lea r8,[string "b" (13F7F21B4h)] 000000013F7F1072 lea rdx,[string "a" (13F7F21B8h)] 000000013F7F1079 lea rcx,[string "%s %s %s\n" (13F7F21C0h)] 000000013F7F1080 call logmessage (13F7F1000h)
And, here is logmessage()‘s prologue, which immediately saves its four arguments in the stack space above its frame.
void logmessage(const char *fmt, ...) { 000000013F7F1000 mov qword ptr [rsp+8],rcx 000000013F7F1005 mov qword ptr [rsp+10h],rdx 000000013F7F100A mov qword ptr [rsp+18h],r8 000000013F7F100F mov qword ptr [rsp+20h],r9
After doing that, the register complication of AMD64 is removed, because everything just sits on the stack. Thus the va_list variable can be re-used because it’s just a by-value pointer to the stack:
va_start(ap, fmt); 000000013F7F1019 lea rbx,[rsp+38h] vfprintf(stdout, fmt, ap); 000000013F7F101E call qword ptr [__imp___iob_func (13F7F2138h)]
And indeed, it appears to work fine:
a b c a b c Press any key to continue . . .
This implementation is interesting to me and I’d love to know the reasoning behind it. I have one big guess: it preserves the calling convention. The other option is to say, “all variadic functions must pass everything on the stack.” Perhaps that additional bit of complexity was undesired, or perhaps there are optimization cases where you’d want variadic functions that don’t immediately use the stack or va_list, but still need CRT compatibility.
Whatever the case, it’s not a big deal.
And, if you were wondering: You can indeed assign va_list pointers on Microsoft’s x64 compiler. GNU forbids that so I’m unsure if that’s intended or an accident on Microsoft’s part.
Grüß Gott, Das ist beileibe ein super Artikel. Mit Abstand das beste, welches meinereiner nunmehr gelesen hab. Weiter so. Mit freundlichem Gruß!
Right here is the right blog for everyone who wishes to find out about this topic. You know so much its almost tough to argue with you (not that I personally will need to…HaHa). You certainly put a brand new spin on a subject that has been written about for a long time. Excellent stuff, just wonderful!|