二木 王者

函数调用约定

对于函数调用约定,我们首先应该知道的是为什么产生函数调用约定?为什么要用函数约定?

首先,我们在写函数时,我们可以知道的是

int func(int a,int b)
{
  .........
}

像这样,我们很容易知道,函数的返回值是什么类型,有几个参数,以及参数是什么类型。对于我们的CPU来说,我们的C语言是先编译成机械码之后,我们CPU才能执行,但是,变成一条一条的机械码之后,我们的CPU怎么才能知道这个函数有几个参数,以及什么类型等等的问题呢?这个时候就需要我们的函数调用约定。

其次,深入函数的调用,对于内存上来说,函数的调用者首先依次把参数压栈,然后调用函数,函数被调用以后,在堆栈中申请空间并取得数据,然后在这个临时开辟的空间中进行计算。函数计算结束以后,调用者或者函数本身就会将这片临时的空间释放,然后将参数出栈,使堆栈恢复原状。

因此自然而然的在进行函数调用时,有两个很重要的问题需要得到明确说明:

1、当参数个数多于一个时,按照什么顺序将参数压入堆栈
2、函数调用后,由谁来将堆栈恢复原状
因此,我们的相对高级的语言在处理上述的这些问题的时候,就有着一些约定。

所以函数调用约定到底是什么呢?函数调用约定就是:函数调用者和被调用的函数体之间关于参数传递、返回值传递、堆栈清除、寄存器使用的一种约定。

一些常见的函数调用约定以及使用

我们一般写函数的格式都是这样的

int func(int a,int b)
{
  .........
}

其实这样写是省略了我们函数调用约定的,这样写是运用了c默认的函数调用约定__cdecl,如果不省略我们的函数调用约定就应该这样写

int __cdecl func(int a,int b)
{
  .........
}

在我们函数名前面加上我们的函数调用约定。

__cdecl(C/C++默认的调用约定)

__cdecl是C Declaration的缩写,表示C\C++默认的函数调用约定。当我们没有写函数调用约定时,我们编译器会自己默认将其加上。

_cdecl规定:

  • 参数从右向左压入堆栈
  • 由调用者负责清理堆栈(传送参数的内存栈是由调用者来维护的,返回值在EAX中)
  • C调用约定允许函数的参数的个数是不固定的。(因此调用参数个数可变的函数只能采用这种方式(如printf))
  • 编译器在编译时会在函数名前加上一个下划线前缀生成修饰名,格式为_functionname。如函数int Add(int a, int b)的修饰名是_Add

__stdcall(Windows的大多数的API函数采用了这种调用方式)

  • 参数从右向左压入堆栈
  • 由被调用者负责清理堆栈(函数自己在退出时清空堆栈,返回值在EAX中)
  • __stdcall调用约定在输出函数名前加上一个下划线前缀,后面加上一个“@”符号和其参数的字节数,格式为_function@number。如函数int sub(int a, int b)的修饰名是_sub@8。

这里__stdcall和__cdecl中第二条主要的区别在于,当调用者负责清理堆栈的时候,调用一次就会需要在调用处编译出恢复栈顶的代码,而被调用者负责清理堆栈的时候,在函数内部编译出恢复栈顶的代码一份就行了。这样就会节省我们的空间。

__fastcall(用于一些需要对参数快速处理的函数)

正如其名,_fastcall调用的主要特点就是快,因为它是通过寄存器来传送参数。

  • 函数的第一个和第二个(从左向右)两个DWORD或更小的参数,通过ecx和edx传递(寄存器传递),其他参数通过桟传递。从第三个参数(如果有的话)开始从右向左的顺序压栈;
  • 函数自身清理堆栈
  • 在函数名之前加上”@”,在函数名后面也加上“@”和参数字节数,例如@function@8

和__stdcall与__cdecl相比最主要的区别是,__fastcall把第1和第2个参数放到了寄存器中,而不是压栈,因为寄存器的读写速度比栈快很多,所以叫做fast call。

__thiscall(C++类成员函数默认调用约定)

thiscall是一个不能明确指明的函数修饰,thiscall不是关键字,thiscall只能用于C++类成员函数的调用。而且thiscall也是C++成员函数缺省的调用约定,由于成员函数调用还有一个this指针,因此需要特殊处理。

  • 参数从右向左压入堆栈
  • 如果参数个数不确定,this指针在所有参数压栈后被压入堆栈。如果参数个数确定,this指针通过ecx传递给被调用者
  • 如果参数个数不确定,调用者清理堆栈,否则函数自己清理堆栈

补充

C++编译时函数名修饰约定规则:

以“?”标识函数名的开始,后跟函数名;

函数名后面以“@@YG”标识参数表的开始,后跟参数表;

2.函数名后面标识调用约定,然后跟参数列表。

__cdecl,@@YA

__stdcall,@@YG

__fastcall,@@YI

参数表以代号表示:

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,

PA–表示指针,后面的代号表明指针类型,如果相同类型的指针连续出现,以“0”代替,一个“0”代表一次重复

参数表的第一项为该函数的返回值类型,其后依次为参数的数据类型,指针标识在其所指数据类型前;

参数表后以“@Z”标识整个名字的结束,如果该函数无参数,则以“Z”标识结束。

例如:

int Test1(char *var1,unsigned long)—–“?Test1@@YGHPADK@Z”
void Test2() —–“?Test2@@YGXXZ”

“int __cdecl a(char)————?a@@YAHD@Z”

参考

(62条消息) C语言的函数调用约定(stdcall+cdecl+thiscall+fastcall)_int stdcall_莲娃的博客-CSDN博客

(60条消息) 函数调用约定的详解_YoungYangD的博客-CSDN博客

(62条消息) 带你玩转Visual Studio——调用约定__cdecl、__stdcall和__fastcall_vs调用规则cdecl_luoweifu的博客-CSDN博客

调用约定(Calling Conventions) 函数名修饰(Name Mangling) 二进制分析工具 - 通用C++ - 蜗牛大锅的博客 (3scard.com)

 评论