Search code examples
cvariadic-functions

va_arg - different behavior on Linux and Windows


I did some test code to demonstrate different output on Windows and Linux

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

void print_vint(va_list args)
{
    int i = va_arg(args, int);
    printf("%d\n", i);
}

void variadic_f(const char* format, ...)
{
    va_list args;
    va_start(args, format);
    print_vint(args);
    print_vint(args);
    va_end(args);
}

int main(int argc, char* argv[])
{
    variadic_f("dummy ", 1, 2);
    return 0;
}

print_vint function has argument of type va_list passed by value. On a Windows machine it is pointer to stack right after the last nonvariadic parameter. Parameter is passed to function by value and function changes it when invoking va_arg. Second invocation of print_vint gets same pointer value (since local copy inside function was changed) and prints same output as first invocation.
Here is Windows output of program invocation:

1
1

I compile same source code on Linux and run. Output is different:

1
2

My conclusion is that Linux uses pointer to opaque structure which has pointer to stack and when va_list is passed by value dereferencing pointer and changing pointer inside structure will be visible in consecutive use of va_list.

When I change print_vint to accept pointer, it works same way on Linux and Windows.

void print_vint(va_list *args)
{
    int i = va_arg(*args, int);
    printf("%d\n", i);
}

Does the C standard specify how va_arg should behave and how execution affects va_list argument?


Solution

  • The error here is that you passed a va_list into another function and called va_arg in that function. Once you do that, it's not permitted to use the va_list again in the caller.

    This is spelled out in section 7.16p3 of the C standard regarding variable arguments:

    The type declared is

    va_list
    

    which is a complete object type suitable for holding information needed by the macros va_start, va_arg, va_end, and va_copy. If access to the varying arguments is desired, the called function shall declare an object (generally referred to as ap in this subclause) having type va_list. The object ap may be passed as an argument to another function; if that function invokes the va_arg macro with parameter ap, the value of ap in the calling function is indeterminate and shall be passed to the va_end macro prior to any further reference to ap.253)

    1. It is permitted to create a pointer to a va_list and pass that pointer to another function, in which case the original function may make further use of the original list after the other function returns.

    So your code triggers undefined behavior by accessing args after its value becomes indeterminate.

    Note that footnote 253 above states that you can pass a pointer to a va_list to do what you want:

    void print_vint(va_list *args)
    {
        int i = va_arg(*args, int);
        printf("%d\n", i);
    }
    
    void variadic_f(const char* format, ...)
    {
        va_list args;
        va_start(args, format);
        print_vint(&args);
        print_vint(&args);
        va_end(args);
    }