这些天我遇到__stdcall
了很多。
MSDN 没有非常清楚地解释它的真正含义,何时以及为什么应该使用它(如果有的话)。
如果有人能提供解释,最好是一两个例子,我将不胜感激。
这个答案涵盖了 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 中那样在每个调用者中重复。这使得代码非常小,尽管大小影响只在大型程序中可见。
(优化编译器有时可以为从同一函数和 args 进行的多个 cdecl 调用中分配的 args 分配空间mov
,而不是总是add esp, n
/ push
。这可以节省指令,但会增加代码大小。例如gcc -maccumulate-outgoing-args
,总是这样做,并且有利于性能以前在较旧的 CPU 上push
是有效的。)
像 printf() 这样的可变参数函数不可能用 __stdcall 正确处理,因为只有调用者才真正知道传递了多少参数来清理它们。被调用者可以做出一些很好的猜测(例如,通过查看格式字符串),但是在 C 中将更多的参数传递给 printf 而不是格式字符串引用是合法的(它们将被默默地忽略)。因此只有 __cdecl 支持可变参数函数,调用者在其中进行清理。
链接器符号名称装饰:
如上面的要点中所述,使用“错误”约定调用函数可能是灾难性的,因此 Microsoft 有一种机制来避免这种情况发生。它运作良好,但如果一个人不知道原因是什么,这可能会令人抓狂。他们选择通过将调用约定编码为带有额外字符的低级函数名称(通常称为“装饰”)来解决此问题,链接器将这些名称视为不相关的名称。默认调用约定是 __cdecl,但可以使用 /G? 显式请求每个调用约定?编译器的参数。
这种类型的所有函数名称都带有下划线前缀,参数的数量并不重要,因为调用者负责堆栈设置和堆栈清理。调用者和被调用者可能会对实际传递的参数数量感到困惑,但至少堆栈规则得到了正确维护。
这些函数名称以下划线为前缀,并附加@ 加上传递的参数的字节数。通过这种机制,不可能调用带有错误数量的参数的函数。调用者和被调用者肯定同意返回一条ret 12
指令,例如弹出 12 个字节的堆栈参数以及返回地址。
您将收到链接时或运行时 DLL 错误,而不是让函数返回,而 ESP 指向调用者不期望的某个位置。(例如,如果您添加了一个新 arg 并且没有重新编译主程序和库。假设您没有通过使较早的 arg 变窄来欺骗系统,例如int64_t
-> int32_t
。)
这些函数名称以 @ 符号开头,并以 @bytes 计数为后缀,很像 __stdcall。前 2 个参数在 ECX 和 EDX 中传递,其余的在堆栈上传递。字节数包括寄存器 args。与 __stdcall 一样,窄 arg likechar
仍会占用 4 字节的 arg 传递槽(寄存器或堆栈上的 dword)。例子:
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。
C/C++ 中的所有函数都有特定的调用约定。调用约定的要点是确定数据在调用者和被调用者之间的传递方式以及谁负责清理调用堆栈等操作。
Windows 上最流行的调用约定是
__stdcall
, 以相反的顺序(从右到左)将参数压入堆栈__cdecl
, 以相反的顺序(从右到左)将参数压入堆栈__clrcall
, 按顺序(从左到右)将参数加载到 CLR 表达式堆栈中。__fastcall
, 存储在寄存器中,然后压入堆栈__thiscall
, 入栈;此指针存储在 ECX 中将此说明符添加到函数声明中实质上是告诉编译器您希望此特定函数具有此特定调用约定。
调用约定记录在这里
Raymond Chen 还从这里开始就各种调用约定的历史(5 部分)做了一个长系列。
__stdcall 是一种调用约定:一种确定如何将参数传递给函数(在堆栈上或在寄存器中)以及函数返回后谁负责清理(调用者或被调用者)的方法。
Raymond Chen 写了一篇关于主要 x86 调用约定的博客,还有一篇不错的CodeProject 文章。
在大多数情况下,您不必担心它们。唯一应该这样做的情况是,如果您调用的库函数使用的不是默认值——否则编译器将生成错误的代码,您的程序可能会崩溃。
不幸的是,对于何时使用它何时不使用它并没有简单的答案。
__stdcall 意味着函数的参数从第一个到最后一个被压入堆栈。这与 __cdecl 不同,这意味着参数从最后一个推到第一个,而 __fastcall 则将前四个(我认为)参数放在寄存器中,其余的放在堆栈上。
你只需要知道被调用者期望什么,或者如果你正在编写一个库,你的调用者可能期望什么,并确保你记录了你选择的约定。
它指定函数的调用约定。调用约定是一组如何将参数传递给函数的规则:以何种顺序,每个地址或每个副本,谁来清理参数(调用者或被调用者)等。
这是需要正确调用 WinAPI 函数的调用约定。调用约定是一组关于如何将参数传递给函数以及如何从函数传递返回值的规则。
如果调用者和被调用代码使用不同的约定,您会遇到未定义的行为(例如看起来很奇怪的 crash)。
C++ 编译器默认不使用 __stdcall - 他们使用其他约定。因此,为了从 C++ 调用 WinAPI 函数,您需要指定它们使用 __stdcall - 这通常在 Windoes SDK 头文件中完成,并且在声明函数指针时也这样做。
__stdcall 表示调用约定(有关详细信息,请参阅此 PDF)。这意味着它指定了函数参数如何从堆栈中推送和弹出,以及谁负责。
__stdcall 只是几个调用约定之一,并在整个 WINAPI 中使用。如果您提供函数指针作为其中一些函数的回调,则必须使用它。通常,您不需要在代码中表示任何特定的调用约定,而只需使用编译器的默认值,上述情况除外(提供对第 3 方代码的回调)。
简单地说,当你调用函数时,它会被加载到堆栈/寄存器中。__stdcall 是一种约定/方式(首先是右参数,然后是左参数...), __decl 是另一种约定,用于将函数加载到堆栈或寄存器上。
如果您使用它们,则指示计算机在链接期间使用该特定方式加载/卸载功能,因此您不会遇到不匹配/崩溃。
否则函数调用者和函数调用者可能使用不同的约定导致程序崩溃。
__stdcall是用于函数的调用约定。这告诉编译器适用于设置堆栈、推送参数和获取返回值的规则。还有许多其他调用约定,例如__cdecl、__thiscall、__fastcall和__naked。
__stdcall是 Win32 系统调用的标准调用约定。
更多细节可以在维基百科上找到。