函数调用约定的详解

版权声明:转载请说明来源 https://blog.csdn.net/weixin_39640298/article/details/84455481

概述

在工作的过程中,我们总是需要调用底层函数或者使用第三方的库,在使用的过程中我就发现了有一些函数前面总有一些__stdcall,之初我只知道那是调用约定,但别人问我什么是调用约定,我也答不上来。今天就把调用约定整理一下,以备下次跟别人吹牛用。

1、什么是调用约定(函数调用约定)

首先让我们看看一个函数被调用都经历了哪几个过程,编译器帮我们做了什么:

  1. 调用要使用的函数,现在把调用者的地址入栈(函数返回时可继续往下执行)
  2. 把函数的参数压栈或者存储到寄存器中
  3. 调转到函数
  4. 把函数使用到的一些寄存器压栈
  5. 执行函数
  6. 处理函数返回值
  7. 对于第3步中的压栈的那些寄存器,恢复他们原来的值
  8. 清空第1步中的压栈参数和处理返回值
  9. 返回到调用者调用时的地址(步骤1已经记录)继续往下执行

从上面的顺序可以看出,2和8是相反的操作,4和7是相反的操作,函数调用约定主要就是定义了1和7步骤中的规则:怎样传递参数,谁负责清除栈上的数据。

函数调用约定: 就是对函数调用的一个约束和规定(规范),描述了函数参数是怎么传递和由谁清除堆栈的。它决定以下三个方面:

  1. 函数参数传递的方式(是否采用寄存器传递参数、采用哪个寄存器传递参数、参数压桟的顺序等)
  2. 函数调用结束后的栈指针由谁恢复(被调用的函数恢复还是调用者恢复)
  3. 函数修饰名的产生方法

我们知道函数由以下几部分构成: 返回值类型 函数名 ( 参数列表 ),如:

void funcA();
int  funcB(int a, int b);

以上是大家所熟知的构成部分,其实函数的构成还有一部分,那就是调用约定。如下:

void  __cdecl  funcA();
void __stdcall funcB();

上面的__cdecl和__stdcall就是调用约定,其中**__cdecl是C/C++的默认调用约定**。最常见的调用约定由__cdecl、__stdcall、__fastcall,其中使用最广泛的是__cdecl和__stdcall。还有一些不常用的调用约定,如__pascal、__thiscall等。

2、调用约定的使用

如上面所述的一样,调用约定书写在函数前面,相当于函数类型的一部分,所以要求函数的声明和定义要有相同的调用约定,例如:

int  Add ( int a, int b );	//这里默认是_cdecl
int  __stdcall  Add( int a, int b )
{
	return  a + b;
};
上面在编译的过程中就会提示出错,因为声明和定义的调用约定不同,正确的应该是:
int __stdcall  Add ( int a, int b );
int __stdcall  Add ( int a, int b )
{
	return  a + b;
}

好了上面解释了什么是调用约定和怎样正确使用调用约定,现在让我们来详细的看看不用调用约定下的规则。为了更好的理解下面的约定,我们先来定义两个概念,大家要记住后面要经常使用,先看下面的代码:

int Add ( int a, int b )
{
	return  a + b;
}
void ShowResult()
{
	cout << add(5,10) << endl; //调用了函数Add()
}

上面的Add()函数,我们叫做“被调用者”,下面的ShowResult()函数我们叫做“调用者”。

3、__cdecl

__cdecl是C Declaration的缩写,表示C\C++默认的函数调用约定。因为是默认的,所以代码中我们很少见到,例如:

int  Add( int a, int b );
int __cdecl  Add(int a, int b); //等同于上面

__cdecl调用方式规定:

  1. 采用桟传递参数,参数从右向左依次入栈
  2. 调用者负责恢复栈顶指针
  3. 编译器在编译时会在函数名前加上一个下划线前缀生成修饰名,格式为_function。如函数int Add(int a, int b)的修饰名是_Add。这个名称在链接时会用到,以后整理动态库时再详细说明了。

参数的压栈顺序和栈的清理我们知道就行了,因为这是编译器的决定的,我们改变不了的。需要注意的是:调用参数个数可变的函数只能采用这种方式(如printf)。

4、__stdcall

__stdcall是Standard Call的缩写,是C++的标准调用方式。WINAPI、CALLBACK实际上就是__stdcall。Windows的大多数的API函数都是采用了这种调用方式。

__stdcall调用方式规定:

  1. 采用桟传递参数,参数从右向左依次入栈
  2. 被调用者负责恢复栈顶指针
  3. 在输出函数名前加上一个下划线前缀,后面加上一个“@”符号和其参数的字节数,格式为_function@number。如函数int Add(int a, int b)的修饰名是_Add@8。

__stdcall与__cdecl最主要的区别是第2条规定:由“被调用者”清空实际上就是把对应参数数目的数据从栈中弹出,这样的缺点就是它不能使用于那些不确定数目的函数,比如:printf。

那这样就有疑问了,这样做有什么好处呢?
好处就是编译出恢复栈顶的代码在函数内部一份就够了,而调用者恢复的方式需要在调用处编译出恢复栈顶的代码。对于程序员来说这就很明白了,这样能省空间啊!

5、__fastcall

__fastcall调用方式规定:

  1. 函数的第一个和第二个(从左向右)32字节参数(或者尺寸更小的)通过ecx和edx传递(寄存器传递),其他参数通过桟传递。从第三个参数(如果有的话)开始从右向左的顺序压栈;
  2. 被调用者恢复栈顶指针;
  3. 在函数名之前加上"@",在函数名后面也加上“@”和参数字节数,例如@function@8;

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

6、__thiscall

thiscall是唯一一个不能明确指明的函数修饰,因为thiscall只能用于C++类成员函数的调用,同时thiscall也是C++成员函数缺省的调用约定。由于成员函数调用还有一个this指针,因此必须特殊处理。

__thiscall调用方式规定:

  1. 采用桟传递参数,参数从右向左入栈。
  2. 如果参数个数确定,this指针通过ecx传递给被调用者;如果参数个数不确定,this指针在所有参数压栈后被压入堆栈;
  3. 对参数个数不定的,调用者清理堆栈,否则由被调函数清理堆栈

当然你可以修改类成员函数的调用方式,比如:

class A
{
public:
	A(int a);
	    
	int __cdecl FuncA(); 	//__cdecl方式调用
	int __stdcall FuncB();	//__stdcall方式调用,差别就是谁清理堆栈
};

7、总结

我们就列举这么多了,其实还有好几个,但是基本都是过时的了。还有一些小的细节我没有说明,现在我们用图表进行汇总一下:
在这里插入图片描述这里需要强调一下:我们上面讨论的编译出的修饰符,是在C编译的情况下。而C++为了实现重载等功能,会采用另一种编译的方法,那个等以后进行整理。

最后写一道笔试题,在C++程序中,下面会出现什么情况:

int Add( int a, int b, int c)
{
	return  a+b+c;
}

int main()
{
	return Add( printf("A"), printf("B"), printf("C") );
};

答案是CBA,具体为什么看上面调用你应该会知道的。

感谢大家,我是假装很努力的YoungYangD(小羊)。

参考资料:
https://blog.csdn.net/sghdls/article/details/73693515
https://blog.csdn.net/luoweifu/article/details/52425733
https://wenku.baidu.com/view/cbf183086294dd88d1d26b00.html

猜你喜欢

转载自blog.csdn.net/weixin_39640298/article/details/84455481