关于调用约定 cdecl、stdcall 和 fastcall 的区别

在计算机科学中, 调用约定(Calling Conventions)是一种定义子过程从调用处接受参数以及返回结果的方法的约定。

函数调用时, 调用者依次把参数压栈, 然后调用函数, 函数被调用以后, 在堆栈中取得数据, 并进行计算. 函数计算结束以后, 或者调用者、或者函数本身修改堆栈, 使堆栈恢复原装. 在参数传递中, 有几个很重要的问题必须得到明确说明:

  • 当参数个数多于一个时, 按照什么顺序把参数压入堆栈
  • 函数调用后, 由谁来把堆栈恢复原装
  • 函数的返回值放在什么地方

CDECL

1
2
int function(int a, int b) //不加修饰默认就是 C 调用约定
int __cdecl function(int a, int b) //明确指定 C 调用约定

cdecl 是 C Declaration 的缩写,表示 C 语言默认的函数调用方法:

  • 参数从右向左压入堆栈
  • 函数本身不清理堆栈,调用者负责清理堆栈,因此 C 调用约定允许函数的参数的个数是不固定的

函数调用 function(1, 2) 调用处翻译成汇编语言将变成:

Assembly
1
2
3
4
push 2
push 1
call function
add esp, 8 ; 注意, 这里调用者再恢复堆栈

被调用函数:

Assembly
1
2
3
4
5
6
7
push ebp ; 保存 ebp, 该寄存器将用来保存堆栈的栈顶指针, 可以在函数退出时恢复
mov ebp, esp ; 保存栈顶指针
mov eax, [ebp + 8H] ; 堆栈 ebp 指向位置之前依次保存有 ebp, cs:eip, a, b, ebp + 8 指向 a
add eax, [ebp + 0CH] ; 堆栈中 ebp + 12 处保存了 b
mov esp, ebp ; 恢复 esp
pop ebp
ret

STDCALL

1
int __stdcall function(int a, int b)

stdcall 是 Standard Call 的缩写,是 C++ 的标准调用方式:

  • 参数从右向左压入堆栈
  • 函数自身修改堆栈

函数调用 function(1, 2) 调用处翻译成汇编语言将变成:

Assembly
1
2
3
push 2 ; 第二个参数入栈
push 1 ; 第一个参数入栈
call function ; 调用函数, 注意此时自动把 cs:eip 入栈

而对于函数自身, 则可以翻译为:

Assembly
1
2
3
4
5
6
7
push ebp ; 保存 ebp 寄存器, 该寄存器将用来保存堆栈的栈顶指针, 可以在函数退出时恢复
mov ebp, esp ; 保存栈顶指针
mov eax, [ebp + 8H] ; 堆栈中 ebp 指向位置之前依次保存有 ebp, cs:eip, a, b, ebp + 8 指向 a
add eax, [ebp + 0CH] ; 堆栈中 ebp + 12 处保存了 b
mov esp, ebp ; 恢复 esp
pop ebp
ret 8

FASTCALL

1
int __fastcall function(int a, int b)

fastcall 是编译器指定的快速调用方式:

  • 函数的第一个和第二个 DWORD 参数(或者尺寸更小的)通过 ecx 和 edx 传递, 其他参数通过从右向左的顺序压栈
  • 被调用函数清理堆栈

总结

summary

简单举例

cdecl 和 stdcall

执行 call 之后

从图中可以了解到:

  • cdecl 平衡堆栈是在 call 结束回来后 add esp,0x8(参数占用空间)
  • stdcall 平衡堆栈是在 call 内部返回时 retn 0x8

参考