动态链接库(Dynamic Link Library)学习笔记

动态链接( Dynamic Linking )是相对于静态链接( Static Linking )而言的。程序设计中,为了能做到代码和模块的重用,程序设计者常常将常用的功能函数做成库,当程序需要实现某种功能时,就直接调用库文件中的函数,从而实现了代码的重用。早期的程序设计中,可重用的函数模块以编译好的二进制代码形式放于静态库文件中,在 MS 的操作系统中是 Lib 为后缀的文件。程序编写时,如果用户程序调用到了静态库文件中的函数,则在程序编译时,编译器会自动将相关函数的二进制代码从静态库文件中复制到用户目标程序,与目标程序一起编译成可执行文件。这样做的确在编码阶段实现了代码的重用,减轻了程序设计者的负担,但并未在执行期实现 重用。如一个程序 a.exe 使用了静态库中的 f() 函数,那么当 a.exe 有多个实例运行时,内存中实际上存在了多份 f() 的拷贝,造成了内存的浪费。

        随着技术的进步,出现了新的链接方式,即动态链接,从根本上解决了静态链接方式带来的问题。动态链接的处理方式与静态链接很 相似,同样是将可重用代码放在一个单独的库文件中(在 MS 的操作系统中是以 dll 为后缀的文件, Linux 下也有动态链接库,被称为 Shared Object so 文件) , 所 不同的是编译器在编译调用了动态链接库的程序时并不将库文件中的函数执行体复制到可执行文件中,而是只在可执行文件中保留一个函数调用的标记。当程序运行 时,才由操作系统将动态链接库文件一并加载入内存,并映射到程序的地址空间中,这样就保证了程序能够正常调用到库文件中的函数。同时操作系统保证当程序有 多个实例运行时,动态链接库也只有一份拷贝在内存中,也就是说动态链接库是在运行期共享的。

        使 用动态链接方式带来了几大好处:首先是动态链接库和用户程序可以分开编写,这里的分开即可以指时间和空间的分开,也可以指开发语言的分开,这样就降低了程 序的耦合度;其次由于动态链接独特的编译方式和运行方式,使得目标程序本身体积比静态链接时小,同时运行期又是共享动态链库,所以节省了磁盘存储空间和运 行内存空间;最后一个是增加了程序的灵活性,可以实现诸如插件机制等功能。用过 winamp 的人都知道,它的 多功能都是以插件的形式提供的,这些插件就是一些动态链接库,主程序事先规定好了调用接口,只要是按照规定的调用接口写的插件,都能被 winamp 调用。

        WIndow 95 98 NT 系列等系统都提供了动态链接库的功能,并且这些操作系统的系统调用大多都是通过动态链接库实现 的,最常见的 NT 系列 OS 中的 KENEL32.dll,USER32.dll,GDI32.dll 等动态链接库文件就包含了大量的系统调用。在 windows 家族中, NT 内核的操作系统在动态链接库机制 上较之前的 95 98 系统要更安全。 95 98 系统在程序调用动态链接库时,将动态链接库加载到 2G -3G 之间的被称为进程共享空间的虚拟地址空间,并且所有进程关于这 1G 的虚拟地址空间的页表都是相同的,也就是说对于所有的进程,这片共享区的页表都指向同一组物理页,这样一来,加载入内存的 动态链接库对所有 正在运行的进程都是可见的。如果一个动态链接库被其中一个进程更改,或其自身崩溃,将影响到所有调用它的进程,如果该动态链接库是系统的动态链接库,那么将导致系统的崩溃。在 Windows NT 系统中,动态链接库被映射到进程 的用户地址空间中,并用 Copy On Write 机制保证动态链接库的共享安全, Copy On Write 可 以理解为写时拷贝。一般情况下,多个运行的进程还是按原来的模式共享同一个动态链接库,直到有进程需要向动态链接库的某个页面写数据时,系统将该页做一个 拷贝,并将新复制页面的属性置为可读可写,最后修改进程的页表使之指向新拷贝的物理页。这样无论该进程怎么修改此页的数据,也不会影响到其他调用了此动态 链接库的进程了。



Windows
下动态链接库的编写

 

         因为本人对 linux 没有太多研究,所以这里只介绍 windwos 环境下动态链接库的编写。

         VC 中新建一个空的 Win32 动态链接库工程 (Win32 Domanic Library ), 然后添加一个 C++ Sourse File 到工程,我这里的文件名取 DllTest.cpp 。然后在文件中添加如下内容:

 //DllTest.cpp

 

 _declspec ( dllexport ) int add(int a,int b)
 {
  return a+b ;
 }

 

 _declspec ( dllexport ) int subtract(int a,int b)
 {
  return a-b;
 }

        接下来编译链接,就会在 debug 目录下生成一个调试版本的动态链接库,该链接库包含了 add subtract 两个可供外部调用的函数。我们注意到,在源文件中多了一个没有见过的语句  _declspec (dllexport ) ,这个语句的作用就是向编译器指出我需要在生成的动态链接库中导出的函数,没有导出的函数是不能被其他程序调用的。要知道一个动态链接库导出了什么函数,可以在命令提示行用命令 "dumpbin -exports DllTest.dll" 来查看 ( 也可以用 VC 工具包中的 depends 使用程序来查看 ) 。以下是用 dumpbin 命令查看 DllTest.dll 而生成的信息:

 

Dump of file DllTest.dll

 

File Type: DLL

 

  Section contains the following exports for DllTest.dll

 

           0 characteristics
    4420BEA4 time date stamp Wed Mar 22 11:04:04 2006
        0.00 version
           1 ordinal base
           2 number of functions
           2 number of names

 

    ordinal hint RVA      name

 

          1    0 0000100A ? add@@YAHHH@Z
          2    1 00001005 ?subtract@@YAHHH@Z

 

  Summary

 

        7000 .data
        1000 .idata
        3000 .rdata
        2000 .reloc
       2A000 .text

 

        可以看到,我们编写的动态链接库导出了两个函数,分别名为 ?add@@YAHHH@Z   ?subtract@@YAHHH@Z ,为什么名字不是 add subtract 呢?这是因为 C++ 为了支持函数的重载,会在编译时将函数的参数类型信息以及返回值类型 信息加入到函数名中,这样代码中名字一样的重载函数,在经过编译后就互相区分开了,调用时函数名也经过同样的处理,就能找到对应的函数了。编译器对函数的重命名规则是与调用方式相关的,在这里采用的是 C++ 的默认调用方式。以此对应的还有 stdcall 方式、 cdecl 方式、 fastcall 方式和 thiscall 方式,不同调用方式的重命名规则不一样。

        需要特别说一下的是 stdcall 方式和 cdecl 方式:

        stdcall 方式(标准调用方式)也即 pascal 调用方式,它的重命名规则是函数名自动加前导的下划线,后面紧跟一个 @ 符号,其后紧跟着参数所占字节数,之所以要跟参数字节数,是因为 stdcall 采用被调函数平衡 堆栈方式,用函数名最后 的数字告诉编译器需要为函数平衡的字节数。例如,如果我们的 DllTest.dll 采用 stdcall 方式编译的话,导出的函数名将会是 _add@8 _subtract@8 ,而函数编译后的汇编代码最后一句一定是 ret8

        cdecl 方式即 C 语言调用方式,它的重命名规则仅仅是在函数名前加下划线(奇怪的是我用 vc6 编译的 c 语言函数,名字没有任何改变),因为 C 语言采用的是调用函数平衡堆栈的方式,所以不需要在函数名中加入参数所占的字节数,这样的堆栈平衡方式也使 C 语言可以编写出参数不固定的函数;同时 C 语言不支持函数重载,因此不需要在函数名中加入参数类型信息和返回值类型 信息。

        更多关于调用方式的介绍请看我收藏的文章 C语言函数调用约定

        动态链接库已经 生成了,接下来就是调用的工作了。调用动态链接库有两种方式:隐式调用和显式调用,下面我们分别来看两种调用方式的具体过程:

 

动态链接库的隐式调用

 

        新建一个空的 Win32 Console Application ,命名为 DllCaller ,向工程中添加名为 DllCaller.cpp C++ Sourse File, 在文件中写入如下代码:

 

#include <iostream >
using namespace std ;

 

//extern int add( int a,int b);
_declspec (dllimport ) int add(int a,int b);

 

int main()
{
         cout <<"3+5="<<add(3,5)<<endl ;

         return 1;
}

 

        编译,没有错误,链接,有两个错误:找不到外部引用符号。要怎样才能让我们的程序找到动态连接库中的函数呢?这里是关键的一步。到刚才的 DllTest 工程目录下,从 debug 文件夹中拷贝生成的 DllTest.dll 文件和 DllTest.lib 文件到 DllCaller 工程目录。然后依次在 vc 中选择菜单: Project -->Settings-->Liink , Object/library Modules 中加入一项文件名: DllTest.lib , 这里的 DllTest.lib 并不是静态库文件,而是 DllTest.dll 的导入库文件,它包含了 DllTest.dll 动态链接库导出的函数信息,只有在工程链接设置里 添加了该文件,才能够使调用了该动态链接库的工程正确链接。完成以上步骤后,我们再编译链接工程,这次没有任何错误!程序可以顺利调用动态连接库文件,正常运行了(为了能使程序找到并加载需要的动态链接库,动态链接库文件必须与调用程序在同一个目录下,或在 path 环境变量指定的目录下)。

        这里需要说明一点,工程中的源文件在调用动态链接库中的函数时,需要提前声明,声名有两种方式,一种是传统的 extern 方式,一种是 _declspec (dllimport ) 方式,这两种方式在代码中我都给出了。其中,第二种方式能使编译过程更快,所以推荐使用。

 

动态链接库的显式调用

 

        比起隐式调用,显示调用更加灵活,而且在编译链接时不需要 lib 导入库文件,也不需要提前声明函数。我们通过 windows 提供的 API 函数来动态加载动态连接库并调用 其中的函数,用完后可以马上释放内存中的动态链接库,十分方便。下面就是显示调用动态链接库的代码:

 

#include <iostream >
#include <windows.h >
using namespace std ;

 

int main()
{
         HINSTANCE hInstance =LoadLibrary ("DllTest.dll");
         typedef int (*AddProc )(int,int );
         AddProc Add=(AddProc )GetProcAddress (hInstance,?add@@YAHHH@Z );
         if(!Add)
         {
                  cout <<"
动态连接库库函数未找到 "<<endl ;
                  return 0;
         }

         cout <<"3+5="<<Add(3,5)<<endl ;

         FreeLibrary ( hInstance );

         return 1;
}

        以上代码并不复杂,首先定义一个实例句柄用来引用由 Windows API 函数 LoadLibrary 加载的动态链接库, LoadLibrary 函数的参数是一个字符串指针,具体调用时我们需要填入需要加载的动态链接库的位置及文件名,加载成功后返回一个实例句柄。接下来我们定义一个函数指针类型,用该类型声明一个函数指针,用来存储 GetProcAddress 函数返回的动态库函数入口地址。 GetProcAddress 能从指定的动态库中查找指定名字的函数,如果查找成功则返回该函数的入口地址,如果失败则返回 NULL 。更多 GetProcAddress 函数的用法请参看 MSDN 。有人可能注意到, GetProcAddress 函数中指定的函数名并不是 add, 而是 ?add@@YAHHH@Z 。这里就和前面将的函数调用方式联系起来了,在 GetProcAddress 函数中,我们指定的函数名必须是编译后经过重命名的函数名,而不是源文件中定义的函数名。这样实际上给我们的调用带来了相当大的麻烦,因为我们不可能去了解每一个经过重命名的导出函数名。好在微软已经给出了解决方法,那就是在编写动态链接库时同时编写一个以 def 为后缀的编译命名参考文件,如果动态链接库工程中有该文件,则编译器会根据该文件指定的函数名来导出动态库函数,关于 def 文件的详细使用方法请参考 MSDN ,这里就不一 赘述。找到需要的动态库函数后,我们就可以按需要对它进行调用,之后调用 FreeLibrary 函数释放动态库。因为动态库是多进程 共享的,因此调用 FreeLibrary 函数并不意味着动态库在内存中被释放,每个动态库都有一个变量用来记录它的共享引用计数,而 FreeLibrary 的功能只是将这个记数减 ,只有当一个动态库的引用计数为 0 时,它才会被操作系统释放。

       

 

隐式调用与显式调用的对比

 

         前 面已经详细介绍了动态链接库的两种调用方法,相比之下,隐式调用在编程时比较简单,指定导入库文件后,不必考虑函数的重命名,就可以直接调用动态库函数。 但由于隐式调用不能指定动态库的加载时机,因此在一个程序开始运行时,操作系统会将该程序需要的动态链接库都加载入内存,势必造成程序初始化的时间过长, 影响用户体验。而显式调用采用动态加载的方法,用到什么加载什么,用完即释放,灵活性较高,可以使程序得到优化。具体运用中到底采用哪种方法,还要依实际 情况而定。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值