函数调用约定(x86 32位)
32 位 x86 函数调用约定
本文讨论的是 32 位 x86 Windows 环境下常见的调用约定。
x86-64 下的参数传递规则与 32 位 x86 有明显区别,不能直接套用本文。
在理解了栈帧之后,我们知道函数调用过程中如何利用栈保存返回地址、参数、局部变量等数据。但这又引出了一个问题:参数究竟是怎么传给函数的?返回值又是怎么传回来的?
汇编语言本身没有像 C/C++ 那样由语言层面定义的“函数”这一抽象,但程序可以通过 call、ret、寄存器和栈等机制实现函数调用
c
int func2(int a, int b)
{
return a + b;
}
cpu最终可能执行的汇编是
asm
add ...
ret
那么a和b是怎么来的?函数如何知道要用这两个数字?这就涉及到函数调用约定
什么是调用约定
或者说调用约定需要解决什么问题
**调用约定(Calling Convention)**是一套规定函数之间如何进行调用和协作的规则,包括参数如何传递、返回值如何传递、哪些寄存器由调用者或被调用者负责保存,以及由谁负责清理调用参数等。
它在程序运行时尤其重要,因为错误的调用约定可能导致程序崩溃或出现未定义行为
在传统 32 位 x86 架构上,C/C++函数调用通常会依照一定的约定来传递参数:
- 参数是通过寄存器或栈传递的
- 返回值通过寄存器传递
- 栈的清理可以由调用者或被调用者负责,具体取决于调用约定
返回值如何传递?
调用约定除了规定参数如何传递,还需要规定函数执行完毕后,返回值放在哪里
在经典的 32 位 x86 C/C++ 调用约定中,整数类型的返回值通常通过 EAX 寄存器传递
c
int add(int a, int b)
{
return a + b;
}
编译后可能出现类似:
asm
mov eax, [ebp+8]
add eax, [ebp+0Ch]
ret
这里
EAX = a + b
函数执行 ret 返回之后,调用者就可以从 EAX 中获得返回值
_stdcall(标准调用约定)
一种常见的调用约定,特别是在 32 位 Windows API 的历史环境中大量使用
- 参数传递顺序:同样从右至左依次压入栈中
- 栈清理:由被调用者(即函数本身)负责清理栈上的参数。在函数执行完毕并返回前,函数会自动调整栈指针
- 不支持可变参数:由于被调用者负责栈清理,无法支持可变参数函数
人话:被调用的函数自己清理自己搞乱的栈
优点:
- 减少了调用者的栈管理负担,代码更简洁
- 在固定参数函数中效率较高,因为调用者无需关心栈指针的调整
缺点:
- 不支持可变参数函数
- 被广泛用于Windows API和某些平台特定的库中,因此不如
__cdecl灵活
函数末尾出现 ret N,cpu会先从栈顶取出返回地址并加载到指令指针寄存器 EIP,然后将 ESP 增加 N,最后跳转到该返回地址继续执行
ret N 会弹出返回地址,并在返回过程中额外将 ESP 增加 N 字节,用于回收调用参数占用的栈空间。
注意:ret N清理的实际上是调用参数的栈空间而非整个栈帧
_cdecl(C/C++程序默认函数调用约定)
在 32 位 MSVC 环境下,__cdecl 是普通 C/C++ 函数常见的默认调用约定,它具有以下特点:
- 参数传递顺序:参数从右至左依次压入栈中
- 栈清理:由调用者负责清理栈上的参数,这意味着每次调用函数时,调用者必须在函数返回后手动调整栈指针
- 可变参数支持:因为调用者负责清理栈,这种调用约定天然支持可变参数函数(如
printf)
人话:调用者清理自己调用的函数搞乱的栈
**优点:**支持可变参数函数,适合需要灵活处理参数数量的函数
**缺点:**栈清理由调用者负责,如果频繁调用函数,栈清理开销会较大
在 call 返回后的后续指令中,如果看到 :
asm
add esp,xxx
通常可以判断调用者正在回收此前压入栈中的参数
_fast call(快速调用约定)
该约定旨在通过寄存器传递参数来提高性能,适用于x86架构
在经典 MSVC 32 位 x86 __fastcall 约定中,参数通过ECX/EDX寄存器传递,剩余参数通过栈传递,被调用者负责栈平衡
最典型特征观察:call指令之前是否有编译器将头两个参数通过 mov 等指令放入 ECX/EDX,而不仅仅是push指令
具体区别
清理方式
__cdecl:调用者负责清理栈。这意味着每次调用函数后,调用者需要在汇编代码中清理参数(通常通过增加ESP寄存器的值)__stdcall:被调用者负责清理栈。在函数返回时,栈的清理操作由函数自动完成,调用者不需要额外的栈清理步骤_fastcall:同样由被调用者通过ret N清理栈的
传递顺序
无论是 __cdecl 还是 __stdcall,参数都从右至左压入栈中。这意味着在定义类似于 int add(int a, int b) 的函数时,参数 b 会比 a 先压入栈
可变参数支持
对于可变参数函数,例如 printf,被调用者无法仅根据函数类型确定实际传入了多少参数,因此无法可靠地自行确定需要清理多少参数栈空间。
__cdecl 让调用者负责清理栈,因此能够支持可变参数函数。
这就是为什么
c
printf("%d %d %d", a, b, c);
这种函数可以工作
评论