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:
- There is only one argument
format
in the stack of called functionerrPrint()
. I can’t believeva_start()
will get arguments from the its parent-functionerrExit()
. - 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
.