调用约定(Calling Convention) 是计算机编程中一个比较底层的设计,它主要涉及:
- 函数参数通过寄存器传递还是栈?
- 函数参数从左到右还是从右到左压栈?
- 是否支持可变参数函数(vararg function or variadic function)?
- 是否需要函数原型?
- 调用者(caller)还是被调用者(called or callee)清理堆栈?
具体参见:Argument Passing and Naming Conventions | Microsoft Docs
函数的调用过程是通过函数栈帧的不断变化实现的:
函数的调用,涉及参数传递、返回值传递、调用后返回,这都是通过栈的变化来实现的。对于常见的三种调用约定而言:
C/C++默认方式;
参数从右向左依次入栈;
由主调函数(调用者)负责栈平衡(清栈)。
windows API 默认方式,在头文件里查看这些API的声明时候用了WINAPI的宏进行了代替,而这个宏就是__stdcall;
参数从右向左依次入栈;
由被调函数自身(即被调用者)负责栈平衡(清栈)。
快速调用方式;所谓快速,这种方式将参数优先从寄存器传入(ECX和EDX),剩下的参数再从右向左入栈。栈位于内存区域,而寄存器位置CPU,故存取方式快于内存;
由被调函数自身(即被调用者)负责栈平衡(清栈)。
每个参数都有自己的地址,但不定长参数无法确认地址,并且函数的个数无法确定。
C/C++中规定了函数参数压栈顺序从右向左,对于不定参数,最后入栈的参数个数,只需要取栈顶就可以得到。
对于含有不定参数的printf函数,其原型是printf(const char* format,…);
其中format确定了printf的参数(通过format的%个数判断)。
假设是从左至右压栈,那么先入栈的是format,然后依次入栈未知参数,此时想要知道参数个数,就必须找到format,而要找到format,就必须知道参数个数,这样就会陷入一个死胡同里面了。
先执行哪个参数和参数的计算顺序有关,而c/c++中没有规定函数参数的计算顺序,这个和编译器有关,代码参数的计算顺序决定了实际输出。
vs的计算顺序是从右至左,clang的计算顺序是从左至右,具体的计算流程分析就很简单了。
对于c/c++函数参数的读取顺序,参数入栈时顺序从右向左入栈,但是在入栈前会先把参数列表里的表达式从右向左算一遍得到表达式的结果,最后再把这些运算结果统一入栈。
在参数入栈前,编译器会先把参数的表达式都处理掉,对于一般的操作来说,参数入栈时取值是直接从变量的内存地址里取的,但是对于a++操作,编译器会开辟一个缓冲区来保存当前a的值,然后再对a继续操作,最后参数入栈时的取值是从缓冲区取,而不是直接从a的内存地址里取。
因为函数参数的计算顺序依照编译器的实现,所以在编码中避免编写诸如 fun(++x, x+y)
这种的程序,其在不同的平台得到的结果可能不一样,但是在面试中可能遇到这样的问题,所以我们需要知其然更要知所以然。
它们均不改变输出函数名中的字符大小写,这和PASCAL调用约定不同,PASCAL约定输出的函数名无任何修饰且全部大写。
1、以“?”标识函数名的开始,后跟函数名;
2、函数名后面以“@@YG”标识参数表的开始,后跟参数表;
3、参数表以代号表示:
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”代
表一次重复;
4、参数表的第一项为该函数的返回值类型,其后依次为参数的数据类型,指针标识在其所指数据类型前
;
5、参数表后以“@Z”标识整个名字的结束,如果该函数无参数,则以“Z”标识结束。
其格式为“?functionname@@YG*****@Z”或“?functionname@@YG*XZ”,例如
int Test1(char *var1,unsigned long)-----“?Test1@@YGHPADK@Z”
void Test2() -----“?Test2@@YGXXZ”
__cdecl调用约定:
规则同上面的_stdcall调用约定,只是参数表的开始标识由上面的“@@YG”变为“@@YA”。
__fastcall调用约定:
规则同上面的_stdcall调用约定,只是参数表的开始标识由上面的“@@YG”变为“@@YI”。
VC++对函数的缺省声明是"__cdecl",将只能被C/C++调用。
主要用于解决this指针问题,使用寄存器传递this指针。返回方式同__stdcall.
__clrcall是C++ .Net里面的。
要求尽可能在寄存器中传递参数。函数名改编为”@@函数名@参数字节数十进制”。这是微软自己添加的标准。/