How are variable arguments implemented in gcc?

19,603

Solution 1

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.

Solution 2

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.

Share:
19,603

Related videos on Youtube

Bruce
Author by

Bruce

Do it right the first time

Updated on June 05, 2022

Comments

  • Bruce
    Bruce almost 2 years
    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?

    • Richard Chambers
      Richard Chambers about 6 years
      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.
    • Ciro Santilli OurBigBook.com
      Ciro Santilli OurBigBook.com over 4 years
    • zwol
      zwol almost 4 years
      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 :-P
  • Puppy
    Puppy over 11 years
    Surely va_arg can't pop it off the stack if't sit a caller-cleanup convention.
  • Bruce
    Bruce over 11 years
    @Joachim: Can you give some illustrations or describe you answer in more detail. I can't visualize what you are saying.
  • Some programmer dude
    Some programmer dude over 11 years
    @DeadMG Of course it doesn't, that's why I put pops inside quotations. :)
  • Dietrich Epp
    Dietrich Epp over 11 years
    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.
  • Some programmer dude
    Some programmer dude over 11 years
    @Bruce Modified my answer, hope you can figure it out better now.
  • Maxim Egorushkin
    Maxim Egorushkin over 11 years
    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.
  • Bruce
    Bruce over 11 years
    @JoachimPileborg: Thanks a lot. I can visualize it much better now.
  • Skizz
    Skizz over 11 years
    @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.
  • Remy Lebeau
    Remy Lebeau almost 8 years
    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.
  • Remy Lebeau
    Remy Lebeau almost 8 years
    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.
  • Holger Peine
    Holger Peine over 7 years
    @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.
  • Peter Cordes
    Peter Cordes over 6 years
    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).
  • Peter Cordes
    Peter Cordes over 6 years
    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.
  • Skizz
    Skizz over 6 years
    @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.
  • Skizz
    Skizz over 6 years
    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.
  • Peter Cordes
    Peter Cordes over 6 years
    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.
  • Peter Cordes
    Peter Cordes over 6 years
    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.
  • zwol
    zwol almost 4 years
    This answer does not appear to address the question OP actually asked.
  • alexander.sivak
    alexander.sivak about 3 years
    @Skizz, why code const int& rf= 3; va_list vl; va_start(vl, rf); has undefined behavior?