动态链接库DLL的加载:隐式加载(载入时加载)和显式加载(运行时加载)

来源:互联网 发布:氧气内衣 知乎 编辑:程序博客网 时间:2024/05/29 04:47
静态链接库在链接时,编译器会将 .obj 文件和 .LIB 文件组织成一个 .exe 文件,程序运行时,将全部数据加载到内存。


如果程序体积较大,功能较为复杂,那么加载到内存中的时间就会比较长,最直接的一个例子就是双击打开一个软件,要很久才能看到界面。这是静态链接库的一个弊端。


动态链接库有两种加载方式:隐式加载和显示加载。
隐式加载又叫载入时加载,指在主程序载入内存时搜索DLL,并将DLL载入内存。隐式加载也会有静态链接库的问题,如果程序稍大,加载时间就会过长,用户不能接受。
显式加载又叫运行时加载,指主程序在运行过程中需要DLL中的函数时再加载。显式加载是将较大的程序分开加载的,程序运行时只需要将主程序载入内存,软件打开速度快,用户体验好。

隐式加载

首先创建一个工程,命名为 cDemo,添加源文件 main.c,内容如下:

找到上节创建的 dllDemo 工程,将 debug 目录下的 dllDemo.lib 和 dllDemo.dll 复制到当前工程目录下。


前面已经说过:.lib 文件包含DLL导出的函数和变量的符号名,只是用来为链接程序提供必要的信息,以便在链接时找到函数或变量的入口地址;.dll 文件才包含实际的函数和数据。所以首先需要将 dllDemo.lib 引入到当前项目。


选择”工程(Project) -> 设置(Settings)“菜单,打开工程设置对话框,选择”链接(link)“选项卡,在”对象/库模块(Object/library modules)“编辑框中输入 dllDemo.lib,如下图所示:





但是这样引入 .lib 文件有一个缺点,就是将源码提供给其他用户编译时,也必须手动引入 .lib 文件,麻烦而且容易出错,所以最好是在源码中引入 .lib 文件,如下所示:
#pragma comment(lib, "dllDemo.lib")


更改上面的代码:

点击确定回到项目,编译、链接并运行,输出结果如下:
Congratulations! DLL is loaded!
a+b=15
a-b=5


在 main.c 中除了用 extern 关键字声明 add() 和 sub() 函数来自外部文件,还可以用 _declspec(dllimport) 标识符声明函数来自动态链接库。


为了更好的进行模块化设计,最好将 add() 和 sub() 函数的声明放在头文件中,整理后的代码如下:


dllDemo.h


main.c

显式加载

显式加载动态链接库时,需要用到 LoadLibrary() 函数,该函数的作用是将指定的可执行模块映射到调用进程的地址空间。LoadLibrary() 函数的原型声明如下所示:
HMODULE  LoadLibrary(LPCTSTR 1pFileName);


LoadLibrary() 函数不仅能够加载DLL(.dll),还可以加载可执行模块(.exe)。一般来说,当加载可执行模块时,主要是为了访问该模块内的一些资源,例如位图资源或图标资源等。LoadLibrary() 函数有一个字符串类型(LPCTSTR)的参数,该参数指定了可执行模块的名称,既可以是一个.dll文件,也可以是一个.exe文件。如果调用成功, LoadLibrary() 函数将返回所加载的那个模块的句柄。该函数的返回类型是HMODULE。 HMODULE类型和HINSTANCE类型可以通用。


当获取到动态链接库模块的句柄后,接下来就要想办法获取该动态链接库中导出函数的地址,这可以通过调用 GetProcAddress() 函数来实现。该函数用来获取DLL导出函数的 地址,其原型声明如下所示:
FARPROC  GetProcAddress(HMODULE hModule, LPCSTR 1pProcName);


可以看到,GetProcAddress函数有两个参数,其含义分别如下所述:
hModule:指定动态链接库模块的句柄,即 LoadLibrary() 函数的返回值。
1pProcName:字符串指针,表示DLL中函数的名字。


首先创建一个工程,命名为 cDemo,添加源文件 main.c,内容如下:

找到上节创建的 dllDemo 工程,将 debug 目录下的 dllDemo.dll 复制到当前工程目录下。注意,只需要 dllDemo.dll,不需要 dllDemo.lib。


运行程序,输出结果与上面相同。


HMODULE 类型、HINSTANCE 类型在 windows.h 中定义;LoadLibrary() 函数、GetProcAddress() 函数是Win32 API,也在 windows.h 中定义。


通过以上的例子,我们可以看到,隐式加载和显式加载这两种加载DLL的方式各有 优点,如果采用动态加载方式,那么可以在需要时才加载DLL,而隐式链接方式实现起来比较简单,在编写程序代码时就可以把链接工作做好,在程序中可以随时调用DLL导出的函数。但是,如果程序需要访问十多个DLL,如果都采用隐式链接方式加载它们的话, 那么在该程序启动时,这些DLL都需要被加载到内存中,并映射到调用进程的地址空间, 这样将加大程序的启动时间。而且,一般来说,在程序运行过程中只是在某个条件满足时才需要访问某个DLL中的某个函数,其他情况下都不需要访问这些DLL中的函数。但是这时所有的DLL都已经被加载到内存中,资源浪费是比较严重的。在这种情况下,就可以采用显式加载的方式访问DLL,在需要时才加载所需的DLL,也就是说,在需要时DLL才会被加载到内存中,并被映射到调用进程的地址空间中。有一点需要说明的是,实际上, 采用隐式链接方式访问DLL时,在程序启动时也是通过调用LoadLibrary() 函数加载该进程需要的动态链接库的。






来源:http://c.biancheng.net/cpp/html/2754.html
相关:http://c.biancheng.net/cpp/html/2753.html:第一个DLL程序:动态链接库DLL教程,30分钟快速上手

参考:

(1)、http://www.debugease.com/vc/2580715.html中:

简单的说,就在于隐式链接,编译器帮助你把LOADLIBRARY的动作做了,而且只是在进程退出时才FREELIBRARY.
而显式连接则是要用户在使用时,才自己去LOADLIBRARY.但它的灵活性好.可以在不用时就FREE掉.而且减少了编译时需要的一些文件,如H,LIB等.  
隐式链接就是用的lib和h文件

显示就直接装载dll文件

(上面这段话应该和http://bbs.csdn.net/topics/340184779八楼中所说的“隐式加载默认是加载到内存中的,始终占用内存;显示加载,你加载时占用内存,释放了就不占用内存了”相对应;)

(2)、http://www.fqyy.org/sunu/archives/912.html中:

Linux下的dlopen、dlsym、dlclose 相当于windows平台的LoadLibrary、GetProcAddress 、FreeLibrary,可以在运行时动态加载动态库,使用其中的导出函数。但是局限在于,这样仅仅能够导出全局函数,而不能导出类的方法。所以一般动态库导出C++类实现的功能时都会设计一大堆的全局函数来包装一下。

(3)、http://blog.csdn.net/lc_910927/article/details/42393121中:(linux下动态链接库(.so)的显式调用和隐式调用)

(4)、http://www.cnblogs.com/hcmfys/archive/2009/03/02/1401124.html:动态链接库的隐式连接与显式连接有什么不同

(5)、http://www.ibm.com/developerworks/cn/linux/l-dynamic-libraries/#list2:Linux 动态库剖析

(6)、http://www.cppblog.com/suiaiguo/archive/2009/07/21/90734.html(DLL入门浅析(5)——使用DLL在进程间共享数据)

(7)、http://m.oschina.net/blog/289272(动态链接库):
         延迟加载DLL:
         一个延迟加载的DLL是这样一个DLL,它被隐式链接,但实际上并没有加载,直到代码试图引用包含在DLL中的一个符号时才加载
延迟加载DLL有用的场合:
          如果应用程序使用了若干个DLL,则其初始化的时间可能会比较长,因为加载程序要将所有需要的DLL都映射到进程的地址空间中。缓解此问题的一个办法是在进程的执行过程中,分开加载DLL。延迟加载DLL可以更加方便的实现此方法
如果在代码中调用一个新函数,并且试图在不包含该函数的旧版本的系统上运行应用程序,则加载程序将报错,并且不允许程序继续执行。需要一种方法以允许应用程序执行,如果发现(在运行时)应用程序运行在一个旧版本系统上,则不用调用缺少的函数。

(8)、http://www.cppblog.com/kenny/archive/2011/04/16/144346.html中:
         二、下面详细介绍exe导入到执行的全过程,以及地址空间的加载。
          1)系统找到在调用CreateProcess时指定的exe文件。
          2)系统创建一个新进程的内核对象。
          3)系统为这个新进程创建一个私有的地址空间。
          4) 系统保留一个足够大的地址空间区域,用来存放exe文件。这个区域的位置在exe文件中设定。默认情况下,exe文件的基地址是0x0400000. (1.编译器处理每个源代码模块,生成obj文件。2.链接程序将所有obj模块的内容组合在一起,生成一个单独的可执行映射文件即exe,该映射文件包含用于可执行模块的所有二进制代码以及全局/静态数据变量,同时也包含一个导入部分,列出了该可执行模块所需要的所有dll模块的名字,对于每个列出的 dll名,该导入部分指明了那些函数和变量符号是被可执行的二进制代码所引用的)
          5)在将exe文件映射到进程的地址空间之后,系统会访问exe 文件中的一个段(这个段列出了一些DLL文件),并列出exe文件代码中调用函数dll文件的部分。然后,系统为每个dll文件调用loadlibrary函数,如果某个dll文件需要调用更多的 dll,那么系统会再次调用loadlibrary函数,来加载这个dll。系统保留一个足够大的地址空间区域,用来存放这个dll文件。默认情况下,微 软创建dll文件基地址0x10000000。 windows提供的所有标准系统dll都有不同的基地址,这样,即使加载到单个地址空间,他们之间也不会重叠。(1.编译器处理每个源代码模块,生成一 个obj模块。2.链接程序将所有obj模块的内容组合在一起,生成一个单独的dll映像文件,该映像文件包含用于dll的所有二进制代码以及全局/静态 数据变量。3.如果链接程序检查到dll的源代码模块至少导出了一个函数或变量,则链接程序同时生成一个单独的lib文件,这个lib文件很小,只是简单地列出了所有被导出的函数和变量的符号名)
           6)当把所有的exe文件和dll文件都映射到进程的地址空间之后,系统就会创建一个线程内核对象,并使用该线程以DLL_PROCESS_ATTACH为参数来调用每个DLL的DllMain函数,当所有映射的DLL都对此通知做出相应后,系统将驱使主线程开始执行exe文件的启动代码(winmainCRTStartup 函数),这个函数负责对c/c++运行时库进行初始化和调用函数入口函数(main 或 winmain)。
          下面强调一些dll和lib的加载区别:
          dll允许可执行模块(.dll文件或.exe文件)仅包含在运行时定位DLL函数的可执行代码所需的信息(即将dll附带的lib加载到可执行模块中)。
          对于lib文件,链接器从静态链接库LIB获取所有被引用函数,并将库同代码一起放到可执行文件中。

(9)、http://blog.csdn.net/chenyujing1234/article/details/7877484:延迟加载DLL
(10)、http://blog.csdn.net/chenyujing1234/article/details/7892746:DLL延迟加载工程分析

0 0
原创粉丝点击