stdcall和cdecl

103

除其他外,调用规范中有两种类型-stdcall和cdecl。我对它们有一些问题:

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

10
有许多种不同的调用约定,其中这两种只是其中之二。http://en.wikipedia.org/wiki/X86_calling_conventions - Mooing Duck
2
请标记正确答案。 - ceztko
9个回答

90

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) 两种方式都应该可行。事实上,在与Windows API交互的代码中,这种情况至少经常发生,因为__cdecl是Visual C++编译器中C和C++程序的默认设置,而WinAPI函数使用__stdcall约定
(3) 两种方式之间不应该有真正的性能差异。

1
对于一个好的例子和关于调用约定历史的Raymond Chen的文章点个赞。对于任何感兴趣的人,那些文章的其他部分也很值得一读。 - OregonGhost
1
+1 给雷蒙德·陈。顺便说一下(闲话):为什么我无法使用博客搜索框找到其他部分?Google 可以找到它们,但 MSDN 博客找不到? - Nordic Mainframe

49

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)

注意:__fastcall 比 __cdecl 更快,STDCALL 是 Windows 64 位默认的调用约定。 - dns
哦,所以它必须弹出返回地址,添加参数块大小,然后跳转到先前弹出的返回地址吗?(一旦调用ret,您就无法清除堆栈,但一旦清除堆栈,您就无法再调用ret(因为您需要在堆栈上将自己埋回到ret,使您回到您还没有清除堆栈的问题)。 - Dmytro
或者,将返回值弹出到寄存器1中,将堆栈指针设置为基指针,然后跳转到寄存器1。 - Dmytro
或者,将堆栈指针值从堆栈顶部移动到底部,清除,然后调用ret。 - Dmytro
1
请注意,在 Windows x64 上,“__fastcall”,“__stdcall”和“__cdecl”都是“__fastcall”的别名,其调用约定相同。无论是 32 位还是 64 位,都有另一种替代调用约定“__vectorcall”。 - Chuck Walbourn
显示剩余2条评论

16

我注意到有人发布文章称,如果从 __stdcall__cdecl 或者从 __cdecl__stdcall 调用函数是无关紧要的。但实际上是有区别的。

原因在于,对于 __cdecl,被调用函数的参数由调用函数从栈中移除;而对于 __stdcall,被调用函数自己从栈中移除其参数。如果你使用 __stdcall 调用一个 __cdecl 函数,则栈不会被清空,因此当 __cdecl 使用基于栈的引用来处理参数或返回地址时,它将使用当前栈指针处的旧数据。如果你使用 __cdecl 调用一个 __stdcall 函数,那么 __stdcall 将清空栈上的参数,然后 __cdecl 再次清空栈,可能会删除调用函数的返回信息。

微软的 C 函数惯例通过修改名称来避免这种问题。 __cdecl 函数以下划线为前缀。 __stdcall 函数以下划线为前缀并以“@”符号和需要移除的字节数结尾。例如,__cdecl f(x) 链接为 _f__stdcall f(int x) 链接为 _f@4,其中 sizeof(int) 是 4 字节。

如果你成功通过了链接器,那么就可以享受调试时的困扰了。


第一段现在是不正确的,因为大多数编译器处理调用约定之间的差异以防止发生这种问题。 - TheMadHau5
调用者和被调用者都必须同意被调用者的调用约定。但是,“从__cdecl调用__stdcall”只是指出调用者自己的父级如何将参数传递给它与其调用其他函数时如何管理堆栈无关。__cdecl和__stdcall都同意哪些寄存器是调用保留的,哪些是调用破坏的,因此没有必要额外保存函数本身不使用的寄存器。 - Peter Cordes
问题在于(正如您所说),如果您将一个__stdcall函数作为__cdecl函数调用,而不是从__cdecl函数中调用。例如,__stdcall foo(int x)可以编写为调用printf。您的答案是正确的,除了第一段陈述与另一件事不同的不同意见。 >.< - Peter Cordes

4
我希望改进@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 '函数'主体 */ 将堆栈中的'a'复制到寄存器A中 将堆栈中的'b'复制到寄存器B中 将A和B相加,并将结果存储在A中 将堆栈中的'c'复制到寄存器B中 将A和B相加,并将结果存储在A中 跳转回调用者代码,同时从堆栈中弹出'a'、'b'和'c'(在这一步中,a、b和c从堆栈中移除,结果存储在寄存器A中)


2

这在函数类型中有明确规定。当你拥有一个函数指针时,如果没有明确指定为stdcall,那么就默认为cdecl。这意味着,如果你有一个stdcall指针和一个cdecl指针,你不能互换它们。两种函数类型可以相互调用而不会出现问题,只是在期望的类型上可能会出现问题。至于速度,它们都扮演同样的角色,只是在稍微不同的位置上,这真的不重要。


1

呼叫者和被呼叫者在调用点必须使用相同的约定 - 这是它能够可靠工作的唯一方式。呼叫者和被呼叫者都遵循预定义的协议 - 例如,谁需要清理堆栈。如果协议不匹配,您的程序会遇到未定义行为 - 可能只是崩溃得非常惊人。

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

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


1

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

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

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

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

是的。

它是如何工作的?

这是函数声明的一部分。

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

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

或者这是链接器的责任吗?

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

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

不是。为什么会呢?

一般来说,我们可以说哪个调用会更快 - cdecl 还是 stdcall?
我不知道。测试一下吧。

0
a) 当调用者调用一个 cdecl 函数时,调用者如何知道是否应该释放堆栈?
cdecl 修饰符是函数原型(或函数指针类型等)的一部分,因此调用者可以从那里获取信息,并相应地采取行动。
b) 如果一个被声明为 stdcall 的函数调用了一个调用约定为 cdecl 的函数,或者反过来,这会不合适吗?
不,这没问题。
c) 通常我们可以说哪种调用方式更快速 - cdecl 还是 stdcall?
一般而言,我不会对此发表任何观点。区别在于当您想要使用 va_arg 函数时会很重要。理论上讲,stdcall 可能更快速并且生成的代码更小,因为它允许将参数弹出与局部变量弹出组合在一起,但是如果聪明的话,cdecl 也可以做到同样的事情。
旨在提高速度的调用约定通常使用寄存器传参。

0

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

然而,有时我们希望由不同编译器编译的二进制代码能够正确地互操作。这时我们需要定义一些称为应用程序二进制接口(ABI)的东西。ABI定义了编译器将C/C++源代码转换为机器码的方式。这将包括调用约定、名称重整和虚函数表布局。cdelc和stdcall是在x86平台上常用的两种不同的调用约定。

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


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接