Variable Argument Functions

Usually the functions defined in C/C++ take constant number of arguments which are decided while declaring the functions. Sometimes we do need the functions that can accept variable number of arguments, Printf() function is a great example of such functions.  Let's see how these functions work in terms of stack building and how they are able to retrieve the arguments passed from the  caller.

int Sum(int n, ...) is the declaration of one such variable argument functions. Just like any normal functions it can have any return type and any number of mandatory parameters of any type. The difference is in ellipsis ... which should be the last parameter to such a functions. Also there should be at least one mandatory parameter to the function, even if this placeholder parameter is not used it is required to access rest of the arguments.

The three macros that are used in function implementation to retrieve the arguments are va_start, va_arg, va_end. va_list is the type that is used to hold the information of variable arguments. This should be set by va_start before accessing any of the variable parameters. va_arg then uses this type to access the parameters one by one. va_end is called to stop the processing, it actually resets the va_list type which was set by va_start to hold the variable parameter information. Lets check one simple example:

int Sum(int n, ...)
{
       int sum=0;
       va_list vaList;
       va_start( vaList, n);
       for (int i=0; i<n; i++)
       {
             sum += va_arg(vaList, short);
       }
       va_end (vaList);
       return sum;
}

This program simply adds all the numbers passed to Sum function, the first mandatory parameter tells the number of variable arguments that are passed to the function. The implementation is simple, first the vaList is initialized using va_start function, then va_arg is used to fetch next variable argument and at the end va_end is called to reset the vaList.

There are few important things to notice here:

  • The function takes an argument which reflects the number of variable arguments passed to the function. Such information is required because the variable argument function has no way to figure out the number of parameters passed on its own. Number of parameters need not be passed by first argument, there can be any other logic used in the function implementation to identify the number of parameters, but it should be part of funciton implemenation it self.

 

  • va_arg takes a parameter which is the type of parameter that it is going to retrieve. This parameter is necessary because va_arg just goes one location up in the stack to search for the next parameter value. So it will fetch 8 bytes on 64bit process, and then return the right value using expected size. So this type needs to be used carefully. Incorrectly using the type parameter will result in  wrong result. For example if I used BYTE in place of Short in the above function to va_arg, I will only be adding the first 1 byte value of all the numbers passed, which will be incorrect if i wanted to have all the numbers upto 2 bytes.

 

  • va_start takes the name of the parameter which is immediately followed by ellipsis .... As mentioned earlier there can be more than one mandatory parameters to variable argument functions, and va_start expects the last mandatory parameter name so that it can identify from where in stack the variable parameter starts and it sets the va_list type to same location.

 Let''s take a look at all this in function disassembly and see how does it owrk, below is the disassembly of the above functin Sum(int n ,...). The comment line above the instructions explains the logic of program flow and variable parameter handling.

//Since the first four parameters are passed via registers, below four instructions will put these parameters to the stack homing space so that all the parameters are there on stack.

    5 000007f6`17231020 894c2408 mov dword ptr [rsp+8],ecx
    5 000007f6`17231024 4889542410 mov qword ptr [rsp+10h],rdx
    5 000007f6`17231029 4c89442418 mov qword ptr [rsp+18h],r8
    5 000007f6`1723102e 4c894c2420 mov qword ptr [rsp+20h],r9

// Adjust the stack for local parameters
    5 000007f6`17231033 4883ec28 sub rsp,28h
    6 000007f6`17231037 c744240800000000 mov dword ptr [rsp+8],0

// The first variable parameter that was passed via rdx is at rsp+38.  So rax now contains the location of first variable parameter. vaList will be set to this location as part of va_start call.
    8 000007f6`1723103f 488d442438 lea rax,[rsp+38h]

 // rsp+10 has the vaList local variable and it has been assigned rax which has the location of first variable parameter.
    8 000007f6`17231044 4889442410 mov qword ptr [rsp+10h],rax
    9 000007f6`17231049 c744240400000000 mov dword ptr [rsp+4],
    9 000007f6`17231051 eb0a jmp vararg!Sum+0x3d (000007f6`1723105d)

// The loop starts
    9 000007f6`17231053 8b442404 mov eax,dword ptr [rsp+4]
    9 000007f6`17231057 ffc0 inc eax
    9 000007f6`17231059 89442404 mov dword ptr [rsp+4],eax

 // The loop condition check

    9 000007f6`1723105d 8b442430 mov eax,dword ptr [rsp+30h]
    9 000007f6`17231061 39442404 cmp dword ptr [rsp+4],eax
    9 000007f6`17231065 7d53 jge vararg!Sum+0x9a (000007f6`172310ba)
 

   11 000007f6`17231067 33c0 xor eax,eax
   11 000007f6`17231069 85c0 test eax,eax
   11 000007f6`1723106b 7420 je vararg!Sum+0x6d (000007f6`1723108d)

 //here we call va_arg. This first fetches the location of current parameter

   11 000007f6`1723106d 488b442410 mov rax,qword ptr [rsp+10h]

 // Adding 8 to current parameter location. So it will start pointing to next parameter.

   11 000007f6`17231072 4883c008 add rax,8

// Stored the location of next parameter to va_list

   11 000007f6`17231076 4889442410 mov qword ptr [rsp+10h],rax
   11 000007f6`1723107b 488b442410 mov rax,qword ptr [rsp+10h]

 // Since the location already points to next parameter, we need to subtract 8 to get the value of current variable parameter.  

   11 000007f6`17231080 488b40f8 mov rax,qword ptr [rax-8]

 // So now rax contains full 8 byte. But we are using Short type in va_arg macro, so only need ax part of  it(i.e. 2 bytes)
   11 000007f6`17231084 0fb700 movzx eax,word ptr [rax]
   11 000007f6`17231087 66890424 mov word ptr [rsp],ax
   11 000007f6`1723108b eb1b jmp vararg!Sum+0x88 (000007f6`172310a8)

// Some instructions which are not relevent to variable argument handling are reomved from here to shorten the output 

//Adding to current sum.

   11 000007f6`172310b0 03c8 add ecx,eax
   11 000007f6`172310b2 8bc1 mov eax,ecx
   11 000007f6`172310b4 89442408 mov dword ptr [rsp+8],eax

 //Looping again.

  12 000007f6`172310b8 eb99 jmp vararg!Sum+0x33 (000007f6`17231053)

 // This is the code for resetting the vaList using va_end. As we know rsp+10 have vaList.
   13 000007f6`172310ba 48c744241000000000 mov qword ptr [rsp+10h],0
   15 000007f6`172310c3 8b442408 mov eax,dword ptr [rsp+8]
   16 000007f6`172310c7 4883c428 add rsp,28h
   16 000007f6`172310cb c3 ret

 

This is an example from AMD64 process, as you can see the caller has no changes in it whether it is calling a normal functions or a variable argument function. It will pass all the parameters and will clean the stack when callee returns. So it is the responsibility of caller to clean up the stack, which is always the case in 64 bit functions. For 32 bit functions there are multiple calling conventions like STDCALL, CDECL etc. Among these CDECL is the one in which Caller cleans up the stack, so for 32 bit variable argument functions, the function should always be declared as __cdecl function.