【问题标题】:stdcall and cdecl标准调用和 cdecl
【发布时间】:2011-03-25 04:18:25
【问题描述】:

有(除其他外)两种类型的调用约定 - stdcallcdecl。我对他们有几个问题:

  1. 调用cdecl函数时,调用者如何 知道它是否应该释放堆栈?在呼叫现场,是否 调用者知道被调用的函数是 cdecl 还是 stdcall 功能 ?它是如何工作的 ?调用者如何知道它是否应该 释放堆栈与否?还是链接者的责任?
  2. 如果一个被声明为 stdcall 的函数调用一个函数(它 有一个调用约定为 cdecl),或者相反,将 这不合适吗?
  3. 一般来说,我们可以说哪个调用会更快 - cdecl 或 标准调用?

【问题讨论】:

标签: c++ stdcall cdecl


【解决方案1】:

Raymond Chen gives a nice overview of what __stdcall and __cdecl does.

(1) 调用者“知道”在调用函数后清理堆栈,因为编译器知道该函数的调用约定并生成必要的代码。

void __stdcall StdcallFunc() {}

void __cdecl CdeclFunc()
{
    // The compiler knows that StdcallFunc() uses the __stdcall
    // convention at this point, so it generates the proper binary
    // for stack cleanup.
    StdcallFunc();
}

It is possible to mismatch the calling convention,像这样:

LRESULT MyWndProc(HWND hwnd, UINT msg,
    WPARAM wParam, LPARAM lParam);
// ...
// Compiler usually complains but there's this cast here...
windowClass.lpfnWndProc = reinterpret_cast<WNDPROC>(&MyWndProc);

这么多代码示例都弄错了,这甚至不好笑。应该是这样的:

// CALLBACK is #define'd as __stdcall
LRESULT CALLBACK MyWndProc(HWND hwnd, UINT msg
    WPARAM wParam, LPARAM lParam);
// ...
windowClass.lpfnWndProc = &MyWndProc;

但是,假设程序员没有忽略编译器错误,编译器将生成正确清理堆栈所需的代码,因为它会知道所涉及函数的调用约定。

(2) 两种方式都应该有效。事实上,至少在与 Windows API 交互的代码中,这种情况经常发生,因为__cdecl is the default for C and C++ programs according to the Visual C++ compilerthe WinAPI functions use the __stdcall convention

(3) 两者之间应该没有真正的性能差异。

【讨论】:

  • +1 是一个很好的例子和 Raymond Chen 关于调用约定历史的帖子。对于任何感兴趣的人,其他部分也值得一读。
  • +1 为 Raymond Chen。 Btw (OT): 为什么我使用博客搜索框找不到其他部分?谷歌找到它们,但没有找到 MSDN 博客?
【解决方案2】:

在 CDECL 参数以相反的顺序压入堆栈时,调用者清除堆栈并通过处理器注册表返回结果(稍后我将其称为“寄存器 A”)。在 STDCALL 中有一个区别,调用者不清除堆栈,而被调用者这样做。

你在问哪个更快。没有人。您应该尽可能使用本机调用约定。只有在没有出路的情况下才更改约定,当使用需要使用特定约定的外部库时。

此外,编译器可能会选择其他约定作为默认约定,即 Visual C++ 编译器使用 FASTCALL,理论上更快,因为处理器寄存器的使用范围更广。

通常,您必须为传递给某个外部库的回调函数提供正确的调用约定签名,即从 C 库对qsort 的回调必须是 CDECL(如果编译器默认使用其他约定,那么我们必须将回调标记为 CDECL)或者各种 WinAPI 回调必须是 STDCALL(整个 WinAPI 是 STDCALL)。

其他常见情况可能是当您存储指向某些外部函数的指针时,即创建指向 WinAPI 函数的指针,其类型定义必须用 STDCALL 标记。

下面是一个例子,展示了编译器是如何做到的:

/* 1. calling function in C++ */
i = Function(x, y, z);

/* 2. function body in C++ */
int Function(int a, int b, int c) { return a + b + c; }

CDECL:

/* 1. calling CDECL 'Function' in pseudo-assembler (similar to what the compiler outputs) */
push on the stack a copy of 'z', then a copy of 'y', then a copy of 'x'
call (jump to function body, after function is finished it will jump back here, the address where to jump back is in registers)
move contents of register A to 'i' variable
pop all from the stack that we have pushed (copy of x, y and z)

/* 2. CDECL 'Function' body in pseudo-assembler */
/* Now copies of 'a', 'b' and 'c' variables are pushed onto the stack */
copy 'a' (from stack) to register A
copy 'b' (from stack) to register B
add A and B, store result in A
copy 'c' (from stack) to register B
add A and B, store result in A
jump back to caller code (a, b and c still on the stack, the result is in register A)

标准调用:

/* 1. calling STDCALL in pseudo-assembler (similar to what the compiler outputs) */
push on the stack a copy of 'z', then a copy of 'y', then a copy of 'x'
call
move contents of register A to 'i' variable

/* 2. STDCALL 'Function' body in pseaudo-assembler */
pop 'a' from stack to register A
pop 'b' from stack to register B
add A and B, store result in A
pop 'c' from stack to register B
add A and B, store result in A
jump back to caller code (a, b and c are no more on the stack, result in register A)

【讨论】:

  • 注意:__fastcall 比 __cdecl 快,STDCALL 是 Windows 64 位的默认调用约定
  • 哦;所以必须pop返回地址,加上参数块大小,然后跳转到之前弹出的返回地址?(一旦调用ret,就不能再清栈,但是一旦清栈,就不能再调用ret(因为你需要把自己埋在堆栈上,让你回到你没有清理堆栈的问题上)。
  • 或者,pop返回reg1,设置栈指针为基指针,然后跳转到reg1
  • 或者,将堆栈指针值从堆栈顶部移动到底部,清理,然后调用 ret
  • 请注意,在 Windows x64 上,__fastcall__stdcall__cdecl 都是相同调用约定的别名,即 __fastcall。 32 位和 64 位都有一个替代调用约定 __vectorcall
【解决方案3】:

我注意到一个帖子说,如果您从 __cdecl 拨打 __stdcall 或反之亦然,这并不重要。确实如此。

原因:在__cdecl 中,传递给被调用函数的参数由调用函数从堆栈中删除,在__stdcall 中,参数由被调用函数从堆栈中删除。如果您使用__stdcall 调用__cdecl 函数,则根本不会清理堆栈,因此最终当__cdecl 使用基于堆栈的引用作为参数或返回地址时,将使用当前堆栈指针处的旧数据.如果您从__cdecl 调用__stdcall 函数,__stdcall 函数会清理堆栈上的参数,然后__cdecl 函数会再次执行此操作,可能会删除调用函数返回的信息。

C 的 Microsoft 约定试图通过修改名称来规避这一点。 __cdecl 函数以下划线为前缀。 __stdcall 函数以下划线为前缀,以 at 符号“@”为后缀,以及要删除的字节数。例如__cdecl f(x) 链接为_f__stdcall f(int x) 链接为_f@4 其中sizeof(int) 为4 个字节)

如果您设法通过链接器,请享受调试混乱。

【讨论】:

  • 第一段(现在)不正确,因为大多数编译器会处理调用约定之间的差异以防止发生此类问题。
  • 调用者和被调用者都必须就被调用者的调用约定达成一致,是的。但是“从 __cdecl 调用 __stdcall”只是指出调用者自己的父级如何将 args 传递给它与调用其他函数时它如何管理堆栈无关。 __cdecl 和 __stdcall 就哪些寄存器是调用保留与调用破坏达成一致,因此不需要额外保存函数尚未使用自身的寄存器。
  • 如果您调用__stdcall 函数就像 __cdecl 函数,问题将是(如您所说)。不是来自 __cdecl 函数。例如__stdcall foo(int x) 可以写成调用printf。你的答案是正确的,除了第一段表示不同意不同的事情。 >.
【解决方案4】:

我想改进@adf88 的回答。我觉得 STDCALL 的伪代码并不能反映它在现实中的发生方式。 'a'、'b' 和 'c' 不会从函数体的堆栈中弹出。相反,它们被ret 指令弹出(在这种情况下将使用ret 12),该指令一举跳回调用者,同时从堆栈。

这是根据我的理解更正的版本:

标准调用:

/* 1. calling STDCALL in pseudo-assembler (similar to what the compiler outputs) */
push on the stack a copy of 'z', then copy of 'y', then copy of 'x'
call
move contents of register A to 'i' variable

/* 2. STDCALL 'Function' body in pseaudo-assembler */ copy 'a' (from stack) to register A copy 'b' (from stack) to register B add A and B, store result in A copy 'c' (from stack) to register B add A and B, store result in A jump back to caller code and at the same time pop 'a', 'b' and 'c' off the stack (a, b and c are removed from the stack in this step, result in register A)

【讨论】:

    【解决方案5】:

    在函数类型中指定。当你有一个函数指针时,如果没有明确的 stdcall,它被假定为 cdecl。这意味着如果你得到一个 stdcall 指针和一个 cdecl 指针,你就不能交换它们。这两种函数类型可以毫无问题地相互调用,当您期望另一种类型时,它只是得到一种类型。至于速度,他们都扮演相同的角色,只是在一个非常不同的地方,这真的无关紧要。

    【讨论】:

      【解决方案6】:

      调用者和被调用者需要在调用点使用相同的约定 - 这是它可以可靠工作的唯一方法。调用者和被调用者都遵循预定义的协议——例如,谁需要清理堆栈。如果约定不匹配,您的程序会遇到未定义的行为 - 可能会严重崩溃。

      这只是每个调用站点需要的 - 调用代码本身可以是具有任何调用约定的函数。

      您不应该注意到这些约定之间的任何实际性能差异。如果这成为问题,您通常需要减少调用次数 - 例如,更改算法。

      【讨论】:

        【解决方案7】:

        这些东西是编译器和平台特定的。除了 C++ 中的 extern "C" 之外,C 和 C++ 标准都没有提及调用约定。

        调用者如何知道它是否应该释放堆栈?

        调用者知道函数的调用约定并相应地处理调用。

        在调用点,调用者是否知道被调用的函数是cdecl还是stdcall函数?

        是的。

        它是如何工作的?

        它是函数声明的一部分。

        调用者如何知道它是否应该释放堆栈?

        调用者知道调用约定并且可以相应地采取行动。

        还是链接器的责任?

        不,调用约定是函数声明的一部分,因此编译器知道它需要知道的一切。

        如果一个声明为 stdcall 的函数调用一个函数(其调用约定为 cdecl),或者反过来,这是否不合适?

        没有。为什么要这样?

        一般来说,我们可以说哪个调用会更快 - cdecl 还是 stdcall ?

        我不知道。测试一下。

        【讨论】:

          【解决方案8】:

          a) 当调用者调用 cdecl 函数时,调用者如何知道它是否应该释放堆栈?

          cdecl 修饰符是函数原型(或函数指针类型等)的一部分,因此调用者从那里获取信息并采取相应行动。

          b) 如果一个声明为 stdcall 的函数调用一个函数(其调用约定为 cdecl),或者反过来,这是否不合适?

          不,没关系。

          c) 一般来说,我们可以说哪个调用会更快 - cdecl 还是 stdcall?

          一般来说,我不会发表任何此类声明。区别很重要,例如。当你想使用 va_arg 函数时。从理论上讲,stdcall 可能更快并生成更小的代码,因为它允许将弹出参数与弹出局部变量结合起来,但是 OTOH 与cdecl,你也可以做同样的事情,如果你很聪明的话.

          旨在更快的调用约定通常会进行一些寄存器传递。

          【讨论】:

            【解决方案9】:

            调用约定与 C/C++ 编程语言无关,而是关于编译器如何实现给定语言的具体细节。如果您始终使用相同的编译器,则无需担心调用约定。

            但是,有时我们希望不同编译器编译的二进制代码能够正确互操作。当我们这样做时,我们需要定义一个称为应用程序二进制接口 (ABI) 的东西。 ABI 定义了编译器如何将 C/C++ 源代码转换为机器代码。这将包括调用约定、名称修改和 v-table 布局。 cdelc 和 stdcall 是 x86 平台上常用的两种不同的调用约定。

            通过将有关调用约定的信息放入源头文件中,编译器将知道需要生成哪些代码才能与给定的可执行文件正确互操作。

            【讨论】:

              猜你喜欢
              • 2012-03-17
              • 2012-05-14
              • 1970-01-01
              • 2013-03-17
              • 2023-03-28
              • 2015-04-16
              相关资源
              最近更新 更多