调用约定

11 篇文章 1 订阅

  调用约定是一种定义函数从调用处接受参数以及返回结果的方法的约定。
  不同调用约定的区别在于:

  • 参数和返回值放置的位置(在寄存器中;在调用栈中;两者混合);
  • 参数传递的顺序(或者单个参数不同部分的顺序);
  • 调用前设置和调用后清理的工作,在调用者和被调用者之间如何分配;
  • 被调用者可以直接使用哪一个寄存器有时也包括在内。(否则的话被当成ABI的细节);
  • 哪一个寄存器被当作volatile的或者非volatile的,并且如果是volatile的,不需要被调用者恢复。

  调用约定与名称修饰(名称修饰决定了代码中的符号名称如何映射到链接器中的符号名)和编程语言中对于大小和格式的分配紧密相关。调用约定,类型表示和名称修饰这三者的统称即应用二进制接口(ABI)。
  不同编译器在实现这些约定总是有细微的差别存在,所以在不同编译器编译出来的代码很难接合起来。另一方面,有些约定被当作一种API标准(如stdcall),编译器实现都较为一致。
  调用约定根据函数调用结束时进行清理的具体的对象是谁而分为被调用者清理、调用者清理和调用者或被调用者清理。
  以下的调用约定是x86上的调用约定。

1. 调用者清理

  调用者清理即调用者自己清理堆栈上的参数(arguments),这样就允许了可变参数列表的实现,如printf()。

1.2 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版本开始,调用函数时,堆栈上的数据必须以16B对齐(之前的版本只需要4B对齐即可)。

int __attribute__((__cdecl__)) add(int a, char b)
{
    return a + b;
}

int main()
{
    int a = 1;
    char b = 2;
    int ret = add(a, b);
    ret += 5;
    return 0;
}

  使用gcc -O0 main.cpp -m32生成汇编代码中,如下分别为函数add和主程序main:

!gcc 4.9
_Z3addic:
.LFB0:
	.cfi_startproc
	pushl	%ebp
	.cfi_def_cfa_offset 8
	.cfi_offset 5, -8
	movl	%esp, %ebp
	.cfi_def_cfa_register 5
	subl	$4, %esp            !esp=esp-4
	movl	12(%ebp), %eax      !eax=ebp+12
	movb	%al, -4(%ebp)       !ebp-4=al
	movsbl	-4(%ebp), %edx      !b=ebp-4=edx
	movl	8(%ebp), %eax       !a=eax=ebp+8
	addl	%edx, %eax          !a+b
	leave
	.cfi_restore 5
	.cfi_def_cfa 4, 4
	ret
	.cfi_endproc
main:
.LFB1:
	.cfi_startproc
	pushl	%ebp
	.cfi_def_cfa_offset 8
	.cfi_offset 5, -8
	movl	%esp, %ebp
	.cfi_def_cfa_register 5
	subl	$16, %esp
	movl	$1, -8(%ebp)        !a=ebp-8=1
	movb	$2, -9(%ebp)        !b=ebp-9=2
	movsbl	-9(%ebp), %eax      !eax=b
	pushl	%eax                !push b
	pushl	-8(%ebp)            !push a
	call	_Z3addic            !add(a, b)
	addl	$8, %esp            !esp+=8
	movl	%eax, -4(%ebp)      !ret=ebp-4=eax
	addl	$5, -4(%ebp)        !ret+=5
	movl	$0, %eax            !return 0
	leave
	.cfi_restore 5
	.cfi_def_cfa 4, 4
	ret
	.cfi_endproc

  从上面的汇编可以看出参数的压栈顺序是从右向左:先将b存放到寄存器中,再处理a,并且b因为是char类型,再进入到函数中时和int进行了对齐。同时调用者负责清除栈中的局部变量(addl $8, %esp)。
  另外函数名为_Z3addic其中ic分别指代int,char

1.2 syscall

  与cdecl类似,参数被从右到左推入堆栈中。EAX, ECX和EDX不会保留值。参数列表的大小被放置在AL寄存器中。 syscall是32位OS/2 API的标准。

1.3 optlink

  参数也是从右到左被推入堆栈。从最左边开始的三个字符变元会被放置在EAX, EDX和ECX中,最多四个浮点变元会被传入ST(0)ST(3)中,虽然这四个参数的空间也会在参数列表的栈上保留。函数的返回值在EAX或ST(0)中。保留的寄存器有EBP, EBX, ESI和EDIoptlinkIBM VisualAge编译器中被使用。

2. 被调用者清理

  如果被调用者要清理栈上的参数,需要在编译阶段知道栈上有多少字节要处理。因此,此类的调用约定并不能兼容于可变参数列表,如printf()。
  然而,这种调用约定也许会更有效率,因为需要解堆栈的代码不要在每次调用时都生成一遍。 使用此规则的函数容易在asm代码被认出,因为它们会在返回前解堆栈。x86 ret指令允许一个可选的16位参数说明栈字节数,用来在返回给调用者之前解堆栈。

2.1 pascal

  基于Pascal语言的调用约定,参数从左至右入栈(与cdecl相反)。被调用者负责在返回前清理堆栈。 此调用约定常见在如下16-bit 平台的编译器:OS/2 1.x,微软Windows 3.x,以及Borland Delphi版本1.x。

2.2 register

  Borland fastcall的别名。

2.3 stdcall

  stdcall是由微软创建的调用约定,是Windows API的标准调用约定。非微软的编译器并不总是支持该调用协议。
  stdcall是Pascal调用约定与cdecl调用约定的折衷:被调用者负责清理线程栈,参数从右往左入栈。其他各方面基本与cdecl相同。但是编译后的函数名后缀以符号"@",后跟传递的函数参数所占的栈空间的字节长度。寄存器EAX, ECX和EDX被指定在函数中使用,返回值放置在EAX中。stdcall对于微软Win32 API和Open Watcom C++是标准。

微软的编译工具规定:PASCAL, WINAPI, APIENTRY, FORTRAN, CALLBACK, STDCALL, __far __pascal, __fortran, __stdcall均是指此种调用约定。

  同样是上面的程序,汇编如下:

!gcc
_Z3addic:
.LFB0:
	.cfi_startproc
	pushl	%ebp
	.cfi_def_cfa_offset 8
	.cfi_offset 5, -8
	movl	%esp, %ebp
	.cfi_def_cfa_register 5
	subl	$4, %esp            !esp=esp-4
	movl	12(%ebp), %eax      !
	movb	%al, -4(%ebp)       !b=al
	movsbl	-4(%ebp), %edx      !edx=b
	movl	8(%ebp), %eax       !eax=a
	addl	%edx, %eax          !a+b
	leave
	.cfi_restore 5
	.cfi_def_cfa 4, 4
	ret	$8
	.cfi_endpro
main:
.LFB1:
	.cfi_startproc
	pushl	%ebp
	.cfi_def_cfa_offset 8
	.cfi_offset 5, -8
	movl	%esp, %ebp
	.cfi_def_cfa_register 5
	subl	$16, %esp
	movl	$1, -8(%ebp)        !a=ebp-8=1
	movb	$2, -9(%ebp)        !b=ebp-9=2
	movsbl	-9(%ebp), %eax      !eax=b
	pushl	%eax                !push b
	pushl	-8(%ebp)            !push a
	call	_Z3addic
	movl	%eax, -4(%ebp)      !ret=ebp-4
	addl	$5, -4(%ebp)        !ret+=5
	movl	$0, %eax            !return 0
	leave
	.cfi_restore 5
	.cfi_def_cfa 4, 4
	ret
	.cfi_endproc

  可以清晰的看到参数的入栈顺序是从右向左,由被调用者自动清除栈,但是可以看到gcc下函数名称并未用@

2.4 fastcall

  此约定还未被标准化,不同编译器的实现也不一致。

2.4.1 Microsoft/GCC fastcall

  MicrosoftGCC__fastcall约定(也即__msfastcall)把第一个(从左至右)不超过32比特的参数通过寄存器ECX/CX/CL传递,第二个不超过32比特的参数通过寄存器EDX/DX/DL,其他参数按照自右到左顺序压栈传递。

2.4.2 Borland fastcall

  从左至右,传入三个参数至EAX, EDXECX中。剩下的参数推入栈,也是从左至右。 在32位编译器Embarcadero Delphi中,这是缺省调用约定,在编译器中以register形式为人知。 在i386上的某些版本Linux也使用了此约定。

!gcc
_Z3addic:
.LFB0:
	.cfi_startproc
	pushl	%ebp
	.cfi_def_cfa_offset 8
	.cfi_offset 5, -8
	movl	%esp, %ebp
	.cfi_def_cfa_register 5
	subl	$8, %esp
	movl	%ecx, -4(%ebp)      !a=ecx  
	movl	%edx, %eax          !eax=edx
	movb	%al, -8(%ebp)       !b=al
	movsbl	-8(%ebp), %edx      !edx=b
	movl	-4(%ebp), %eax      !eax=a
	addl	%edx, %eax          !a+b
	leave
	.cfi_restore 5
	.cfi_def_cfa 4, 4
	ret
	.cfi_endproc
main:
.LFB1:
	.cfi_startproc
	pushl	%ebp
	.cfi_def_cfa_offset 8
	.cfi_offset 5, -8
	movl	%esp, %ebp
	.cfi_def_cfa_register 5
	subl	$16, %esp
	movl	$1, -8(%ebp)            !a=ebp-8=1
	movb	$2, -9(%ebp)            !b=ebp-9=2
	movsbl	-9(%ebp), %edx          !edx=b
	movl	-8(%ebp), %eax          !eax=a
	movl	%eax, %ecx              !ecx=eax=a
	call	_Z3addic
	movl	%eax, -4(%ebp)
	addl	$5, -4(%ebp)            !清理工作
	movl	$0, %eax
	leave
	.cfi_restore 5
	.cfi_def_cfa 4, 4
	ret
	.cfi_endproc

  从上面的可以看出fastcall会优先使用寄存器,并且由调用者清除栈。

3. 调用者或被调用者清理

3.1 thiscall

  在调用C++非静态成员函数时使用此约定。基于所使用的编译器和函数是否使用可变参数,有两个主流版本的thiscall

  • 对于GCC编译器,thiscall几乎与cdecl等同:调用者清理堆栈,参数从右到左传递。差别在于this指针,thiscall会在最后把this指针推入栈中,即相当于在函数原型中是隐式的左数第一个参数。
  • 在微软Visual C++编译器中,this指针通过ECX寄存器传递,其余同cdecl约定。当函数使用可变参数,此时调用者负责清理堆栈(参考cdecl)。thiscall约定只在微软Visual C++ 2005及其之后的版本被显式指定。其他编译器中,thiscall并不是一个关键字(反汇编器如IDA使用__thiscall)。

  对上面的代码修改如下:


class test{
public:
int __attribute__((__thiscall__)) add(int a, char b)
{
    return a + b;
}
};

int main()
{
    test t;
    int a = 1;
    char b = 2;
    int ret = t.add(a, b);
    ret += 5;
    return 0;
}

  编译得到的汇编代码如下:

!gcc
_ZN4test3addEic:
.LFB0:
	.cfi_startproc
	pushl	%ebp
	.cfi_def_cfa_offset 8
	.cfi_offset 5, -8
	movl	%esp, %ebp
	.cfi_def_cfa_register 5
	subl	$8, %esp
	movl	%ecx, -4(%ebp)
	movl	12(%ebp), %eax
	movb	%al, -8(%ebp)       !b=al
	movsbl	-8(%ebp), %edx      !edx=b=al
	movl	8(%ebp), %eax       !a=ebp+8
	addl	%edx, %eax          !a+b
	leave
	.cfi_restore 5
	.cfi_def_cfa 4, 4
	ret	$8
	.cfi_endproc
main:
.LFB1:
	.cfi_startproc
	leal	4(%esp), %ecx
	.cfi_def_cfa 1, 0
	andl	$-16, %esp
	pushl	-4(%ecx)
	pushl	%ebp
	.cfi_escape 0x10,0x5,0x2,0x75,0
	movl	%esp, %ebp
	pushl	%ecx
	.cfi_escape 0xf,0x3,0x75,0x7c,0x6
	subl	$20, %esp
	movl	%gs:20, %eax
	movl	%eax, -12(%ebp)
	xorl	%eax, %eax
	movl	$1, -20(%ebp)               !a=ebp-20=1
	movb	$2, -21(%ebp)               !b=ebp-21=2
	movsbl	-21(%ebp), %edx             !edx=b
	leal	-22(%ebp), %eax             !eax=&(ebp-22)
	subl	$8, %esp
	pushl	%edx                        !push b
	pushl	-20(%ebp)                   !push a
	movl	%eax, %ecx                  !ecx=&t
	call	_ZN4test3addEic
	addl	$8, %esp                    !清理工作
	movl	%eax, -16(%ebp)             !ret
	addl	$5, -16(%ebp)               !ret+=5
	movl	$0, %eax
	movl	-12(%ebp), %ecx
	xorl	%gs:20, %ecx
	je	.L5
	call	__stack_chk_fail
.L5:
	movl	-4(%ebp), %ecx
	.cfi_def_cfa 1, 0
	leave
	.cfi_restore 5
	leal	-4(%ecx), %esp
	.cfi_def_cfa 4, 4
	ret
	.cfi_endproc

  可以看到入栈顺序为从右向左,且最后会压入test实例的地址,清理工作由调用者进行。

4 其他

4.1 WINAPI

  WINAPI是平台的缺省调用约定。Windows操作系统上默认是StdCallWindows CE上默认是Cdecl

4.2 x86-64调用约定

4.2.1 微软x86-64调用约定

  在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。

4.2.2 System V AMD64 ABI

  此约定主要在Solaris,GNU/Linux,FreeBSD和其他非微软OS上使用。头六个整型参数放在寄存器RDI, RSI, RDX, RCX, R8和R9上;同时XMM0到XMM7用来放置浮点变元。对于系统调用,R10用来替代RCX。同微软x64约定一样,其他额外的参数推入栈,返回值保存在RAX中。 与微软不同的是,不需要提供影子空间。在函数入口,返回值与栈上第七个整型参数相邻。
  64bit程序添加任何的调用约定都会报告该约定被忽略的warning。


//此处添加任何约定都会被忽略掉
int add(int a, char b)
{
    return a + b;
}

int main()
{
    int a = 1;
    char b = 2;
    int ret = add(a, b);
    ret += 5;
    return 0;
}
_Z3addic:
.LFB0:
	.cfi_startproc
	pushq	%rbp
	.cfi_def_cfa_offset 16
	.cfi_offset 6, -16
	movq	%rsp, %rbp
	.cfi_def_cfa_register 6
	movl	%edi, -4(%rbp)      !a=edi
	movl	%esi, %eax          !eax=esi
	movb	%al, -8(%rbp)       !b=eax
	movsbl	-8(%rbp), %edx      !edx=b
	movl	-4(%rbp), %eax      !eax=a
	addl	%edx, %eax          !a+b
	popq	%rbp
	.cfi_def_cfa 7, 8
	ret
	.cfi_endproc
main:
.LFB1:
	.cfi_startproc
	pushq	%rbp
	.cfi_def_cfa_offset 16
	.cfi_offset 6, -16
	movq	%rsp, %rbp
	.cfi_def_cfa_register 6
	subq	$16, %rsp
	movl	$1, -8(%rbp)            !a=rbp-8=1
	movb	$2, -9(%rbp)            !b=rbp-9=2
	movsbl	-9(%rbp), %edx          !edx=b
	movl	-8(%rbp), %eax          !eax=a
	movl	%edx, %esi              !esi=b
	movl	%eax, %edi              !edi=a
	call	_Z3addic    
	movl	%eax, -4(%rbp)          !ret
	addl	$5, -4(%rbp)            !ret=ret+5
	movl	$0, %eax                
	leave
	.cfi_def_cfa 7, 8
	ret
	.cfi_endproc

  可以看到linux上64位调用约定压栈顺序为从从右向左,并由被调用者自行清理栈。

5 总结

调用约定压栈顺序谁清除栈上参数可变参数个数的函数函数名修饰规范
cdecl从右向左调用者支持下划线+函数名
syscall从右向左调用者支持下划线+函数名
syscall从右向左被调用者不支持下划线+函数名+@+参数的字节数
fastcall从左向右被调用者不支持@+函数名+@+参数的字节数

关于函数名修饰规范我并未复现出来,具体来源于网上。

6 参考

x86调用约定
调用约定

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值