__stdcall、__cdecl与__fastcall的区别

 
__stdcall与__cdecl的区别

 

1.参数入栈的方式


在函数调用过程中,会使用堆栈,这三个表示不同的堆栈调用方式和释放方式。
比如说__cdecl,它是标准的c方法的堆栈调用方式,就是在函数调用时的参数压入堆栈是与函数的声明顺序相反的,其它两个可以看MSDN,不过这个对我们编程没有太大的作用


2.调用约定

调用约定(Calling convention)决定以下内容:函数参数的压栈顺序,由调用者还是被调用者把参数弹出栈,以及产生函数修饰名的方法。MFC支持以下调用约定:

_cdecl

按从右至左的顺序压参数入栈,由调用者把参数弹出栈。对于“C”函数或者变量,修饰名是在函数名前加下划线。对于“C++”函数,有所不同。

如函数void test(void)的修饰名是_test;对于不属于一个类的“C++”全局函数,修饰名是?test@@ZAXXZ。

这是MFC缺省调用约定。由于是调用者负责把参数弹出栈,所以可以给函数定义个数不定的参数,如printf函数。

_stdcall

按 从右至左的顺序压参数入栈,由被调用者把参数弹出栈。对于“C”函数或者变量,修饰名以下划线为前缀,然后是函数名,然后是符号“@”及参数的字节数,如 函数int func(int a, double b)的修饰名是_func@12。对于“C++”函数,则有所不同。

所有的Win32 API函数都遵循该约定。


_fastcall

头 两个DWORD类型或者占更少字节的参数被放入ECX和EDX寄存器,其他剩下的参数按从右到左的顺序压入栈。由被调用者把参数弹出栈,对于“C”函数或 者变量,修饰名以“@”为前缀,然后是函数名,接着是符号“@”及参数的字节数,如函数int func(int a, double b)的修饰名是@func@12。对于“C++”函数,有所不同。

未来的编译器可能使用不同的寄存器来存放参数。


thiscall

仅仅应用于“C++”成员函数。this指针存放于CX寄存器,参数从右到左压栈。thiscall不是关键词,因此不能被程序员指定。


naked call

采用1-4的调用约定时,如果必要的话,进入函数时编译器会产生代码来保存ESI,EDI,EBX,EBP寄存器,退出函数时则产生代码恢复这些寄存器的内容。naked call不产生这样的代码。

naked call不是类型修饰符,故必须和_declspec共同使用,如下:

__declspec( naked ) int func( formal_parameters )

{

// Function body

}


3.过时的调用约定

原来的一些调用约定可以不再使用。它们被定义成调用约定_stdcall或者_cdecl。例如:

#define CALLBACK __stdcall

#define WINAPI __stdcall

#define WINAPIV __cdecl

#define APIENTRY WINAPI

#define APIPRIVATE __stdcall

#define PASCAL __stdcall

 

 

Visual C++ Compiler Options可以指定的Calling Convention有 3种:
     /Gd /Gr /Gz

     这三个参数决定了:

     1.函数参数以何种顺序入栈,右到左还是左到右。
     2.在函数运行完后,是调用函数还是被调用函数清理入栈的参数。
     3.在编译时函数名字是如何转换的。

     下面我们分别详细介绍:

     1./Gd
         这是编译器默认的转换模式,对一般函数使用 C的函数调用转换方式__cdecl,
         但是对于C++ 成员函数和前面修饰了__stdcall __fastcall的函数除外。

     2./Gr
         对于一般函数使用__fastcall函数调用转换方式,所有使用__fastcall的函数
         必须要有函数原形。但对于C++ 成员函数和前面修饰了__cdecl __stdcall 的函数除外。

     3./Gz
         对于所有 C函数使用__stdcall函数调用转换方式,但对于可变参数的 C函数以
         及用__cdecl __fastcall修饰过的函数和C++ 成员函数除外。所有用__stdcall
         修饰的函数必须有函数原形。

         事实上,对于x86系统,C++ 成员函数的调用方式有点特别,将成员函数的this
         指针放入ECX,所有函数参数从右向左入栈,被调用的成员函数负责清理入栈的
         参数。对于可变参数的成员函数,始终使用__cdecl的转换方式。

     下面该进入主题,分别讲一下这三种函数调用转换方式有什么区别:

     1.__cdecl
         这是编译器默认的函数调用转换方式,它可以处理可变参数的函数调用。参数
         的入栈顺序是从右向左。在函数运行结束后,由调用函数负责清理入栈的参数。
         在编译时,在每个函数前面加上下划线(_),没有函数名大小写的转换。即
                   _functionname

     2.__fastcall
         有一些函数调用的参数被放入ECX,EDX中,而其它参数从右向左入栈。被调用
         函数在它将要返回时负责清理入栈的参数。在内嵌汇编语言的时候,需要注意
         寄存器的使用,以免与编译器使用的产生冲突。函数名字的转换是:
                   @functionname@number
         没有函数名大小写的转换,number表示函数参数的字节数。由于有一些参数不
         需要入栈,所以这种转换方式会在一定程度上提高函数调用的速度。

     3.__stdcall
       函数参数从右向左入栈,被调用函数负责入栈参数的清理工作。函数名转换格
       式如下:
                  _functionname@number

     下面我们亲自写一个程序,看看各种不同的调用在编译后有什么区别,我们的被调
     用函数如下:

     int function(int a, int b)
     {
         return a + b;
     }

     void main()
     {
         function(10, 20);
     }

     1.__cdecl

         _function
                  push     ebp
                  mov      ebp, esp
                  mov      eax, [ebp+8]        ;参数1
                  add      eax, [ebp+C]        ;加上参数2
                  pop      ebp
                  retn
         _main
                  push     ebp
                  mov      ebp, esp
                  push     14h                 ;参数 2入栈
                  push     0Ah                 ;参数 1入栈
                  call     _function           ;调用函数
                  add      esp, 8              ;修正栈
                  xor      eax, eax
                  pop      ebp
                  retn

     2.__fastcall

         @function@8
                  push     ebp
                  mov      ebp, esp            ;保存栈指针
                  sub      esp, 8              ;多了两个局部变量
                  mov      [ebp-8], edx        ;保存参数 2
                  mov      [ebp-4], ecx        ;保存参数 1
                  mov      eax, [ebp-4]        ;参数 1
                  add      eax, [ebp-8]        ;加上参数 2
                  mov      esp, ebp            ;修正栈
                  pop      ebp
                  retn
         _main
                  push     ebp
                  mov      ebp, esp
                  mov      edx, 14h            ;参数 2给EDX
                  mov      ecx, 0Ah            ;参数 1给ECX
                  call     @function@8         ;调用函数
                  xor      eax, eax
                  pop      ebp
                  retn

     3.__stdcall

         _function@8
                  push     ebp
                  mov      ebp, esp
                  mov      eax, [ebp]          ;参数 1
                  add      eax, [ebp+C]        ;加上参数 2
                  pop      ebp
                  retn     8                   ;修复栈
         _main
                  push     ebp
                  mov      ebp, esp
                  push     14h                 ;参数 2入栈
                  push     0Ah                 ;参数 1入栈
                 call     _function@8    ;函数调用
                  xor      eax, eax
                  pop      ebp
                  retn

     可见上述三种方法各有各的特点,而且_main必须是__cdecl,一般WIN32的函数都是
     __stdcall。而且在Windef.h中有如下的定义:

     #define CALLBACK __stdcall
     #define WINAPI  __stdcall

你可能感兴趣的:(__stdcall、__cdecl与__fastcall的区别)