解释x86架构的系统调用约定:
(1)函数压栈的顺序
(2)谁来清理栈、负责栈平衡
调用约定基本概念
调用约定(calling convention)是为了简化函数间数据传递的复杂性而设计的,它设定了函数之间相互作用的规则。它们是应用程序二进制接口(Application Binary Interface,ABI)的一部分,ABI是对代码交互方式的最底层定义[1]。
调用约定必须定义一些规则[1],包括:
(1)参数位置:调用者将参数从哪里传递给被调用者(是栈还是寄存器)?
(2)参数排序:参数将如何排列,是在栈上还是在寄存器中?
(3)栈清理:如果使用了栈,哪个函数负责从栈中清除值(是调用者负责,还是被调用者负责)?
(4)寄存器访问:调用者可以使用哪些寄存器,而无须备份原值并在返回之前恢复它们?
(5)返回值:调用者将如何以及在哪里从被调用者那里获取返回值?
调用约定可能会基于以下几个因素而有所不同[1]:
(1)架构(x86还是ARM)
(2)操作系统(UN*X还是Windows)
(3)编程语言(C还是Java)
(4)编译器(GCC还是Microsoft)
常用的三种调用约定
调用约定 | 参数压栈顺序 | 栈的清理 | 寄存器使用 | 适用场景 |
__cdecl | 从右往左压栈 | 调用方 | 不使用寄存器 | 通用函数 |
__stdcall | 从右往左压栈 | 被调用方 | 不使用寄存器 | Windows API 函数 |
__fastcall | ECX/EDX传递前两个参数 剩下的从右往左依次入栈 | 被调用方 | 使用特定寄存器 | 性能要求高的函数 |
__cdecl
- 参数传递:参数从右到左压入栈中。
- 栈的清理:由调用者负责清理栈。
- 寄存器使用:通常不使用寄存器传递参数,所有参数都通过栈传递。
- 特点:适用于大多数 C/C++ 标准库函数;由调用者负责清理栈。
__stdcall
- 参数传递:参数从右到左压入栈中。
- 栈的清理:由被调用者负责清理栈。
- 寄存器使用:通常不使用寄存器传递参数,所有参数都通过栈传递。
- 特点:适用于 Windows API 函数;由被调用者负责清理栈。
__fastcall
- 参数传递:前两个参数(通常是整数或指针)通过寄存器传递,其余参数从右到左压入栈中。
- 栈的清理:由调用者负责清理栈。
- 寄存器使用:使用特定的寄存器(如
ECX
和EDX
)传递前两个参数,其余参数通过栈传递。 - 特点:适用于对性能要求较高的函数;由于使用寄存器传递参数,函数调用速度较快。
如何确定程序的调用约定
1. 查看编译器文档:编译器通常有默认的调用约定。例如,GCC默认使用__cdecl,而MSVC在某些情况下默认使用__stdcall;
2. 查看代码:在代码中,函数的调用约定可以显式声明(未声明时编译器使用默认的调用约定);
int __cdecl add(int a, int b){
return a + b;
}
3. 反汇编查看:通过反汇编代码中堆栈的清理方式进行区分。
参考引用
[1] 《x86 汇编与逆向工程:软件破解与防护的艺术》
[2] 自变量传递和命定 | Microsoft Learn
[3] 【汇编 C】C语言常用的三种函数调用约定:__cdecl、__stdcall、__fastcall_cdecl stdcl-CSDN博客