为什么这个可变参数函数在Windows x64上的第4个参数上失败?

时间:2009-04-07 21:28:06

标签: c windows

下面是包含可变参数函数和调用可变参数函数的代码。我希望它会适当地输出每个数字序列。它在编译为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);
}

请注意,传递到4的{​​{1}}可能会遇到上述问题。是的,确实调用者应该使用myfuncsizeof的结果,或者只是将数字strlen放到4的某处。但重点是编译器没有捕获这个(可变函数的常见危险)。

这里做的正确的事情是消除可变参数功能,并用更好的机制来替换它,提供类型安全性。但是,我想记录这个问题,并收集更详细的信息,以确定此平台上存在此问题的确切原因并进行清单。

5 个答案:

答案 0 :(得分:6)

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

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

我猜这种情况即使对于可变函数也是如此(因为它会使va_ *宏变得更复杂,因此会让我感到奇怪)。

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

答案 1 :(得分:4)

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

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

这在Win32上正常工作的原因是size_t是一个不同大小的类型,具体取决于平台。对于32位窗口,它将是32位,在64位窗口上,它将是64位。因此,在32位窗口上,它恰好匹配您正在使用的数据类型的大小。

答案 2 :(得分:2)

原因是因为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);

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

  • 为什么这是第四个论点?可能是因为前三个是寄存器?
  • 如何以类型安全的便携方式避免这种情况?
  • 这是否会在使用64位值的其他64位平台上发生(忽略某些64位平台上size_t可能是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位值。这是未定义的行为。

答案 3 :(得分:2)

可变函数只是弱类型检查。特别是,函数签名没有为编译器提供足够的信息来知道函数假定的每个参数的类型。

在这种情况下,size_t在Win32上为32位,在Win64上为64位。它必须在大小上变化才能执行其定义的角色。因此,对于一个variadic函数来正确地提取类型为size_t的参数,调用者必须确保编译器可以在调用模块的编译时告诉该参数是该类型的。

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

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

然而只有出现才能正常工作。你实际上站在Undefined Behavior领域,“undefined”确实意味着“未定义”:任何事情都可能发生。 在其他平台上,任何事情都可能发生。

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

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

答案 4 :(得分:1)

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

你已经发现C对类型进行快速松散(参见签名和推广),因此显式强制转换是最明显的解决方案。通常使用UL或ULL等明确定义的整数常量可以看到这种情况。

对传递值的大多数健全性检查将是特定于应用程序或不可移植的(例如,指针有效性)。您可以使用hack,例如强制要求发送预定义的哨兵值,但这并非在所有情况下都是绝对可靠的。

最佳做法是记录大量文档,执行代码评审和/或编写单元测试。