__stdcall的含义和用法是什么?

91
最近我经常遇到 __stdcall
MSDN并没有非常清楚地解释它的真正意义,何时以及为什么应该使用它(如果有必要)。
如果有人能够提供一个解释,最好附带一两个示例,那将不胜感激。

你几乎不需要明确指定函数的调用约定。我已经编程C++超过20年了,老实说,我从来没有担心过这个问题。只需编写简单可移植的C++代码,将其余的工作交给编译器等等 - 如果你需要做其他事情,那么你可能要么做错了什么,要么正在处理非常特殊的情况(在这种情况下,我希望你有充分的理由,并已经查阅了文档以确保你所做的是正确的)。简而言之,只需省略这些限定词,不要担心它。 - undefined
9个回答

90
这个答案涉及32位模式。(Windows x64只使用2种约定:普通的(如果有名称,称为__fastcall),以及__vectorcall,除了如何传递SIMD向量参数(例如__m128i)外,两者相同)。

传统上,C语言函数调用是通过调用者将一些参数推入堆栈,调用函数,然后弹出堆栈来清除那些被推入的参数。

/* example of __cdecl */
push arg1
push arg2
push arg3
call function
add esp,12    ; effectively "pop; pop; pop"

注意:默认的约定(如上所示)称为__cdecl。
另一个最流行的约定是__stdcall。在其中,参数再次由调用者推送,但堆栈由被调用者清理。它是Win32 API函数的标准约定(由<windows.h>中的WINAPI宏定义),有时也被称为“Pascal”调用约定。
/* example of __stdcall */
push arg1 
push arg2 
push arg3 
call function // no stack cleanup - callee does this

这似乎是一个次要的技术细节,但如果调用者和被调用者之间对于堆栈的管理存在分歧,那么堆栈将被破坏,而这种破坏很难恢复。 由于__stdcall执行堆栈清理,因此执行此任务的(非常微小的)代码仅在一个地方找到,而不是像__cdecl一样在每个调用者中重复。 这使得代码稍微变小,尽管大小影响只在大型程序中才可见。
(优化编译器有时可以留出空间,以便从同一函数中进行多个cdecl调用分配的参数,并将其移动到其中,而不是总是add esp,n / push。这可以节省指令,但可能会增加代码大小。例如,gcc -maccumulate-outgoing-args总是这样做,在旧CPU上性能良好,之前的push不太高效。)

可变参数函数,例如printf(),使用__stdcall是不可能正确处理的,因为只有调用者真正知道传递了多少个参数以便清理它们。被调用方可以做出一些好的猜测(比如查看格式字符串),但在C语言中,向printf()传递比格式字符串引用更多的参数是合法的(它们将会被静默忽略)。因此,只有__cdecl支持可变参数函数,其中调用者负责清理。

链接器符号名称修饰:
如上面的一个项目所述,使用“错误”的约定调用函数可能会导致灾难性后果,因此微软公司有一种机制来避免这种情况发生。它很有效,但如果不知道原因,可能会让人抓狂。 他们选择通过在低级函数名称中添加额外字符(通常称为“修饰”)来将调用约定编码到内部函数名称中,并且这些名称被链接器视为不相关的名称。默认的调用约定是__cdecl,但每个约定都可以通过编译器的/G?参数显式请求。

__cdecl (cl /Gd ...)

这种类型的所有函数名都以下划线为前缀,参数数量并不重要,因为调用者负责堆栈设置和堆栈清理。调用方和被调用方可能会混淆实际传递的参数数量,但至少堆栈规则得到了正确保持。

__stdcall (cl /Gz ...)

这些函数名以下划线为前缀,并附加@加上传递的参数字节数。通过这种机制,不可能使用错误数量的参数调用函数。例如,调用方和被调用方一定会同意返回ret 12指令,以弹出12个字节的堆栈参数以及返回地址。

如果您添加了一个新的参数而没有重新编译主程序和库,则会在链接时或运行时DLL错误,而不是使函数返回ESP指向调用者不希望的位置。(假设您没有通过使早期的arg变窄来愚弄系统,例如int64_t -> int32_t)。

__fastcall (cl /Gr ...)

这些函数名以@符号开头,并用@字节数后缀,类似于__stdcall。前两个参数传递在ECX和EDX中,其余的参数传递在堆栈上。字节数包括寄存器参数。与__stdcall一样,一个窄的参数像char仍然使用了一个4字节的参数传递槽(一个寄存器或者堆栈上的双字)。 例如:

Declaration                        ----------------------->    decorated name


void __cdecl foo(void);            ----------------------->    _foo

void __cdecl foo(int a);           ----------------------->    _foo

void __cdecl foo(int a, int b);    ----------------------->    _foo

void __stdcall foo(void);          ----------------------->    _foo@0
 
void __stdcall foo(int a);         ----------------------->    _foo@4

void __stdcall foo(int a, int b);  ----------------------->    _foo@8

void __fastcall foo(void);         ----------------------->    @foo@0
 
void __fastcall foo(int a);        ----------------------->    @foo@4

void __fastcall foo(int a, int b); ----------------------->    @foo@8

请注意,在C ++中,用于函数重载的常规名称修饰机制被使用,而不是同时使用@8。因此,您只会在extern "C"函数中看到实际数字。例如,https://godbolt.org/z/v7EaWs

81

C/C++中的所有函数都有特定的调用约定。调用约定的目的是确定数据在调用方和被调用方之间如何传递以及谁负责操作,例如清除调用堆栈。

在Windows上最流行的调用约定包括:

  • __stdcall,按相反顺序(从右到左)将参数推送到堆栈上。
  • __cdecl, 按相反顺序(从右到左)将参数推送到堆栈上。
  • __clrcall,按顺序(从左到右)将参数加载到CLR表达式堆栈中。
  • __fastcall,存储在寄存器中,然后推送到堆栈上。
  • __thiscall,推送到堆栈上;this指针存储在ECX寄存器中。

将此说明符添加到函数声明中基本上告诉编译器您希望该特定函数具有此特定的调用约定。

这些调用约定在此处有文档记录:

Raymond Chen还撰写了一系列关于各种调用约定历史的长篇文章(共5部分),起始点在这里。


x64 构建怎么样? - Pablo Ariel

7

__stdcall是一种调用约定:一种确定如何将参数传递给函数(在堆栈或寄存器中)以及谁负责在函数返回后清理的方式(调用者还是被调用者)。

Raymond Chen写了一篇关于主要x86调用约定的博客文章,还有一篇不错的CodeProject文章

大多数情况下,您不必担心它们。唯一需要考虑的情况是当您调用使用其他默认值的库函数时,否则编译器会生成错误的代码,导致程序可能崩溃。


6

很遗憾,关于何时使用以及何时不使用它并没有简单的答案。

__stdcall意味着函数参数从第一个到最后一个被推送到堆栈上。这与__cdecl相反,__cdecl意味着参数从最后一个到第一个被推送,而__fastcall将前四个(我想)参数放在寄存器中,其余的则放在堆栈上。

你只需要知道被调用者期望什么,或者如果你正在编写一个库,你的调用者可能会期望什么,并确保记录你选择的约定。


2
__stdcall__cdecl仅在返回后清理的责任(以及修饰)方面有所不同。它们的参数传递方式相同(从右到左)。您所描述的是Pascal调用约定。 - a3f

3
这是WinAPI函数需要正确调用的一种调用约定。调用约定是一组规则,规定了参数如何传递到函数中以及返回值如何从函数中传递出来。
如果调用者和被调用代码使用不同的约定,则可能会遇到未定义的行为(例如像这样一个看起来奇怪的崩溃)。
C++编译器默认不使用__stdcall - 它们使用其他约定。因此,为了从C++调用WinAPI函数,您需要指定它们使用__stdcall - 这通常在Windoes SDK头文件中完成,并且在声明函数指针时也要这样做。

3

它指定了一个函数的调用规则。调用规则是一组规则,用于确定参数如何传递给函数:参数的顺序,是通过地址还是通过复制,由谁来清理参数(调用方还是被调用方)等。


3

__stdcall是一种调用约定(详见此PDF)。这意味着它指定了如何从堆栈中推送和弹出函数参数,并确定谁负责。

__stdcall只是几种调用约定之一,在WINAPI中广泛使用。如果您为某些函数提供函数指针作为回调,则必须使用它。通常情况下,您不需要在代码中指定任何特定的调用约定,而只需使用编译器的默认值,除非是上述情况(向第三方代码提供回调)。


1
简单来说,当你调用函数时,它会加载到堆栈/寄存器中。__stdcall是一种约定/方式(先右参数,再左参数...),__cdecl是另一种约定,用于加载在堆栈或寄存器中的函数。
如果你使用它们,就指示计算机在链接期间使用特定的方法来加载/卸载函数,因此不会出现不匹配/崩溃。
否则,函数调用者和被调用者可能使用不同的约定,导致程序崩溃。

1

__stdcall是用于函数的调用约定。这告诉编译器在设置堆栈、推送参数和获取返回值方面适用的规则。还有许多其他的调用约定,如__cdecl__thiscall__fastcall__naked

__stdcall是Win32系统调用的标准调用约定。

更多详细信息可以在Wikipedia上找到。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接