DLL的导出函数

      我们在编写动态库时,经常会用到类似extern “C”, __declspec(dllexport)的修饰符,以及有时候还会有def文件,__declspec(dllexport)和def文件是生成导出函数的两种方式,选用不同的导出方式对dll中的导出函数有什么影响,以及extern “C”的使用与否有什么区别,本文对上述限定符分别进行验证。
      使用vs2017生成一个默认的dll工程,新建一个.h文件,加入以下代码:

bool __stdcall Test(int a, int b)
	{
		std::cout << __FUNCTION__ << std::endl;
		return true;
	}

      编译该dll工程,生成了Dll1.dll,使用Dependency Walker打开会发现该dll没有导出函数:
在这里插入图片描述

一,__declspec(dllexport)

      给Test函数加上__declspec(dllexport)修饰符,那么它就是一个导出函数了,编译dll工程,使用Dependency Walker打开Dll1.dll,此时有一个导出函数,但是函数名却不是代码中的Test,而是一个加了很多字符的新函数:?Test@@YG_NHH@Z:
在这里插入图片描述
      这是由于我们的导出函数是__stdcall的调用约定,?表示一个函数的开始,函数名后面以"@@YG"标识参数表的开始,后跟参数表,比如H表示一个int类型,具体的细节可以去查阅资料,本文重点不在这。
      我们再给导出函数加上一个extern “C”的限定,打开编译后的Dll1.dll:
在这里插入图片描述
      此时的导出函数为_Test@8,如果大家熟悉c语言的dll导出函数命名规则的话,就知道这是c语言的命名规范,@8表示参数占8个字节。也就是说extern "C"会以c命名规范来修改我们的导出函数名(准确来说,extern “C” 是告诉编译器,让它按C的方式编译),它只能用于导出全局函数这种情况 而不能导出一个类的成员函数。

二,.DEF模块定义文件

      去掉Test函数前的__declspec(dllexport)修饰符,新建一个Dll1.def文件
在这里插入图片描述
      在def文件中加入以下内容:

LIBRARY
EXPORTS
    		; Explicit exports can go here
		Test

      其中Test就是我们的导出函数,编译工程,打开生成的Dll1.dll:
在这里插入图片描述
      终于看到了熟悉的Test函数了,先不要激动,我们再来给Test加上extern “C”试试,打开编译后的Dll1.dll,可以发现使用def文件声明导出函数生成的dll中导出函数符号与代码中函数名完全一致,且加不加extern “C”修饰都是一样的。

三,__declspec(dllexport)和def文件的区别

      动态库的加载使用有两种方式:显示调用和隐式调用。显示调用指的是使用LoadLibrary将dll加载到我们进程空间内,然后再调用GetProcAddress获取指定名称的导出函数的地址,转换为我们声明的对应参数和返回值的指针,调用这个指针,则完成dll中导出函数的使用;隐式调用指的是依赖于.h文件,和.lib文件,此处的.lib中保存的是dll的导出函数符号表,并不保存具体的代码实现,编译时编译器会根据lib自动找到对应导出函数的声明,完成编译,然后在运行时,再动态加载lib对应的dll文件,很明显,lib中保存了对应dll的名称,用notepad++打开Dll1.lib可以验证:
在这里插入图片描述

1,显式调用

1,def方式
      新建一个win32控制台工程,测试代码如下:

#include <iostream>
#include <windows.h>

typedef bool(__stdcall *Fun)(int a, int b);

int main()
{
	HMODULE hLib = LoadLibraryA("Dll1.dll");
	if (nullptr == hLib)
	{
		std::cout << "LoadLibraryA fail, error:" << GetLastError() << std::endl;
		return 0;
	}

	Fun fun = (Fun)GetProcAddress(hLib, "Test");
	if (nullptr == fun)
	{
		std::cout << "GetProcAddress fail, error:" << GetLastError() << std::endl;
		return 0;
	}

	fun(1, 2);

    std::cout << "Hello World!\n"; 
	getchar();
}

      输出结果如下:
在这里插入图片描述
      很明显,使用def方式的导出函数可以显示加载并调用。

2,__declspec(dllexport)方式
      此处我们先不加extern “C”,使用同样的测试代码,输出如下:
在这里插入图片描述
      报错127,找不到指定的函数。然后我们再加上extern “C”,依旧还是报错127,大家可以自己验证。

      那么通过__declspec(dllexport)修饰过的导出函数是不能通过LoadLibrary来显示加载调用嘛,我们从两个方面再来验证这个问题:

第一种尝试:
      修改GetProcAddress的导出函数名,根据上面我们使用Dependency Walker查看的__declspec(dllexport)的非extern “C”模式的导出函数名为:?Test@@YG_NHH@Z,我们将代码改为:

Fun fun = (Fun)GetProcAddress(hLib, "?Test@@YG_NHH@Z");

显示加载调用成功。
在这里插入图片描述在这里插入图片描述
第二种尝试:
在dll工程中加入代码:

#pragma comment(linker, "/EXPORT:Test=?Test@@YG_NHH@Z")

这时我们再查看dll的导出函数,就会发现函数名字表中已经有了我们想要的Test。
在这里插入图片描述      但我们发现原来的那个 ?Test@@YG_NHH@Z 函数还在,这时就可以把 __declspec() 修饰去掉,只需要 pragma 指令即可。这样导出函数表中就只有一个Test函数了。
由此可见,在不修改原始dll代码的基础上,通过__declspec(dllexport)方式实现导出函数的dll库,也是可以通过LoadLibrary来显示加载调用的。

2,隐式调用

      通过.h和.lib加载并使用dll中的导出函数,__declspec(dllexport)和def没有区别,此处就不再演示,大家可以自己验证。(前提是工程中配置了生成导出符号文件.lib)

四,结论

      根据上述测试代码验证,我们可以得到以下结论:
1,dll要实现导出函数有两种方式:使用__declspec(dllexport)修饰导出函数,或者新增def文件加入导出函数名。

2,使用def文件生成的dll导出函数,加不加extern “C”都没区别,最终符号表中的符号就是代码中的函数名,可以直接用LoadLibrary来进行显示调用。

3,使用__declspec(dllexport)修饰的导出函数,不能直接使用LoadLibrary来显示调用,必须要结合lib来隐式调用,这是因为编译器帮我们进行了导出函数名转换这一步骤,我们也可以使用以下命令来进行手动转换(在dll中使用),此时可以不再需要__declspec(dllexport)。ps:可是去掉了又如何知道实际的导出函数名呢,这个用法比较尴尬 —_—

#pragma comment(linker, "/EXPORT:Test =?Test@@YG_NHH@Z ")

4,extern “C” 声明只对根据c++规则进行修改的导出函数才有用,并将该导出函数按照c的规则进行重命名,比如?Test@@YG_NHH@Z =》_Test@8。没有修改修改命名的导出函数,加不加extern “C” 没区别,比如def模块定义文件实现的导出函数。

5,如果要导出C++文件中的函数,并且不让编译器改动函数名,用def文件导出函数。

备注:如果大家感兴趣的话,可以自行验证一下c语言的dll导出函数。

发布了78 篇原创文章 · 获赞 79 · 访问量 6万+

猜你喜欢

转载自blog.csdn.net/bajianxiaofendui/article/details/95509941