剖析VC++函数调用约定

Visual C/C++ 的编译器提供了几种函数调用约定,了解这些函数调用约定的含义及它们之间的区别可以帮助我们更好地调试程序。在这篇文章里,我就和大家共同探讨一些关于函数调用约定的内容。

     Visual C/C++ 的编译器支持如下的函数调用约定:

 

关键字
 清理堆栈
 参数入栈顺序
 函数名称修饰(C)
 
__cdecl
 调用函数
 右 à 左
 _ 函数名
 
__stdcall
 被调用函数
 右 à 左
 _ 函数名@ 数字
 
__fastcall
 被调用函数
 右 à 左
 @ 函数名@ 数字
 
thiscall( 非关键字)
 被调用函数
 右 à 左
 /
 

 

     上面这张表只简单地列出了每种函数调用约定的特点,既然这篇文章题目的前两个字是“ 剖析” ,哪能这么容易就完事!?下面就对上面这四种函数调用约定逐个“ 剖析” :

     一、__cdecl 函数调用约定

     这是C 和C++ 程序默认的函数调用约定,参数按从右到左的顺序压入堆栈,由调用函数负责清理堆栈,把参数弹出栈。也正是因为用来传送参数的堆栈是由调用函数维护的,所以实现可变参数的函数只能使用这种函数调用约定。因为每一个调用它的函数都要包含清理堆栈的代码,所以编译后的可执行文件的大小要比调用__stdcall 函数的大。使用这种函数调用约定时,修饰后的函数名只是在原函数名前加上了一个_ (下划线),并且不改变函数的大小写。对于__cdecl ,我们一般不特别指出,因为它是C 和C++ 程序默认的函数调用约定,所以只有将编译选项设置成/Gz (stdcall )或/Gr (fastcall )时,我们才有必要在函数名前显式地指出采用这种函数调用约定。下面举一个例子:

 

int __cdecl Sumcdecl(int a, int b, int c)

{

int i = 1000;

short j = 2000;

int k = 3000;

int rEBP = 0;

int value = 0;

 

// ...

 

return (a + b + c);

 

}

 

调用:Sumcdecl(10, 20, 30);

 
 

 

函数体及调用语句如上所示,修饰后的函数名为_Sumcdecl ,堆栈和寄存器状态如下(一行表示4 个字节):

 

0
   value
 
0
   rEBP
 
3000
   k
 
2000
   j
 
1000
   i
 
 
   <---------EBP
 
 
  
 
10
   a
 
20
   b
 
30
   c
 
 
  
 
[ 未使用]
   ECX
 
[ 未使用]
   EDX
 

 

     口说无凭,代码能说明一切,下面的程序乃Win32 console application (.exe )是也:

 

#include "iostream.h"

#include "stdio.h"

 

extern "C" __declspec(dllexport) int __cdecl Sumcdecl(int a, int b, int c)

{

// 声明局部变量

     int i = 1000;

     short j = 2000;

     int k = 3000;

     int rEBP = 0;

     int value = 0;

 

     // 显示局部变量的地址

     cout << " 局部变量的地址:" << endl;

     cout << &value << "    <-----------value" << endl;

     cout << &rEBP << "    <-----------rEBP" << endl;

     cout << &k << "    <-----------k" << endl;

     cout << &j << "    <-----------j" << endl;

     cout << &i << "    <-----------i" << endl;

 

     // 显示寄存器的值

     cout << " 寄存器:" << endl;

     __asm mov rEBP, ebp;

     printf("0x%08X    <-----------EBP/n", rEBP);

 

     // 显示函数参数的地址

     cout << " 函数参数的地址:" << endl;

     cout << &a << "    <-----------a" << endl;

     cout << &b << "    <-----------b" << endl;

     cout << &c << "    <-----------c" << endl;

 

     // 通过 EBP 寄存器获得堆栈中的数据并显示

     cout << " 通过EBP 获取堆栈中的数据:" << endl;

     __asm mov eax, [ebp - 4];

     __asm mov value, eax;

     cout << "i: " << value << endl;

 

     __asm mov eax, [ebp - 8];

     __asm mov value, eax;

     cout << "j: " << (short )value << endl;

 

     __asm mov eax, [ebp - 12];

     __asm mov value, eax;

     cout << "k: " << value << endl;

 

     __asm mov eax, [ebp + 8];

     __asm mov value, eax;

     cout << "a: " << value << endl;

 

     __asm mov eax, [ebp + 12];

     __asm mov value, eax;

     cout << "b: " << value << endl;

 

     __asm mov eax, [ebp + 16];

     __asm mov value, eax;

     cout << "c: " << value << endl;

 

     // 返回

     return (a + b + c);

 

}

 

// 主函数

int main(int argc, char * argv[])

{

 

     Sumcdecl(10, 20, 30);

 

     return 0;

 

}

 
 

 

     在我的机器上,运行结果如下:

 

局部变量的地址:

0x0012FF0C    <-----------value

0x0012FF10    <-----------rEBP

0x0012FF14    <-----------k

0x0012FF18    <-----------j

0x0012FF1C    <-----------i

寄存器:

0x0012FF20    <-----------EBP

函数参数的地址:

0x0012FF28    <-----------a

0x0012FF2C    <-----------b

0x0012FF30    <-----------c

通过EBP 获取堆栈中的数据:

i: 1000

j: 2000

k: 3000

a: 10

b: 20

c: 30
 

 

     函数声明部分的extern “C” 表示连接规范(Linkage Specification )采用C ,而不是C++ ,不加extern “C” 的情况我会在后面统一讨论。__declspec(dllexport) 表示将该函数导出,将生成.lib 文件,以便我们验证函数名是怎样修饰的。关于修饰后的函数名,我们可以使用VC98/bin 目录下的dumpbin 工具来验证:

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

       在进行函数调用时,有几种调用方法,主要有__cdecl,__stdcall,__fastcall,__thiscall
,__clrcall,naked call。它们决定以下内容:1)函数参数的压栈顺序,2)由调用者还是被调用者把参数弹出栈,3)以及产生函数修饰名的方法。

1、__stdcall调用约定:用于调用Win32 API函数。函数的参数自右向左通过栈传递,除非参数是指针或引用,否则是按值传递。在主调用函数中负责压栈,在被调用函数中负责弹出堆栈中的参数,并负责恢复堆栈。不能实现变参函数。采用这种调用方式的函数需要一个函数原型。如果函数是可变参数的,则编译器会将函数的调用方式改为__cdecl调用。
2、__cdecl调用约定:是C和C++程序的缺省调用方式。函数参数的压栈顺序是从右到左,由主调用函数进行参数压栈并且恢复堆栈。由于主调用函数管理堆栈,所以可以实现变参函数。由于每一个调用它的函数都包含清空堆栈的代码,所以产生的可执行文件大小会比调用_stdcall函数的大。注意:对于可变参数的成员函数,始终使用__cdecl的转换方式。
3、__fastcall调用约定:其调用的主要特点就是快,因为在尽可能的情况下,它是通过寄存器来传送参数的(实际上,它用ECX和EDX传送前两个双字(DWORD)或更小的参数,剩下的参数仍旧自右向左压栈传送),被调用的函数在返回前清理传送参数的内存栈。
4、__thiscall调用约定:应用于"C++"成员函数,并且是不采用可变参数的C++成员函数的默认调用方式。函数参数的压栈顺序是从右到左,被调用者清理堆栈,所以不能实现可变参数。在x86系统中,this指针存放于CX/ECX寄存器中,而不是栈中。使用__thiscall的原因之一是当成员函数的默认调用方式是__clrcall的时候,使用__thiscall可以使某个成员函数可以被本地代码调用。
5、__clrcall调用约定:它与.NET Framework有关系,用于指定函数只能被托管代码(managed code)调用。这样编译器就不会生成原生的入口点了。
6、naked call调用约定:这是VC里一种给高级用户使用的调用约定,它实际上就是没有规范,用户可以通过内嵌汇编来实现任意想要的调用约定。naked call不是类型修饰符,故必须和__declspec共同使用。
7、过时的调用约定:__pascal,__fortran和__syscall调用方式现在已不支持。可以用现今支持的函数调用方式配合合适的链接器选项来实现同样的功能。WINDOWS.H现在支持WINAPI宏,它可以为目标转换成合适的调用方式。

备注:
1、关键字__stdcall、__cdecl和__fastcall可以直接加在要输出的函数前。它们对应的命令行参数分别为/Gz、/Gd和/Gr。缺省状态为/Gd,即__cdecl。
2、调用约定可以通过工程设置:如在VS2008中,Project->Properties->Configuration Properties->C/C++->Advanced->Calling Convention。默认为__cdecl(/Gd)。

函数名字修饰约定:
a、C编译时函数名修饰约定规则:
1、__stdcall调用约定:在输出函数名前加上一个下划线前缀,后面加上一个"@"符号和其参数的总字节数(10进制),格式为_functionname@number ,不进行大小写转换。如:void Input(int &m,int &n),被修饰成:_Input@8
2、__cdecl调用约定:仅在输出函数名前加上一个下划线前缀,格式为_functionname,不进行大小写转换。
3、__fastcall调用约定:在输出函数名前加上一个"@"符号,后面也是一个"@"符号和其参数的字节数,格式为@functionname@number,不进行大小写转换。
b、C++编译时函数名修饰约定规则:
1、__stdcall调用约定:以"?"标识函数名的开始,后跟函数名;函数名后面以"@@YG "标识参数表的开始,后跟参数表;参数表以代号表示:
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"标识整个名字的结束,如果该函数无参数,则以"XZ"标识结束。
其格式为"?functionname@@YG*****@Z "或"?functionname@@YG*XZ "。
如:int Test1(char *var1,unsigned long),被修饰成:?Test1@@YGHPADK@Z
  void Test2(),被修饰成:?Test2@@YGXXZ
2、__cdecl调用约定:规则同上面的__stdcall调用约定,只是参数表的开始标识由上面的"@@YG"变为"@@YA"。
3、__fastcall调用约定:规则同上面的__stdcall调用约定,只是参数表的开始标识由上面的"@@YG"变为"@@YI"。

注意:
1、_beginthread需要__cdecl的线程函数地址,_beginthreadex和CreateThread需要__stdcall的线程函数地址。
2、一般WIN32的函数都是__stdcall。而且在Windef.h中有如下的定义:
#define CALLBACK __stdcall
#define WINAPI __stdcall
3、对于extern "C" _declspec(dllexport) int __cdecl Add(int a, int b);
修饰符的书写顺序应该为:
typedef int (__cdecl*FunPointer)(int a, int b);

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值