函数调用约定

函数调用约定

一、什么是函数调用约定?

我们都知道C或者C++中,在调用函数的时候,函数的参数通过栈来传递。如果是一个参数的话非常好控制,但是如果是多个参数,就会遇到很多问题。比如:它们参数的传递顺序是什么样?它们在内存中是什么形式?栈帧是如何销毁的(调用者还是被调用者弹出)?

为了解决这些问题,引入了一个概念:函数调用约定。

二、常见的函数调用约定

常见的函数调用约定有这五种:__ stdcall、__ cdecl、__ fastcall、__ thiscall、__ naked call。

2.1 __ stdcall

stdcall是StandardCall的缩写,看见名字就知道这是C++的调用约定方式。这种方式规定所有的参数是从右向左依次入栈,如果是调用类成员方法的时候,需要将隐藏的this指针(对象的地址)作为最后一个参数入栈。当函数返回时,__ stacall采用自动清栈的方式,使用的指令是 retnX,X表示参数占用的字节数,CPU在ret之后自动弹出X个字节的堆栈空间。由于是自动的清栈方式,必须在编译时期就确定好参数的个数。

2.2 __ cdecl

这个也很好区别,看见大写的c,就知道cdecl是C语言的调用约定方式。它是C语言缺省的调用约定。这种调用方式规定参数从右向左依次入栈。但在函数返回之后,需要手动清栈。由于是手动方式清栈,被调用函数就不会要求调用者传递参数的个数。这也就导致无论参数的形式,在编译时期都不会产生错误。

2.3 __fastcall

__ fastcall顾名思义,就是很快的函数调用约定。这种方式规定,将前两个参数(或者若干个)通过寄存器进行传递,剩下的参数还是通过入栈的方式传递。返回方式和 __ stdcall类似。

在X64的平台上,会默认使用__ fastcall调用约定。

1、前4个参数(在Linux 64上是6个寄存器RDI,RSI接着后面的寄存器)从左向右传入寄存器RCX、RDX、R8、R9中,后面的参数从右向左入栈。

2、浮点前4个参数传入XMM0,XMM1,XMM2,XMM3中,其它参数传递到堆栈中。

3、被调用函数的返回值是整数时,则返回值被存放于RAX;浮点数返回在XMM0中

4、RBX、RBP、R12 - R15被划分为被调用者保存寄存器,是使用前需要push的。

详细可以根据《深入理解计算机系统基础》第三章 3.7过程学习。

2.4 __ thiscall

__ thiscall也很好理解,这是传递C++类成员方法参数的调用约定(传递this指针)。当使用对象 .函数名 调用 成员方法时,会先将对象的地址传递给ecx寄存(VC中),到达成员函数内部的时候,将ecx存储的值赋给this指针。

三、常见的函数名修饰约定

在C和C++中函数在内部并不是只通过函数名来标识的,而是通过修饰名来识别的,否则C++无法拥有多态的特性。修饰名是在编译时期生成的。

3.1 C语言修饰名约定

函数名转换修饰名字,大小写不改变为前提。

3.1.1__ stdcall 名字修饰:

在函数名前加一个下划线,在函数名后加 "@"和参数的字节数。如同:_functionname@number。

3.1.2 __ cdecl 名字修饰:

在函数名前加下划线,如同:_functionname

3.1.3 __ fastcall 名字修饰:

在函数名前加 "@",后面和__ stdcall一样,在函数名后加 ”@“ 和参数的字节数。如同:

@functionname@number 。

3.2 C++修饰名约定

3.2.1 __ stdcall 名字修饰:

在函数名前加一个 ?,在函数名后加 "@@YG"作为参数开始的标志,后面跟参数表代号,
如果代号出现多次,使用 '0' 代替。最后结束以 ”@Z“ 来标识。

格式如下:?functionname@@YG 参数表代号 @Z。

例子:int Test1(char var1,unsigned long) -----“?Test1@@YGHDI@Z”

参数表代号如下:

X——void,

D——char,

E——unsigned char,

F——short,

H——int,

I——unsigned int,

J——long,

K——unsigned long,

M——float,

N——double,

_N——bool,

3.2.2 __ cdecl 名字修饰

VC++对函数默认的声明是" __cedcl "。

首先在函数名前加一个 ' ? ', 函数名后是"@@YA",
表示参数开始标志,后面跟参数表代号,
如果参数出现多次,使用 '0' 代替,最后以 "@Z" 结束。

格式如下:?functionname@@YA 参数表代号 @Z。

例子:int Test1(char \*var1,unsigned long)  -----“?Test1@@YAHPADK@Z”

3.2.3 __ fastcall 名字修饰

规则同上面的*_stdcall*调用约定,只是参数表的开始标识由上面的*"@@YG"*变为*"@@YI"*。

格式如下:?functionname@@YI参数表代号 @Z。

总结

  • __cdecl:它是c/c+*默认的调用方式。实参是以参数列表从右至左入栈,栈空间需要手动释放。主要用在带有可变参数的函数上。
  • __stdcall:需要显示指定。实参是以参数列表从右至左入栈,栈空间自动释放。但若函数含有可变参数,那么即使显示指定了_stdcall,编译器也会自动把其改为_cdecl。(没有可变参数)
  • __thiscall:它是类的非静态成员函数默认的调用约定,不能用在含有可变参数的函数上。实参是以参数列表从右至左入栈,栈空间自动释放。(但是类的非静态成员函数内部都含有一个this指针,该指针不是存在函数堆栈上,而是直接存放在寄存器 (x86存放在ecx寄存器) 上)。
  • fastcall:快速调用。它的实参不是放在函数堆栈上,而是直接存在寄存器上,所以不存在入栈出栈、函数堆栈释放。

需要注意:全局函数或类静态成员函数,若没有指定调用,约定默认是__cdecl

posted @ 2023-01-08 00:05  baobaobashi  阅读(259)  评论(0编辑  收藏  举报