Click here to Skip to main content
15,887,371 members
Please Sign up or sign in to vote.
3.00/5 (1 vote)
See more:
Hello guys.
My question is: In what size char, short, __int32, __int64 are passed to a x64 function accepting variable args?
I googled this but only found calling convention, nothing about the size of args passed.
By debugging, I found that char, short, __int32 are passed as 32 bit values, __int64 is passed as a 64 bit value. but no formal reference I found for this. Is this correct? any article? Thanks in advance
mr.abzadeh

Edit: by size I meant padding. char, short, int32 are padded to 32 bit values. Every param is 64 bit aligned in stack, but smaller size params are padded to 32 bit, not 64 bit.

Edit: Disassembly of this simple code shows that (__int64)ch1 is passed as 64 bit value(mov qword ptr [rsp+20h],rax), char, short, __int32 are passed as 32 bit(regesters r9d, r8d, edx)
C++
char ch1 = 'A';
//00007FF68CE27265 C6 44 24 30 41       mov         byte ptr [ch1],41h  
	wprintf(L"%c %d %ld %lld", ch1, (short)ch1, (__int32)ch1, (__int64)ch1);
//00007FF68CE2726A 48 0F BE 44 24 30    movsx       rax,byte ptr [ch1]  
//00007FF68CE27270 0F BE 4C 24 30       movsx       ecx,byte ptr [ch1]  
//00007FF68CE27275 0F BE 54 24 30       movsx       edx,byte ptr [ch1]  
//00007FF68CE2727A 44 0F BE 44 24 30    movsx       r8d,byte ptr [ch1]  
//00007FF68CE27280 44 89 44 24 34       mov         dword ptr [rsp+34h],r8d  
//00007FF68CE27285 48 89 44 24 20       mov         qword ptr [rsp+20h],rax  
//00007FF68CE2728A 44 8B C9             mov         r9d,ecx  
//00007FF68CE2728D 44 8B C2             mov         r8d,edx  
//00007FF68CE27290 8B 44 24 34          mov         eax,dword ptr [rsp+34h]  
//00007FF68CE27294 8B D0                mov         edx,eax  
//00007FF68CE27296 48 8D 0D 7B 68 2B 00 lea         rcx,[byValue+298h (07FF68D0DDB18h)]  
//00007FF68CE2729D FF 15 C5 C9 3C 00    call        qword ptr [__imp_wprintf (07FF68D1F3C68h)]  
Posted
Updated 3-Sep-15 7:10am
v4

x64 doesn't necessarily affect the size of variables: a char will always be 8 bits (on modern computers, anyway), a short 16, an int32 will be 32, and an int64 will be 64.
That doesn't mean that they will be passed to a function as that size however, and parameters are generally not "combined" when the are added to the stack, but "stretched" to preserve the stack granularity becuase stack locations are always assigned in "groups" of a certain number of bytes - normally 4 bytes or 32 bits.
So a single byte value will occupy one whole stack location - 32 bits - as will a short and a int32. And int64 will occupy two stack locations - 64 bits.

It doesn't matter that this is accepting a variable number of parameters, this happens with all functions, and even with structs unless they are "packed". It's simply a lot simpler (and more efficient in time terms) if the stack entries are always the same size.

If you like, think about it in terms of a stacks of coins: if they are all the same size it's a lot easier to work with than if you start combining 4 wildly different size coins in the same stack! :laugh:
 
Share this answer
 
Comments
mr.abzadeh 2-Sep-15 14:17pm    
I meant padding. smaller types when passed to vararg functions, they are padded to 32 bit values. This doesn't occur in non-vararg functions
The only difference in X64 ABI (Application binary interface) for standard and varargs is that floating point arguments are passed in XMM registers and standard registers, all other characteristics are the same:
1. The first 4 parameters are passed in registers as FASTCALL. Parameters in excess of 4 are passed on the stack.
2. The parameters from 8 up to 64 bits are passed in registers right justified. Means that a char parameter will occupy the lower 8 bits of the register, a widechar the lower 16 bits, an int the lower 32bits and an I64 the whole register. The same system is used for parameters on the stack that has always a dimension of 64bits, menaing that each push or pop is 64bits wide (this is also an HW requirement when the CPU is in x64 mode). Please see MSDN documentation[^]:
Quote:
The first four integer arguments are passed in registers. Integer values are passed (in order left to right) in RCX, RDX, R8, and R9. Arguments five and higher are passed on the stack. All arguments are right-justified in registers. This is done so the callee can ignore the upper bits of the register if need be and can access only the portion of the register necessary.


3. Other size in excess of 64 bits, i.e. __m128, are passed only by reference. The compiler creates an hidden local copy of the variable then pass the pointer to such hidden copy.

All information are available on the MS site here[^]. Reading carefully you'll get all you need.

EDIT to add explanation due to answer upgrade.

The way used by compiler for the right justify are compiler specific, so it could be a simple byte move, a sign extended move to a 16, 32 or 64bits. In your case the first four instructions depends on the cast you applied.

Please refer to the code:
C#
char ch1 = 'A';
//00007FF68CE27265 C6 44 24 30 41       mov         byte ptr [ch1],41h
    wprintf(L"%c %d %ld %lld", ch1, (short)ch1, (__int32)ch1, (__int64)ch1);
    //This is just a conversion because the 5th parameter will be passed on the stack
//00007FF68CE2726A movsx       rax,byte ptr [ch1]    //Cast to 64 bits int (__int64)ch1.

    //Apply casts for values to pass in registers RCX RDX R8 R9
//00007FF68CE27270 movsx       ecx,byte ptr [ch1]    //Cast to 32 bits int (__int32)ch1
//00007FF68CE27275 movsx       edx,byte ptr [ch1]    //Byte extended to 32bits
//00007FF68CE2727A movsx       r8d,byte ptr [ch1]    //Cast to short extended to 32bits (short)ch1

    //This code have been generated with optimizations off this clearly exposed on
    //the next line where a copy of converted value is saved in local variable @ [rsp+34h]
//00007FF68CE27280 44 89 44 24 34       mov         dword ptr [rsp+34h],r8d

    //Push the 5th parameter on the stack. P.s. The stack is 64bits wide.
//00007FF68CE27285 48 89 44 24 20       mov         qword ptr [rsp+20h],rax

    //Now loads the registers RCX RDX R8 R9 as required by x64 ABI
    //RCX = format string
    //RDX = ch1
    //R8  = (short)ch1
    //R9  = (__int32)ch1
//00007FF68CE2728A 44 8B C9             mov         r9d,ecx
//00007FF68CE2728D 44 8B C2             mov         r8d,edx
//00007FF68CE27290 8B 44 24 34          mov         eax,dword ptr [rsp+34h]  //Get the value and...
//00007FF68CE27294 8B D0                mov         edx,eax  /store it in the low 32 bits of RDX
//00007FF68CE27296 48 8D 0D 7B 68 2B 00 lea         rcx,[byValue+298h (07FF68D0DDB18h)]  //format string

    //Call wprintf()
//00007FF68CE2729D FF 15 C5 C9 3C 00    call        qword ptr [__imp_wprintf (07FF68D1F3C68h)]
 
Share this answer
 
v2
Comments
mr.abzadeh 3-Sep-15 12:59pm    
I edited the question, adding disassembly of a simple code. It shows that 8 bit and 16 bit values are converted to 32 bit values, then pushed to stack (or 4 registers)
Frankie-C 3-Sep-15 14:27pm    
See my update to the answer.
mr.abzadeh 5-Sep-15 13:02pm    
Very good explanation. Extension to 32 bit is compiler specific, or not garanteed. Thanks

This content, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)



CodeProject, 20 Bay Street, 11th Floor Toronto, Ontario, Canada M5J 2N8 +1 (416) 849-8900