繁体   English   中英

__stdcall 的含义和用法是什么?

[英]What is the meaning and usage of __stdcall?

这些天我经常遇到__stdcall

MSDN 并没有很清楚地解释它的真正含义,何时以及为什么应该使用它,如果有的话。

如果有人能提供解释,最好是一两个例子,我将不胜感激。

此答案涵盖 32 位模式。 (Windows x64 仅使用 2 种约定:普通约定(如果它有名称,则称为__fastcall )和__vectorcall ,除了传递像__m128i这样的 SIMD 向量参数的方式外,它们是相同的)。


传统上,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 调用分配的 args 分配空间,这些调用从同一函数和mov args 到其中,而不是总是add esp, n / push 。这可以节省指令但可以增加代码大小。例如gcc -maccumulate-outgoing-args总是这样做,并且在push有效之前有利于旧 CPU 的性能。)

像 printf() 这样的可变参数函数不可能用 __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 计数为后缀,很像__stdcall。 前 2 个参数在 ECX 和 EDX 中传递,其余的在堆栈上传递。 字节数包括寄存器 args。 与 __stdcall 一样,像char这样的窄 arg 仍会占用一个 4 字节的 arg 传递槽(寄存器或堆栈上的双字)。 例子:

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 中使用。 如果您提供函数指针作为其中一些函数的回调,则必须使用它。 通常,您不需要在代码中指定任何特定的调用约定,而只需使用编译器的默认值,除了上面提到的情况(提供对 3rd 方代码的回调)。

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

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

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

简单地说,当您调用函数时,它会加载到堆栈/寄存器中。 __stdcall 是一种约定/方式(首先是右参数,然后是左参数......),__decl 是另一种用于在堆栈或寄存器上加载函数的约定。

如果您使用它们,您会指示计算机在链接期间使用该特定方式加载/卸载该功能,因此您不会遇到不匹配/崩溃的情况。

否则函数被调用者和函数调用者可能会使用不同的约定导致程序崩溃。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM