Skip to content
Advertisement

why called function can get arguments of parent-function by va_start()?

I implemented a function with variable arguments below:

#include <stdio.h>
#include <string.h>
#include <stdarg.h>

#define ERR_BUFFER_SIZE 4096

void errPrint(const char *format, ...)
{
    va_list argptr;
    va_start(argptr, format);

    char buf[ERR_BUFFER_SIZE];
    vsnprintf(buf, sizeof(buf), format, argptr);
    fprintf(stderr, "%s", buf);
    
    va_end(argptr);
}

Then I hope to implement another named “errExit()” based on the function above. I just tried like below.It works as I hoped but I dont’t think it correct.

void errExit(const char *format, ...)
{
    errPrint(format);
    exit(EXIT_FAILURE);
}

I tried errExit("hello,%s,%s,%s", "arg1","arg2", "arg3"); and it printed “hello,arg1,arg2,arg3” correctly.

But after I added two line code like below, it throwed error Segmentation fault.

void errExit(const char *format, ...)
{
    char buf[4096];//added
    strcpy(buf, format);//added
    
    errPrint(format);
    exit(EXIT_FAILURE);
}

I am very confused. According to what I learned´╝Ü

  1. There is only one argument format in the stack of called function errPrint(). I can’t believe va_start() will get arguments from the its parent-function errExit().
  2. Since errPrint() works “correctly”, why it doesn’t work after I add the two lines code? It seems that the code added have no effect.

(My English is not well, hoping you everyone can stand my statement. Thank you! )

Advertisement

Answer

Other answers explain what you need to do. This answer is about the asm details of why it happened to work.


In your first errExit, if you look at the compiler-generated assembly for x86-64, all the incoming args would still be in registers when calling errPrint(format);. Even though you don’t tell the compiler to pass them, it doesn’t do anything that uses registers before calling that other function.

So it happens to work even though you didn’t explicitly pass along any args beyond the first (format). It compiles as if you’d written a function that passed along its first 6 args, assuming the x86-64 System V calling convention, and that the args were all integer or pointer (such as char*).

In the C abstract machine, your code is meaningless and has undefined behaviour when errPrint makes a va_list argptr and passes it on to a function (vsnprintf) that references more than 0 elements of that list.

It might help to look at asm for a function that does pass on multiple args:

void foo(char *a, char *b, char *c, char *d, char *f);
int  bar(char *a, char *b, char *c, char *d, char *f)
{
    foo(a, b, c, d, f);
    return 1;   // some code after the call, so it can't compile to a tailcall
}

On Godbolt, compiling for Linux with GCC12:

# GCC12 -O3
bar:
    # incoming args are in RDI, RSI, RDX, RCX, R8, R9  in that order
    # same place a callee will look for them, so passing them on is trivial
        sub     rsp, 8        # re-align the stack so RSP%16 == 0
        call    foo
        mov     eax, 1        # return-value register = 1
        add     rsp, 8        # restore the stack pointer
        ret                   # pop return address into RIP

Your errExit has equivalent asm before the call, so if there are register args, they get passed on even though you didn’t tell the C compiler about that.

errExit:
        sub     rsp, 8
        xor     eax, eax       # Variadic functions get AL = # of args passed in XMM regs
        call    errPrint
        mov     edi, 1
        call    exit           # GCC knows exit() is noreturn

If there was any code before call errPrint, such as a call to strcpy, that would of course step on those arg-passing registers. So they have different values when call errPrint runs, except for the one arg you told the C compiler about. It will save that in a call-preserved register across a call strcpy.


If you’d compiled this for a calling convention with fewer register args, like 32-bit x86 (gcc -m32) where there are only stack args, the pointers that vsnprintf references with %s conversions be from errExit‘s stack frame, above any args it intended to pass. So probably a stack slot of padding for alignment, then in a debug build maybe a saved EBP, maybe even errExit‘s own return address.

If you’re curious, use %p conversions to print the pointer values instead of trying to dereference and segfaulting.


C doesn’t have a way to specify that it should pass on only the register args; if you want to pass on an unknown number of args from a ..., you have to do it with a va_list which works for any number of args, including 7 or more on x86-64 System V. That’s a good thing because some targets wouldn’t have any register args, so just saving/restoring the register args would pass on 0 args.

I’m showing asm only to understand why one version happened to work, not as a suggestion for anything you can do to write safe and portable C that can compile to asm like this, even if there’s no other work before the call errPrint.

User contributions licensed under: CC BY-SA
6 People found this is helpful
Advertisement