以上四种都是调用约定,会影响编译器对函数名的修饰规则、函数堆栈的清理方式、参数的传递方式。
区别简介
__stdcall
__stdcall是Pascal方式清理C方式压栈,通常用于Win32 Api中,函数采用从右到左的压栈方式,自己在退出时清空堆栈。VC将函数编译后会在函数名前面加上下划线前缀,在函数名后加上”@”和参数的字节数。 int f(void *p) –>> _f@4(在外部汇编语言里可以用这个名字引用这个函数).
__cdecl
__cdecl (The C default calling convention)即C调用约定按从右至左的顺序压参数入栈,由调用者把参数弹出栈。对于传送参数的内存栈是由调用者来维护的。另外,在函数名修饰约定方面也有所不同。 _cdecl是C和C++程序的缺省调用方式。每一个调用它的函数都包含清空堆栈的代码,所以产生的可执行文件大小会比调用_stdcall函数的大。
__fastcall
__fastcall调用的主要特点就是快,因为它是通过寄存器来传送参数的(实际上,它用ECX和EDX传送前两个双字(DWORD)或更小的参数,剩下的参数仍旧自右向左压栈传送,被调用的函数在返回前清理传送参数的内存栈),在函数名修饰约定方面,它和前两者均不同。__fastcall方式的函数采用寄存器传递参数,VC将函数编译后会在函数名前面加上”@”前缀,在函数名后加上”@”和参数的字节数。
__thiscall
__thiscall是C++成员函数的默认调用约定,__thiscall不是关键字,不能进行显示指定。参数是从右向左压栈,由被调用的函数清理堆栈。而且使用ecx寄存器来传递this指针。(注意:并不是所有的成员函数调用都是通过ecx来实现的,得看具体的编译器)
深入分析
接下来从汇编代码的角度来分析。
先写一段简单代码。我用的是vs2013。
class Test{
public:
int __stdcall say(int a, int b){
return a + b;
}
};
int _tmain(int argc, _TCHAR* argv[])
{
Test t;
int a = t.say(1, 2);
return 0;
}
查看汇编语言。
这里有两个方法。
1、调试运行,在想看代码的地方打断点,然后菜单栏调试-》窗口-》反汇编。我们在int a=t.say(1,2)处打断点,可以看到停在了这里。
这个窗口里的就是汇编代码。
2、设置工程项目属性。配置属性-》c/c+±》输出文件-》汇编程序输出设为程序集、机器码和源代码 (/FAcs)
编译生成后可以看到目录下生成了cod文件。
打开cod搜索Test:say,定位到这个地方。
这些就是Test:say函数的汇编代码。
这里我们使用第一种。
__stdcall调用Test:say处的代码:
Test t;
int a = t.say(1, 2);
01337FE8 push 2
01337FEA push 1
01337FEC lea eax,[t]
01337FEF push eax
01337FF0 call Test::say (0133122Bh)
01337FF5 mov dword ptr [a],eax
__cdecl调用Test:say处的代码:
Test t;
int a = t.say(1, 2);
00867FE8 push 2
00867FEA push 1
00867FEC lea eax,[t]
00867FEF push eax
00867FF0 call Test::say (086154Bh)
00867FF5 add esp,0Ch
00867FF8 mov dword ptr [a],eax
先push 2再push 1,可以看到参数是从右向左入栈的。
两者最大区别就是__cdecl的call指令后面多了个add esp,0Ch 。0Ch十进制为12,在调用Test:say函数前有三个push的操作,一个数据占4个字节,所以是12。这个其实是清除调用栈里面的参数。因为调用栈是从高地址从低地址增长的,所以是add指令。
这个地方就是__cdecl的调用者清栈操作。下面看__stdcall的清栈操作。
__stdcall时Test:say函数的代码如下:
class Test{
public:
int __stdcall say(int a, int b){
009F7F80 push ebp
009F7F81 mov ebp,esp
009F7F83 sub esp,0C0h
009F7F89 push ebx
009F7F8A push esi
009F7F8B push edi
009F7F8C lea edi,[ebp-0C0h]
009F7F92 mov ecx,30h
009F7F97 mov eax,0CCCCCCCCh
009F7F9C rep stos dword ptr es:[edi]
return a + b;
009F7F9E mov eax,dword ptr [a]
009F7FA1 add eax,dword ptr [b]
}
009F7FA4 pop edi
009F7FA5 pop esi
009F7FA6 pop ebx
009F7FA7 mov esp,ebp
009F7FA9 pop ebp
009F7FAA ret 0Ch
__cdecl时Test:say函数的代码如下:
class Test{
public:
int __cdecl say(int a, int b){
00917F80 push ebp
00917F81 mov ebp,esp
00917F83 sub esp,0C0h
00917F89 push ebx
00917F8A push esi
00917F8B push edi
00917F8C lea edi,[ebp-0C0h]
00917F92 mov ecx,30h
00917F97 mov eax,0CCCCCCCCh
00917F9C rep stos dword ptr es:[edi]
return a + b;
00917F9E mov eax,dword ptr [a]
00917FA1 add eax,dword ptr [b]
}
00917FA4 pop edi
00917FA5 pop esi
00917FA6 pop ebx
00917FA7 mov esp,ebp
00917FA9 pop ebp
00917FAA ret
最大的区别是ret,__stdcall是ret 0Ch,__cdecl是ret。
ret 0Ch起到的作用是:
pop eip
add esp,0ch
相当于把清栈操作放到函数里了,这样调用者就不用在函数返回后再清理栈。这个我就有点疑惑了,__stcall是比__cdecl少了点指令,但也就一条,编译出来的程序大小应该不会有多大差距。
__thiscall调用Test:say处的代码:
Test t;
int a = t.say(1, 2);
012C7FE8 push 2
012C7FEA push 1
012C7FEC lea ecx,[t]
012C7FEF call Test::say (012C1555h)
012C7FF4 mov dword ptr [a],eax
对比__stdcall可以看到lea eax变成了lea ecx,__thiscall少了个push指令。也就是说__thiscall没把this指针入栈,函数内靠读取ecx寄存器取得this指针。
参考:
1、__cdecl,__stdcall,__fastcall,__pascal,__thiscall 的区别
2、【C/C++】__stdcall、__cdcel和__fastcall定义与区别