函数调用约定
函数参数怎么传递和由谁清除堆栈
炒冷饭,都快要忘记自己有一个博客了 (x
什么是函数调用约定
在函数被调用的过程中,编译器都进行了以下的工作:
把调用者的地址压入栈
把函数的参数压入栈或者存储到寄存器当中
调转到被引用函数
把函数使用的寄存器压入栈
执行函数
处理函数返回值
将第三步中压栈的寄存器恢复到原始值
清空第一部中的压栈参数和处理返回地址
返回到调用者调用时的地址(即步骤一时记录的地址)
函数调用约定,就是对函数调用的一个约束和规定(规范),描述了函数参数是怎么传递和由谁清除堆栈的。(堆栈平衡?)
x64的前四个参数使用rcx,rdx,r8,r9传递,之后的参数通过栈来传递
它决定以下三个方面:
- 函数参数传递的方式(是否采用寄存器传递函数,采用那个寄存器调用函数,参数压栈顺序等)
- 函数调用结束后的栈指针由谁恢复(被调用者恢复或是被调用的函数恢复)
- 函数修饰名的产生方法
我们构造一个函数的时候,会规定返回类型和函数名(参数列表),如:
1 |
|
除此之外,还有另外一部分,就是函数的调用约定,由系统自动生成,也可以有我们来手动编写规定,如下:
1 |
|
常见的调用约定
- c:__cdecl 、__stdcall、__fastcall、naked、__pascall
- c++:__cdecl 、__stdcall、__fastcall、naked、__pascall、__thiscall
调用约定的使用
调用约定书写在函数的前面,相当于函数类型的一部分。要求函数的声明和定义要有相同的调用约定。
1 |
|
以上在编译过程中就会提示出错,因为声明和定义的调用约定不同。正确应该是:
1 |
|
不同调用下的规则
首先我们定义两个概念,即“被调用者”和“调用者”。如下Add()函数就是“被调用者”,ShoowResult()函数就是“调用者”。
1 |
|
__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指针在所有参数压栈后被压入堆栈
- 对于参数个数不确定的由调用者问清理堆栈,否则由被调函数清理。