本條目描述x86架构微處理器的调用约定。
调用约定描述了被调用代码的接口:
- 原子(标量)参数或复杂参数独立部分的分配顺序
- 参数是如何被传递的(放置在堆栈上,或是寄存器中,亦或两者混合)
- 被调用者应保存调用者的哪个寄存器
- 调用函数时如何为任务准备堆栈,以及任务完成如何恢复
这与编程语言中对于大小和格式的分配紧密相关。另一个密切相关的是名字修饰,这决定了代码中的符号名称如何映射到链接器中的符号名。调用约定,类型表示和名称修饰这三者的统称,即是众所周知的应用二进制接口(ABI)。
不同编译器在实现这些约定总是有细微的差别存在,所以在不同编译器编译出来的代码很难接合起来。另一方面,有些约定被当作一种API标准(如stdcall),编译器实现都较为一致。
历史背景
在微型電腦出现之前,计算机厂商几乎都会提供一份操作系统和为不同编程语言编写的编译器。平台所使用的调用约定都是由厂商的软件实现定义的。
在Apple Ⅱ出现之前的早期微机几乎都是“裸机”,少有一份OS或编译器的,即是IBM PC也是如此。IBM PC兼容机的唯一的硬件标准是由Intel处理器(8086, 80386)定义的,并由IBM分发出去。硬件扩展和所有的软件标准(BIOS调用约定)都开放有市场竞争。
一群独立的软件公司提供了操作系统,不同语言的编译器以及一些应用软件。基于不同的需求,历史实践和开发人员的创造力,这些公司都使用了各自不同的调用约定,往往差异很大。
在IBM兼容机市场洗牌后,微软操作系统和编程工具(有不同的调用约定)占据了统治地位,此时位于第二层次的公司如Borland和Novell,以及开源项目如GCC,都还各自维护自己的标准。互操作性的规定最终被硬件供应商和软件产品所采纳,简化了选择可行标准的问题。
调用者清理
在这些约定中,调用者自己清理堆栈上的实参(arguments),这样就允许了可变参数列表的实现,如printf()。
cdecl
cdecl(C declaration,即C声明)是源起C语言的一种调用约定,也是C语言的事实上的标准。在x86架构上,其内容包括:
#函数实参在线程栈上按照从右至左的顺序依次压栈。
#函数结果保存在寄存器EAX/AX/AL中
#浮点型结果存放在寄存器ST0中
#编译后的函数名-{}-前-{}-缀以一个下划线字符(_)
#调用者负责从线程栈中弹出实参(即清栈)
#8比特或者16比特长的整形实参提升为32比特长。
#受到函数调用影响的寄存器(volatile registers):EAX、ECX、EDX、ST0 – ST7、ES、GS
#不受函数调用影响的寄存器:EBX、EBP、ESP、EDI、ESI、CS、DS
#RET指令从函数被调用者返回到调用者(实质上是读取寄存器EBP所指的线程栈之处保存的函数返回地址并加载到IP寄存器)
Visual C++规定函数返回值如果是POD值且长度如果不超过32比特,用寄存器EAX传递;长度在33-64比特范围内,用寄存器EAX:EDX传递;长度超过64比特或者非POD值,则调用者为函数返回值预先分配一个空间,把该空间的地址作为隐式参数传递给被调函数。
GCC的函数返回值都是由调用者分配空间,并把该空间的地址作为隐式参数传递给被调函数,而不使用寄存器EAX。GCC自4.5版本开始,调用函数时,堆栈上的数据必须以16字节对齐(之前的版本只需要4字节对齐即可)。
考虑下面的C代码片段:
{{syntaxhighlight |lang=c |code=
int callee(int, int, int);
int caller(void)
{
return callee(1, 2, 3) + 5;
}
}}
在x86上,会产生如下汇编代码(Intel语法):
在函数返回后,调用的函数清理了堆栈。在cdecl的理解上存在一些不同,尤其是在如何返回值的问题上。结果,x86程序经过不同OS平台的不同编译器编译后,会有不兼容的情况,即使它们使用的都是“cdecl”规则并且不会使用系统调用。
某些编译器返回简单的数据结构,长度大致占用两个寄存器,放在寄存器对EAX:EDX中;大点的结构和类对象需要异常处理器的一些特殊处理(如一个定义的构造函数,析构函数或赋值),存放在内存上。为了放置在内存上,调用者需要分配一些内存,并且让一个指针指向这块内存,这个指针就作为隐藏的第一个参数;被调用者使用这块内存并返回指针——返回时弹出隐藏的指针。
在Linux/GCC中,将浮点数值推入调用栈采用了浮点装载指令fld和存储并弹出指令fstp。下面是传递一个双精度浮点值参数的子例程调用例子:
sub esp, 8
fld QWORD PTR double_value
lea esp, [esp-8]
fstp QWORD PTR [esp]
call callee
add esp, 16
这里采用了16字节堆栈对齐,使用这种迂回方法确保能以正确的格式推入堆栈。
cdecl调用约定通常作为x86 C编译器的默认调用规则,许多编译器也提供了自动切换调用约定的选项。如果需要手动指定调用规则为cdecl,编译器可能会支持如下语法:
其中__cdecl修饰符需要在函数原型中给出,在函数声明中会覆盖掉其他的设置。
syscall
与cdecl类似,参数被从右到左推入堆栈中。EAX, ECX和EDX不会保留值。参数列表的大小被放置在AL寄存器中。
syscall是32位OS/2 API的标准。
optlink
参数也是从右到左被推入堆栈。从最左边开始的三个字符变元会被放置在EAX, EDX和ECX中,最多四个浮点变元会被传入ST(0)到ST(3)中——虽然这四个参数的空间也会在参数列表的栈上保留。函数的返回值在EAX或ST(0)中。保留的寄存器有EBP, EBX, ESI和EDI。
optlink在IBM VisualAge编译器中被使用。
被调用者清理
如果被调用者要清理栈上的参数,需要在编译阶段知道栈上有多少字节要处理。因此,此类的调用约定并不能兼容于可变参数列表,如printf()。然而,这种调用约定也许会更有效率,因为需要解堆栈的代码不要在每次调用时都生成一遍。
使用此规则的函数容易在asm代码被认出,因为它们会在返回前解堆栈。x86 ret指令允许一个可选的16位参数说明栈字节数,用来在返回给调用者之前解堆栈。代码类似如下:
pascal
基于Pascal语言的调用约定,参数从左至右入栈(与cdecl相反)。被调用者负责在返回前清理堆栈。
此调用约定常见在如下16-bit 平台的编译器:OS/2 1.x,微软Windows 3.x,以及Borland Delphi版本1.x。
register
Borland fastcall的别名。
stdcall
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均是指此种调用约定。
WINAPI是平台的缺省调用约定。Windows操作系统上默认是StdCall;Windows CE上默认是Cdecl。
fastcall
此约定还未被标准化,不同编译器的实现也不一致。
Microsoft/GCC fastcall
Microsoft或GCC的__fastcall约定(也即__msfastcall)把第一个(从左至右)不超过32比特的参数通过寄存器ECX/CX/CL传递,第二个不超过32比特的参数通过寄存器EDX/DX/DL,其他参数按照自右到左顺序压栈传递。
Borland fastcall
从左至右,传入三个参数至EAX, EDX和ECX中。剩下的参数推入栈,也是从左至右。
在32位编译器Embarcadero Delphi中,这是缺省调用约定,在编译器中以register形式为人知。
在i386上的某些版本Linux也使用了此约定。
调用者或被调用者清理
thiscall
在调用C++非静态成员函数时使用此约定。基于所使用的编译器和函数是否使用可变参数,有两个主流版本的thiscall。
对于GCC编译器,thiscall几乎与cdecl等同:调用者清理堆栈,参数从右到左传递。差别在于this指针,thiscall会在最后把this指针推入栈中,即相当于在函数原型中是隐式的左数第一个参数。
在微软Visual C++编译器中,this指针通过ECX寄存器传递,其余同cdecl约定。当函数使用可变参数,此时调用者负责清理堆栈(参考cdecl)。thiscall约定只在微软Visual C++ 2005及其之后的版本被显式指定。其他编译器中,thiscall并不是一个关键字(反汇编器如IDA使用__thiscall)。
寄存器保持
调用约定的另一部份是要保证哪些寄存器在子例程调用之后保持原来的值。这种行为叫做寄存器保持(preservation)。
调用者保存的寄存器
依据大多数编译器所遵循的,EAX、EDX和ECX是在一个过程或函数内可以自由使用而不需保存的。
正如其名字所暗示的,这些通用寄存器通常持有临时而易失(volatile)的信息,故而可以被任何子例程覆写。因此,将每个这种寄存器压入堆栈是调用者的责任,如果想要在子例程调用之后复原它们的值的话。
被调用者保存的寄存器
其它寄存器被用来持有长存而不易失(non-volatile)的值,也就是应当跨越调用而被保持。
换句话说,在调用者发起过程调用的时候,它可以预期这些寄存器在被调用者返回之后保持原来的值。因此,在开始时保存(压入堆栈)和在返回调用者之前复原(相应的弹出堆栈)这些寄存器是被调用者的责任。与前面的情况一样,这种做法只应当在被调用者变更了的寄存器上进行。
x86-64调用约定
x86-64调用约定得益于更多的寄存器可以用来传递参数值,而且不兼容的调用约定也更少了。但是由于所用于64-位数据模型的不同而有2种主流的规则。
微软x64调用约定
在Windows x64环境下编译代码时,只有一种调用约定,也就是说32位下的各种约定在64位下统一成一种了。
微软x64调用约定使用RCX、RDX、R8、R9四个寄存器用于存储函数调用时的4个参数(从左到右),使用XMM0、XMM1、XMM2、XMM3来传递浮点变量。其他的参数直接入栈(从右至左)。整型返回值放置在RAX中,浮点返回值在XMM0中。少于64位的参数并没有做零扩展,此时高位充斥着垃圾。
在微软x64调用约定中,调用者的一个职责是在调用函数之前(无论实际的传参使用多大空间),在栈上的函数返回地址之上(靠近栈顶)分配一个32字节的“影子空间”;并且在调用结束后从栈上弹掉此空间。影子空间是用来给RCX, RDX, R8和R9提供保存值的空间,即使是对于少于四个参数的函数也要分配这32个字节。
例如, 一个函数拥有5个整型参数,第一个到第四个放在寄存器中,第五个就被推到影子空间之外的栈顶。当函数被调用,此栈用来组成返回值——影子空间32位+第五个参数。
在x86-64体系下,Visual Studio 2008在XMM6和XMM7中(同样的有XMM8到XMM15)存储浮点数。结果对于用户写的汇编语言例程,必须保存XMM6和XMM7(x86不用保存这两个寄存器),这也就是说,在x86和x86-64之间移植汇编例程时,需要注意在函数调用之前/之后,要保存/恢复XMM6和XMM7。
System V AMD64 ABI
此约定主要在Solaris,GNU/Linux,FreeBSD和其他非微软OS上使用。头六个整型参数放在寄存器RDI, RSI, RDX, RCX, R8和R9上;同时XMM0到XMM7用来放置浮点变元。对于系统调用,R10用来替代RCX。同微软x64约定一样,其他额外的参数推入栈,返回值保存在RAX中。与微软不同的是,不需要提供影子空间。在函数入口,返回值与栈上第七个整型参数相邻。
前面的例子在Linux上将编译为:
x86调用约定列表
下面是x86调用约定的一个列表 || AX, DX, BX || LTR (Pascal) || 被调用者
| 在堆栈低端地址。返回指针在堆栈高端地址。
|-
| watcall || Watcom C/C++ || AX, DX, BX, CX || RTL (C) || 被调用者
| 返回指针于SI。
|-
| rowspan="8" | IA-32 || cdecl || 类Unix(GCC) || || RTL (C) || 调用者
| 在返回结构/类的时候,调用代码分配空间并通过在堆栈上的一个隐藏参数传递到这个空间的一个指针。被调用函数向这个地址写返回值。
堆栈对齐在16-字节边界,由于一次GCC变更。
|-
| cdecl || Microsoft || || RTL (C) || 调用者 || 在返回结构/类的时候,
- 平坦旧数据 (POD)返回值小于等于32位的返回于EAX寄存器,
- POD返回值33–64位大小的通过EAX:EDX寄存器返回。
- 非POD返回值或大于64-位的值,调用代码分配空间并通过在堆栈上的一个隐藏参数传递到这个空间的一个指针。被调用函数向这个地址写返回值。
堆栈对齐在4-字节边界。
|-
|-
| stdcall || Microsoft || || RTL (C) || 被调用者
| 也被GCC支持。
|-
| fastcall || Microsoft || ECX, EDX || RTL (C) || 被调用者
| 如果不是成员函数就返回指针在堆栈上。也被GCC支持。
|-
| register || Delphi, Free Pascal, Linux || EAX, EDX, ECX || LTR (Pascal) || 被调用者
|
|-
| thiscall || Windows (Microsoft Visual C++) || ECX || RTL (C) || 被调用者
| 缺省用于成员函数。
|-
| vectorcall || Windows (Microsoft Visual C++) || ECX, EDX, [XY]MM0–5 || RTL (C) || 被调用者
| 扩展自fastcall。也被ICC和Clang支持 || Windows (Microsoft Visual C++, GCC, Intel C++ Compiler, Delphi), UEFI || RCX/XMM0, RDX/XMM1, R8/XMM2, R9/XMM3 || RTL (C) || 调用者
| 堆栈对齐在16字节边界。32字节影子空间在堆栈上。指定的8个寄存器只能用于参数1到4。对于C++类,隐藏的参数是第一个参数,因此传递于RCX。
|-
| vectorcall || Windows (Microsoft Visual C++, Clang, ICC) || RCX/[XY]MM0, RDX/[XY]MM1, R8/[XY]MM2, R9/[XY]MM3 + [XY]MM4–5 || RTL (C) || 调用者 || 扩展自MS x64。
|-
| System V AMD64 ABI, BSD, macOS, OpenVMS (GCC, Intel C++ Compiler, Clang, Delphi) || RDI, RSI, RDX, RCX, R8, R9, [XYZ]MM0–7 || RTL (C) || 调用者
| 堆栈对齐在16字节边界。128字节在栈帧之下。内核接口使用RDI、RSI、RDX、R10、R8和R9。在C++中,是第一个参数。
|-
| regcall || Windows, Linux (Clang, ICC) || RAX, RCX, RDX, RDI, RSI, R8, R9, R10, R11, R12, R14, R15 + [XYZ]MM0-15 + R10 在Linux之上 || RTL (C) || 调用者 || 尽可能多使用寄存器来传递参数和返回值。
|}
注释:
- GCC、clang和较小程度上的ICC能够在任何平台上使用任何调用约定,要如此可以使用特性比如、和
}}
评论 (0)