C漫谈-调用约定
软件开发中,我们的每个程序模块都应当遵守一定的规范来保证他们可以正确的使用对方,现在,我们称这个规范为ABI(应用二进制接口),一个ABI规范了诸如数据类型的大小、调用约定、系统调用约定、目标文件的二进制格式等等,我们今天要讨论的就是调用约定部分。
调用约定
所谓调用约定,就是约定如下内容如何进行:
- 参数和返回值放置的位置(在寄存器中;在调用栈中;两者混合)
- 参数传递的顺序(或者单个参数不同部分的顺序)
- 调用前设置和调用后清理的工作,在调用者和被调用者之间如何分配
- 被调用者可以直接使用哪一个寄存器有时也包括在内。(否则的话被当成ABI的细节)
- 哪一个寄存器被当作
volatile
的或者非volatile
的(如果是volatile
的,不需要被调用者恢复)
简单来说,就是调用者和被调用者两方共同约定关于如何调用函数的细则。x86
架构下,常见的调用约定有stdcall
、cdecl
、fastcall
和thiscall
等。
x86调用约定
cdecl
cdecl
是x86
架构下,C语言的事实调用约定,其细则如下:
- 函数实参在线程栈上从右至左依次压栈
- 函数结果保存在
EAX/AX/AL
中 - 浮点结果存放在
ST0
中 - 函数名前缀以一个下划线
- 调用者负责清理参数栈
- 8bits和16bits整形提升为32bits
- 受到函数调用影响的寄存器(
volatile
):EAX
、ECX
、EDX
、ST0 – ST7
、ES
、GS
- 不受函数调用影响的寄存器(
non-volatile
):EBX
、EBP
、ESP
、EDI
、ESI
、CS
、DS
不过该约定在实施时产生了细微的不同,Visual C++规定返回值如果是POD值且长度不超过32bits,用EAX
传递,长度在33-64bits用EAX:EDX
传递,超过64bits或非POD值,调用这会为函数预先分配一个空间,将地址作为隐式参数传递给被调用者;GCC返回值都是由调用者分配空间,并将地址作为隐式参数传递给被调用者。
stdcall
stdcall
是由微软创建的调用约定,是pascal
约定和cdecl
约定的折衷方案,其与cdecl
的主要区别是:
- 被调用者负责清理线程栈
- 函数名前缀以一个下划线且后缀以一个
@
和其参数所占的栈空间字节长度
gcc fastcall
由GCC实现的fastcall
约定第一个不超过32bits的参数通过ECX/CX/CL
传递,第二个不超过32bits的参数通过EDX/DX/DL
传递,其余从右到左压栈。
thiscall
thiscall
是为了实现C++非静态成员函数调用而创造的约定,在GCC中,thiscall
与cdecl
基本相同,但它会在最后压入this
指针,在Visual C++中,this
通过ECX
传递。
x86_64调用约定
x86_64
调用约定得到了一定程度上的统一,有两种主流规则:
System V AMD64 ABI
System V AMD64 ABI是大多数非Windows系统使用的ABI方案,其调用约定为:
- 前六个整形参数放在
RDI
、RSI
、RDX
、RCX
、R8
和R9
中,同时XMM0 - XMM7
用于放置浮点变元,对于系统调用,用R10
用来替代RCX
,其他额外参数入栈 - 返回值保存在
RAX
中(浮点数被保存在XMM0
) - 不受函数调用影响的寄存器:
RBX
,RBP
,RSP
、R12 - R15
微软x86_64调用约定
微软x86_64调用约定是Windows使用的调用约定:
- 使用
RCX
、RDX
、R8
和R9
用于函数调用的前四个参数,使用XMM0 - XMM3
传递浮点变量,其他参数入栈 - 整数返回值放在
RAX
中,浮点返回值位于XMM0
- 调用者需要在函数返回地址之上(参数之前)分配一个32字节的影子空间,调用结束后由调用者清理