初识 C/C++ 函数调用约定(实例分析)

引言

常用函数调用约定:

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一样。(没环境,我没验证)

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值