(除其他外)有两种调用约定-stdcall和cdecl。我对他们有几个问题:
- 调用cdecl函数时,调用者如何知道是否应释放堆栈?在呼叫站点,呼叫者是否知道被调用的函数是cdecl还是stdcall函数?它是如何工作的 ?调用方如何知道是否应该释放堆栈?还是链接器的责任?
- 如果一个声明为stdcall的函数调用一个函数(其调用约定为cdecl),或者相反,这是否不合适?
- 通常,我们可以说哪个调用会更快-cdecl或stdcall吗?
(除其他外)有两种调用约定-stdcall和cdecl。我对他们有几个问题:
Answers:
雷蒙德陈给出了一个什么样的很好的概述__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 ++编译器这是C和C ++程序的默认设置,并且WinAPI函数使用该__stdcall
约定。
(3)两者之间不应有实际的性能差异。
在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)
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)
我注意到有一个帖子说,无论您是__stdcall
从a __cdecl
还是从visa 致电都没关系。是的
原因是: __cdecl
传递给被调用函数__stdcall
的参数被调用函数从堆栈中删除,在中,被调用函数从堆栈中删除了参数。如果您使用调用__cdecl
函数,__stdcall
则根本不会清理堆栈,因此最终当__cdecl
使用对参数或返回地址使用基于堆栈的引用时,最终将在当前堆栈指针处使用旧数据。如果__stdcall
从中调用函数__cdecl
,则该__stdcall
函数将清除堆栈上的参数,然后__cdecl
函数再次执行该操作,可能会删除调用函数的返回信息。
Microsoft的C约定试图通过改写名称来规避此问题。甲__cdecl
函数的前缀以下划线。甲__stdcall
待去除以下划线和后缀at符号“@”和字节数功能前缀。例如__cdecl
f(x)链接为_f
,__stdcall f(int x)
链接为4个字节,_f@4
其中sizeof(int)
如果您设法通过链接器,请享受调试的乐趣。
我想改善@ adf88的答案。我觉得STDCALL的伪代码不能反映实际情况。函数主体中的堆栈不会弹出“ a”,“ b”和“ c”。取而代之的是,它们被ret
指令弹出(ret 12
在这种情况下将被使用),该指令一键跳转回调用者,同时从堆栈中弹出“ 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
/* 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)
这些东西是特定于编译器和平台的。除了extern "C"
在C ++中,C或C ++标准都没有关于调用约定的任何内容。
呼叫者如何知道是否应该释放堆栈?
调用者知道该函数的调用约定,并相应地处理该调用。
在呼叫站点,呼叫者是否知道被调用的函数是cdecl还是stdcall函数?
是。
它是如何工作的 ?
它是函数声明的一部分。
调用方如何知道是否应该释放堆栈?
调用方知道调用约定,可以采取相应的措施。
还是链接器的责任?
不,调用约定是函数声明的一部分,因此编译器知道它需要知道的所有内容。
如果一个声明为stdcall的函数调用一个函数(其调用约定为cdecl),或者相反,这是否不合适?
不,为什么要这样?
通常,我们可以说哪个调用会更快-cdecl或stdcall吗?
我不知道。测试一下。
a)调用方调用cdecl函数时,调用方如何知道是否应释放堆栈?
该cdecl
改性剂是函数原型的一部分(或函数指针类型等),使主叫方获得从那里的信息和相应地动作。
b)如果一个声明为stdcall的函数调用一个函数(其调用约定为cdecl),或者相反,这是否不合适?
不,还好。
c)一般来说,我们可以说哪个调用会更快-cdecl或stdcall?
通常,我不会发表任何此类声明。区别很重要,例如。当您想使用va_arg函数时。从理论上讲,这可能会stdcall
更快并且生成更小的代码,因为它允许将弹出参数与弹出局部变量结合起来,但是OTOH与cdecl
,如果您很聪明,也可以做同样的事情。
旨在提高速度的调用约定通常会进行一些寄存器传递。