链接库动态链接库详细介绍

2016-02-19 11:50 28 1 收藏

图老师设计创意栏目是一个分享最好最实用的教程的社区,我们拥有最用心的各种教程,今天就给大家分享链接库动态链接库详细介绍的教程,热爱PS的朋友们快点看过来吧!

【 tulaoshi.com - 编程语言 】

windows中,链接库分为两种类型:静态链接库.lib和动态链接库.dll。其中动态链接库在被使用的时候,通常还提供一个.lib,称为引入库,它主要提供被Dll导出的函数和符号名称,使得链接的时候能够找到dll中对应的函数映射。
静态链接库和动态链接库的作用相似,都是提供给其他程序进行调用的资源。其中,动态链接库的调用方法分隐式调用(静态导入调用)和显示调用(动态导入调用)。 

编译环境
Microsoft Visual Stdio 2010
--------------------------------------------------------------------------------
DLL导出符号
例,首先生成一个dll1.dll和dll1.lib
代码如下:

// DLL1工程,dll1.cpp
// _declspec(dllexport)为导出符号
_declspec(dllexport) int add(int a, int b)
{
return a + b;
}

利用微软的depends工具查看dll1.dll,导出的符号如下:
http://www.cnblogs.com/monotone/ 
其中各字段意义:Ordinal(符号序号,后面使用GetProcAddress的时候,参考的数值),Hint(这个我也不是太明白,据说是不用了解),Function(这个就是函数导出后的符号名称了),EntryPoint(这个是函数在DLL中的地址)。
这里之所以函数的名称变成了这样子,是因为使用的编译器默认使用C++方式进行编译,由于C++支持重载,那么需要给函数名增加额外的符号,来使与同名的重载函数区分开来,才能在DLL中通过符号名来进行定位。
这里可以做个简单的测试,新建控制台测试工程DllTest如下。
代码如下:

// DllTest工程,DllTest.cpp
#include iostream
using namespace std;
int main(void)
{
// extern int add(int a, int b);
// _declspec(dllimport)是导入声明,这种方式比上面的方式更有效,同时编译器能边编译出更加高效的代码。
_declspec(dllimport) int add(int a, int b);
cout add(1,2) endl;
getchar();
return 0;
}

编译链接,提示链接错误 error LNK2019: unresolved external symbol "__declspec(dllimport) int cdecl add(int,int)" (__imp_?add@@YAHHH@Z) referenced in function _main,很明显的编译器在编译的时候,把add函数也给重命名了,并且和上面用depends查看的一样。意思是没有找到这个符号的定义。

添加代码后如下:(注意,我这里两个工程的输出目录都是在和解决方案同目录的debug下,为了避免每次修改都重新拷贝lib文件,直接使用相对路径声明。)
代码如下:

// DllTest工程,DllTest.cpp
#include iostream
using namespace std;
#pragma comment(lib, "../debug/dll1.lib") // 显示的声明要链接dll1.lib,隐式调用
int main(void)
{
// extern int add(int a, int b);
// _declspec(dllimport)是导入声明,这种方式比上面的方式更有效,同时编译器能边编译出更加高效的代码。
_declspec(dllimport) int add(int a, int b);
cout add(1,2) endl;
getchar();
return 0;
}

编译运行后,使用depends工具对DllTest.exe查看其依赖的输入信息如下:
image 
可以看出,DllTest.exe通过dll1.lib,引入了对dll1.dll的依赖。

--------------------------------------------------------------------------------

DLL提供的头文件
通常情况下,当得到一个.dll的时候,我们无法得知其提供了哪些函数调用(准确来说,应该是调用方式。因为我们可以利用depends工具查看dll导出的函数及其序号,当然也许可能有其他的方式去知道具体怎么使用,但是肯定无法得知内部具体实现细节。),因此为了方便被使用,通常会提供一个对应该dll的.h文件,来声明其提供给客户端使用的方式和说明等信息。客户端使用该头文件对所使用的接口进行导入。但是为了避免很多地方都出现这些函数的声明,通常在客户端直接在.h文件中对所有接口进行导入,而在Dll编译时,则作为导出使用。方法如下:
代码如下:

// DLL1工程,dll1.h
#ifndef DLL1_API
#define DLL1_API _declspec(dllimport)
#endif
// 以上代码表示,如果在包含该头文件之前,没有定义DLL1_API宏,那么后面所有DLL1_API宏都展开为_declspec(dllimport),即导入。
// 因为通常情况下客户端不会去定义这个宏(当然,假设这个宏不会被客户端中其他文件定义),所以客户端使用该头文件的时候,都是用于导入。
DLL1_API int add(int a, int b);

代码如下:

// DLL1工程,dll1.cpp
#define DLL1_API _declspec(dllexport)
// 注意上面这行,在头文件被包含前,先定义了DLL1_API这个宏,使得头文件中DLL1_API都被展开为_declspec(dllexport)了,从而声明函数作为导出。
#include "dll1.h"
// 在头文件中进行了导出声明的函数,就不用再声明导出了。
int add(int a, int b)
{
return a + b;
}

相应的,TestDll工程中包含.h文件后,也不用再去申明了。
代码如下:

// DllTest工程,DllTest.cpp
#include iostream
using namespace std;
#include "../dll1/dll1.h" // 包含该头文件之后,后面就不需要再申明了
#pragma comment(lib, "../debug/dll1.lib") // 显示的声明要链接dll1.lib,隐式调用
int main(void)
{
cout add(1,2) endl;
getchar();
return 0;
}

以上基本解释了为什么通常引用dll的时候都有一个头文件,并且头文件内有很多#ifndef之类的东东了。

--------------------------------------------------------------------------------
动态链接库导出类
当然,动态链接库也能导出类,要注意的是声明的方式为class DLL1_API CSample,而不是DLL1_API class CSample。
同时,要注意导出类的同时,其所有成员函数也已经导出,但是仍然遵循类成员变量访问权限限制。
如果单独导出类的成员函数(声明方式和全局函数一样),那么在客户端可以实例化类对象,并调用导出的成员函数,不能调用没导出的成员函数(即使是public的)。

--------------------------------------------------------------------------------
改编了的符号名
在导出符号时,讲过C++会对函数名进行改编,以支持函数重载。那么就会存在一个问题,如果使用不用的C++编译器(导致编译出的符号名不同)或者客户端使用C编译器调用,就会出现LNK2019这样的链接错误,找不到符号。这个问题很大的限制了DLL的使用范围。
解决方法1:
使用extern “C”(注意这个C一定要大写)前置申明,表明函数是以C的方式编译链接的。C方式编译连接导出的函数不会改编符号名,因而可以避免上述问题。
代码如下:

// DLL1工程,dll1.h
#ifndef DLL1_API
#define DLL1_API extern "C" _declspec(dllimport)
#endif
// 以上代码表示,如果在包含该头文件之前,没有定义DLL1_API宏,那么后面所有DLL1_API宏都展开为_declspec(dllimport),即导入。
// 因为通常情况下客户端不会去定义这个宏(当然,假设这个宏不会被客户端中其他文件定义),所以客户端使用该头文件的时候,都是用于导入。
DLL1_API int add(int a, int b);
//class CSample
//{
//public:
// DLL1_API int substract(int a,int b);// 这种情况下,导出类成员函数,编译不能通过的
//};

这里要注意.h和.cpp中都要加上extern “C”,使得导入和导出都用C编译方式。
代码如下:

// DLL1工程,dll1.cpp
#define DLL1_API extern "C" _declspec(dllexport)
// 注意上面这行,在头文件被包含前,先定义了DLL1_API这个宏,使得头文件中DLL1_API都被展开为_declspec(dllexport)了,从而声明函数作为导出。
#include "dll1.h"
// 在头文件中进行了导出声明的函数,就不用再声明导出了。
int add(int a, int b)
{
return a + b;
}
//
//int CSample::substract(int a,int b)
//{
// return a - b;
//}

然后用depends进行查看:
image 
导出的函数符号名和函数声明时一样了。由于客户端使用的时候,导入也是用的extern “C”方式,因此客户端在编译链接的时候,也使用的是函数原名称符号。
显示,由于使用的是C编译链接方式,C++的类和成员函数的导出就不能用这种方式了。
此外,如果我们在给函数声明加上标准调用约定:DLL1_API int _stdcall add(int a, int b);(注意,在函数的定义中也要加上_stdcall)。那么编译出来的结果使用depends查看符号名为_add@8,也就是说符号名称又改了。
解决方法2:
使用模块定义文件.def,这种文件的格式规范查看MSDN,搜索.def即可。其中LIBRARY命令用于指名该def文件用于导出库文件,EXPORTS用于指名导出函数符号名。也就是说,.def文件主要就用于控制导出符号等信息。
代码如下:

LIBRARY dll1
EXPORTS
add11=add

我这里给add函数别名为add11,注意同时需要在.h文件中声明add为add11(作用就是提供给客户端使用,当然其实也可以直接在客户端声明函数为add11,前提是你知道该函数的定义方式等)。一旦提供了.def之后,.cpp中提供的任何调用约定都不再生效,因为.def指定了生成的符号名了。这里只要明白,.def控制了Dll的导出符号,客户端使用的时候,只要提供了声明,并且链接上.lib文件,就能够使用了。
补充一句:VC6.0以后的IDE都需要在链接选项(LINK)的input-》module define file中,指明.def文件,编译器才会去使用这个.def文件。

备注:
这一块关于调用约定,以及加不加extern “C”,目前我还比较混乱。等了解了之后,再来这里进行补充,欢迎看客给我提供有关这一块比较好的介绍资料。

--------------------------------------------------------------------------------
显示链接(动态导入链接)
前面有提到让导出的函数没有名字,那么客户端如何对其进行调用呢。就是使用符号表中的ordinal了(可以通过工具进行查看),当然也可以使用函数名进行导入。动态导入不需要lib文件和.h文件(如果知道函数名的话)。
代码如下:

// DllTest工程,DllTest.cpp
#include iostream
using namespace std;
#include windows.h
int main(void)
{
HMODULE hModule = ::LoadLibraryA("dll1.dll");
if(NULL != hModule)
{
typedef int (*ADDPROC)(int a, int b);
//ADDPROC add = (ADDPROC)::GetProcAddress(hModule, "add11"); // 通过函数名导入
ADDPROC add = (ADDPROC)::GetProcAddress(hModule, MAKEINTRESOURCEA(1)); // 通过ordinal导入,注意第二个参数此时数值要放在低字节位置,具体参见MSDN说明。
if(NULL != add)
cout add(1,1) endl;
::FreeLibrary(hModule);
}
getchar();
return 0;
}

值得一提的是,根据函数名称进行动态导入的时候,实际上也是根据符号名的,也就是说,dll提供的符号名要和提供给用户的函数声明一致。

此外,动态导入动态链接库时,.exe时无法查看到需要的输入信息的。

--------------------------------------------------------------------------------
后记
第一次写这么详细和这么长的博文,可以看出来写到后面的时候已经越来越粗糙了。因为写到后来的时候,自己都已经不知道要写什么了,因为如果再扩展下去,需要增加的东西就太多了。同时也有一部分原因是后面的内容我理解的不是太透彻,同时使用的也很少。总之这会我脑子已经相当混乱了。在这里对那些写博客的大侠们表示深深的敬佩。

最后要申明的是,这些都是我自己参考视频整理总结出来的,肯定有不专业甚至不对的地方,恳请提出指导,我在学习理解之后,会及时进行修改,谢谢。

来源:http://www.tulaoshi.com/n/20160219/1598774.html

延伸阅读
从前文可知,DLL在程序编制中可作出巨大贡献,它提供了具共性代码的复用能力。但是,正如一门高深的武学,若被掌握在正义之侠的手上,便可助其仗义江湖;但若被掌握在邪恶之徒的手上,则必然在江湖上掀起腥风血雨。DLL正是一种这样的武学。DLL一旦染上了魔性,就不再是正常的DLL程序,而是DLL木马,一种恶贯满盈的病毒,令特洛伊一夜之间国...
摘要 文中介绍了一种实现VB调用C语言程序的方法-动态连接库法,给出了动态链接库制作的一般框架,通过实例描述了动态连接库的实现及其制做方法、编程步骤,这种方法具有普遍的意义。 关键词:VB,动态连接库DLL,C程序 一、引言 VisualBasic(以下简称VB)是一种可视化的面向对象的编程语言,以其编程简单、快捷等特点,深受广...
3  利用DLLs实现数据传输 3.1 DLLs中的全局内存 Windows规定:DLLs并不拥有它打开的任何文件或它分配的任何全局内存块。这些对象由直接或间接调用DLLs的应用程序拥有。这样,当应用程序中止时,它拥有的打开的文件自动关闭,它拥有的全局内存块自动释放。这就意味着保存在DLLs全局变量中的文件和全局内存块...
由于我们经常要调用一些第三方厂商或其他编译器编写的动态链接库,但是一般都不提供源文件或.lib文件,而作为VC隐式链接到DLL (implicitly link to the DLL)调用,这些却是必需的。本文将主要讨论在没有源文件及.lib输入库文件或欲调用 Windows未公开函数的情况下重建.Lib文件的方法。在建立之前,我们首先要了解一下DLL输出函数的几种方式。...
方法一:windows提供了一套函数,用于加载动态链接库中的符号(函数和变量),调用这些函数去加载: 1. HINSTANCE LoadLibrary( LPCTSTR lpLibFileName); 2. FARPROC GetProcAddress( HMODULE hModule,  LPCWSTR lpProcName); 3. BOOL FreeLibrary(  HMODULE hLibModule); 这最直观的一种方法,同时也是最麻烦的一种办法。 方...

经验教程

995

收藏

19
微博分享 QQ分享 QQ空间 手机页面 收藏网站 回到头部