【问题标题】:__cdecl results in larger executable than __stdcall?__cdecl 导致比 __stdcall 更大的可执行文件?
【发布时间】:2023-03-19 09:49:01
【问题描述】:

我发现了这个:

因为堆栈被调用函数清理,__stdcall 调用约定创建比 __cdecl更小的可执行文件,其中 必须为每个函数调用生成堆栈清理代码

假设我有 2 个函数:

void __cdecl func1(int x)
{
    //do some stuff using x
}

void __stdcall func2(int x, int y)
{
    //do some stuff using x, y
}

这里是main()

int main()
{
    func1(5);
    func2(5, 6);
}

IMO,清理对func1(5) 的调用堆栈是main() 的责任,而func2 将清理对func2(5,6) 的调用堆栈,对吧?

四个问题:

1.对于main()中对func1的调用,清理堆栈是main的责任,所以编译器会在调用前后插入一些代码(清理堆栈的代码) func?像这样:

int main()
{
    before_call_to_cdecl_func(); //compiler generated code for stack-clean-up of cdecl-func-call
    func1(5);
    after_call_to_cdecl_func(); //compiler generated code for stack-clean-up of cdecl-func-call

    func2(5, 6);
}

2.对于main()中对func2的调用,清理堆栈是func2自己的工作,所以我推测,在调用之前或之后不会在main()中插入代码func2,对吧?

3.因为func2__stdcall,所以我推测编译器会自动插入代码(清理堆栈)如下:

void __stdcall func1(int x, int y)
{
    before_call_to_stdcall_func(); //compiler generated code for stack-clean-up of stdcall-func-call
    //do some stuff using x, y
    after_call_to_cdecl_func(); //compiler generated code for stack-clean-up of stdcall-func-call
}

我猜对了?

4.最后,回到引用的话,为什么__stdcall 的可执行文件比__cdecl 小?而且linux中没有__stdcall这样的东西,对吧?是不是意味着linux elf在win下总是比exe大?

【问题讨论】:

  • stdcall 不能用于可变参数函数,这就是您在 Linux 上看不到它的原因。它仅在 Windows 上用于 win32 API 调用。本机 Windows 编译器通常使用某种形式的 fastcall 或 cdecl。
  • 那么,linux只支持cdecl
  • @David:实际上他们倾向于使用__stdcall__cdecl,因为整个WINAPI 是__stdcall__cdecl,我从来没有遇到过默认为@987654349 的编译器@nor 和广泛使用它的 API
  • @Necrolis 我的日常编译器 Delphi 默认使用 fastcall。 MSVC 默认对 C++ 成员函数使用 fastcall。
  • @Alcott 甚至没有那么简单。考虑 x64。在 Windows 和 Linux(不了解 Mac)上,只有一种 x64 调用约定。

标签: c++ c compiler-construction calling-convention


【解决方案1】:
  1. 它只会在调用之后插入代码,也就是重置堆栈指针,只要在哪里调用参数即可。*
  2. __stdcall 在调用点不会生成任何清理代码,但是需要注意的是,编译器可以将多个__cdecl 调用的堆栈清理累积到一个清理中,或者它可以延迟清理以防止管道停顿。
  3. 忽略这个例子中的倒序,不,它只会插入代码来清理__cdecl函数,函数参数的设置是不同的(不同的编译器生成/喜欢不同的方法)。
  4. __stdcall 更像是 Windows 的东西,请参阅 this。二进制文件的大小取决于对__cdecl funcs 的调用次数,更多的调用意味着更多的清理代码,而__stdcall 只有1 个单一的清理代码实例。但是,您不会看到大小增加那么多,因为每次调用最多只有几个字节。

*区分清理和设置调用参数很重要。

【讨论】:

  • 如果没有插入代码来清理__stdcall函数,那么堆栈将如何清理?你的意思是,__stdcall 函数的帧堆栈将在函数完成时消失,这意味着堆栈被清理了吗?
  • @Alcott:不用担心,我的 SO 答案中有很多错字:(
  • @Alcott:我稍微改变了措辞,__stdcall 函数将它们的清理代码嵌入到函数本身中(堆栈清理由返回完成,请参阅RETN imm8/imm16 汇编指令),@987654332 @ 函数要求编译器在任何有参数的调用站点发出清理(这允许 var arg 函数)。
  • 知道了。那么__cdecl__stdcall中设置调用参数有什么不同呢?
  • @Alcott:不,它们的设置方式相同
【解决方案2】:

从历史上看,第一个 C++ 编译器使用的等价于 __stdcall。从实施质量的角度来看,我希望 使用 __cdecl 约定的 C 编译器和 C++ 编译器 __stdcall(当时被称为 Pascal convensions)。 这是早期 Zortech 编译正确的一件事。

当然,可变参数函数仍必须使用__cdecl 约定。这 如果被调用者不知道要清理多少,就无法清理堆栈。

(请注意,C 标准经过精心设计,允许 __stdcall C 中的约定也是如此。我只知道一个编译器 然而,利用了这一点;当时现有代码的数量 在没有原型的情况下调用 vararg 函数是巨大的, 虽然标准宣布它被破坏,但编译器实现者并没有 想要破解他们客户的代码。)

在很多环境中,似乎有一种非常强烈的坚持倾向 C 和 C++ 约定是相同的,可以采用 extern "C++" 函数的地址,并将其传递给编写的函数 在 C 中调用它。 IIRC,例如,g++ 不处理

extern "C" void f();

void f();

具有两种不同的类型(尽管标准要求它),并且 允许将静态成员函数的地址传递给 pthread_create,例如。结果是这样的编译器使用 到处都是完全相同的约定,在英特尔上,它们是 相当于__cdecl

许多编译器都有扩展以支持其他约定。 (为什么他们 不要使用标准的extern "xxx",我不知道。) 然而,这些扩展是多种多样的。微软把属性 直接在函数名之前:

void __stdcall func( int, int );

, g++ 把它放在函数后面的特殊属性子句中 声明:

void func( int, int ) __attribute__((stdcall));

C++11 增加了指定属性的标准方式:

void [[stdcall]] func( int, int );

它没有将stdcall 指定为属性,但它确实指定了 附加的属性(除了那些在标准中定义的)可能是 指定,并且依赖于实现。我希望 g++ 和 VC++ 在其最新版本中接受这种语法,至少在 C++11 中是这样 被激活。属性的确切名称(__stdcallstdcall、 等)可能会有所不同,因此您可能希望将其包装在宏中。

最后:在开启优化的现代编译器中, 调用约定的差异可能可以忽略不计。 const 之类的属性(不要与 C++ 关键字混淆 const)、regparmnoreturn 可能会产生更大的影响, 在可执行文件大小和性能方面。

【讨论】:

  • 如果我简单地定义一个这样的函数:void foo();,那么是__cdecl吗?
  • C++ 无法使用 __cdecl,因为 __cdecl 不知道对象指针 。他们不得不使用不同的调用约定。 AFAIK,微软称之为 __thiscall...
  • @Malkocoglu:C++ 可以很容易地使用__cdeclthis 将只是在堆栈上传递,就像它为基于 stdcall 的 COM 接口所做的那样
  • @Necrolis:是的,对于类成员可变参数函数,它们使用您描述的方案。然而,C 程序不能调用这种特殊的 __cdecl,因为它不知道 对象指针!从二元的角度来看,它们是不同的……
  • @Malkocoglu:您只需定义一个虚拟参数来放置 this 指针(再次参见 COM 接口,尤其是 Direct X)。但是 C 不能通过调用 UB 或使用 COM 等特殊接口来调用类成员函数...
【解决方案3】:

这个调用约定人群是新的64位ABI的历史。

http://en.wikipedia.org/wiki/X86_calling_conventions#x86-64_calling_conventions

还有针对不同架构的 ABI 方面。 (如 ARM) 并非所有架构对所有架构都执行相同的操作。所以不要费心去想这个调用约定的事情!

http://en.wikipedia.org/wiki/Calling_convention

EXE 大小的改进是微不足道的(可能不存在),不要打扰...

__cdecl__stdcall 灵活得多。可变数量的参数灵活性,清理代码(指令)的重要性,__cdecl 函数可以用错误数量的参数调用,这不一定会导致严重的问题!但是__stdcall 的情况总是出错!

【讨论】:

  • 64 位 API 仅在以 64 位模式编译时使用(如果有的话——如果还有其他选项,我不会感到惊讶)。如果您编写了正确的 C++(如果您不编写,编译器会抱怨),甚至是正确的 C(但编译器无法知道 C 的情况,并且很多遗留代码不是“正确的”根据C标准),使用__stdcall没有问题(因为__stdcall中调用可变参数函数的约定与__cdecl中的相同)。
  • @james Kanze:这两个 64 位 API(Microsoft 和 EveryoneElse)没有类似于 __stdcallcdecl 的选项或变体。
  • @MSalters 此类选项或变体通常不是官方 API 的一部分。编译器仍然可以提供它们。 (就此而言,编译器可以使用某种全局代码生成,并为每个函数使用不同的调用约定,具体取决于函数对参数的处理。)
  • @JamesKanze:当编译器同时看到调用者和被调用者时,这当然是很有可能的。但在这种情况下,无论如何,优化都可以通过调用约定来做真正奇怪的事情(通常作为副作用,例如在重新排序来自序言或结语的指令时)。在“Everyone Else”的情况下,如果调用者和被调用者使用不同的编译器和非标准选项编译,则可能存在互操作性问题 - 链接器通常无法修补。
  • @MSalters 是的。一旦编译器可以同时看到调用者和被调用者,一切都会发生,我们就不能真正谈论 ABI。扩展编译器以支持其他调用约定是一种中间解决方案:您保证被调用者和所有调用者将使用支持其他调用约定的编译器进行编译。 (我可以想象调用约定仅对某些类型的函数是最佳的。您编写该类型的函数,添加属性声明,并获得更快的代码。)
【解决方案4】:

其他人已经回答了您问题的其他部分,所以我将添加我对尺寸的回答:

4.最后,回到引用的话,为什么 __stdcall 导致的可执行文件比 __cdecl 小?

这似乎不是真的。我通过编译libudis 来测试它,无论有没有stdcall 调用约定。首先没有:

$ clang -target i386-pc-win32 -DHAVE_CONFIG_H -Os -I.. -I/usr/include -fPIC -c *.c && strip *.o
$ du -cb *.o
6524    decode.o
95932   itab.o
1434    syn-att.o
1706    syn-intel.o
2288    syn.o
1245    udis86.o
109129  totalt

还有。启用 stdcall 的是 -mrtd 开关:

$ clang -target i386-pc-win32 -DHAVE_CONFIG_H -Os -I.. -I/usr/include -fPIC -mrtd -c *.c && strip *.o
7084    decode.o
95932   itab.o
1502    syn-att.o
1778    syn-intel.o
2296    syn.o
1305    udis86.o
109897  totalt

如您所见,cdecl 以几百字节击败了 stdcall。可能是我的测试方法有缺陷,或者 clang 的 stdcall 代码生成器很弱。但我认为,对于现代编译器,调用者清理提供的额外灵活性意味着它们将始终使用 cdecl 而不是 stdcall 生成更好的代码。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-12-17
    • 1970-01-01
    相关资源
    最近更新 更多