The use of variable arguments list is a standard feature of 'C' language, and as such must be enforced on any machine for which exist a C compiler.
When we say any machine we mean that independently from the way used for parameters passing, registers, stack or both, we must have the feature.
In effect what is really needed to implement the functionality is the deterministic nature of the process. It is not relevant if parameters are passed in stack, register, both, or other MCU custom ways, what is important is that the way it is done is well defined and always the same.
If this property is respected we are sure that we can always walk the parameters list, and access each of them.
Actually the method used to pass parameters for each machine or system, is specified in the ABI (Application Binary Interface, see https://en.wikipedia.org/wiki/Application_binary_interface), following the rules, in reverse, you can always backtrack parameters.
Anyway on some system, the vast majority, the simple reverse engineering of the ABI isn't sufficient to recover parameters, i.e. parameter sizes different from standard CPU register/stack size, in this case you need more info about the parameter you are looking for: the operand size.
Let review the variable parameter handling in C. First you declare a function having a single parameter of type integer, holding the count of parameters passed as variable arguments, and the 3 dots for variable part:
int foo(int cnt, ...);
To access variable arguments normally you use the definitions in <stdarg.h>
header in the following way:
int foo(int cnt, ...)
{
va_list ap; //pointer used to iterate through parameters
int i, val;
va_start(ap, cnt); //Initialize pointer to the last known parameter
for (i=0; i<cnt; i++)
{
val = va_arg(ap, int); //Retrieve next parameter using pointer and size
printf("%d ", val); // Print parameter, an integer
}
va_end(ap); //Release pointer. Normally do_nothing
putchar('\n');
}
On a stack based machine (i.e. x86-32bits) where the parameters are pushed sequentially the code above works more or less as the following:
int foo(int cnt, ...)
{
char *ap; //pointer used to iterate through parameters
int i, val;
ap = &cnt; //Initialize pointer to the last known parameter
for (i=0; i<cnt; i++)
{
/*
* We are going to update pointer to next parameter on the stack.
* Please note that here we simply add int size to pointer because
* normally the stack word size is the same of natural integer for
* that machine, but if we are using different type we **must**
* adjust pointer to the correct stack bound by rounding to the
* larger multiply size.
*/
ap = (ap + sizeof(int));
val = *((int *)ap); //Retrieve next parameter using pointer and size
printf("%d ", val); // Print parameter, an integer
}
putchar('\n');
}
Please note that if we access types different from int
e/o having size different from native stack word size, the pointer must be adjusted to always increase of a multiple of stack word size.
Now consider a machine that use registers to pass parameters, for simplicity we consider that no operand could be larger than a register size, and that the allocation is made using the registers sequentially (also note the pseudo assembler instruction mov val, rx
that loads the variable val
with contents of register rx
):
int foo(int cnt, ...)
{
int ap; //pointer used to iterate through parameters
int i, val;
/*
* Initialize pointer to the last known parameter, in our
* case the first in the list (see after why)
*/
ap = 1;
for (i=0; i<cnt; i++)
{
/*
* Retrieve next parameter
* The code below obviously isn't real code, but should give the idea.
*/
ap++; //Next parameter
switch(ap)
{
case 1:
__asm mov val, r1; //Get value from register
break;
case 2:
__asm mov val, r2;
break;
case 3:
__asm mov val, r3;
break;
.....
case n:
__asm mov val, rn;
break;
}
printf("%d ", val); // Print parameter, an integer
}
putchar('\n');
}
Hope the concept is clear enough now.