c++ __stdcall的含义和用法是什么?

nfs0ujit  于 2023-11-19  发布在  其他
关注(0)|答案(9)|浏览(167)

这几天我遇到了很多__stdcall
MSDN并没有很清楚地解释它的真正含义,什么时候以及为什么应该使用它,如果有的话。
如果有人能提供一个解释,我将不胜感激,最好有一两个例子。

2skhul33

2skhul331#

这个答案涵盖了32位模式。(Windows x64只使用两种约定:普通约定(如果有名称的话,称为__fastcall)和__vectorcall,除了__m128i这样的SIMD向量参数如何传递之外,__vectorcall是相同的)。
传统上,C函数调用是这样进行的:调用者将一些参数压入堆栈,调用函数,然后弹出堆栈以清理那些压入的参数。

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

字符串
注意:默认约定(如上所示)称为__cdecl。
另一个最流行的约定是__stdcall。在它中,参数再次由调用方压入,但堆栈由被调用方清理。它是Win32 API函数的标准约定(由中的WINAPI宏定义<windows.h>),有时也称为“Pascal”调用约定。

/* example of __stdcall */
push arg1 
push arg2 
push arg3 
call function // no stack cleanup - callee does this


这看起来像是一个次要的技术细节,但如果调用方和被调用方之间对如何管理堆栈存在分歧,堆栈将以一种不太可能恢复的方式被销毁。执行此任务的(非常小的)代码只在一个地方找到,而不是像在__cdecl中那样在每个调用者中重复。这使得代码稍微变小,尽管大小影响只在大型程序中可见。
(优化编译器有时会为从同一个函数进行的多个cdecl调用中分配的args和mov args留出空间,而不是总是add esp, n/push。这节省了指令,但会增加代码大小。例如gcc -maccumulate-outgoing-args总是这样做,并且在push高效之前对旧CPU的性能很好。)
像printf()这样的变量函数是impossible to get right with __stdcall,因为只有调用者才真正知道为了清理它们传递了多少参数。(例如,通过查看格式字符串),但是在C中传递给printf的参数比格式字符串引用多是法律的因此只有__cdecl支持可变参数函数,调用者在其中进行清理。

链接器符号名称修饰:

正如上面的要点所提到的,使用“错误”约定调用函数可能是灾难性的,因此Microsoft有一种机制来避免这种情况发生。它运行良好,尽管如果不知道原因是什么,它可能会让人抓狂。他们选择通过将调用约定编码到带有额外字符的低级函数名称中来解决这个问题(通常被称为“装饰”),链接器会将这些名称视为不相关的名称。默认调用约定是__cdecl,但可以使用/G?参数向编译器显式请求每个名称。

__cdecl(cl /Gd.)

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

__stdcall(cl /Gz ...)

这些函数名都以下划线为前缀,并在后面加上@加上传递的参数的字节数。通过这种机制,不可能调用带有错误数量的参数的函数。调用者和被调用者肯定同意使用ret 12指令返回,例如,使用返回地址弹出12字节的堆栈参数沿着。
你会得到一个链接时间或运行时DLL错误,而不是一个函数返回ESP指向调用者不期望的地方。(例如,如果你添加了一个新的arg,并且没有重新编译主程序和库。假设你没有通过使早期的arg变窄来欺骗系统,比如int64_t-> int32_t。)

_fastcall(cl /Gr ...)

这些函数名以@符号开头,并以@bytes count作为后缀,很像__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

nr9pn0ug

nr9pn0ug2#

C/C++中的所有函数都有一个特定的调用约定。调用约定的目的是确定数据如何在调用者和被调用者之间传递,以及谁负责清理调用堆栈等操作。
Windows上最流行的调用约定是

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

将这个说明符添加到函数声明中实际上是告诉编译器,您希望这个特定的函数具有这个特定的调用约定。
调用约定记录在此处

Raymond Chen还从这里开始做了一个关于各种呼叫惯例历史的长系列(5部分)。

ioekq8ef

ioekq8ef3#

__stdcall是一种调用约定:一种确定参数如何传递给函数(在堆栈上或寄存器中)以及谁负责在函数返回后进行清理(调用者或被调用者)的方法。
Raymond Chen写了一个blog about the major x86 calling conventions,还有一个很好的CodeProject article
在大多数情况下,你不必担心它们,唯一需要担心的情况是,你调用的库函数使用了默认值以外的东西--否则编译器将生成错误的代码,你的程序可能会崩溃。

vshtjzan

vshtjzan4#

不幸的是,没有简单的答案,什么时候使用它,什么时候不使用。
__stdcall意味着函数的参数从第一个推到最后一个。这与__cdecl相反,它意味着参数从最后一个推到第一个,以及__fastcall,它将前四个(我认为)参数放在寄存器中,其余的放在堆栈上。
你只需要知道被调用者期望什么,或者如果你正在编写一个库,你的调用者可能期望什么,并确保你记录了你选择的约定。

ldfqzlk8

ldfqzlk85#

这是WinAPI函数需要正确调用的调用约定。调用约定是一组关于如何将参数传递到函数以及如何从函数传递返回值的规则。
如果调用者和被调用的代码使用不同的约定,那么您将遇到未定义的行为(如such a strange-looking crash)。
C编译器默认不使用__stdcall-它们使用其他约定。因此,为了从C调用WinAPI函数,您需要指定它们使用__stdcall -这通常在Windoes SDK头文件中完成,并且在声明函数指针时也要这样做。

vojdkbi0

vojdkbi06#

它指定了函数的调用约定。调用约定是一组如何将参数传递给函数的规则:以何种顺序,每个地址或每个副本,谁来清理参数(调用者或被调用者)等。

bsxbgnwa

bsxbgnwa7#

__stdcall表示一个调用约定(详见this PDF)。这意味着它指定了函数参数如何从堆栈中推送和弹出,以及谁负责。
__stdcall只是几种调用约定之一,并且在整个WINAPI中使用。如果您提供函数指针作为其中一些函数的回调,则必须使用它。通常,您不需要在代码中指定任何特定的调用约定,而只需使用编译器的默认值,除了上面提到的情况(提供回调给第三方代码)。

goucqfw6

goucqfw68#

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

yxyvkwin

yxyvkwin9#

__stdcall是用于函数的调用约定。这告诉编译器适用于设置堆栈,推送参数和获取返回值的规则。还有许多其他调用约定,如**__cdecl**,__thiscall__fastcall和**_naked**。
__stdcall是Win32系统调用的标准调用约定。

更多细节可以在Wikipedia上找到。

相关问题