现在的位置: 首页 > 综合 > 正文

【C/C++】总结常用的函数调用约定

2018年01月12日 ⁄ 综合 ⁄ 共 2432字 ⁄ 字号 评论关闭

 

一、函数调用的基本步骤

 

函数调用大致包括以下几个步骤。

(1)参数入栈:将参数从右向左依次压入系统栈中。

(2)返回地址入栈:将当前代码区调用指令的下一条指令地址压入栈中,供函数返回时继续执行。

(3)代码区跳转:处理器从当前代码区跳转到被调用函数的入口处。

(4)栈帧调整:具体包括:

保存当前栈帧状态值,已备后面恢复本栈帧时使用(EBP入栈)。

将当前栈帧切换到新栈帧(将ESP值装入EBP,更新栈帧底部)。

给新栈帧分配空间(把ESP减去所需空间的大小,抬高栈顶)。

 

 

 

二、函数调用约定:

 

调用约定的声明

参数入栈顺序

恢复栈平衡的位置

__cdecl

右→左

母函数

__fastcall

右→左

子函数

__stdcall

右→左

子函数

 

 

 

三、具体的表现

(1) __cdecl

       函数本身不清理堆栈,调用者负责清理堆栈。由于这种变化,C调用约定允许函数的参数的个数是不固定的

(2) __fastcall

      函数的第一个和第二个DWORD参数(或者尺寸更小的)通过ecx和edx传递,其他参数通过从右向左的顺序压栈

      被调用函数清理堆栈

(3) __stdcall

     参数从右向左压入堆栈

     函数自身修改堆栈

 

 

四、VC下的表现
      在VC的工程设置中可以设置函数调用方式,从表现可以看出明显的差别。
(1)如果设置成__cdecl*,那么对于下面的代码,汇编表现为:
       00402692   push        8
00402694   push        5
00402696   push        3
00402698   call        @ILT+275(GetNum) (00401118)
0040269D   add         esp,0Ch
(2)如果设置成__stdcall,那么对于下面的代码,汇编表现为:
00402692   push        8
00402694   push        5
00402696   push        3
00402698   call        @ILT+280(GetNum) (0040111d)
(3)如果设置成__fastcall,那么对于下面的代码,汇编表现为:
00402692   push        8
00402694   mov         edx,5
00402699   mov         ecx,3
0040269E   call        @ILT+170(GetNum) (004010af)
      
       

 

 

 

五、其他调用

(1) thiscall

    thiscall是唯一一个不能明确指明的函数修饰,因为thiscall不是关键字。它是C++类成员函数缺省的调用约定。由于成员函数调用还有一个

    this指针,因此必须特殊处理。

   Microsoft Visual C++提供了thiscall调用约定,它把this保存到ECX寄存器中,并由非静态函数负责从堆栈中清除参数。

   GNU g++编译器把this指针看作是动态成员函数的暗含的第一个参数,并使用cdecall约定中的方法来传递参数。

    表现:

    参数从右向左入栈

    如果参数个数确定,this指针通过ecx传递给被调用者;如果参数个数不确定,this指针在所有参数压栈后被压入堆栈。

    对参数个数不定的,调用者清理堆栈,否则函数自己清理堆栈

 

对于thiscall,在C++的类中可以定义,那么不管在VC中如何设置函数调用约定,下面的表现都是一定的,即:

func函数有子函数进行清栈,而func1函数有母函数进行清栈。

 

 

 

(2) naked call
       这是一个很少见的调用约定,一般程序设计者建议不要使用。编译器不会给这种函数增加初始化和清理代码,更特殊的是:
       你不能用return返回返回值,只能用插入汇编返回结果,这一般用于实模式驱动程序设计。
六、总结

          要想把所有的调用约定都写完,就需要一本单独的书来完成了。调用约定一般指的是语言、编译器和CPU方面的,当你遇到一些很少见的

    编译器编译出的代码时,你就需要好好的研究一下了。在一些情况下需要特别提到:优化的代码,自定义汇编语言代码,和系统调用。

       当一个函数被输出供其它程序员使用时(如库函数),让这些函数遵循人们熟知的调用约定是很重要的,这样可以是程序员很快的掌握函数

    接口。另一方面,如果一个函数仅仅是为了内部程序使用的,那这个函数的调用约定要在程序内部为人所熟知。因此好的编译器会代替某些调用

    约定来产生运行速度更快的代码。例如在使用Microsoft Visual C++/GL选项或者使用GNU gcc/g++regparm关键字时,就会出现这种情况。

       当程序员在使用汇编语言时,他又完全的控制权来决定函数的参数如何传递。汇编语言程序员可以用任何他们认为合理的方式来传递参数,

    除非他们想让自己写的函数被其它程序员引用。传统的汇编语言一般在obfuscation routinesshellcode中见到。

       系统调用是用来请求一个系统服务的特殊的函数调用方式。系统调用通常会将操作系统从用户模式切换为内核模式,以使操作系统内核提供

    用户所请求的服务。不同操作系统和不同的CPU的系统调用的方式也是不同的。

 


 

抱歉!评论已关闭.