stdcall和cdecl

Rak*_*wal 82 c++ cdecl stdcall

除其他外,还有两种类型的调用约定--stdcallcdecl.我对他们几乎没有问题:

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

In *_*ico 72

雷蒙德陈给出了一个什么样的很好的概述__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();
}
Run Code Online (Sandbox Code Playgroud)

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

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);
Run Code Online (Sandbox Code Playgroud)

如此多的代码示例错了,甚至都不好笑.应该是这样的:

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

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

(2)两种方式都应该有效.实际上,至少在与Windows API交互的代码中,这种情况经常发生,因为__cdecl根据Visual C++编译器WinAPI函数使用__stdcall约定是C和C++程序的默认程序.

(3)两者之间不应存在真正的性能差异.


adf*_*f88 41

在CDECL中,参数以相反的顺序被压入堆栈,调用者清除堆栈并通过处理器注册表返回结果(稍后我将其称为"寄存器A").在STDCALL中有一个区别,调用者不能清除栈,而calle也是如此.

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

此外,编译器可能会选择其他约定作为默认约定,即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; }
Run Code Online (Sandbox Code Playgroud)

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)
Run Code Online (Sandbox Code Playgroud)

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)
Run Code Online (Sandbox Code Playgroud)


小智 15

我注意到一个帖子说如果你打电话给__stdcall一个__cdecl或反之亦然无关紧要.确实如此.

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

微软的C惯例试图通过破坏名称来绕过这一点.甲__cdecl函数的前缀以下划线.甲__stdcall函数以下划线前缀和at符号"@"和要除去的字节数与后缀.例如__cdeclF(X)被链接为_f,__stdcall f(int x)被链接为_f@4其中sizeof(int)是4个字节)

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


gol*_*lem 5

我想改进@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)

Run Code Online (Sandbox Code Playgroud)