stdcall和cdecl

时间:2010-08-04 09:51:51

标签: c++ stdcall cdecl

除其他外,还有两种类型的调用约定 - stdcall cdecl 。我对他们的问题很少:

  1. 调用cdecl函数时,调用者如何? 知道它是否应该释放堆栈?在呼叫站点,是吗 调用者知道被调用的函数是cdecl还是stdcall 功能?它是如何工作的 ?调用者如何知道它是否应该 是否释放了堆栈?或者它是连接者的责任吗?
  2. 如果声明为stdcall的函数调用一个函数( 有一个调用约定为cdecl),反之亦然 这不合适吗?
  3. 一般情况下,我们可以说哪个呼叫会更快--cdecl或 stdcall?

9 个答案:

答案 0 :(得分:72)

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 :(得分:41)

在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)

STDCALL:

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

答案 2 :(得分:14)

我注意到有一条帖子说,如果您从__stdcall拨打__cdecl或反之亦然,则无关紧要。确实如此。

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

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

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

答案 3 :(得分:3)

我希望改进@ adf88的答案。我觉得STDCALL的伪代码并没有反映它在现实中的发生方式。 &#39; a&#39;,&#39; b&#39;和&#39; c&#39;不会从函数体的堆栈中弹出。相反,它们会被ret指令弹出(在这种情况下会使用ret 12),它一下子跳回呼叫者,同时弹出&#39; a&#39;,& #39; b&#39;和&#39; c&#39;从堆栈。

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

STDCALL:

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

答案 4 :(得分:2)

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

答案 5 :(得分:1)

调用者和被调用者需要在调用时使用相同的约定 - 这是它可靠地工作的唯一方式。调用者和被调用者都遵循预定义的协议 - 例如,谁需要清理堆栈。如果惯例不匹配,你的程序会运行到未定义的行为 - 可能只会崩溃。

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

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

答案 6 :(得分:1)

这些是编译器和平台特定的。除了C ++中的extern "C"之外,C和C ++标准都没有说任何关于调用约定的内容。

  

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

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

  

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

  

它是如何运作的?

这是函数声明的一部分。

  

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

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

  

或者它是联系人的责任吗?

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

  

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

没有。为什么要这样?

  

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

我不知道。测试一下。

答案 7 :(得分:0)

  

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

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

  

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

不,没关系。

  

c)一般来说,我们可以说哪个电话会更快--cdecl或stdcall?

一般来说,我会避免任何此类陈述。区别很重要,例如。当你想使用va_arg函数时。理论上,可能stdcall更快并生成更小的代码,因为它允许组合弹出参数弹出本地,但OTOH与cdecl组合,你也可以做同样的事情,如果你很聪明。

旨在提高速度的调用约定通常会执行一些寄存器传递。

答案 8 :(得分:-1)

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

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

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