va_list misbehavior on Linux

て烟熏妆下的殇ゞ 提交于 2021-02-18 10:55:08

问题


I have some code that converts variadic parameters into a va_list, then passes the list on to a function that then calls vsnprintf. This works fine on Windows and OS X, but it is failing with odd results on Linux.

In the following code sample:

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

char *myPrintfInner(const char *message, va_list params)
{
    va_list *original = &params;
    size_t length = vsnprintf(NULL, 0, message, *original);
    char *final = (char *) malloc((length + 1) * sizeof(char));
    int result = vsnprintf(final, length + 1, message, params);

    printf("vsnprintf result: %d\r\n", result);
    printf("%s\r\n", final);

    return final;
}

char *myPrintf(const char *message, ...)
{
    va_list va_args;
    va_start(va_args, message);

    size_t length = vsnprintf(NULL, 0, message, va_args);
    char *final = (char *) malloc((length + 1) * sizeof(char));
    int result = vsnprintf(final, length + 1, message, va_args);

    printf("vsnprintf result: %d\r\n", result);
    printf("%s\r\n", final);

    va_end(va_args);

    return final;
}

int main(int argc, char **argv)
{
    char *test = myPrintf("This is a %s.", "test");
    char *actual = "This is a test.";
    int result = strcmp(test, actual);

    if (result != 0)
    {
        printf("%d: Test failure!\r\n", result);
    }
    else
    {
        printf("Test succeeded.\r\n");
    }

    return 0;
}

The output of second vsnprintf call is 17, and the result of strcmp is 31; but I don't get why vsnprintf would return 17 seeing as This is a test. is 15 characters, add the NULL and you get 16.

Related threads that I've seen but do not address the topic:

  • Pass va_list or pointer to va_list?
  • Passing one va_list as a parameter to another

With @Mat's answer (I am reusing the va_list object, which is not allowed), this comes squarely around to the first related thread I linked to. So I attempted this code instead:

char *myPrintfInner(const char *message, va_list params)
{
    va_list *original = &params;
    size_t length = vsnprintf(NULL, 0, message, params);
    char *final = (char *) malloc((length + 1) * sizeof(char));
    int result = vsnprintf(final, length + 1, message, *original);

    printf("vsnprintf result: %d\r\n", result);
    printf("%s\r\n", final);

    return final;
}

Which, per the C99 spec (footnote in Section 7.15), should work:

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.

But my compiler (gcc 4.4.5 in C99 mode) gives me this error regarding the first line of myPrintfInner:

test.c: In function ‘myPrintfInner’: 
test.c:8: warning: initialization from incompatible pointer type

And the resulting binary produces the exact same effect as the first time around.


Found this: Is GCC mishandling a pointer to a va_list passed to a function?

The suggested workaround (which wasn't guaranteed to work, but did in practice) is to use arg_copy first:

char *myPrintfInner(const char *message, va_list params)
{
    va_list args_copy;
    va_copy(args_copy, params);

    size_t length = vsnprintf(NULL, 0, message, params);
    char *final = (char *) malloc((length + 1) * sizeof(char));
    int result = vsnprintf(final, length + 1, message, args_copy);

    printf("vsnprintf result: %d\r\n", result);
    printf("%s\r\n", final);

    return final;
}

回答1:


As Mat notes, the problem is that you're reusing the va_list. If you don't want to restructure your code as he suggests, you can use the C99 va_copy() macro, like this:

char *myPrintfInner(const char *message, va_list params)
{
    va_list copy;

    va_copy(copy, params);
    size_t length = vsnprintf(NULL, 0, message, copy);
    va_end(copy);

    char *final = (char *) malloc((length + 1) * sizeof(char));
    int result = vsnprintf(final, length + 1, message, params);

    printf("vsnprintf result: %d\r\n", result);
    printf("%s\r\n", final);

    return final;
}

On compilers that don't support C99, you may be able use __va_copy() instead or define your own va_copy() implementation (which will be non-portable, but you can always use compiler / platform sniffing in a header file if you really need to). But really, it's been 13 years — any decent compiler should support C99 these days, at least if you give it the right options (-std=c99 for GCC).




回答2:


The problem is that (apart from the missing return statement) you're re-using the va_list parameter without resetting it. That's not good.

Try something like:

size_t myPrintfInnerLen(const char *message, va_list params)
{
    return vsnprintf(NULL, 0, message, params);
}

char *myPrintfInner(size_t length, const char *message, va_list params)
{
    char *final = (char *) malloc((length + 1) * sizeof(char));
    int result = vsnprintf(final, length + 1, message, params);

    printf("vsnprintf result: %d\r\n", result);
    printf("%s\r\n", final);

    return final;
}

char *myPrintf(const char *message, ...)
{
    va_list va_args;
    va_start(va_args, message);
    size_t length = myPrintfInnerLen(message, va_args);
    va_end(va_args);
    va_start(va_args, message);
    char *ret = myPrintfInner(length, message, va_args);
    va_end(va_args);
    return ret;
}

(And turn on your compiler's warnings.)

I don't think the footnote you point to means what you think it does. I read it as: if you pass a va_list directly (as a value, not a pointer), the only thing you can do in the caller is va_end it. But if you pass it as a pointer, you could, say, call va_arg in the caller if the callee didn't "consume" all the va_list.

You could try with va_copy though. Something like:

char *myPrintfInner(const char *message, va_list params)
{
    va_list temp;
    va_copy(temp, params);
    size_t length = vsnprintf(NULL, 0, message, temp);
    ...


来源:https://stackoverflow.com/questions/9937505/va-list-misbehavior-on-linux

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!