【问题标题】:va_list misbehavior on LinuxLinux 上的 va_list 不当行为
【发布时间】:2012-03-30 05:37:09
【问题描述】:

我有一些代码可以将可变参数转换为va_list,然后将列表传递给调用vsnprintf 的函数。这在 Windows 和 OS X 上运行良好,但在 Linux 上失败并出现奇怪的结果。

在以下代码示例中:

#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;
}

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

我看过但未涉及该主题的相关主题:


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

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

per the C99 spec(第 7.15 节中的脚注)应该可以工作:

允许创建指向 va_list 的指针并传递该指针 到另一个函数,在这种情况下,原始函数可能会使 在其他函数返回后进一步使用原始列表。

但是我的编译器(C99 模式下的 gcc 4.4.5)给了我这个关于 myPrintfInner 第一行的错误:

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

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


找到这个:Is GCC mishandling a pointer to a va_list passed to a function?

建议的解决方法(不能保证有效,但在实践中确实有效)是首先使用arg_copy

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

【问题讨论】:

  • 您的myPrintf 函数缺少return 语句。我希望你的编译器会警告你。
  • 呸,骗子!复制粘贴失败。
  • 您的新代码与旧代码做的事情完全相同:original 指向params,因此传递*original 与传递params 完全相同。您真正的问题似乎是您不了解va_lists 的工作原理:它们本质上是指向参数堆栈的指针,并且指针在使用时会递增。因此,当您使用相同的 va_list 两次时,第二次您将指针递增到参数列表的末尾。

标签: c variadic-functions


【解决方案1】:

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

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

在不支持 C99 的编译器上,您可能可以使用 use __va_copy() instead or define your own va_copy() implementation(这将是不可移植的,但如果您确实需要,您始终可以在头文件中使用编译器/平台嗅探)。但实际上,已经有 13 年了——现在任何体面的编译器都应该支持 C99,至少如果你给它正确的选项(-std=c99 GCC)。

【讨论】:

  • 我正在使用带有 C99 的 GCC,但如果您看到我修改后的问题,我已经尝试过,但结果并不好。似乎在 x86_64 上被破坏了。
  • 这很奇怪。我刚刚在 x86_64 Linux 上尝试了上面给出的确切代码,它对我有用。
  • 这是我的确切经验:pastebin.ca/2133787 - 我认为我所做的与你没有什么不同。你用的是什么版本的 gcc?我用该信息更新了我的帖子。
  • 您的myPrintf 函数中有一些杂物,可能是早期尝试遗留下来的。事实上,你根本没有打电话给myPrintfInner,这可能就是修复它没有帮助的原因。 (提示:在试验代码时,始终保留一个试验前副本。版本控制系统对此很有用,但即使是一个普通的旧重命名备份副本也可以工作。学习使用 diff .)
  • 嗯,这很尴尬。我想我应该更加尊重代码碎片。
【解决方案2】:

问题在于(除了缺少的 return 语句)您正在重新使用 va_list 参数而不重置它。这不好。

尝试类似:

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

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

我不认为你指向的脚注意味着你认为它的作用。我把它读作:如果你直接传递一个va_list(作为一个值,而不是一个指针),你在调用者中唯一能做的就是va_end它。但是如果你将它作为指针传递,你可以说,如果被调用者没有“消耗”所有va_list,则在调用者中调用va_arg

不过,您可以尝试使用va_copy。比如:

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

【讨论】:

  • 谢谢。你能看看我修改过的帖子吗?
  • 用另一个选项编辑了我的答案。
  • Works For Me(tm) on x86_64 with GCC,虽然我首先“消费”了副本。但我真的建议坚持在调用者中重置 va_list。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2017-05-12
  • 2019-03-02
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-04-26
相关资源
最近更新 更多