1

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! )

sixsixqaq
  • 39
  • 6
  • You probably want to create another function, `verrPrint(const char *format, va_list ap)` then call that function in `errExit()`. Then refactor `errPrint(const char *format, ...)` to to call `verrPrint()`. – Allan Wind Sep 04 '22 at 04:10
  • 1
    See also [Repeated use of a variadic function argument doesn't work](https://stackoverflow.com/a/41411702/15168), which outlines that you often need a pair of functions, one taking ellipsis `, ...)` and the other taking a `va_list args)` at the end of the argument list, as suggested in the oldest answer below. – Jonathan Leffler Sep 04 '22 at 06:20

3 Answers3

4

In errExit() you need to pass the variable arguments to another function via a va_list argument. As you were not, vsnprintf will eventually crash trying to access non-existing arguments on the stack.

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

#define ERR_BUFFER_SIZE 4096

void verrPrint(const char *format, va_list ap) {
    char buf[ERR_BUFFER_SIZE];
    vsnprintf(buf, sizeof(buf), format, ap);
    fprintf(stderr, "%s", buf);
}

void errPrint(const char *format, ...) {
    va_list ap;
    va_start(ap, format);
    verrPrint(format, ap);
    va_end(ap);
}

void errExit(const char *format, ...) {
    va_list ap;
    va_start(ap, format);
    verrPrint(format, ap);
    va_end(ap);
    exit(EXIT_FAILURE);
}

int main(void) {
    errPrint("What is the meaning of %s\n", "life?");
    errExit("%d\n", 42);
    return 0;
}
Allan Wind
  • 23,068
  • 5
  • 28
  • 38
  • OK,I learned it. I used to try so. I prefer to use `va_arg()`, `va_list()` .etc as less as possible because I feel unfamiliar with them. Now it seems to be impossible. Thank you! – sixsixqaq Sep 04 '22 at 04:57
  • @sixsixqaq — you can achieve your desired result, but not in the way you are trying to do it. You need to use the auxilliary function taking the `va_list` argument directly. Note that `va_list` is a type, not a function; `va_arg` is necessarily a function-like macro, not a function. – Jonathan Leffler Sep 04 '22 at 06:24
2

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.

Peter Cordes
  • 328,167
  • 45
  • 605
  • 847
1

Thank you everyone.I have learned your from your answers. I choose to change my code like below.

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

#define ERR_BUFFER_SIZE 4096

void errPrint(const char *format, va_list arg_list)
{
    char buf[ERR_BUFFER_SIZE];
    vsnprintf(buf, sizeof(buf), format, arg_list);
    fprintf(stderr, "%s", buf);
}

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

void v_exec(void (*func)(const char *, va_list), const char *format, ...)
{
    va_list arg_list;
    va_start(arg_list, format);

    func(format, arg_list);

    va_end(arg_list);
}

int main(void) 
{ 
    v_exec(errExit,"%s%d", "hello",520);
    return 0;
}
sixsixqaq
  • 39
  • 6