__fastcall
),以及__vectorcall
,除了如何传递SIMD向量参数(例如__m128i
)外,两者相同)。
传统上,C语言函数调用是通过调用者将一些参数推入堆栈,调用函数,然后弹出堆栈来清除那些被推入的参数。
/* example of __cdecl */
push arg1
push arg2
push arg3
call function
add esp,12 ; effectively "pop; pop; pop"
/* example of __stdcall */
push arg1
push arg2
push arg3
call function // no stack cleanup - callee does this
可变参数函数,例如printf(),使用__stdcall是不可能正确处理的,因为只有调用者真正知道传递了多少个参数以便清理它们。被调用方可以做出一些好的猜测(比如查看格式字符串),但在C语言中,向printf()传递比格式字符串引用更多的参数是合法的(它们将会被静默忽略)。因此,只有__cdecl支持可变参数函数,其中调用者负责清理。
链接器符号名称修饰:
如上面的一个项目所述,使用“错误”的约定调用函数可能会导致灾难性后果,因此微软公司有一种机制来避免这种情况发生。它很有效,但如果不知道原因,可能会让人抓狂。
他们选择通过在低级函数名称中添加额外字符(通常称为“修饰”)来将调用约定编码到内部函数名称中,并且这些名称被链接器视为不相关的名称。默认的调用约定是__cdecl,但每个约定都可以通过编译器的/G?参数显式请求。
这种类型的所有函数名都以下划线为前缀,参数数量并不重要,因为调用者负责堆栈设置和堆栈清理。调用方和被调用方可能会混淆实际传递的参数数量,但至少堆栈规则得到了正确保持。
这些函数名以下划线为前缀,并附加@加上传递的参数字节数。通过这种机制,不可能使用错误数量的参数调用函数。例如,调用方和被调用方一定会同意返回ret 12
指令,以弹出12个字节的堆栈参数以及返回地址。
如果您添加了一个新的参数而没有重新编译主程序和库,则会在链接时或运行时DLL错误,而不是使函数返回ESP指向调用者不希望的位置。(假设您没有通过使早期的arg变窄来愚弄系统,例如int64_t
-> int32_t
)。
这些函数名以@符号开头,并用@字节数后缀,类似于__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
@8
。因此,您只会在extern "C"
函数中看到实际数字。例如,https://godbolt.org/z/v7EaWs。C/C++中的所有函数都有特定的调用约定。调用约定的目的是确定数据在调用方和被调用方之间如何传递以及谁负责操作,例如清除调用堆栈。
在Windows上最流行的调用约定包括:
__stdcall
,按相反顺序(从右到左)将参数推送到堆栈上。__cdecl
, 按相反顺序(从右到左)将参数推送到堆栈上。__clrcall
,按顺序(从左到右)将参数加载到CLR表达式堆栈中。__fastcall
,存储在寄存器中,然后推送到堆栈上。__thiscall
,推送到堆栈上;this指针存储在ECX寄存器中。将此说明符添加到函数声明中基本上告诉编译器您希望该特定函数具有此特定的调用约定。
这些调用约定在此处有文档记录:
Raymond Chen还撰写了一系列关于各种调用约定历史的长篇文章(共5部分),起始点在这里。
__stdcall是一种调用约定:一种确定如何将参数传递给函数(在堆栈或寄存器中)以及谁负责在函数返回后清理的方式(调用者还是被调用者)。
Raymond Chen写了一篇关于主要x86调用约定的博客文章,还有一篇不错的CodeProject文章。
大多数情况下,您不必担心它们。唯一需要考虑的情况是当您调用使用其他默认值的库函数时,否则编译器会生成错误的代码,导致程序可能崩溃。
很遗憾,关于何时使用以及何时不使用它并没有简单的答案。
__stdcall意味着函数参数从第一个到最后一个被推送到堆栈上。这与__cdecl相反,__cdecl意味着参数从最后一个到第一个被推送,而__fastcall将前四个(我想)参数放在寄存器中,其余的则放在堆栈上。
你只需要知道被调用者期望什么,或者如果你正在编写一个库,你的调用者可能会期望什么,并确保记录你选择的约定。
__stdcall
和__cdecl
仅在返回后清理的责任(以及修饰)方面有所不同。它们的参数传递方式相同(从右到左)。您所描述的是Pascal调用约定。 - a3f它指定了一个函数的调用规则。调用规则是一组规则,用于确定参数如何传递给函数:参数的顺序,是通过地址还是通过复制,由谁来清理参数(调用方还是被调用方)等。
__stdcall是一种调用约定(详见此PDF)。这意味着它指定了如何从堆栈中推送和弹出函数参数,并确定谁负责。
__stdcall只是几种调用约定之一,在WINAPI中广泛使用。如果您为某些函数提供函数指针作为回调,则必须使用它。通常情况下,您不需要在代码中指定任何特定的调用约定,而只需使用编译器的默认值,除非是上述情况(向第三方代码提供回调)。
__stdcall是用于函数的调用约定。这告诉编译器在设置堆栈、推送参数和获取返回值方面适用的规则。还有许多其他的调用约定,如__cdecl、__thiscall、__fastcall和__naked。
__stdcall是Win32系统调用的标准调用约定。
更多详细信息可以在Wikipedia上找到。