函数的调用约定描述了执行函数时返回地址和参数的出入栈规律。不同公司开发的C语言编译器都有各自的函数调用约定,而且这些调用约定的差异性很大。随着IBM兼容机对市场进行洗牌后,微软操作系统和编程工具占据了统治地位。除微软之外,仍有零星的几家公司和开源项目GNU C在维护自己的调用约定。下面将介绍几款比较流行的函数调用约定。

x86/x86_64

❏ stdcall调用约定
■ 在调用函数时,参数将按照从右向左的顺序依次压入栈中,例如下面的function函数,其参数入栈顺序依次是second、first:
​​​​​​​​​​​​​​​​int function(int first, int second)​​
■ 函数的栈平衡操作(参数出栈操作)是由被调用函数完成的。通过代码retn x可在函数返回时从栈中弹出x字节的数据。当CPU执行RET指令时,处理器会自动将栈指针寄存器ESP向上移动x个字节,来模拟栈的弹出操作。例如上面的function函数,当function函数返回时,它会执行该指令把参数second和first从栈中弹出来,再到返回地址处继续执行。
■ 在函数的编译过程中,编译器会在函数名前用下划线修饰,其后用符号@修饰,并加上入栈的字节数,因此函数function最终会被编译为_function@8。

❏ cdecl调用约定
■ cdecl调用约定的参数压栈顺序与stdcall相同,皆是按照从右向左的顺序将参数压入栈中。
■ 函数的栈平衡操作是由调用函数完成的,这点与stdcall恰恰相反。stdcall调用约定使用代码retn x平衡栈,而cdecl调用约定则通常会借助代码leave、pop或向上移动栈指针等方法来平衡栈。
■ 每个函数调用者都含有平衡栈的代码,因此编译生成的可执行文件会较stdcall调用约定生成的文件大。
cdecl是GNU C编译器的默认调用约定。但GNU C在64位系统环境下,却使用寄存器作为函数参数的传递方式。函数调用者按照从左向右的顺序依次将前6个整型参数放在通用寄存器RDI、RSI、RDX、RCX、R8和R9中;同时,寄存器XMM0~XMM7用来保存浮点变量,而RAX寄存器则用于保存函数的返回值,函数调用者负责平衡栈。

❏ fastcall调用约定
■ fastcall调用约定要求函数参数尽可能使用通用寄存器ECX和EDX来传递参数,通常是前两个int类型的参数或较小的参数,剩余参数再按照从右向左的顺序逐个压入栈中。
■ 函数的栈平衡操作由被调用函数负责完成。
除此之外,还有很多调用约定,如thiscall、nakedcall、pascal等,有兴趣的读者可以自行研究。

ARM64

ARM64 调用约定
- 参数在 x0-x7 寄存器中传递,其余在堆栈中传递
- ret 命令用于返回 Link 寄存器中的地址(默认值为 x30)
- 函数的返回值存储在 x0 或 x0+x1 中,具体取决于它是 64 位还是 128 位
- x8 是间接结果寄存器,用于传递间接结果的地址位置,例如函数返回一个大结构的地方
- 使用 B 操作码跳转到函数。
- 带链接分支 (BL) 将下一条指令的地址(在 BL 之后)复制到分支前的链接寄存器 (x30)
因此,BL 用于子程序调用
BR调用用于分支注册,例如br x8
BLR 代码用于跳转寄存器并将下一条指令的地址(在 BL 之后)存储到链接寄存器(x30)中

⬆︎TOP