引言
常用函数调用约定:
1. cdecl
2. stdcall
3. fastcall
概念
调用约定:一种协议,一种函数调用方和被调用方(函数本身)一起约定的一个协议。如果没有函数,那也就没有调用约定。有了这个约定的协议后,被调用方就知道了当被调用时,怎么获取入参,怎么传递返回值。
协议内容涉及哪些?
1. 函数参数怎么传递(传递顺序,传递到哪里,怎么返回值)
2. 函数的名称怎么修饰
详解
结论
调用约定 | 谁传递入参(入栈) | 入参传递到哪 | 谁来将入参复原(出栈) | 参数传递顺序 | 名字修饰 |
---|---|---|---|---|---|
cdecl | 函数调用方 | 栈 | 函数调用方 | 从右往左顺序入栈 | 下划线+函数名 |
stdcall | 函数调用方 | 栈 | 函数本身 | 从右往左顺序入栈 | 下划线+函数名+@+参数的字节数 |
fastcall | 函数调用方 | 寄存器+栈 | 函数本身 | 除了寄存器的参数,剩余参数从右往左的顺序入栈 | @+函数名+@+参数的字节数 |
验证(以下都是在windows上验证)
代码
int _cdecl TestCdecl(int a = 1, int b = 2, int c = 3)
{
return a;
}
int _stdcall TestStdCall(int a = 1, int b = 2, int c = 3)
{
return a;
}
int _fastcall TestFastCall(int a = 1, int b = 2, int c = 3)
{
return a;
}
int main()
{
int a = TestCdecl();
int b = TestStdCall();
int c = TestFastCall();
return 0;
}
cdecl
1. 查看int a = TestCdecl()汇编代码,如下所示
24: int a = TestCdecl();
00A9236B 6A 03 push 3
00A9236D 6A 02 push 2
00A9236F 6A 01 push 1
00A92371 E8 2B F1 FF FF call TestCdecl (0A914A1h)
00A92376 83 C4 0C add esp,0Ch
00A92379 89 45 F8 mov dword ptr [a],eax
2. esp 是栈寄存器(位于栈顶),push 是入栈指令,比如如果esp = 0x700a,push 3后,esp寄存器就会变为0x7006, call 是调用函数
push 3
push 2
push 1
这三条指令就是函数的调用方将函数的参数从右往左的顺序压入栈中,每个参数都是int 4个字节(32位程序),每压入一个参数,esp寄存器都会-4(为啥是减4,因为栈的地址是递减的),总共三个参数,那么esp就要减12,十六进制就是0xcH
add esp, 0cH
这个就和上面压入了三个入参正好对上,上面减去12个字节,调用完函数后,需要恢复,所以又加了回来。
mov dword ptr [a], eax
这里是把寄存器eax的值传给变量a,从这里可以猜测到返回值是被函数本身存在eax寄存器的。我们进入函数内部看下,是不是我们猜测的。下图是函数内部汇编:
int _cdecl TestCdecl(int a = 1, int b = 2, int c = 3)
9: {
006D18E0 55 push ebp
006D18E1 8B EC mov ebp,esp
006D18E3 81 EC C0 00 00 00 sub esp,0C0h
006D18E9 53 push ebx
006D18EA 56 push esi
006D18EB 57 push edi
10: return a;
006D18EC 8B 45 08 mov eax,dword ptr [a]
11: }
006D18EF 5F pop edi
006D18F0 5E pop esi
006D18F1 5B pop ebx
006D18F2 8B E5 mov esp,ebp
006D18F4 5D pop ebp
006D18F5 C3 ret
mov eax,dword ptr [a]
正好和我们猜测的一样,函数返回值是保存在eax寄存器中的。mov eax, dword ptr [a]前面的汇编是保存寄存器,执行完后就pop出来恢复。ebp 寄存器是帧指针,寄存器有很多分类,不同的分类有不同的用途,具体可以看下相关介绍。
从这里验证,cdecl 是由函数调用方入参(入栈),入参顺序从右往左,函数调用方恢复入参(出栈),函数返回值是通过eax寄存器传递。
名字修饰怎么看?
这里改下代码(windows 符号默认不导出,貌似只有导出,才能看到,不导出不知道能不能看),为什么加extern C,因为C++ 本身有自己的函数签名。extern C 就是标识这里是c语言代码。
extern "C"
{
__declspec(dllexport) int _cdecl TestCdecl(int a = 1, int b = 2, int c = 3)
{
return a;
}
__declspec(dllexport) int _stdcall TestStdCall(int a = 1, int b = 2, int c = 3)
{
return a;
}
__declspec(dllexport) int _fastcall TestFastCall(int a = 1, int b = 2, int c = 3)
{
return a;
}
}
int main()
{
int a = TestCdecl();
int b = TestStdCall();
int c = TestFastCall();
return 0;
}
然后用dumpbin.exe /exports 应用程序查看,如下图
stdcall
1. 查看汇编
push 3
push 2
push 1
参数入栈,入栈顺序从右往左
mov dword ptr [b],eax
返回值还是eax寄存器返回。
但是没有参数出栈的代码了?看下函数内部:
int _stdcall TestStdCall(int a = 1, int b = 2, int c = 3)
14: {
007117C0 55 push ebp
007117C1 8B EC mov ebp,esp
007117C3 81 EC C0 00 00 00 sub esp,0C0h
007117C9 53 push ebx
007117CA 56 push esi
007117CB 57 push edi
15: return a;
007117CC 8B 45 08 mov eax,dword ptr [a]
16: }
007117CF 5F pop edi
007117D0 5E pop esi
007117D1 5B pop ebx
007117D2 8B E5 mov esp,ebp
007117D4 5D pop ebp
007117D5 C2 0C 00 ret 0Ch
mov eax, dword ptr [a]
这个是返回1,和上面一样。
ret 0CH
0x0CH正好 = 12和上面移动add esp, 0ch 值一样,查下ret 就知道这里类似 ret 返回然后 esp + 0ch。
从这里验证,stdcall 是由函数调用方入参(入栈),入参顺序从右往左, 函数本身恢复入参(出栈),函数返回值是通过eax寄存器传递。
fastcall
1. 查看汇编
push 3
mov edx,2
mov ecx,1
可以看到前两个int 参数,直接放入寄存器,第三个参数才是入栈,fastcall:头两个类型(4字节),或者占更少字节的参数被放入寄存器,其它剩下的参数按从右到左的顺序压入栈。
mov dword ptr [b],eax
返回值还是eax寄存器返回。
貌似也没有出栈的代码?看下函数内部:
int _fastcall TestFastCall(int a = 1, int b = 2, int c = 3)
19: {
00711790 55 push ebp
00711791 8B EC mov ebp,esp
00711793 81 EC D8 00 00 00 sub esp,0D8h
00711799 53 push ebx
0071179A 56 push esi
0071179B 57 push edi
0071179C 89 55 EC mov dword ptr [b],edx
0071179F 89 4D F8 mov dword ptr [a],ecx
20: return a;
007117A2 8B 45 F8 mov eax,dword ptr [a]
21: }
007117A5 5F pop edi
007117A6 5E pop esi
007117A7 5B pop ebx
007117A8 8B E5 mov esp,ebp
007117AA 5D pop ebp
007117AB C2 04 00 ret 4
mov eax, dword ptr [a]
这个是返回1,和上面一样。
ret 4
这里因为只有一个参数入栈,所以ret 4 类似于 ret 返回然后 esp + 04h。
从这里验证,fastcall 是由函数调用方入参,头两个类型(4字节),或者占更少字节的参数被放入寄存器,其它剩下的参数按从右到左的顺序压入栈。函数本身恢复入参(出栈),函数返回值是通过eax寄存器传递。
其它
thiscall(成员函数调用约定是thiscall,其它函数不指定默认是cdecl)
就是多了一个隐形的this指针。
1. windows 下是通过ecx 寄存器传递this指针,其它的和stdcall传递一样(验证过)
2. linux 下是类似函数第一个参数看待,其它和cdecl一样。(没环境,我没验证)