函数调用约定

函数参数怎么传递和由谁清除堆栈

炒冷饭,都快要忘记自己有一个博客了 (x

什么是函数调用约定

在函数被调用的过程中,编译器都进行了以下的工作:

  1. 把调用者的地址压入栈

  2. 把函数的参数压入栈或者存储到寄存器当中

  3. 调转到被引用函数

  4. 把函数使用的寄存器压入栈

  5. 执行函数

  6. 处理函数返回值

  7. 将第三步中压栈的寄存器恢复到原始值

  8. 清空第一部中的压栈参数和处理返回地址

  9. 返回到调用者调用时的地址(即步骤一时记录的地址)

函数调用约定,就是对函数调用的一个约束和规定(规范),描述了函数参数是怎么传递和由谁清除堆栈的。(堆栈平衡?)

x64的前四个参数使用rcx,rdx,r8,r9传递,之后的参数通过栈来传递

它决定以下三个方面:

  • 函数参数传递的方式(是否采用寄存器传递函数,采用那个寄存器调用函数,参数压栈顺序等)
  • 函数调用结束后的栈指针由谁恢复(被调用者恢复或是被调用的函数恢复)
  • 函数修饰名的产生方法

我们构造一个函数的时候,会规定返回类型和函数名(参数列表),如:

1
2
void funcA();
void funcB(int a,int b);

除此之外,还有另外一部分,就是函数的调用约定,由系统自动生成,也可以有我们来手动编写规定,如下:

1
2
void __cdecl funA();
void __stdcall funcB();

常见的调用约定

  • c:__cdecl 、__stdcall、__fastcall、naked、__pascall
  • c++:__cdecl 、__stdcall、__fastcall、naked、__pascall、__thiscall

调用约定的使用

调用约定书写在函数的前面,相当于函数类型的一部分。要求函数的声明和定义要有相同的调用约定。

1
2
3
4
5
int Add(int a,int b);   //默认是__cdecl
int __stdcall Add(int a,int b)
{
return a+b;
}

以上在编译过程中就会提示出错,因为声明和定义的调用约定不同。正确应该是:

1
2
3
4
5
int __stdcall Add(int a,int b);
int __stdcall Add(int a,int b)
{
return a+b;
}

不同调用下的规则

首先我们定义两个概念,即“被调用者”和“调用者”。如下Add()函数就是“被调用者”,ShoowResult()函数就是“调用者”。

1
2
3
4
5
6
7
8
9
int Add(int a, int b)
{
return a+b;
}

void ShowResult()
{
cout<<add(5,10)<<endl;
}

__cdecl

__cdecl是C Declaration的缩写,表示C\C++默认的函数调用约定

调用方式

  • 采用栈传递参数,参数从右向左依次入栈
  • 由调用者恢复栈顶指针
  • 编译器在编译时会在函数名前加上一个下划线前缀生成修饰名,格式为_function。如Add()的修饰名是_Add()

注意:调用参数个数可变的函数只能采用这种方式

__stdcall

__stdcall是Standard Call的缩写,是C++的标准调用方式。

调用方式

  • 采用栈传递参数,参数从右向左依次入栈
  • 由被调用者负责恢复栈顶指针
  • 在输出函数名前加上一个下划线前缀,后面加一个@符号和其参数的字节数,格式为_function@number。如函数Add的修饰名是_Add@8

__stdcall与__cdecl最主要的区别是第2条规定:由“被调用者”清空实际上就是把对应参数数目的数据从栈中弹出,这样的缺点就是它不能使用于那些不确定数目参数的函数。

好处在于只需要在函数内部编译出恢复栈顶的代码,而调用者恢复则需要在调用出编译出恢复栈顶的代码。

__fastcall

__fastcall是快速调用,因为有部分参数可以通过寄存器直接传递,效率比较高。

调用方式

  • 函数的第一个和第二个(从左向右)32字节参数(或者尺寸更小的)通过ecx和edx传递(寄存器传递),其他参数通过桟传递。从第三个参数(如果有的话)开始从右向左的顺序压栈
  • 由被调用者恢复栈顶指针
  • 在函数名前加上@,在函数名后加@和参数字节数,格式为@function@number

__thiscall

__thiscall是唯一一个不能明确指明的函数修饰,因为thiscall只能用于C++类成员函数的调用,同时thiscall也是C++成员函数缺省的调用约定。由于成员函数调用还有一个this指针,因此必须特殊处理。

调用方式

  • 采用栈传递参数,参数自右向左入栈
  • 如果参数的个数确定,this指针通过ecx传递给被调用者;如果参数个数不确定,this指针在所有参数压栈后被压入堆栈
  • 对于参数个数不确定的由调用者问清理堆栈,否则由被调函数清理。

函数调用约定
https://shmodifier.github.io/2023/06/19/函数调用约定/
作者
Modifier
发布于
2023年6月19日
许可协议