调用约定与符号名

目的:

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

调用约定:

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

调用约定入栈方式清栈责任
__stdcall从右到左函数自己
__cdecl从右到左调用者
__fastcall从右到左(先 EDX、ECX,再到堆栈)被调用者

调用协议常用场合:

  1. __stdcallWindows API常用的函数调用约定

  2. __cdeclC/C++默认的函数调用约定,cdecl是C语言里面函数调用使用得最多的协议(可以说绝大多数都是这个,Unix/Linux下面清一色都是这个……),主要特性是使用eax寄存器保存返回值(通常对于比较小的类型如此),调用方清栈(所以可以支持变参函数),另外它也是C/C++默认的调用协议(也就是说不额外声明的话,默认就是cdecl的)

  3. __fastcall:适用于对性能要求较高的场合。

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

项目代码中常用的是__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. 这篇不错
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值