【问题标题】:Why does this variadic function fail on 4th parameter on Windows x64?为什么这个可变参数函数在 Windows x64 上的第 4 个参数上失败?
【发布时间】:2009-04-07 21:28:05
【问题描述】:

下面是包含可变参数函数和调用可变参数函数的代码。我希望它会适当地输出每个数字序列。它在编译为 32 位可执行文件时会这样做,但在编译为 64 位可执行文件时不会。

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

#ifdef _WIN32
#define SIZE_T_FMT "%Iu"
#else
#define SIZE_T_FMT "%zu"
#endif


static void dumpargs(size_t count, ...) {

    size_t i;
    va_list args;

    printf("dumpargs: argument count: " SIZE_T_FMT "\n", count);

    va_start(args, count);

    for (i = 0; i < count; i++) {

        size_t val = va_arg(args, size_t);
        printf("Value=" SIZE_T_FMT "\n", val);
    }
    va_end(args);
}

int main(int argc, char** argv) {

    (void)argc;
    (void)argv;

    dumpargs(1, 10);
    dumpargs(2, 10, 20);
    dumpargs(3, 10, 20, 30);
    dumpargs(4, 10, 20, 30, 40);
    dumpargs(5, 10, 20, 30, 40, 50);

    return 0;
}

这是编译为 64 位时的输出:

dumpargs: argument count: 1
Value=10
dumpargs: argument count: 2
Value=10
Value=20
dumpargs: argument count: 3
Value=10
Value=20
Value=30
dumpargs: argument count: 4
Value=10
Value=20
Value=30
Value=14757395255531667496
dumpargs: argument count: 5
Value=10
Value=20
Value=30
Value=14757395255531667496
Value=14757395255531667506

编辑:

请注意,可变参数函数拉出size_t 的原因是因为它的实际用途是用于接受指针和长度列表的可变参数函数。自然地,长度参数应该是size_t。在某些情况下,调用者可能会传入一个众所周知的长度:

void myfunc(size_t pairs, ...) {
    va_list args;
    va_start(args, count);

    for (i = 0; i < pairs; i++) {
        const void* ptr = va_arg(args, const void*);
        size_t len = va_arg(args, size_t);
        process(ptr, len);
    }
    va_end(args);
}

void user(void) {
    myfunc(2, ptr1, ptr1_len, ptr2, 4);
}

请注意,传递给myfunc4 可能会遇到上述问题。是的,调用者确实应该使用sizeofstrlen 的结果,或者只是将数字4 放入size_t 某处。但关键是编译器没有捕捉到这一点(可变参数函数的常见危险)。

这里正确的做法是消除可变参数函数,并用提供类型安全的更好的机制取而代之。但是,我想记录这个问题,并收集更详细的信息来说明这个问题在这个平台上存在的确切原因以及它的表现。

【问题讨论】:

  • 好问题。据我所知,只有 C++ 允许未命名的函数参数。我认为即使 C99 也不允许这样做。这一点,在编写纯 C 代码时,你不能这样做。 comeaucomputing.com/techtalk/#argwithnoname

标签: c windows


【解决方案1】:

所以基本上,如果一个函数是可变参数的,它必须符合某个调用约定(最重要的是,调用者必须清理 args,而不是 callie,因为 callie 不知道会有多少个 args)。

它在 4 日开始发生的原因是因为 calling convention used on x86-64。据我所知,Visual C++ 和 gcc 的前几个参数都使用寄存器,然后使用堆栈。

我猜测即使是可变参数函数也是如此(这让我觉得很奇怪,因为它会使 va_* 宏变得更加复杂)。

在 x86 上,标准 C 调用约定是始终使用堆栈。

【讨论】:

    【解决方案2】:

    问题在于您使用 size_t 来表示值的类型。这是不正确的,这些值实际上是 Win64 上的正常 32 位值。

    Size_t 只能用于根据平台的 32 位或 64 位改变大小的值(例如指针)。将代码更改为使用 int 或 __int32,这应该可以解决您的问题。

    这在 Win32 上运行良好的原因是 size_t 是不同大小的类型,具体取决于平台。对于 32 位窗口,它将是 32 位,而在 64 位窗口上,它将是 64 位。因此,在 32 位窗口上,它恰好与您使用的数据类型的大小相匹配。

    【讨论】:

    • 也许我的示例没有适当的真实世界签名:我在接受指针/长度对列表的可变参数函数时遇到了这个问题。自然地,长度参数应该是 size_t。但是,在某些情况下,众所周知的长度会被传递到函数中,并且会失败。
    • 问题是,为什么它与前三个值一起工作? ABI 有什么问题吗?我知道在linux上,有些寄存器是用来传递参数的,难道这三个是在寄存器中传递的,其余的(不起作用)在堆栈上?
    • +1,但请注意 size_t 不是“用于在 32 位或 64 位上改变大小的值”,而是用于 size_t 的东西。 ptrdiff_t,各种大小的int等,都会改变大小,但不要用size_t做那个!
    • @J. Oberhaus:可以使用size_t,只要确保调用站点也会传递一个size_t,即。写 (size_t)10 而不是 10。当你在 C 中传递 10 时,它是一个 int。同样适用于指针,如果要传递NULL ptr,它必须是(void*)0,而不是0。
    • @jpalecek 这是我的一个迫切问题,为什么:这是否发生在所有/大多数编译器上? C 规范是否说明了任何关于保证的内容,或者它是否忽略了这个问题并假设 100% 的论点是正确的?
    【解决方案3】:

    可变参数函数仅经过弱类型检查。特别是,函数签名没有为编译器提供足够的信息来了解函数所假定的每个参数的类型。

    在这种情况下,size_t 在 Win32 上为 32 位,在 Win64 上为 64 位。它必须像这样改变大小才能执行其定义的角色。因此,要让可变参数函数正确提取 size_t 类型的参数,调用者必须确保编译器能够在编译时在调用模块中判断参数属于该类型。

    不幸的是10int 类型的常量。没有定义的后缀字母将常量标记为 size_t 类型。您可以将这一事实隐藏在特定于平台的宏中,但这并不比在调用站点上写 (size_z)10 更清楚。

    由于 Win64 中使用的实际调用约定,它似乎部分工作。从给出的示例中,我们可以看出函数的前四个整数参数是在寄存器中传递的,其余的则在堆栈中。这允许正确读取计数和前三个可变参数。

    但它只有看起来起作用。您实际上正处于未定义行为领域,而“未定义”确实意味着“未定义”:任何事情都可能发生。 在其他平台上,任何事情都可能发生。

    因为可变参数函数是隐式不安全的,所以编码器有一个特殊的负担,以确保在编译时已知的每个参数的类型与在运行时假定的参数类型相匹配。

    在某些接口众所周知的情况下,可能会警告类型不匹配。例如,gcc 经常可以识别出printf() 的参数类型与格式字符串不匹配,并发出警告。但是在所有可变参数函数的一般情况下这样做是困难

    【讨论】:

      【解决方案4】:

      这是因为size_t 在 32 位 Windows 上定义为 32 位值,在 64 位 Windows 上定义为 64 位值。当第 4 个参数传递给可变参数函数时,高位似乎未初始化。抽出来的第4个和第5个值其实是:

      Value=0xcccccccc00000028
      Value=0xcccccccc00000032
      

      我可以通过对所有参数进行简单的强制转换来解决这个问题,例如:

      dumpargs(5, (size_t)10, (size_t)20, (size_t)30, (size_t)40, (size_t)50);
      

      然而,这并不能回答我所有的问题;如:

      • 为什么它是第四个参数?可能是因为前 3 个在寄存器中?
      • 如何以一种类型安全的可移植方式避免这种情况?
      • 在使用 64 位值的其他 64 位平台上是否会发生这种情况(忽略 size_t 在某些 64 位平台上可能是 32 位)?
      • 无论目标平台如何,我都应该将这些值作为 32 位值提取出来吗?如果将 64 位值推入可变参数函数会导致问题吗?
      • 标准对这种行为有何规定?

      编辑:

      我真的很想从The Standard 那里得到一个报价,但它不能超链接,而且要花钱给purchase and download。因此,我认为引用它会侵犯版权。

      引用comp.lang.c FAQ,很明显,当编写一个采用variable number of arguments 的函数时,您无法为类型安全做任何事情。由make sure that each argument either perfectly matches or is explicitly cast 的调用者决定。没有隐式转换。

      这对于那些了解 C 和 printf 的人来说应该是显而易见的(请注意 gcc 具有 check printf-style format strings 的特性),但不那么明显的是不仅类型不是隐式转换,而且如果大小的类型与提取的内容不匹配,您可能有未初始化的数据,或通常未定义的行为。放置参数的“槽”可能未初始化为 0,并且可能没有“槽”——在某些平台上,您可以传递 64 位值,并在可变参数函数中提取两个 32 位值.这是未定义的行为。

      【讨论】:

        【解决方案5】:

        如果您是编写此函数的人,那么您的工作就是正确编写可变参数函数和/或正确记录函数的调用约定。

        您已经发现 C 对类型的处理既快又松(另请参阅签名和提升),因此显式转换是最明显的解决方案。这在使用 UL 或 ULL 等明确定义整数常量时经常出现。

        大多数对传递值的完整性检查将是特定于应用程序的或不可移植的(例如指针有效性)。您可以使用诸如强制发送预定义的哨兵值之类的技巧,但这并非在所有情况下都不会出错。

        最佳做法是大量记录文档、执行代码审查和/或编写单元测试时牢记此错误。

        【讨论】:

          猜你喜欢
          • 2011-07-28
          • 1970-01-01
          • 2019-01-16
          • 1970-01-01
          • 1970-01-01
          • 2022-12-08
          • 2021-08-14
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多