How are variable arguments implemented in gcc?
Asked Answered
U

3

32
int max(int n, ...)

I am using cdecl calling convention where the caller cleans up the variable after the callee returns.

I am interested in knowing how do the macros va_end, va_start and va_arg work?

Does the caller pass in the address of the array of arguments as the second argument to max?

Unthinkable answered 11/9, 2012 at 13:58 Comment(3)
This blog posting, amd64 and va_arg has a great discussion about how the va_arg set of functions for variable arguments can differ between machine architectures and the call conventions, the ABI, that is used with particular processors. Modern processors with more registers available than the old x86 architecture allow for passing arguments in registers as well as on the stack.Coursing
Related: #5273203Pneumectomy
Regrettably, all of the answers to this question are more or less wrong. (They are vaguely accurate for ABIs defined a long time ago, but the calling convention is much more complicated for any ABI defined in the past 20+ years and involves putting stuff in registers as well as on the stack. Yes, even for "CISC" processors.) I would write a better answer but it would take me all afternoon and I need to do my actual job today :-PHidden
O
40

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:

  1. Optimising compilers and some RISC CPUs store parameters in registers rather than use the stack. The presence of the ... parameter would switch off this ability and for the compiler to use the stack.
Ordinarily answered 11/9, 2012 at 14:27 Comment(12)
This is really quite platform-specific, as many calling conventions (including the common x64, PPC, ARM) pass most of their parameters in registers. Many platforms do not put the return address on the stack, one or two platforms have stacks that grow upwards instead of downwards, and some calling conventions place arguments on the stack in the opposite order.Dorty
@DietrichEpp: I know. But hopefully it gets some basics across. I put some notes into the answer to reflect the various ways stacks work. Still, to cover most of the different ways the compiler implements this would take a much longer answer. The simple way would be to find the macro definitions and see they expand to and hopefully there's no spooky compiler magic going on.Ordinarily
variadic parameters only work on 32bit with the cdecl calling convention, which passes parameters only on the stack and in reverse order to allow the caller to decide how many parameters to pass. Only the caller sets up the call stack and cleans it up after the call returns, which is a vital key to variadic usage. Other 32bit calling conventions either use a mix of registers and stack, or mix caller/callee responsibilities about setup/cleanup, thus making variadic parameters impossible to use.Bowler
On 64bit, variadic parameters are still possible, but a va_arg() implementation would be very complex, requiring compiler support and not just user-mode code.Bowler
@RemyLebau Is there any way to get a 64bit gcc (gcc 5 or 6 on x86-64 Linux) to use the calling conventions Skizz described so nicely above? I'm asking because I would like to have my students implement a variadic function in C as an exercise problem "by hand" (i.e. without the va_* macros) in order to get a practical understanding of parameter passing via the stack, but I don't want them to install a 32bit gcc only for this one exercise.Kit
The presence of the ... parameter would switch off this ability and for the compiler to use the stack. Actually no, on x86-64 both the Windows and System V calling conventions still pass args in the same registers for variadic or not. The Windows calling convention requires "shadow space" above the return address before stack args (if any), so a variadic function can just dump the 4 registers into the shadow space and index its args as an array. (The caller is required to duplicate FP args in integer and XMM registers for variadic functions).Nocturnal
So Windows is optimized for variadic functions at the expense of normal functions. But x86-64 System V requires variadic functions to be more complex to figure out where their args are. gcc's implementation dumps the arg passing regs to the stack (including xmm if al != 0, indicating that there are some FP args in registers), then treats this as a disjoint array for va_arg. Normal modern code doesn't call non-inlined variadic functions in tight loops, and out-of-order execution makes the dead stores of unused regs not very expensive anyway.Nocturnal
@PeterCordes: No, the operating system doesn't make any requirements on how parameters are handled within an application, the only specifications the OS defines is how parameters are passed to the OS itself, it's the compiler that defines how parameters are implemented within an application. The way the parameters are handled in my answer is heavily influenced by the architecture of the original 8088/6 processors and the state of software engineering at the time, these days with much faster processors and advances in SE it might be done differently but that's what we're stuck with.Ordinarily
And does GCC specify how arguments are handled? I only ask because GCC is a very versatile, cross platform compiler and the way arguments are implemented is probably defined more by the target architecture than the compiler, a 88000 processor would have a very different strategy to that of a Z80 for example, but GCC would be able to target either given the same source code.Ordinarily
All the major C compilers choose to follow standard calling conventions / ABIs so you can call library functions in libraries compiled by a different compiler on the same target platform. On x86-64 those calling conventions (x86-64 System V, and the x86-64 Windows convention) both use register args even for variadic functions. See stackoverflow.com/questions/6212665 for examples of calling printf. So no, the compiler can't just make something up, and no, ... doesn't disable register arg passing. And yes, of course different targets have different calling conventions.Nocturnal
On 32-bit x86 Windows, one of the standard calling conventions (__stdcall I think) has the callee pop the args from the stack (with ret 8 instead of just ret for example). But even when that's the default, variadic functions don't use that; they use caller-pops (i.e. __cdecl). So ... can affect the calling convention chose, but it doesn't have to disable register arg passing. An ABI design where that was the case is possible, but is not the rule.Nocturnal
@Skizz, why code const int& rf= 3; va_list vl; va_start(vl, rf); has undefined behavior?Clemmieclemmons
G
12

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.

Gobioid answered 11/9, 2012 at 14:13 Comment(6)
Surely va_arg can't pop it off the stack if't sit a caller-cleanup convention.Bethesde
@Joachim: Can you give some illustrations or describe you answer in more detail. I can't visualize what you are saying.Unthinkable
@DeadMG Of course it doesn't, that's why I put pops inside quotations. :)Gobioid
@Unthinkable Modified my answer, hope you can figure it out better now.Gobioid
That's how it works on a 32-bit platform. On a 64-bit platform some arguments are passed through registers, others on the stack, hence the 64-bit implementation is more complex.Sangraal
@JoachimPileborg: Thanks a lot. I can visualize it much better now.Unthinkable
I
1

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.

Ingenue answered 10/6, 2018 at 19:24 Comment(2)
This answer does not appear to address the question OP actually asked.Hidden
@Hidden I am not sure; see here, which is similar in terms of content (I am not qualified to judge, but perhaps you are).Shanleigh

© 2022 - 2024 — McMap. All rights reserved.