• 动态链接库(七)--解决不同调用约定导致的名字改编问题


    写在前面

    前面提到不同的编译器生成的动态链接库,会使导出函数发生名字改编.

    即使编译器相同,不同的调用约定也会使导出函数发生名字改编,改编的规则也不尽相同.

    关于不同编译器,不同调用约定的名字改编规则,可以参考:动态链接库(二)–动态链接库的创建 中介绍的名字改编规则.

    关于如何指定函数的调用约定,可以参考:动态链接库(扩展)–调用约定.

    为突出DLL项目使用DLL的项目调用约定不同导致的名字改编问题,先统一两个项目的编译器,下面会分别模拟解决C编译器和C++编译器下两个项目不同调用约定导致的名字改编问题.

    不同编译器导致的名字改编问题,解决参考:动态链接库(六)–解决不同编译器导致的名字改编问题.

    解决C编译器下不同调用约定导致的名字改编问题

    根据:动态链接库(六)–解决不同编译器导致的名字改编问题,可以知道,DLL项目的编译器及调用约定 和 调用Dll的项目一致的话,即使发生了名字改编,也依旧可以同.h中原始的函数声明名正常调用, 如下:

    DLL1项目使用C++编译器, 默认的__cdecl调用约定.

    //Dll1.h
    #ifdef DLL1_API
    #else
    #define DLL1_API __declspec(dllimport) 
    #endif
    
    
    //int __declspec(dllimport) add(int a, int b);
    int DLL1_API add(int a, int b);
    
    //int __declspec(dllimport) subtract(int a, int b);
    int DLL1_API subtract(int a, int b);
    
    
    //Dll.cpp
    #define DLL1_API __declspec(dllexport)
    #include "Dll1.h"
    
    int DLL1_API add(int a, int b)
    {
    	return a + b;
    }
    
    int subtract(int a, int b)
    {
    	return a - b;
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28

    DLL_test项目中的main.cpp调用, DLL_test项目同样使用C++编译器, 默认的__cdecl调用约定.

    //DLL_test项目中的main.cpp
    #include 
    using namespace std;
    #include "Dll1.h"
    int main()
    {
    	cout << "累加函数测试: " << add(5, 3) << endl;
    	cout << "减法函数测试: " << subtract(5, 3) << endl;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    编译运行可正常调用:
    1

    因此这里模拟 DLL项目使用DLL的项目 都是C编译器编译,但DLL项目的导出函数使用WINAPI调用约定__stdcall, 而使用DLL的项目使用默认的C调用约定__cdecl.

    这里有两种方式指定导出函数的调用约定:
    (1) 修改项目属性中默认的调用约定
    (2) 在函数声明实现中指定调用约定

    修改项目属性中默认的调用约定

    修改DLL1项目的默认的调用约定,项目属性 –》C/C++ -》高级 –》调用约定选择__stdcall, 如下:
    2
    这样在导出函数声明实现中不指定调用约定时,默认的就是__stdcall调用约定了.

    修改Dll1项目如下:

    //DLL1项目
    //Dll1.h
    #ifdef DLL1_API
    #else
    #define DLL1_API __declspec(dllimport) 
    #endif
    
    #ifdef __cplusplus
    extern "C"
    {
    #endif
    	
    	int DLL1_API add(int a, int b);
    
    	
    	int DLL1_API subtract(int a, int b);
    
    #ifdef __cplusplus
    };
    #endif
    
    
    //Dll.cpp
    #define DLL1_API __declspec(dllexport)
    #include "Dll1.h"
    
    int  add(int a, int b)
    {
    	return a + b;
    }
    
    int  subtract(int a, int b)
    {
    	return a - b;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35

    虽然函数声明实现中没有指定调用约定, 但会默认为__stdcall调用约定.

    生成后使用dumpbin命令查看导出函数有按照C编译器下__stdcall调用约定的改编规则:
    关于不同调用约定的命名规则,可参考:动态链接库(扩展)–调用约定.
    3

    修改CTest项目main.c如下:

    //CTest项目中的main.c
    #include 
    #include 
    #pragma comment(lib, "D:\\vs2010_application\\CTest\\CTest\\CTest\\Dll1.lib")
    #include "Dll1.h"
    
    int main()
    {
    	printf("2 + 3 = %d\n",add(2, 3));
    	printf("5 - 2 = %d\n", subtract(5, 2));
    
    	system("pause");
    	return;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    更新到调用该DLL的C项目中去,编译发现有如下报错提示:
    4
    这里编译失败是因为DLL1项目默认的调用约定是__stdcall, 而C项目CTest默认的调用约定是__cdecl, 所以这里在CTest项目中调用add和subtract时,会以__cdecl调用约定去调用.

    相当于在CTest项目中,Dll1.h中的函数声明被理解成是__cdecl调用约定的, 这里就和DLL1项目中实现不一致了.

    前面有说过全局函数声明和实现的调用约定必须一致, 这里不一致, 所以导致就找不到add和subtract两个函数的实现了.

    这里可以通过修改CTest项目中包含的Dll1.h头文件,显示的指定两个从Dll1.dll中导入函数的调用约定为_stdcall, 如下:
    5

    重新编译运行可正常调用:
    6

    这也能算作一种在C编译器下不同调用约定导致的名字改编问题 的解决方法! 即在动态链接库的头文件中显示的指定函数的调用约定,前提是你知道dll中实现时的调用约定, 因为编写DLL1项目时,声明和实现时的调用约定必须一致!

    因为这里DLL1是我们自己写的,可以知道是__stdcall调用约定,但实际开发中DLL可能由其他渠道获得,未必知道其调用约定.

    当然这里可以使用dumpbin命令查看导出函数,根据其名字改编规则去推断是何种调用约定.

    还有一种笨方法就是在.h头文件中一个一个调用约定去试,反正就那么几个,总有一个对的上!

    这里的问题是,通过C编译生成的dll,不同调用约定导致的名字改编,在C项目中竟然能通过原始函数名调用!

    这里也可以通过dumpbin –imports CTest.exe命令查看输出的CTest.exe从其他dll中导入的导入函数, 提取的挂机部分如下:
    7
    找到从咱们自己写的Dll1.dll中导入的函数,发现都是有发生不同调用约定导致的名字改编的,说明确实是能通过原始函数名调用发生名字改编后的函数的.

    这里尝试在vs2010的C++项目和MFC项目中,vs2013的C项目、C++项目以及MFC项目中, 使用同一dll1.dll实验是否也是这样.

    因篇幅过长,点击跳转测试案例动态链接库–dll使用示例, 更新到vs2010的C项目、C++项目、MFC项目,更新到vs2013的C项目、C++项目、MFC项目中, 均能通过原始函数名正常调用.

    示例中也有证明, 这是因为在CTest项目包含的Dll1.h头文件中,显式的指明了C编译和__stdcall调用约定, 因此通过原始函数名调用时就会以 C编译__stdcall调用约定的改编规则名_add@8 和 _subtract@8 去dll查找对应实现.

    在导出函数声明实现中显示指定调用约定

    这里先将DLL1项目的默认调用约定切换回之前的__cdecl, 如下:
    8

    然后修改DLL1项目代码如下:

    //Dll1.h
    #ifdef DLL1_API
    #else
    #define DLL1_API __declspec(dllimport) 
    #endif
    
    #ifdef __cplusplus
    extern "C"
    {
    #endif
    	
    	int DLL1_API __stdcall add(int a, int b);
    
    	int DLL1_API __stdcall subtract(int a, int b);
    
    #ifdef __cplusplus
    };
    #endif
    
    //Dll.cpp
    #define DLL1_API __declspec(dllexport)
    #include "Dll1.h"
    
    int __stdcall add(int a, int b)
    {
    	return a + b;
    }
    
    int  __stdcall subtract(int a, int b)
    {
    	return a - b;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32

    重新生成后,使用dumpbin命令查看依旧还是C编译下__stdcall调用约定的改编规则名:
    9

    更新lib,dll以及.h到C项目中,CTest无需变动代码, 上面的示例专场已经证明了是能正常调用的:
    10

    更新到C++项目同理也能够正常调用:
    11

    更新到MFC项目中同理依旧能正常调用:
    12
    13

    通用解决方法

    以上的示例中,DLL的导出函数的调用约定 不同于 使用导出函数项目的调用约定,因此导出函数会发生名字改编,但依旧能通过原始函数名调用发生名字改编后的函数, 这是因为在CTest项目包含的Dll1.h头文件中,显式的指明了C编译和__stdcall调用约定, 因此通过原始函数名调用时就会以 C编译__stdcall调用约定的改编规则名_add@8 和 _subtract@8 去dll查找对应实现.

    若要想不发生名字改编,即 即使调用约定不同,也不要发生名字改编,这里可以通过模块定义文件来实现.

    模块定义文件用于链接过程,为链接器提供有关链接程序的导出符号、属性以及其他信息.

    因此使用模块定义文件(.def) 可以解决相同编译器(这里C编译器,下面也会有C++编译器部分的内容)下不同调用约定导致的名字改编问题.

    这里修改DLL1项目,为DLL1项目添加一个模块定义文件(.def), 右键项目 –》 添加新项 –》选择模块定义文件(.def), 如下:
    14
    注意:一个DLL项目只能有一个模块定义文件,因此命名最好和项目名一致!

    这里尝试再添加模块定义文件会有以下提示:
    15

    打开Dll1.def,如下:
    16

    其中LIBRARY用来指定动态链接库的内部名字,这句不是必须的,这里不再展开讨论。

    EXPORTS的作用是表明DLL将要导出的函数,以及为这些导出函数指定符号名。

    这里在该项下添加以下内容:

    //Dll1.def
    LIBRARY
    EXPORTS
    add
    subtract
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    当链接器在链接时,会分析这个DEF文件,当发现在EXPORTS语句下面有add和subtract这两个符号名,并且他们与源文件中定义的add和subtract函数的名字是一样的时候,它就会以add和subtract这两个符号名导出相应的函数.

    EXPORTS中有没有实现的导出函数的符号时,编译DLL项目时就会报错提示.

    重新生成DLL1,使用dumpbin命令查看:
    17
    可以发现导出的函数名即为模块定义文件中指定的符号名称.

    注意这里我们在Dll1.h 中也有指定导出声明__declspec(dllexport) ,但导出的依旧是以模块定义文件中指定的导出符号为准.

    使用模块定义文件后可省略声明中的导出声明:
    18

    重新生成DLL1项目,使用dumpbin命令查看,结果同没有省略时一样:
    19

    将使用模块定义文件生成的Dll1.dll更新配置到CTest项目中,编译运行能正常调用(有发生名字改编都能正常调用,这里没有发生名字改编当然能正常调用), 这里主要看一下输出的CTest.exe从Dll1.dll中导入的函数:
    20
    可以看到从Dll1.dll中导入的函数已经是模块定义文件中指定的没有发生名字改编的了.

    解决C++环境下不同调用约定导致的名字改编问题

    这里模拟DLL项目和使用DLL的项目都是C++编译器编译,但DLL项目的导出函数使用WINAPI调用约定__stdcall, 而使用DLL的项目使用默认的C调用约定__cdecl.

    修改DLL1项目,首先在项目属性中将默认的调用约定换回__cdecl, 后续直接在导出函数的声明实现前显示的添加__stdcall调用约定声明.
    21

    为演示问题,这里再将此前模块定义文件中EXPORTS项下的导出符号注释掉,通过添加英文的分号(;)注释:
    22

    然后修改Dll1.h如下:

    //Dll1.h
    #ifdef DLL1_API
    #else
    #define DLL1_API __declspec(dllimport) 
    #endif
    
    int DLL1_API __stdcall add(int a, int b);
    	
    int DLL1_API __stdcall subtract(int a, int b);
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    修改Dll1.cpp如下:

    //Dll.cpp
    #define DLL1_API __declspec(dllexport)
    #include "Dll1.h"
    
    int __stdcall add(int a, int b)
    {
    	return a + b;
    }
    
    int  __stdcall subtract(int a, int b)
    {
    	return a - b;
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    重新编译生成Dll1.dll, 使用dumpbin命令查看, 符合C++编译器下__stdcall调用约定的名字改编规则:
    调用约定的名字改编的规则依旧同 动态链接库(扩展)–调用约定 中介绍的C++编译下__stdcall调用约定的改编规则一致.
    23

    更新动态库到C项目CTest中,编译会有以下提示:
    24

    这里也有放在vs2013的C项目中测试,也是相同的编译报错:
    25

    发现虽然是C++编译生成的Dll1.dll,但在vs2010和vs2013的C项目中引用时还是会以Dll1.h声明中指定的 C编译、__stdcall调用约定(声明中没有指定则会以使用DLL的项目的编译方式C和默认调用约定__cdecl)的改编规则的名字_add@8 和_subtract@8 去引用的,即在项目中调用从dll中导入的函数,会以该项目包含的DLL.h头文件中指定的编译环境、调用约定对应的改编规则名字去dll中查找实现.

    因为这里dll中导出的函数名是按照 C++编译环境__stdcall调用约定改编规则改编成?add@@YGHHH@Z 和 ?subtract@@YGHHH@Z ,又因为在C项目CTest包含的Dll1.h中没有指定extern”C”限定符,所以这里在C项目中以 C编译环境__stdcall调用约定改编规则名**_add@8 和 _subtract@8** 去dll中查找的,所以才找不到,报错无法解析的符号.

    通过上面的C编译__stdcall生成的例子也可以验证,前面例子Dll1.h中都有显示的指定extern “C” 限定符和__stdcall调用约定,因此在使用Dll1.h的C、C++以及MFC项目中调用add和subtract函数时,都会以 C编译__stdcall调用约定 的改编规则名**_add@8 和 _subtract@8** 去dll中查找,这就解释了通过原始函数名调用时为什么能正常调用了!

    那么解决名字改编问题,是否可以概括成:想办法知道我们的项目中使用的从dll导入的函数的编译环境和调用约定, 然后在项目中包含的dll.h头文件中的函数声明中显示的添加编译声明和调用约定声明呢?.

    这种概括是片面的, 例这种方式没法在这样上面那种场景下使用,即使用C++编译__stdcall 生成的Dll1.dll, 放在C项目中使用,因为extern "C"是C++引入的关键字,因此这里就没法在C项目包含的Dll1.h中添加extern “C” 或 extern “C++” 限定符.

    你可能没见过使用**extern “C++”**的语句,因为extern “C” 或 extern “C++” 只能在C++下使用,而C++下已经是默认的C++编译了,再加上extern “C++” 岂不是脱裤子放屁嘛…

    ps: 在C++项目中使用extern “C++”语句是可以通过编译的,msdn说明如下:
    26

    这里想到的解决方法有两个:
    ① 因为没法在C项目包含的Dll1.h中添加extern “C”限定符,所以只得重新修改Dll1项目,添加extern “C”限定符使用C编译。这里就是 动态链接库(六)–解决不同编译器导致的名字改编问题 中提到的方式.

    使用模块定义文件自定义导出函数符号,以解决不同调用约定导致的名字改编问题.
    修改DLL1项目中的Dll1.def,取消之前的注释:

    ;Dll1.def
    LIBRARY
    
    EXPORTS
    add
    subtract
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    重新编译生成,使用dumpbin 命令查看:
    27

    更新到C项目CTest中,编译依旧有错误提示:
    28
    可以看到DLL1项目的Dll1.h 头文件中的声明没有指定extern “C++”,当然在C项目中也没有extern “C++”限定符,因此在main.c中调用add和subtract时,会默认以C编译__stdcall 调用约定改编规则名_add@8 和 _subtract@8 去dll中查找,但dll中的导出函数的名字为模块定义文件中指定的add 和 subtract,所以这里才编译失败!

    这里编译方式不同(DLL项目为C++, 使用DLL的项目为C),因此需先解决不同编译方式的名字改编问题—步骤①.

    调用约定不同(DLL项目为__stdcall, 使用DLL的项目为__cdecl),因此还需解决不同调用约定导致的名字改编问题——步骤②.

    本以为只需一步就能解决,现在看来必须通过对应方式解决对应问题.

    通过extern “C” 限定符解决C 和 C++编译环境之间的名字改编问题, 而模块定义文件则用来解决不同调用约定导致的名字改编问题.

    重新修改DLL1项目,在Dll1.h 中条件编译添加extern “C”限定符, 如下:

    //Dll1.h
    #ifdef DLL1_API
    #else
    #define DLL1_API __declspec(dllimport) 
    #endif
    
    #ifdef __cplusplus
    extern "C"
    {
    #endif
    
    	int DLL1_API __stdcall add(int a, int b);
    
    	int DLL1_API __stdcall subtract(int a, int b);
    
    #ifdef __cplusplus
    };
    #endif
    
    
    //Dll.cpp
    #define DLL1_API __declspec(dllexport)
    #include "Dll1.h"
    
    int __stdcall add(int a, int b)
    {
    	return a + b;
    }
    
    int  __stdcall subtract(int a, int b)
    {
    	return a - b;
    }
    
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35

    修改Dll1.def如下:

    ;Dll1.def
    LIBRARY
    
    EXPORTS
    add
    subtract
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    重新编译生成,使用dumpbin命令查看:
    29

    更新到C项目CTest中,可正常编译运行:
    30

    总结

    综上,我们可以指定使用extern “C” 限定符解决不同编译器导致的名字改编问题使用模块定义文件解决不同调用约定导致的名字改编问题.

    注意:
    extern “C” 只能解决不同编译器导致的名字改编问题,而不能解决不同调用约定导致的名字改编问题.

    同理,模块定义文件只能解决不同调用约定导致的名字改编问题,而不能解决不同编译器导致的名字改编问题.

    若使用Dll的项目的编译环境和调用约定均和DLL导出函数的不同,两种方式需组合使用.

    此外,我们还了解了在项目中调用DLL头文件声明的函数时会以.h头文件中指定的编译方式和调用约定对应的改编规则名去dll中查找函数实现。若.h头文件中没有指定则会以 当前项目的编译方式和默认调用约定 对应的改编规则名去dll中查找函数实现.

  • 相关阅读:
    谷粒学苑_第十天
    Redis:内存淘汰机制
    进制原理
    mysql数据库基础
    力扣5. 最长回文子串(双指针、动态规划)
    scratch猫捉老鼠 电子学会图形化编程scratch等级考试一级真题和答案解析2022年9月
    ArcPy图斑编号-根据字段长度自动补齐
    22.11.8打卡 Codeforces Round #831 (Div. 1 + Div. 2) A~C
    c++核心准则
    GIT 基础命令使用
  • 原文地址:https://blog.csdn.net/SNAKEpc12138/article/details/126273499