stdcall和cdecl


89

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

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

9
调用约定有多种类型,其中只有两种。 en.wikipedia.org/wiki/X86_calling_conventions
鸣叫鸭

1
请标记正确答案
ceztko

Answers:


77

雷蒙德陈给出了一个什么样的很好的概述__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)两者之间不应有实际的性能差异。


+1是一个很好的例子,还有Raymond Chen关于召集会议历史的帖子。对于任何有兴趣的人,其他内容也很不错。
OregonGhost

+1为雷蒙德·陈(Raymond Chen)。顺便说一句(旧版):为什么我无法使用博客搜索框找到其他部分?Google找到了它们,但找不到MSDN Blogs?
Nordic Mainframe

44

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

注意:__fastcall比__cdecl快,并且STDCALL是Windows 64位的默认调用约定
dns

哦 因此它必须弹出返回地址,添加参数块大小,然后跳到先前弹出的返回地址?您将需要重新埋葬以重新进入堆栈,这又使您回到尚未清理堆栈的问题。)
Dmitry

或者,弹出返回reg1,将堆栈指针设置为基本指针,然后跳至reg1
Dmitry

或者,将堆栈指针值从堆栈顶部移至底部,进行清洁,然后调用ret
Dmitry

15

我注意到有一个帖子说,无论您是__stdcall从a __cdecl还是从visa 致电都没关系。是的

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

Microsoft的C约定试图通过改写名称来规避此问题。甲__cdecl函数的前缀以下划线。甲__stdcall待去除以下划线和后缀at符号“@”和字节数功能前缀。例如__cdeclf(x)链接为_f__stdcall f(int x)链接为4个字节,_f@4其中sizeof(int)

如果您设法通过链接器,请享受调试的乐趣。


3

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


2

在函数类型中指定。当您具有函数指针时,如果未明确指定stdcall,则假定为cdecl。这意味着,如果获得stdcall指针和cdecl指针,则无法交换它们。两种函数类型可以相互调用而不会出现问题,只是在期望另一种类型时得到一种。至于速度,它们都扮演着相同的角色,只是位置略有不同,这确实无关紧要。


1

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

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

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


1

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

呼叫者如何知道是否应该释放堆栈?

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

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

是。

它是如何工作的 ?

它是函数声明的一部分。

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

调用方知道调用约定,可以采取相应的措施。

还是链接器的责任?

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

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

不,为什么要这样?

通常,我们可以说哪个调用会更快-cdecl或stdcall吗?

我不知道。测试一下。


0

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

cdecl改性剂是函数原型的一部分(或函数指针类型等),使主叫方获得从那里的信息和相应地动作。

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

不,还好。

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

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

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


-1

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

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

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

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.