本文主要是介绍新书推荐:11.6 调用约定,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!
本节必须掌握的知识点:
函数调用约定
11.6.1 函数调用约定
什么是函数调用约定?就是告诉编译器:怎么传递参数,怎么传递返回值,怎么平衡堆栈,函数怎样命名。当一个程序中所有的函数都是自己实现的,则不需要和任何人约定,但是如果程序中调用第三方的函数,则需要和对方约定上述事宜了。
常见的调用约定有__stdcall,__cdecl,__fastcall, __pascal,Windows32位系统中,微软就开始使用__fastcall调用约定了,到了x64系统,则同一采用__fastcall调用约定了(参见《X86汇编基础教程》 第三部分64位汇编)。
实验一百二十三:__cdecl调用约定
在VS中新建项目11-6-1.c:
/*
__cdecl调用约定
*/
#include <stdio.h>
#include <stdlib.h>
int funAdd(int x, int y)
{
return x + y;
}
int main(void)
{
int rel = funAdd(1, 2);//这里是堆栈传参
/*
00401068 push 2 //将2压入堆栈
0040106A push 1 //将1压入堆栈
0040106C call @ILT+0(funAdd)(00401005)//调用者
00401071 add esp, 8 //平衡堆栈(外平栈)
00401074 mov dword ptr[ebp - 4], eax
*/
printf("%d\n",rel);
system("pause");
return 0;
}
●运行结果:
3
请按任意键继续. . .
●参数传递:有堆栈传参、寄存器传参、堆栈+寄存器传参、call后续区传参这四种方式。
那么怎么决定参数传递顺序的呢?要看具体的调用约定。
●调用约定:
下面列举了常见的调用约定有三种形式:
上面的代码中,并没有用告诉编译器是哪种调用约定,在VS下默认的调用约定是__cdecl,我们来一一验证下。
参数压栈顺序:从右往左入栈,funAdd(1,2);执行从右往左压栈顺序,先push 2再 push 1,与__cdecl执行参数压栈顺序是一致的。我们接着看平衡堆栈,__cdecl的平衡堆栈要求是:调用者清理栈,且是外平栈。我们再来看下我们写的代码, call @ILT+0(funAdd) (00401005), funAdd(0x00401005)是当前的函数,main函数调用了 funAdd(0x00401005)函数,所以main函数是调用者,接着add esp,8,是外平栈。
以上完全符合__cdecl调用约定规则。
我们可以函数定义中指定函数的调用约定,例如:
int __cdecl funAdd(int x,int y) //这样键入__cdecl就可以了
实验一百二十四:__stdcall调用约定
在VS中新建项目11-6-2.c:
/*
__stdcall调用约定
*/
#include <stdio.h>
#include <stdlib.h>
int __stdcall funAdd(int x, int y)
{
return x + y;
}
int main(void)
{
int rel = funAdd(1, 2);
printf("%d\n", rel);
system("pause");
return 0;
}
●运行结果:
3
请按任意键继续. . .
●代码分析:
我们来看以下反汇编代码:
int __stdcall funAdd(int x, int y)
{
00301847 dec dword ptr [ebx+45030845h]
return x + y;
0030184D or al,5Fh
}
0030184F pop esi
00301850 pop ebx
00301851 add esp,0C0h
00301857 cmp ebp,esp
00301859 call __RTC_CheckEsp (0301226h)
0030185E mov esp,ebp
00301860 pop ebp
00301861 ret 8 ;内平栈
int main(void)
{
00301880 push ebp
00301881 mov ebp,esp
…
int rel = funAdd(1, 2);
003018A8 push 2 ;从右往左入栈
003018AA push 1
003018AC call _funAdd@8 (03011E5h)
003018B1 mov dword ptr [rel],eax
printf("%d\n", rel);
003018B4 mov eax,dword ptr [rel]
003018B7 push eax
003018B8 push offset string "%d\n" (0307B30h)
003018BD call _printf (030104Bh)
003018C2 add esp,8
system("pause");
return 0;
003018DC xor eax,eax
}
__stdcall调用约定:参数从右往左压入堆栈,且是函数自己本身清理堆栈。
实验一百二十五:__fastcall调用约定
在VS中新建项目11-6-3.c:
/*
__fastcall调用约定
*/
#include <stdio.h>
#include <stdlib.h>
int __fastcall funAdd(int x, int y, int i, int j)
{
return x + y + i + j;
}
int main(void)
{
int rel = funAdd(1,2,3,4);
printf("%d\n", rel);
system("pause");
return 0;
}
●运行结果:
10
请按任意键继续. . .
●代码分析:
我们来看以下反汇编代码:
int __fastcall funAdd(int x, int y, int i, int j)
{
00B51720 push ebp
00B51721 mov ebp,esp
…
return x + y + i + j;
00B51750 mov eax,dword ptr [x]
00B51753 add eax,dword ptr [y]
00B51756 add eax,dword ptr [i]
00B51759 add eax,dword ptr [j]
}
…
00B5176C mov esp,ebp
00B5176E pop ebp
00B5176F ret 8 ;平衡2个堆栈参数
int main(void)
{
00B51890 push ebp
00B51891 mov ebp,esp
…
int rel = funAdd(1,2,3,4);
00B518B8 push 4 ;第四个参数
00B518BA push 3 ;第三个参数
00B518BC mov edx,2 ;第二个参数
00B518C1 mov ecx,1 ;第一个参数
00B518C6 call @funAdd@16 (0B51082h)
00B518CB mov dword ptr [rel],eax
printf("%d\n", rel);
00B518CE mov eax,dword ptr [rel]
00B518D1 push eax
00B518D2 push offset string "%d\n" (0B57B30h)
00B518D7 call _printf (0B5104Bh)
00B518DC add esp,8
system("pause");
return 0;
00B518F6 xor eax,eax
}
由上述反汇编代码可知,第1,2个参数使用ecx和edx寄存器传参,第3,4个参数使用堆栈传参,堆栈传参仍然是从右往左顺序入栈。堆栈平衡为内平衡,被调函数负责平衡2个参数占用的8个字节堆栈空间。
由于寄存器传参的速度最快,因此称为fastcall调用约定,可惜32位程序只能使用ecx和edx两个寄存器参与传参,剩余参数使用堆栈传参。x64处理器中的fastcall调用约定可以使用rcx,rdx,r8,r94个寄存器传参,剩余参数使用堆栈传参。
总结
__fastcall通过寄存器传参,传递参数不破坏堆栈的平衡,所以它不需要平栈,它的执行效率最高。但是当它的参数的数量大于2时,它就会采取堆栈+寄存器的方式传参,反而会降低执行效率。
C语言中默认采用的调用约定是“_cdecl”;_stdcall为WINAPI的调用约定,_fastcall为Windows内核中常见的调用约定。此外还有C++中有this call调用约定(_thiscall)。
这篇关于新书推荐:11.6 调用约定的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!