int max(int n, ...)
I am using cdecl
calling convention where the caller cleans up the variable after the callee returns.
I a
int max(int n, const char *msg,...)
{
va_list args;
char buffer[1024];
va_start(args, msg);
nb_char_written = vsnprintf(buffer, 1024, msg, args);
va_end(args);
printf("(%d):%s\n",n,buffer);
}
As arguments are passed on the stack, the va_
"functions" (they are most of the time implemented as macros) simply manipulate a private stack pointer. This private stack pointer is stored from the argument passed to va_start
, and then va_arg
"pops" the arguments from the "stack" as it iterates the parameters.
Lets say you call the function max
with three parameters, like this:
max(a, b, c);
Inside the max
function, the stack basically looks like this:
+-----+ | c | | b | | a | | ret | SP -> +-----+
SP
is the real stack pointer, and it's not really a
, b
and c
that on the stack but their values. ret
is the return address, where to jump to when the function is done.
What va_start(ap, n)
does is take the address of the argument (n
in your function prototype) and from that calculates the position of the next argument, so we get a new private stack pointer:
+-----+ | c | ap -> | b | | a | | ret | SP -> +-----+
When you use va_arg(ap, int)
it returns what the private stack pointer points to, and then "pops" it by changing the private stack pointer to now point at the next argument. The stack now look like this:
+-----+ ap -> | c | | b | | a | | ret | SP -> +-----+
This description is of course simplified, but shows the principle.
Generically, how I grok target.def, when a function prototype is declared with ( ,...) the compiler sets up a parse tree marked with a varargs flag and references to the types of the named arguments. For strict C conformance each named argument should get whatever additional info is necessary appended to setup a va_list when that parameter is the named field of va_start and as a possible return to va_arg(), but most compilers just generate this info for the last named argument. When the function is defined its prologue generator notes the varargs flag was set and adds the code necessary to set up any hidden fields it adds to the frame that have known offsets the va_start macro can reference.
When it finds a reference to that function it creates additional parse and code generation trees for each argument representing the ..., that may introduce additional hidden fields of runtime type info, such as array bounds, that is appended to the fields setup for va_start and va_arg for the named arguments. This combined tree determines what code gets generated to copy the parameter values onto the frame, the prologue sets up what's necessary for va_start to create a va_list starting at an arbitrary or last named parameter, and each invocation of va_arg() generates inline code that references any parameter specific hidden fields used to validate at compile time the expected return is assignment compatible with the expression usage being compiled, and perform any required argument promotions/coercions. The sum of named field value sizes and hidden field sizes determines what value is compiled after the call, or in the function epilogue for callee cleanup models, to adjust the frame upon return.
Each of these steps has processor and calling convention dependencies, encapsulated in the config/proc/proc.c and proc.h files, that override the simplistic default definitions of va_start() and va_arg() that assume each argument has a fixed size allocated some distance above the first named argument on a stack. For some platforms or languages parameter frames implemented as separate malloc()s are more desirable than a fixed size stack. Also note these usages are not thread safe; it is unsafe to pass a va_list reference to another thread without unspecified means of ensuring the parameter frame is not made invalid due to function return or abort of the thread.
If you look at the way the C language stores the parameters on the stack, the way the macros work should become clear:-
Higher memory address Last parameter
Penultimate parameter
....
Second parameter
Lower memory address First parameter
StackPointer -> Return address
(note, depending on the hardware the stack pointer maybe one line down and the higher and lower may be swapped)
The arguments are always stored like this1, even without the ...
parameter type.
The va_start
macro just sets up a pointer to the first function parameter, e.g.:-
void func (int a, ...)
{
// va_start
char *p = (char *) &a + sizeof a;
}
which makes p
point to the second parameter. The va_arg
macro does this:-
void func (int a, ...)
{
// va_start
char *p = (char *) &a + sizeof a;
// va_arg
int i1 = *((int *)p);
p += sizeof (int);
// va_arg
int i2 = *((int *)p);
p += sizeof (int);
// va_arg
long i2 = *((long *)p);
p += sizeof (long);
}
The va_end
macro just sets the p
value to NULL
.
NOTES:
...
parameter would switch off this ability and for the compiler to use the stack.