编译器实现函数调用时所遵循的一系列规则称为函数的“调用约定(Calling Convention)”,x86-64平台上的编译器随着操作系统的不同而有不同的约定。Windows上采用的是Wx64/Vector的标准,而类unix上采用systemV AMD64 ABI的调用标准。统一的调用约定保证了程序的最大可移植性。
systemV(苹果)
__attribute__((ms_abi))或__attribute__((sysv_abi))或__attribute__((cdecl))或__attribute__((StdCall))
调用约定可以通过在函数前这是来控制(Linux下)
// cdecl调用约定
int __attribute__((cdecl)) add_cdecl(int a, int b) {
return a + b;
}
// stdcall调用约定
int __attribute__((stdcall)) add_stdcall(int a, int b) {
return a + b;
}
int main() {
int sum1 = add_cdecl(1, 2);
int sum2 = add_stdcall(1, 2);
return 0;
}
函数调用一个涉及函数的参数,函数的返回值,堆栈的清空与重置,寄存器的设置四个方面。
//栈帧的清空与重置
栈帧中存放有与每个函数调用相关的返回地址,实参,局部变量,返回值,以及暂存器的寄存器等信息。在进程中的虚拟地址中,栈的空间是由高地址向低地址增加的,通过rbp-4/rbp-8等修改地址的偏移值来给地址赋值从而保存代码中的变量的值。
当函数结束的时候,通过调用leave这个指令可以清空栈的内存地址
这个图随时要看要记着
Push rbp //报错调用者函数的rbp(当前函数的栈帧基址)
Move rbp rsp //重新设置一个函数的栈帧基址,rsp代表获取一个新的栈地址,具体值由编译器维护
。。。
。。。
Pop rbp //恢复调用者函数的rbp
Ret //返回调用者
参数的传递
SysV调用规则是对于整形和指针类型的实参,分别需要特定的寄存器保存参数,分别是rdi,rsi,rdx,cdx,r8,r9,按照函数定义时的参照从左到右依次传值。若一个函数的参数超过了6个,则余下的通过栈内存进行传送。多出来的参数从左到右依次被压入栈中。(30到40行代码)
而浮点数则会存在xmm0~xmm7这8个寄存器中。
Linux上
// cdecl和fastcall
Cdecl通过寄存器传参数,fastcall通过函数压栈传参,传参的方向一般都是从右往左传值。
返回值的约定
sysV约定:当函数产生整数返回值小于64位的时候,通过寄存器rax来存储。大于64位的时候低位通过rax存储,高位通过rdx存储。对于复合类型的返回值,编译器可能直接使用栈内存进行中转。
对于浮点型的返回值。类似参数传递。放到特殊的寄存器中xmm0~xmm7返回