stdcall和cdecl

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

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

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)双方都应该工作。 实际上,至less在与Windows API交互的代码中,这种情况经常发生,因为根据Visual C ++编译器__cdecl是C和C ++程序的默认值, WinAPI函数使用__stdcall约定 。

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

在CDECL中,参数被以相反的顺序压入堆栈,调用者清除堆栈,结果通过处理器registry返回(稍后我将称之为“注册A”)。 在STDCALL中有一个区别,主叫方不清除堆栈。

你问哪一个更快。 没有人。 只要可以,您应该使用本地调用约定。 仅在没有出路的情况下更改约定,而使用需要使用某种约定的外部库时。

此外,编译器还可以select其他的约定,例如Visual C ++编译器使用FASTCALL,由于处理器寄存器的使用更广泛,理论上它更快。

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

其他常见情况可能是当您存储指向某些外部函数的指针时,即创build指向WinAPI函数的指针时,其types定义必须用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 copy of 'y', then 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 pseaudo-assembler */ /* Now copies push onto the stack are 'a', 'b' and 'c' variables */ 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, result 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 copy of 'y', then 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) 

我注意到有一个post说,如果你从__cdecl调用__stdcall ,或者反过来,这并不重要。 它确实。

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

C的微软公约试图通过篡改名字来规避这种情况。 __cdecl函数带有下划线前缀。 __stdcall函数带有下划线前缀,后缀为“@”和要删除的字节数。 例如, __cdecl f(x)链接为_x__stdcall f(int x)链接为_f@4 ,其中sizeof(int)为4个字节)

如果你设法通过链接器,享受debugging混乱。

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

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

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 

/* 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. pseaudo-assembler中的STDCALL'Function'主体* /
复制'a'(从堆栈)到注册A
复制'b'(从堆栈)到寄存器B
添加A和B,将结果存储在A中
复制'c'(从堆栈)到寄存器B
添加A和B,将结果存储在A中
跳回到调用者代码,同时popup'a','b'和'c'堆栈(a,b和
c在这个步骤中被从堆栈中移除,导致寄存器A)

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

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

你不应该注意到那些公约之间在性能上有任何真正的差别。 如果这成为一个问题,你通常需要减less电话 – 例如,改变algorithm。

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

这些东西是编译器和平台特定的。 C语言和C ++标准都没有提到除C ++中的extern "C"之外的其他约定。

来电者如何知道是否应该释放堆栈?

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

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

是。

它是如何工作的 ?

它是函数声明的一部分。

来电者如何知道是否应该释放堆栈?

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

还是连接器的责任?

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

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

不,为什么要这样?

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

我不知道。 testing它。

a)当调用者调用一个cdecl函数时,调用者怎么知道它是否应该释放栈?

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

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

不,还好。

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

总的来说,我不会这样说。 区分很重要,例如。 当你想使用va_arg函数。 从理论上讲, stdcall可能更快,生成的代码更小,因为它允许将popup的参数与popup的当地人结合起来,但是OTOH与cdecl一样,如果你聪明,也可以做同样的事情。

旨在加快速度的调用惯例通常会进行一些注册。

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

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

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