调用惯例是调用方和被调用方对于函数如何调用的一个明确的约定,只有双方都遵守同样的约定,函数才能被正确地调用。如果不这样的话,函数将无法正确运行。假设有一个foo函数
int foo0(int n, float m)
{
int a = 0, b = 0;
...
}
如果函数的调用方在传递参数时先压入参数n,再压入参数m,而foo函数却认为其调用方应该先压入参数m,后压入参数n,那么不难想象foo内部的m和n的值将会被交换。
再者如果函数的调用方决定利用寄存器传递参数,而函数本身却仍然认为参数通过栈传递,那么显然函数无法获取正确的参数。因此,毫无疑问函数的调用方和被调用方对于函数如何调用须要有一个明确的约定。
一个调用惯例一般会规定如下几个方面的内容:
-
函数参数的传递顺序和方式
函数参数的传递方式有很多种方式,
最常见的一种是通过栈传递。函数的调用方将参数压入栈中,函数自己再从栈中将参数取出。对于有多个参数的函数,调用惯例要规定函数调用方将参数压栈的顺序:是从左至右,还是从右至左。有些调用惯例还允许使用寄存器传递参数,以提高性能。 -
栈的维护方式
在函数将参数压栈之后,函数体会被调用,此后需要将被压入栈中的参数全部弹出,以使得栈在函数调用前后保持一致。这个弹出的工作可以由函数的调用方来完成,也可以由函数体本身来完成。
-
名字修饰的策略
为了链接的时候对调用惯例进行区分,调用惯例要对函数本身的名字进行修饰。不同的调用惯例有不同的名字修饰策略。
下表介绍了几项主要的调用惯例的内容。
| 调用惯例 | 出栈方 | 参数传递 | 名字修饰 |
|---|---|---|---|
| cdecl | 函数调用方 | 从右至左的顺序压参数入栈 | 下划线+函数名 |
| stdcall | 函数本身 | 从右至左的顺序压参数入栈 | 下划线+函数名+@+参数的字节数,如函数int func(int a,double b)的修饰名是_func@12 |
| fastcall | 函数本身 | 头两个DWORD(4字节)类型或者占更少字节的参数被放入寄存器,其他剩下的参数按从右到左的顺序压入栈 | @+函数名+@+参数的字节数 |
| pascal | 函数本身 | 从左至右的顺序压参数入栈 | 较为复杂,参加pascal文档 |
C++自己还有一种特殊的调用惯例,称为thiscall,专用于类成员函数的调用。其特点随编译器不同而不同,在VC里是this指针存放于ecx寄存器,参数从右到左压栈,而对于gcc、thiscall和cdecl完全一样,只是将this看作是函数的第一个参数。
下面举个例子具体说明一下调用约定,在C语言里,存在着多个调用惯例,而默认的调用惯例是cdecl。任何一个没有显式指定调用惯例的函数都默认是cdecl惯例。对于函数foo的声明,他的完整形式是:
int _cdecl foo(int n, float m)
因此foo被修饰之后就变成_foo。在调用foo的时候,按照cdecl的参数传递方式,具体的堆栈操作如下。
- 将
m压入栈。 - 将
n压入栈。 - 调用
_foo,此步又分为两个步骤:- 将
返回地址(即调用_foo之后的下一条指令的地址)压入栈 - 跳转到
_foo执行
- 将
当函数返回之后:sp = sp + 8(参数出栈,由于不需要得到出栈的数据,所以直接调整栈顶位置就可以了)。因此进入foo函数之后,栈上大致是如下图所示。

然后在foo里面要保存一系列的寄存器,包括函数调用方的ebp寄存器,以及要为a和b两个局部变量分配空间(参见栈和栈帧的那一篇文章),最终的栈的构成会如下图所示。

在以上布局中,如果想访问变量n,实际的地址是使用ebp+8。当foo返回的时候,程序首先会使用pop恢复保存在栈里的寄存器,然后从栈里取得返回地址,返回到调用方。调用方再调整ESP将堆栈恢复。
参考书籍:程序员的自我修养
本文深入探讨了函数调用约定的重要性和具体实现细节,包括参数传递顺序、方式及栈的维护,强调了调用方与被调用方遵循相同约定的必要性。列举了几种常见的调用约定,如cdecl、stdcall、fastcall等,并详细解释了它们的特点。
1万+

被折叠的 条评论
为什么被折叠?



