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

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

编辑:

请注意,variadic函数将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地方。 但重点在于编译器没有捕捉到这个问题(可变参数函数常见的危险)。

这里要做的正确的事情是消除可变参数,并用一个更好的机制来代替它,从而提供types安全性。 但是,我想logging下这个问题,并且收集更详细的信息,确切地说明在这个平台上存在这个问题的原因,并且像这样performance出来。

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

之所以开始发生在4日是因为在x86-64上使用的调用约定 。 据我所知,visual c ++和gcc都使用寄存器作为前几个参数,然后使用堆栈。

我猜测,即使是可变参数(这种情况确实使我感到奇怪,因为它会使va_宏变得更加复杂)也是如此。

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

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

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

这在Win32上正常工作的原因是,size_t是一个不同的大小类型取决于平台。 32位窗口将是32位,而在64位窗口将是64位。 所以在32位的窗口中,恰好与您所使用的数据类型的大小相匹配。

可变参数函数只是弱类型检查。 特别是,函数签名不能提供足够的信息让编译器知道函数假设的每个参数的类型。

在这种情况下,Win32上的size_t是32位,Win64上是64位。 它必须像这样大小不一,才能完成其定义的角色。 因此,对于一个可变参数函数来说,正确的参数是size_t类型的,调用者必须确保编译器能够在调用模块的编译时间告诉参数是那种类型的。

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

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

但它似乎只是工作。 你实际上站在未定义的行为领域,而“未定义”确实意味着“未定义”:任何事情都可能发生。 在其他平台上,任何事情都可能发生。

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

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

原因是因为size_t在32位Windows上定义为32位值,而在64位Windows上定义为64位值。 当第四个参数传入可变参数函数时,高位看起来是未初始化的。 被拉出的第四和第五个值实际上是:

 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那里得到一个报价,但这不是超链接的,而且花钱购买和下载 。 所以我相信引用它会是版权侵犯。

通过引用comp.lang.c FAQ ,可以清楚地看出,在编写一个带有可变数量参数的函数时 ,对于类型安全来说,没有什么可以做的。 由调用者来确定每个参数要么完全匹配,要么被明确地施放 。 没有隐式转换。

对于那些理解C和printf人来说,这一点应该是显而易见的(注意gcc有一个检查printf风格的格式字符串的功能 ),但不是那么明显的是,不仅隐式地转换类型,而且如果类型与提取的内容不匹配,可以有未初始化的数据或一般未定义的行为。 参数所在的“插槽”可能不会被初始化为0,并且可能没有“插槽” – 在某些平台上,您可以传递一个64位值,并在可变参数函数中提取两个32位值。 这是未定义的行为。

如果你是编写这个函数的人,那么你的工作是正确地写入可变参数函数,并且/或者正确地记录你函数的调用约定。

你已经发现C在类型上快速松散(参见签名和提升),所以显式的转换是最明显的解决方案。 这是常见的整数常量显式定义像UL或ULL的东西。

对传递值的大多数理智检查将是应用程序特定的或不可移植的(例如指针有效性)。 你可以使用黑客来强制发送预定义的哨兵值,但是在所有情况下都不是绝对可靠的。

最好的做法是重写文档,执行代码评论,和/或编写单元测试。