调用约定与符号名

2021-02-08  本文已影响0人  _张鹏鹏_

目的:

这里对【函数调用约定与Name Mangling】之间的关系进行总结,方便后续查找。

调用约定:

__stdcall__cdecl__fastcall是三种函数调用约定,函数调用约定会影响函数参数的入栈方式、栈内数据的清除方式、编译器函数名的修饰规则等。

调用约定 入栈方式 清栈责任 使用场合
__stdcall 从右到左 函数自己 Windows API常用的函数调用约定
__cdecl 从右到左 调用者 C/C++默认的函数调用约定(Linux下基本都是这种)
__fastcall 从右到左(先 EDX、ECX,再到堆栈) 被调用者 适用于对性能要求较高的场合

编译时,如果没有显式声明调用约定,一般默认都是__cdecl。当然这也要看平台,delphi默认是__stdcall

C语言之所以默认不使用 __stdcall , 是因为C语言所对应的函数传入的参数是可变的,只有函数调用方才能知道到底有多少个参数,这种情况下,栈的清理作业便无法进行。如果在C语言中函数的参数固定的话,指定 __stdcall 是没有问题的。

Windows提供的DLL文件内的函数基本上都是_stdcall调用方式,有两个原因:

  1. 绝大部分的语言都支持__stdcall方式,为了能让其他语言能够使用DLL的话,就得使用__stdcall的方式。
  2. 节约内存,解释如下:

C语言中,在调用函数后,需要执行栈清理处理指令。
指的是把不需要的数据从接收和传递函数的参数时使用的内存上的栈区域清理出去。
该命令是在程序编译时由编译器自动附加到程序中的,编译器默认将该处理附在函数调用方。
在同一个程序中,同样的函数可能会被反复调用多次,而如果是同样的函数,栈清理处理的内容也是一样。由于该处理是在调用函数一方,因此就会导致同一处理被反复进行,造成内存浪费。
栈清理处理,比起在函数调用方进行,在反复被调用的函数一方进行时,程序整体要小一些。
这时所使用的就是 _stdcall。在函数前加上 _stdcall 就可以把栈清理处理变为在被调用函数一方进行。下面是图示:

节约内存.png

项目代码中常用的是__stdcall__cdecl,且对于导出的函数经常采用__cdecl调用方式,对回调函数则采用__stdcall的调用方式。

// 回调函数使用__stdcall,导出的函数使用__cdecl方式
 typedef void (__stdcall *pFunCB_Msg)(u32 dwHandle, u32 wMsgType, s8 *szMsgBody, u32 dwMsgLen);
 extern "C" __declspec(dllexport) bool __cdecl SetCBFun_Msg(pFunCB_Msg cbFunMsg, u32 dwHandle = 0, long dwContextFun = 0);

阶段总结:

1.导出的接口一般如下:
extern "C" __declspec(dllexport) u32 _cdecl Test(int a, int b)
2.回调函数一般如下:
typedef void (__stdcall *pFunCB_Msg)(u32 dwHandle, u32 wMsgType, s8 *szMsgBody, u32 dwMsgLen)
3.除了可变参数的API函数调用,一般都是__stdcall的调用习惯。

血案:

delphi默认编译选项是__stdcall,delphi编写的动态库函数,在VC++中调用(默认__cdecl方式调用),总是造成程序的崩溃;
解决方法:在delphi编写动态库函数时,显示声明__stdcall调用方式,VC++就会使用__stdcall方式去调用,这样就解决了问题。

Name Mangling:

影响符号名的除了C++和C的区别、编译器的区别之外,还要考虑调用约定导致的Name Mangling

规则:

C语言编译器函数名称修饰规则:

  1. __stdcall:编译后,函数名被修饰为“functionname@number”。
  2. __cdecl:编译后,函数名被修饰为“functionname”。
  3. __fastcall:编译后,函数名给修饰为“@functionname@nmuber”。

注:“functionname”为函数名,“number”为参数字节数。
注:函数实现和函数定义时如果使用了不同的函数调用协议,则无法实现函数调用。

C++语言编译器函数名称修饰规则:

  1. __stdcall:编译后,函数名被修饰为“?functionname@@YG******@Z”。
  2. __cdecl:编译后,函数名被修饰为“?functionname@@YA******@Z”。
  3. __fastcall:编译后,函数名被修饰为“?functionname@@YI******@Z”。

注:“******”为函数返回值类型和参数类型表。
注:函数实现和函数定义时如果使用了不同的函数调用协议,则无法实现函数调用。

示例:

这里提供一个示例:

// cpp文件
#ifndef _TEST_
#define _TEST_

extern "C" __declspec(dllexport) void __stdcall  hello(int a,int b);
extern "C" __declspec(dllexport) void __cdecl  world(int a,int b);

__declspec(dllexport) void __stdcall  daniel(int a,int b);
__declspec(dllexport) void __cdecl  zpp(int a,int b);

#endif

dll中的函数在被调用时是以函数名或函数编号的方式被索引的,可以使用dumpbin /exports来查看编译器生成的符号名,以下示例是在visual studio2013下的结果:

File Type: DLL
  Section contains the following exports for Project2.dll
    00000000 characteristics
    6020B7AC time date stamp Mon Feb 08 12:01:48 2021
        0.00 version
           1 ordinal base
           4 number of functions
           4 number of names
    ordinal hint RVA      name
          1    0 0001127B ?daniel@@YGXHH@Z = @ILT+630(?daniel@@YGXHH@Z)
          2    1 0001101E ?zpp@@YAXHH@Z = @ILT+25(?zpp@@YAXHH@Z)
          3    2 000111EF _hello@8 = @ILT+490(_hello@8)
          4    3 00011019 world = @ILT+20(_world)

由于C++标准并没有规定Name-Mangling的方案,所以不同编译器使用的规则是不同的,这样的话,不同编译器编译出来的目标文件.obj 是不通用的。

因为同一个函数,使用不同的Name-Mangling在obj文件中就会有不同的名字。如果dll里的函数重命名规则跟dll的使用者采用的重命名规则不一致,那就会找不到这个函数。

为了使得dll可以通用些,很多时候都要使用C的Name-Mangling方式,即对每一个导出函数声明为extern “C”

如果导出函数使用了extern”C” _cdecl,这个时候dll里的名字就是原始名字。使用这种方式任何一个支持C语言的编译器,它编译出来的obj文件可以共享、链接成可执行文件。这是一种标准,如果dll与其使用者都采用这种约定,那么就可以解决函数重命名规则不一致导致的错误。所以目前项目中要导出的函数以这种导出。

对于采用_stdcall调用约定,即使使用了extern”C”,还需要对导出函数进行重命名,才能达到和extern”C” _cdecl相同的效果。

参考文献:

  1. C++知识回顾之__stdcall、__cdcel和__fastcall三者的区别
  2. __cdecl、__stdcall、__fastcall 与 __pascal 浅析
  3. dll 导出函数名的那些事
  4. extern”C” _cdecl
  5. 这篇不错
  6. _stdcall调用
上一篇下一篇

猜你喜欢

热点阅读