在这些约定中,调用者自己清理堆栈上的参数(arguments),这样就允许了可变参数列表的实现,如printf()
。
cdecl(C declaration,即C声明)是源起C语言的一种调用约定,也是C语言的事实上的标准。在x86架构上,其内容包括:
GCC的函数返回值都是由调用者分配空间,并把该空间的地址作为隐式参数传递给被调函数,而不使用寄存器EAX。GCC自4.5版本开始,调用函数时,堆栈上的数据必须以16B对齐(之前的版本只需要4B对齐即可)。
考虑下面的C代码片段:
int callee(int, int, int);
int caller(void)
{
register int ret;
ret = callee(1, 2, 3);
ret += 5;
return ret;
}
在X86上,会产生如下汇编代码:
caller:
pushl %ebp
movl %esp,%ebp
pushl $3
pushl $2
pushl $1
call callee
addl $12,%esp
addl $5,%eax
leave
ret
cdecl调用约定通常作为x86 C编译器的默认调用规则,许多编译器也提供了自动切换调用约定的选项。如果需要手动指定调用规则为cdecl,编译器可能会支持如下语法:
return_type _cdecl funct();
与cdecl类似,参数被从右到左推入堆栈中。EAX, ECX和EDX不会保留值。参数列表的大小被放置在AL寄存器中(?)。 syscall是32位OS/2 API的标准。
参数也是从右到左被推入堆栈。从最左边开始的三个字符变元会被放置在EAX, EDX和ECX中,最多四个浮点变元会被传入ST(0)到ST(3)中----虽然这四个参数的空间也会在参数列表的栈上保留。函数的返回值在EAX或ST(0)中。保留的寄存器有EBP, EBX, ESI和EDI。 optlink在IBM VisualAge编译器中被使用。
如果被调用者要清理栈上的参数,需要在编译阶段知道栈上有多少字节要处理。因此,此类的调用约定并不能兼容于可变参数列表,如printf()。然而,这种调用约定也许会更有效率,因为需要解堆栈的代码不要在每次调用时都生成一遍。 使用此规则的函数容易在asm代码被认出,因为它们会在返回前解堆栈。x86 ret指令允许一个可选的16位参数说明栈字节数,用来在返回给调用者之前解堆栈。代码类似如下:
ret 12
基于Pascal语言的调用约定,参数从左至右入栈(与cdecl相反)。被调用者负责在返回前清理堆栈。 此调用约定常见在如下16-bit 平台的编译器:OS/2 1.x,微软Windows 3.x,以及Borland Delphi版本1.x。
stdcall是由微软创建的调用约定,是Windows API的标准调用约定。非微软的编译器并不总是支持该调用协议。GCC编译器如下使用:
int __attribute__((__stdcall__ )) func()
stdcall是Pascal调用约定与cdecl调用约定的折衷:被调用者负责清理线程栈,参数从右往左入栈。其他各方面基本与cdecl相同。但是编译后的函数名后缀以符号"@",后跟传递的函数参数所占的栈空间的字节长度。寄存器EAX, ECX和EDX被指定在函数中使用,返回值放置在EAX中。stdcall对于微软Win32 API和Open Watcom C++是标准。
微软的编译工具规定:
PASCAL, WINAPI, APIENTRY, FORTRAN, CALLBACK, STDCALL, __far __pascal, __fortran, __stdcall
均是此规定.
在调用C++非静态成员函数时使用此约定。基于所使用的编译器和函数是否使用可变参数,有两个主流版本的thiscall。 对于GCC编译器,thiscall几乎与cdecl等同:调用者清理堆栈,参数从右到左传递。差别在于this指针,thiscall会在最后把this指针推入栈中,即相当于在函数原型中是隐式的左数第一个参数。
在微软Visual C++编译器中,this指针通过ECX寄存器传递,其余同cdecl约定。当函数使用可变参数,此时调用者负责清理堆栈(参考cdecl)。thiscall约定只在微软Visual C++ 2005及其之后的版本被显式指定。其他编译器中,thiscall并不是一个关键字(反汇编器如IDA使用__thiscall)。
微软x64调用约定使用%rdi, %rsi, %rdx, %rcx,%r8, %r9 六个寄存器用于存储函数调用时的6个参数(从左到右),使用XMM0, XMM1, XMM2, XMM3来传递浮点变量。其他的参数直接入栈(从右至左)。整型返回值放置在RAX中,浮点返回值在XMM0中。少于64位的参数并没有做零扩展,此时高位充斥着垃圾。
在微软x64调用约定中,调用者的一个职责是在调用函数之前(无论实际的传参使用多大空间),在栈上的函数返回地址之上(靠近栈顶)分配一个32字节的“影子空间”;并且在调用结束后从栈上弹掉此空间。影子空间是用来给RCX, RDX, R8和R9提供保存值的空间,即使是对于少于四个参数的函数也要分配这32个字节。