关于Linux上的c:va_list行为异常

va_list misbehavior on Linux

我有一些代码将可变参数转换为va_list,然后将列表传递给一个函数,然后调用vsnprintf。在Windows和OS X上可以正常运行,但在Linux上却无法正常运行。

在以下代码示例中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
#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

"
, result);
    printf("%s

"
, 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

"
, result);
    printf("%s

"
, 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!

"
, result);
    }
    else
    {
        printf("Test succeeded.

"
);
    }

    return 0;
}

第二个vsnprintf调用的输出为17,strcmp的结果为31;但是我不明白为什么vsnprintf会返回17,因为This is a test.是15个字符,添加NULL会得到16。

我已经看到但未解决该主题的相关主题:

  • 传递va_list或指向va_list的指针?
  • 将一个va_list作为参数传递给另一个

使用@Mat的答案(我正在重用va_list对象,这是不允许的),这正好涉及到我链接到的第一个相关线程。所以我尝试了这段代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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

"
, result);
    printf("%s

"
, final);

    return final;
}

根据C99规范(第7.15节中的脚注),应该可以运行以下命令:

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.

但是我的编译器(在C99模式下为gcc 4.4.5)给我有关myPrintfInner第一行的错误:

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

生成的二进制文件产生与第一次相同的效果。

发现了这一点:GCC是否对传递给函数的va_list的指针进行了错误处理?

建议的解决方法(虽然不能保证能正常工作,但实际上是可以做到的)是首先使用arg_copy

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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

"
, result);
    printf("%s

"
, final);

    return final;
}


正如Mat所指出的那样,问题在于您正在重用va_list。如果您不想按照他的建议重组代码,则可以使用C99 va_copy()宏,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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

"
, result);
    printf("%s

"
, final);

    return final;
}

在不支持C99的编译器上,您可以改用__va_copy()或定义自己的va_copy()实现(这是不可移植的,但是如果确实需要,您始终可以在头文件中使用编译器/平台监听)需要)。但是,实际上,如今,任何体面的编译器都应支持C99已有13年了,至少如果您为其提供了正确的选项(对于GCC,则为-std=c99)。


问题是(除了缺少的return语句之外)您正在重新使用va_list参数而不进行重新设置。这不好。

尝试类似的东西:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
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

"
, result);
    printf("%s

"
, 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;
}

(并打开编译器的警告。)

我认为您指出的脚注并不意味着您认为的含义。我将其读取为:如果直接传递va_list(作为值,而不是指针),则在调用方中唯一可以做的就是va_end。但是,如果将其作为指针传递,则可以说,如果被叫方没有"消耗"所有va_list,则可以在调用方中调用va_arg

您可以尝试使用va_copy。就像是:

1
2
3
4
5
6
char *myPrintfInner(const char *message, va_list params)
{
    va_list temp;
    va_copy(temp, params);
    size_t length = vsnprintf(NULL, 0, message, temp);
    ...