【问题标题】:__cdecl or __stdcall on Windows?__cdecl 或 __stdcall 在 Windows 上?
【发布时间】:2011-06-28 18:07:57
【问题描述】:

我目前正在为 Windows 开发一个 C++ 库,它将作为 DLL 分发。我的目标是最大化二进制互操作性;更准确地说,我的 DLL 中的函数必须可以从使用多个版本的 MSVC++ 和 MinGW 编译的代码中使用,而无需重新编译 DLL。但是,我对哪种调用约定最好感到困惑,cdeclstdcall

有时我会听到诸如“C 调用约定是唯一保证跨编译器相同的”之类的声明,这与“There are some variations in the interpretation of cdecl, particularly in how to return values”之类的声明形成鲜明对比。这似乎并没有阻止某些库开发人员(例如 libsndfile)在他们分发的 DLL 中使用 C 调用约定,而没有任何明显的问题。

另一方面,stdcall 调用约定似乎定义明确。据我所知,基本上所有 Windows 编译器都必须遵循它,因为它是用于 Win32 和 COM 的约定。这是基于这样的假设,即不支持 Win32/COM 的 Windows 编译器不会很有用。论坛上发布的许多代码 sn-ps 将函数声明为 stdcall,但我似乎无法找到一篇清楚地解释为什么的帖子。

那里有太多相互矛盾的信息,我运行的每次搜索都会给我不同的答案,这并不能真正帮助我在两者之间做出决定。我正在寻找一个清晰、详细、有争议的解释,说明为什么我应该选择一个而不是另一个(或者为什么两者是等价的)。

请注意,这个问题不仅适用于“经典”函数,还适用于虚拟成员函数调用,因为大多数客户端代码将通过“接口”与我的 DLL 交互,纯虚拟类(遵循描述的模式,例如 herethere)。

【问题讨论】:

  • “cdecl 的解释有一些变化,特别是在如何返回值方面” - 这大致意味着在 x86 上使用 cdecl 的不同操作系统可能使用不同的 cdecl 变体,即它们使用不同的 ABI两者都称为“cdecl”。 Windows 确定了变化,任何在 Windows 上运行并且不尊重 Windows 选择的实现都将无法调用 Windows cdecl 函数,所以正如你所说的 stdcall,它对 Windows 编程没用,并且有一些标准 C很难实现的功能。
  • __stdcall 调用约定用于调用 Win32 API 函数。 docs.microsoft.com/en-us/cpp/cpp/stdcall?view=vs-2017

标签: c++ windows dll calling-convention binary-compatibility


【解决方案1】:

我刚刚做了一些实际测试(使用 MSVC++ 和 MinGW 编译 DLL 和应用程序,然后将它们混合)。看起来,使用cdecl 调用约定我得到了更好的结果。

更具体地说:stdcall 的问题在于 MSVC++ 会破坏 DLL 导出表中的名称,即使在使用 extern "C" 时也是如此。例如,foo 变为 _foo@4。这仅在使用__declspec(dllexport) 时发生,在使用 DEF 文件时不会发生;但是,在我看来,DEF 文件是一个维护麻烦,我不想使用它们。

MSVC++ 名称修改带来了两个问题:

  • 在 DLL 上使用 GetProcAddress 会稍微复杂一些;
  • 默认情况下,MinGW 不会在修饰名称前添加取消标记(例如,MinGW 将使用 foo@4 而不是 _foo@4),这会使链接复杂化。此外,它还引入了看到“非下划线版本”的 DLL 和应用程序在野外弹出与“下划线版本”不兼容的风险。

我尝试了cdecl 约定:MSVC++ 和 MinGW 之间的互操作性完美运行,开箱即用,并且名称在 DLL 导出表中保持未修饰。它甚至适用于虚拟方法。

出于这些原因,cdecl 显然是我的赢家。

【讨论】:

  • 值得注意的是,这不适用于 64 位 DLL,因为 __stdcall 和 __cdecl 通常都被忽略 - 因此在导出的 C 函数上不会发生名称修改。
  • @jtbr 导出的 C++ 函数怎么样(即没有 extern "C")?
  • @Ela782 C++ 总会有一些名称修饰,以支持函数重载。但这不是标准化的,因此可能因编译器而异。
  • @jtbr 对不起,我的意思不是修改这个名字。我的意思是在具有导出 C++ 函数的 64 位 DLL 中是否也忽略 __stdcall 和 __cdecl。
  • @Ela782 是的;我相信 __stdcall 和 __cdecl 在所有 x86-64 编译中都被忽略了。见wikipedia
【解决方案2】:

两种调用约定的最大区别在于“__cdecl”将函数调用后的堆栈平衡负担放在调用者身上,这允许函数具有可变数量的参数。 “__stdcall”约定本质上“更简单”,但在这方面不太灵活。

另外,我相信托管语言默认使用 stdcall 约定,因此如果您使用 cdecl,任何使用 P/Invoke 的人都必须明确声明调用约定。

因此,如果您的所有函数签名都将被静态定义,我可能会倾向于 stdcall,如果不是 cdecl。

【讨论】:

    【解决方案3】:

    在安全性方面,__cdecl 约定“更安全”,因为它是调用者需要释放堆栈。 __stdcall 库中可能发生的情况是开发人员可能忘记正确释放堆栈,或者攻击者可能通过破坏 DLL 的堆栈(例如通过 API 挂钩)注入一些代码,然后调用者不会检查这些堆栈。 我没有任何 CVS 安全示例表明我的直觉是正确的。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2011-10-14
      • 1970-01-01
      • 1970-01-01
      • 2023-03-19
      • 2013-02-09
      • 2012-07-20
      • 1970-01-01
      • 2011-01-05
      相关资源
      最近更新 更多