在编译器之间匹配 va_list 类型

Posted

技术标签:

【中文标题】在编译器之间匹配 va_list 类型【英文标题】:Matching va_list types between compilers 【发布时间】:2010-09-29 18:31:31 【问题描述】:

我有一个由一堆动态加载的模块组成的项目。最初,一切都是用 MSVC 2003 构建的,但最近我一直在努力让它与 GCC 一起工作。一切都很顺利,除了一个问题。对于 64 位代码,GCC 和 MSVC 不同意 va_list 是什么。对于 32 位,一切似乎都很好。 64 位不匹配导致的问题是,使用一个编译器构建的模块有一个带有 va_list 参数的公共函数,并且该函数是从另一个编译器构建的模块调用的。

除了第 7.15 节变量参数<stdarg.h>,第 3 段之外,规范没有说明 va_list 是什么:

声明的类型是

va_list

这是一个对象类型,适合保存宏va_startva_argva_endva_copy

那段只是意味着这都是依赖于编译器的东西 - 那么,有没有办法让这两个编译器就 64 位 va_list 的内容达成一致?为了对我的系统影响最小,最好让 GCC 与 MSVC va_list 匹配,但我会采取任何我能得到的解决方案。

感谢您的帮助!

编辑:

我进行了一些 32 位测试,但我也遇到了问题,这让我感到惊讶,因为据说任何 32 位英特尔平台之间没有 ABI 差异。我使用的 MSVC 代码库将所有可变参数函数宏定义为:

typedef char *va_list;
#define intsizeof(n)    ((sizeof(n) + sizeof(int) - 1) &~(sizeof(int) - 1))
#define va_start(ap, v) (ap = (va_list)&(v) + intsizeof(v))
#define va_arg(ap, t)   (*(t *) ((ap += intsizeof(t)) - intsizeof(t)))
#define va_end(ap)      (ap = (va_list)0)

我已经从实际项目中进行了一些简化,但这是我用于测试的代码。使用 GCC,这段代码肯定没有正确地得到我的论点。也许这只是一个错误,正如 Zack 在下面建议的那样?

再次编辑:

我得到以下 32 位测试应用程序的工作结果:-O0-O0-O2,但不是 -O3-Os-Oz

typedef char *va_list;
#define intsizeof(n)    ((sizeof(n) + sizeof(int) - 1) &~(sizeof(int) - 1))
#define va_start(ap, v) (ap = (va_list)&(v) + intsizeof(v))
#define va_arg(ap, t)   (*(t *) ((ap += intsizeof(t)) - intsizeof(t)))
#define va_end(ap)      (ap = (va_list)0)

int printf(const char *format, ...);

int f(int n, ...)

  int r = 0;
  va_list ap;

  va_start(ap, n);  
  while (n--)
    r = va_arg(ap, int);
  va_end(ap);

  return r;


int main(int argc, char **argv)

  int r;

  r = f(1, 1, 2, 3, 4, 5);
  printf("%x\n", r);

  r = f(2, 1, 2, 3, 4, 5);
  printf("%x\n", r);

  r = f(3, 1, 2, 3, 4, 5);
  printf("%x\n", r);

  r = f(4, 1, 2, 3, 4, 5);
  printf("%x\n", r);

  r = f(5, 1, 2, 3, 4, 5);
  printf("%x\n", r);

  return 0;

【问题讨论】:

呃。您将 MSVC 的 <stdarg.h> 中的 va_* 定义复制到一个文件中,然后用 GCC 编译该文件,是这样吗?因为那绝对行不通,并且不会告诉您任何有用的信息。您绝对必须使用 GCC 的 <stdarg.h> 来定义使用 GCC(以及用于 MSVC 的 MSVC)编译的可变参数函数,否则您的代码被错误编译。 这个测试你需要做的是将f移动到它自己的文件中,将va_*的所有手定义替换为#include <stdarg.h>,在main上面放上“extern int f(int n, ...);”那个文件,用 GCC 编译一个,用 MSVC 编译另一个,然后链接这两个目标文件。应该在 x32 或 x64 上在任一方向(MSVC 调用 GCC 或 GCC 调用 MSVC)上工作。跨度> 是的,当关闭内联或将其放入单独的编译单元时,它工作得很好。无论如何,这是与不匹配的 va_list 类型完全不同的问题,正如您所说,这是编译器的错误。 【参考方案1】:

由于 MSVC 定义了 Win64 ABI,您在 GCC 中发现了一个错误。请在GCC bugzilla举报。

【讨论】:

va_list 不是 ABI 的一部分吗?无论如何,我的长期计划是使用 clang/llvm,所以我想我应该尽早检查一下。很可能 GCC 在较新的版本中还可以——在这种情况下,我被困在一种“特殊”的旧 GCC 上。 肯定 va_list 是 ABI 的一部分。 ABI 的目的是确保给定目标 CPU+OS 的所有编译器生成兼容的代码,其中包括可变参数函数。 ...我最近看到一堆 GCC 的 Win64 支持补丁,所以这可能适用于当前版本或至少是开发树。 @Zack,可变参数函数工作正常——仅仅传递 va_list 本身是行不通的。感谢您的健全性检查 - 我会做更多的测试,看看会发生什么。 这也确实应该起作用 - 考虑vfprintf 和类似的 - 但有一些陷阱。尝试获取va_list 对象的地址并传递它,而不是传递对象本身。 (短版的陷阱:将 va_list 对象按值传递给另一个函数后,它在调用者中处于不确定状态,唯一可移植的事情就是立即调用 va_end 。事实并非如此如果你传递一个指向对象的指针。) 哦,还有...将指针传递给指针与传递指针非常不同。【参考方案2】:

由于va_list 似乎没有 ABI(或者至少 MSVC 和 GCC 不同意 ABI),您可能需要自己编组这些参数。

我能想到的解决这个问题的最直接方法是将变量参数编组到动态分配的内存块中,并将指针传递给该块。

当然,这样做的缺点是完全改变了当前使用 va_args 的函数的接口。

【讨论】:

但我必须知道有多少参数 才能将它们粘贴在动态块中。这意味着每个想要执行va_start 然后使用va_list 调用另一个函数的函数都必须知道如何识别它们 - 对于printf 类似的函数,这意味着大量额外的解析,其中传递va_list 的重点是将所有格式字符串解析合并到一个函数中(如vsprintf)。【参考方案3】:

因为没有关于如何处理 va_args 的标准,如果您需要此功能在交叉编译平台中保持一致,您最好推出自己的版本。我们没有这样做,并且最近在为我们的代码库支持其他目标时被烧毁了多次。如果其他人有更好的解决方案,我很乐意犯错:)

【讨论】:

好的,那么我如何推出自己的版本?我写了一个适用于 IA32 的,但 gcc 似乎并没有像我希望的那样为 Win64 泄露参数。【参考方案4】:

尝试通过汇编级调试器(例如 ollydbg)运行它,因为您的问题可能不在于 va_args,而在于编译器期望传递 args 的方式(gcc 可能期望它们采用 linux 格式,由于 msvc 正在使用 win64 __fastcall),如果有的话,这将使情况更加清晰。另一种相当老套的方法是尝试修改用于 64 位参数的定义,例如将 msvc 宏导入 gcc 标头(当然使用项目本地副本),看看是否有任何补救措施。

【讨论】:

使用不同编译器构建的模块之间的可变函数调用,因此 ABI 匹配。只是 va_list 的实际内部实现让我很苦恼。【参考方案5】:

您使用的是什么类型的?严格来说,WinXX 只为可变参数 (see documentation for wsprintf in user32.dll) 定义字符、字符串、指针和整数的行为。因此,如果您传递浮点值或结构,则结果在技术上未针对 Windows 平台指定。

【讨论】:

这个项目中没有任何类型的浮点,所以应该没问题。如果建筑物被扔掉,我会感到惊讶,但我可以调查一下。通常只打印整数类型和字符串。 我认为这个答案完全是错误的,顺便说一句...user32.dllwsprintf 版本没有提到“%f”,而朋友在其文档中没有提到 意味着您通常不能在 win32 上通过 varargs 传递浮点数。 C 标准确实需要通过可变参数传递浮点和结构才能工作。 @Zack,我更新了我的答案以反映这不是为 Windows 平台指定的。这确实意味着您不应该将这些类型传递给 wsprintf(...) 这是一个 user32.dll 函数。对于在 Windows 之上实现的语言(例如 C 或 C++)来说,这没有任何意义。所以如果你想要编译器之间的兼容性,你应该坚持平台强制你支持的类型。在这种情况下,您很幸运 wsprintf 是从 user32.dll 导出的。 @MSN:如果 WinXX 不支持通过 varargs 传递浮点类型和结构,它就不能声称是 C 的一致实现(微软肯定会做出这样的声明)。而且,除非每个人都可以在某个地方获得wsprintf 的另一种实现而不是那个,否则那个可能确实支持浮点,并且您的所有断言都基于文档中的疏忽。我向他们的文档反馈地址发送了一个关于它的查询。 @Zack,WinXX 不是 C 的一致实现。C 语言的绑定具有由 WinXX 平台定义的语义。 wsprintf 是使用可变参数的函数之一,该可变参数指定为仅采用指针、字符串和整数。如果您搜索“wsprintf windows floating point”,您会发现它不支持浮点值。它基本上是一个不推荐使用的函数,但它确实为整数和指针的可变参数隐式指定了 ABI。如果您想知道,C 标准没有为任何平台上的可变参数指定特定的 ABI。

以上是关于在编译器之间匹配 va_list 类型的主要内容,如果未能解决你的问题,请参考以下文章

jni 编译错误error: unknown type name '__va_list'

.NET 参考在我的项目(类库)中不可用。获得编译器错误“处理器架构之间的不匹配”

Rust 不匹配的类型混淆编译器

编译器检查函数声明和函数实际调用参数之间的格式化字符串是否匹配

va_list 在 C 和 C++ 之间是不是不兼容? [关闭]

为啥编译器告诉我没有运算符与我的“if”和“else if”语句中的操作数类型匹配?