【问题标题】:Is GCC mishandling a pointer to a va_list passed to a function?GCC 是否错误地处理了指向传递给函数的 va_list 的指针?
【发布时间】:2011-11-08 07:49:56
【问题描述】:

问题'Pass va_list or pointer to va_list?' 的答案引用了标准(ISO/IEC 9899:1999 - §7.15 'Variable arguments <stdarg.h>, footnote 212)明确表示:

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

我正在编译一些代码,可以通过以下示例来说明(实际代码要复杂得多,原始函数比这里显示的要多得多)。

vap.c

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

static void test_ptr(const char *fmt, va_list *argp)
{
    int x;
    x = va_arg(*argp, int);
    printf(fmt, x);
}

static void test_val(const char *fmt, va_list args)
{
    test_ptr(fmt, &args);
}

static void test(const char *fmt, ...)
{
    va_list args;
    va_start(args, fmt);   /* First use */
    test_val(fmt, args);
    va_end(args);
    va_start(args, fmt);   /* Second use */
    test_ptr(fmt, &args);
    va_end(args);
}

int main(void)
{
    test("%d", 3);
    return 0;
}

错误信息

当我编译它时(在带有 GCC 4.1.2 或 4.5.1 的 RHEL5 上),我收到以下错误消息。请注意 4.5.1 错误消息提供了多少信息 - GCC 团队的改进值得祝贺!

$ gcc --version
gcc (GCC) 4.5.1
Copyright (C) 2010 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

$ /usr/bin/gcc --version
gcc (GCC) 4.1.2 20080704 (Red Hat 4.1.2-44)
Copyright (C) 2006 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

$ gcc -c vap.c
vap.c: In function ‘test_val’:
vap.c:13:5: warning: passing argument 2 of ‘test_ptr’ from incompatible pointer type
vap.c:4:13: note: expected ‘struct __va_list_tag (*)[1]’ but argument is of type ‘struct __va_list_tag **’
$ /usr/bin/gcc -c vap.c
vap.c: In function ‘test_val’:
vap.c:13: warning: passing argument 2 of ‘test_ptr’ from incompatible pointer type
$ 

我在使用 GCC/LLVM 4.2.1 和 GCC 4.6.1 的 MacOS X Lion 上收到相同的消息:

$ /usr/bin/gcc --version
i686-apple-darwin11-llvm-gcc-4.2 (GCC) 4.2.1 (Based on Apple Inc. build 5658) (LLVM build 2335.15.00)
Copyright (C) 2007 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

$ gcc --version
gcc (GCC) 4.6.1
Copyright (C) 2011 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

$

问题

  • 有人能解释为什么test_val() 函数不能将va_list 作为参数传递给test_ptr(),而test() 函数(它创建了va_list)可以吗?

  • GCC 抱怨 test_val() 中指针的间接传递是否正确?

在这两种情况下,我都可以模糊地看到答案,但我无法简洁地描述它。我认为test_val() 中的代码滥用了va_list,代码无法编译很好——但我想在修复它之前确定一下。


2012-03-30 更新

这周我去处理有问题的代码。在进行更改之前,我去查找了错误函数的使用位置——它们不是!因此,我通过删除函数(4 个外部可见但未使用的函数,加上 2 个包含有问题代码的静态函数)解决了我的编译错误问题。这比必须弄清楚如何处理混乱要简单得多。 (这也解释了为什么从来没有任何证据表明代码会导致运行时问题。)

【问题讨论】:

    标签: c variadic-functions


    【解决方案1】:

    这是一个已知问题。在某些架构(尤其是 x86-64)上,va_list 需要比指向堆栈的简单指针更复杂,例如,因为某些参数可能以其他方式在寄存器中或带外传递(参见 @ 987654321@ 用于 x86-64 上va_list 的定义)。

    在此类架构上,通常将va_list 设为数组类型,以便va_list 类型的参数将调整为指针类型,而不是整个结构,只需要传递一个指针。

    这不应该违反 C 标准,它只说 va_list 必须是一个完整的对象类型,甚至明确说明传递 va_list 参数实际上可能不会克隆必要的状态:va_list 对象如果它们作为参数传递并在被调用函数中使用,则具有不确定的值。

    但是即使将va_list 设为数组类型是合法的,它仍然会导致您遇到的问题:由于va_list 类型的参数具有“错误”类型,例如struct __va_list_tag * 而不是struct __va_list_tag [1],它在数组和指针之间的差异很重要的情况下会爆炸。

    真正的问题不是 gcc 警告的类型不匹配,而是按指针而不是按值参数传递语义:test_val() 中的 &amp;args 指向中间指针变量而不是 va_list 对象;忽略警告意味着您将在指针变量上调用 test_ptr() 中的 va_arg(),这应该会返回垃圾(如果幸运的话,可能会返回段错误)并破坏堆栈。

    一种解决方法是将您的va_list 包装在一个结构中并传递它。另一个解决方案I've seen in the wild,甚至是here on SO,是使用va_copy 创建参数的本地副本,然后传递一个指向该参数的指针:

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

    这在实践中应该可行,但从技术上讲,它可能是也可能不是未定义的行为,具体取决于您对标准的解释:

    如果va_copy() 被实现为宏,则不会执行任何参数调整,并且args 不是va_list 类型可能很重要。然而,由于va_copy() 是宏还是函数未指定,人们可能会争辩说它至少可能是一个函数,并且参数调整隐含在为宏给出的原型。要求官员澄清甚至提交缺陷报告可能是个好主意。

    您还可以使用构建系统通过定义像 HAVE_VA_LIST_AS_ARRAY 这样的配置标志来处理该问题,这样您就可以为您的特定架构做正确的事情:

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

    【讨论】:

    • 我认为即使是标准也允许 va_copy 在平台上作为函数实现,这样做会产生正确的语义,它会隐含地要求它在以下情况下作为宏实现否则无法实现正确的语义。
    【解决方案2】:

    此问题并非特定于 va_list。以下代码会产生类似的警告:

    typedef char *test[1];
    
    void x(test *a)
    {
    }
    
    void y(test o)
    {
        x(&o);
    }
    

    问题源于 C 处理也是数组的函数变量的方式,这可能是由于数组作为引用而不是值传递的事实。 o 的类型与test 类型的局部变量的类型不同,在这种情况下:char *** 而不是char *(*)[1]

    回到手头的原始问题,解决它的简单方法是使用容器结构:

    struct va_list_wrapper {
        va_list v;
    };
    

    并且将点传递给它不会有打字问题。

    【讨论】:

    • 以“o 的类型”开头的句子应以char **char *[1] 结尾。给出的实际类型是 &amp;oa 的类型。
    【解决方案3】:

    正如其他人所指出的,当va_list 是数组类型时,此问题就会显现出来。这是标准允许的,它只说va_list 必须是一个“对象类型”

    您可以像这样修复test_val() 函数:

    static void test_val(const char *fmt, va_list args)
    {
        va_list args_copy;
    
        /* Note: This seemingly unnecessary copy is required in case va_list
         * is an array type. */
        va_copy(args_copy, args);
        test_ptr(fmt, &args_copy);
        va_end(args_copy);
    }
    

    【讨论】:

      【解决方案4】:

      我认为va_list 必须声明为数组类型,当声明为函数的参数时,它“归结为”指针类型。因此,&amp; 应用于 test_val 中的 va_list 类型会产生一个指向指针类型的指针,而不是指向数组类型的指针,但是,test_ptr 函数将其参数之一声明为指向数组的指针type,也就是test函数中实际提供的。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2018-04-05
        • 1970-01-01
        • 2015-08-05
        • 2021-07-17
        • 1970-01-01
        • 2013-07-21
        • 1970-01-01
        • 2017-08-09
        相关资源
        最近更新 更多