一、 调用约定
函数调用约定是应用程序二进制接口的一部分,描述了执行函数时返回值和参数入栈的方式,不同公司开发的C语言编译器都有不同的ABI标准,这些调用约定差异很大,如果调用约定不相同,那么程序是无法正常进行工作的。
二、x86位调用约定
随着IBM兼容机对市场进行洗牌后,微软的操作系统和编程工具占据了市场的主导地位,但是还是有少数公司和GNU在维护着自己的调用约定,除了编译器的调用约定之外,不同的解释语言的解释器也执行着对应的调用约定。
2.1 __cdecl
__cdecl 是 C 和 C++ 程序的默认调用约定
栈平衡方式 | 由调用者完成 |
---|---|
入栈顺序 | 从右到左 |
返回值传递 | 由EAX/AX/AL传递 浮点型结果存放在寄存器ST0 |
函数命名 | 下划线字符 (_) 作为名称的前缀 |
2.2 __stdcall
__stdcall调用约定用于调用 Win32 API 函数,是32位模式下常用的三种标准之一。
栈平衡方式 | 由被调者完成 |
---|---|
入栈顺序 | 从右到左 |
返回值传递 | 由EAX/AX/AL传递 浮点型结果存放在寄存器ST0 |
函数命名 | 下划线 (_) 的名称为前缀 该名称后跟符号 (@) 后跟参数的字节数 因此,声明为 int func( int a, double b ) 的函数按如下所示进行修饰:_func@12 |
2.3 __fastcall
__fastcall从名字就可以看出他的速度比前面两种调用方式更快,因为他使用了两个寄存器传递参数,多出的参数才使用栈传递,寄存器传参数不需要入栈出栈操作,并且寄存器速度远高于内存,因此该方式的速度更快。
栈平衡方式 | 由被调者完成 |
---|---|
入栈顺序 | 在自变量列表中按从左到右的顺序找到的前两个 DWORD 或更小自变量将在 ECX 和 EDX 寄存器中传递 所有其他自变量在堆栈上从右向左传递 |
返回值传递 | 由EAX/AX/AL传递 浮点型结果存放在寄存器ST0 |
函数命名 | At 符号 (@) 是名称的前缀 参数列表中的字节数(在十进制中)前面的 at 符号是名称的后缀。 |
从上面三种方式可以看出函数调用时栈传递方式有两种,一种是栈传递参数,另一种是寄存器传递参数,在系统调用时会从用户态转换成内核态,x86架构下会从TSS中查找对应特权级的栈,用户栈和内核栈是不同的两段内存,且内核栈与用户栈是隔离开的,要想让内核获取到参数,需要进行数据拷贝,也就意味着增加额外的工作量,效率也就降低了,不可能采用这种方式传递参数,因此使用寄存器传递参数是最快的方式。
三、x64调用约定
默认情况下,x64 应用程序二进制接口 (ABI) 使用四寄存器 fast-call 调用约定。 系统在调用堆栈上分配空间作为影子存储,供被调用方保存这些寄存器。
整数参数在寄存器 RCX、RDX、R8 和 R9 中传递。 浮点数参数在 XMM0L、XMM1L、XMM2L 和 XMM3L 中传递。 16 字节参数按引用传递。
对于原型函数,在传递参数之前,所有参数都将转换为所需的被调用方类型。 调用方负责为被调用方的参数分配空间。 调用方必须始终分配足够的空间来存储 4 个寄存器参数,即使被调用方不使用这么多参数。 此约定简化了对非原型 C 语言函数和 vararg C/C++ 函数的支持。 对于 vararg 或非原型函数,任何浮点值都必须在相应的通用寄存器中重复。 调用之前,必须将除前 4 个参数外的其他参数存储在影子存储后面的堆栈中。