99

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

  1. 当调用 cdecl 函数时,调用者如何知道它是否应该释放堆栈?在调用点,调用者是否知道被调用的函数是 cdecl 还是 stdcall 函数?它是如何工作的 ?调用者如何知道它是否应该释放堆栈?还是链接者的责任?
  2. 如果一个声明为 stdcall 的函数调用一个函数(其调用约定为 cdecl),或者反过来,这是否不合适?
  3. 一般来说,我们可以说哪个调用会更快 - cdecl 或 stdcall ?
4

9 回答 9

85

Raymond Chen 很好地概述了什么__stdcall__cdecl什么。

(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();
}

调用约定可能不匹配,如下所示:

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根据 Visual C++ 编译器WinAPI 函数使用__stdcall约定,这是 C 和 C++ 程序的默认设置。

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

于 2010-08-04T09:58:15.737 回答
46

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

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

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

通常,您必须为传递给某个外部库的回调函数提供适当的调用约定签名,即qsort从 C 库调用的回调必须是 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)
于 2010-08-04T10:28:34.657 回答
16

我注意到一个帖子说,如果你__stdcall从 a 中调用 a__cdecl或反之亦然,这并不重要。确实如此。

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

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

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

于 2012-09-17T18:29:39.623 回答
3

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

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

标准呼叫:

/* 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)

于 2015-10-25T01:42:19.817 回答
2

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

于 2010-08-04T09:59:43.627 回答
1

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

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

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

于 2010-08-04T09:59:06.353 回答
1

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

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

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

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

是的。

它是如何工作的 ?

它是函数声明的一部分。

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

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

还是链接者的责任?

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

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

不,为什么要这样做?

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

我不知道。测试一下。

于 2010-08-04T10:01:55.693 回答
0

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

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

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

不,还好。

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

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

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

于 2010-08-04T10:05:46.950 回答
0

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

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

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

于 2010-08-04T10:16:31.027 回答