Is GCC mishandling a pointer to a va_list passed to a function?
This is a known problem. On some architectures (in particular x86-64), va_list
needs to be more complex than a simple pointer to the stack, for example because some arguments might be passed in registers or out-of-band in some other way (see this answer for the definition of va_list
on x86-64).
On such architectures, it is common to make va_list
an array type so that parameters of type va_list
will be adjusted to pointer types, and instead of the whole structure, only a single pointer needs to be passed.
This should not violate the C standard, which only says that va_list
must be a complete object type and even explicitly accounts for the fact that passing a va_list
argument might not actually clone the necessary state: va_list
objects have indeterminate value if they are passed as arguments and consumed in the called function.
But even if making va_list
an array type is legal, it still leads to the problems you experienced: As parameters of type va_list
have the 'wrong' type, eg struct __va_list_tag *
instead of struct __va_list_tag [1]
, it will blow up in cases where the difference between arrays and pointers matter.
The real problem is not the type mismatch gcc warns about, but the by-pointer instead of by-value argument passing semantics: &args
in test_val()
points to the intermediate pointer variable instead of the va_list
object; ignoring the warning means that you'll invoke va_arg()
in test_ptr()
on the pointer variable, which should return garbage (or segfault if you're lucky) and corrupt the stack.
One workaround is to wrap your va_list
in a structure and pass that around instead. Another solution I've seen in the wild, even here on SO, is to use va_copy
to create a local copy of the argument and then pass a pointer to that:
static void test_val(const char *fmt, va_list args)
{
va_list args_copy;
va_copy(args_copy, args);
test_ptr(fmt, &args_copy);
va_end(args_copy);
}
This should work in practice, but technically it might or might not be undefined behaviour, depending on your interpretation of the standard:
If va_copy()
is implemented as a macro, no parameter adjustments are performed, and it might matter that args
is not of type va_list
. However, as it is unspecified whether va_copy()
is a macro or a function, one might argue that it at least could be a function and parameter adjustments are implicitly assumed in the prototype given for the macro. It might be a good idea to ask the officials for clarification or even file a defect report.
You could also use your build system to deal with the issue by defining a configuration flag like HAVE_VA_LIST_AS_ARRAY
so you can do the right thing for your particular architecture:
#ifdef HAVE_VA_LIST_AS_ARRAY
#define MAKE_POINTER_FROM_VA_LIST_ARG(arg) ((va_list *)(arg))
#else
#define MAKE_POINTER_FROM_VA_LIST_ARG(arg) (&(arg))
#endif
static void test_val(const char *fmt, va_list args)
{
test_ptr(fmt, MAKE_POINTER_FROM_VA_LIST_ARG(args));
}
The problem is not specific to va_list
. The following code results in a similar warning:
typedef char *test[1];
void x(test *a)
{
}
void y(test o)
{
x(&o);
}
The problem stems from the way C handles function variables that are also arrays, probably due to the fact that arrays are passed as reference and not by value. The type of o
is not the same as the type of a local variable of type test
, in this case: char ***
instead of char *(*)[1]
.
Returning to the original issue at hand, the easy way to work around it is to use a container struct:
struct va_list_wrapper {
va_list v;
};
and there would be no typing problems passing a point to it.