版权归原作者所有,如有侵权,请联系我们

[科普中国]-动态链接库

科学百科
原创
科学百科为用户提供权威科普内容,打造知识科普阵地
收藏

概述

动态链接提供了一种方法,使进程可以调用不属于其可执行代码的函数。函数的可执行代码位于一个 DLL 文件中,该 DLL 包含一个或多个已被编译、链接并与使用它们的进程分开存储的函数。DLL 还有助于共享数据和资源。多个应用程序可同时访问内存中单个 DLL 副本的内容。

使用动态链接库可以更为容易地将更新应用于各个模块,而不会影响该程序的其他部分。例如,您有一个大型网络游戏,如果把整个数百MB甚至数GB的游戏的代码都放在一个应用程序里,日后的修改工作将会十分费时,而如果把不同功能的代码分别放在数个动态链接库中,您无需重新生成或安装整个程序就可以应用更新。

动态链接库文件,是一种不可执行的二进制程序文件,它允许程序共享执行特殊任务所必需的代码和其他资源。Windows 提供的DLL文件中包含了允许基于 Windows 的程序在 Windows 环境下操作的许多函数和资源。一般被存放在电脑的"C:\Windows\System32" 目录下。

Windows 中,DLL 多数情况下是带有 ".dll" 扩展名的文件,但也可能是 ".ocx"或其他扩展名;Linux系统中常常是 ".so" 的文件。它们向运行于 Windows操作系统下的程序提供代码、数据或函数。程序可根据 DLL 文件中的指令打开、启用、查询、禁用和关闭驱动程序。

背景DLL的最初目的是节约应用程序所需的磁盘和内存空间。在一个传统的非共享库中,一部分代码简单地附加到调用的程序上。如果两个程序调用同一个子程序,就会出现两份那段代码。相反,许多应用共享的代码能够切分到一个DLL中,在硬盘上存为一个文件,在内存中使用一个实例(instance)。DLL的广泛应用使得早期的视窗能够在紧张的内存条件下运行。

DLL提供了如模块化这样的共享库的普通好处。模块化允许仅仅更改几个应用程序共享使用的一个DLL中的代码和数据而不需要更改应用程序自身。这种模块化的基本形式允许如Microsoft Office、Microsoft Visual Studio、甚至Microsoft Windows自身这样大的应用程序使用较为紧凑的补丁和服务包。

模块化的另外一个好处是插件的通用接口使用。单个的接口允许旧的模块与新的模块一样能够与以前的应用程序运行时无缝地集成到一起,而不需要对应用程序本身作任何更改。这种动态扩展的思想在ActiveX中发挥到了极致。

尽管有这么多的优点,使用DLL也有一个缺点:DLL地狱,也就是几个应用程序在使用同一个共享DLL库发生版本冲突。这样的冲突可以通过将不同版本的问题DLL放到应用程序所在的文件夹而不是放到系统文件夹来解决;但是,这样将抵消共享DLL节约的空间。目前,Microsoft .NET将解决DLL hell问题当作自己的目标,它允许同一个共享库的不同版本并列共存。由于现代的计算机有足够的磁盘空间和内存,这也可以作为一个合理的实现方法1。

特征内存管理在Win32中,DLL文件按照片段(sections)进行组织。每个片段有它自己的属性,如可写或是只读、可执行(代码)或者不可执行(数据)等等。这些section可分为两种,一个是与绝对地址寻址无关的,所以能被多进程公用;另一个是与绝对地址寻址有关的,这个就必须由每个进程有自己的副本专用。sections的这种二分类,在编译DLL时就已经由编译器、链接器给标注好了。所以在装入DLL时,装入器知道哪些sections在内存物理地址空间只需要有一份,供多个进程共用(映射到各个进程的内存逻辑地址空间,所以逻辑地址可以不同); 哪些sections必须是进程使用自己的专用副本。

具体说,DLL装入时需考虑下述情形:

局部变量——每个线程都有自己的栈,DLL内部的局部变量随所在函数被执行而在各自线程的调用栈上开辟存储空间。

全局变量

const全局变量——放入const节中,多进程共享;

非const全局变量——放入各个进程各自专用的data节中。即DLL装入时各个进程复制一份自己专用的DLL的data节。但是,对于一个进程内的多个线程并发访问这种进程空间全局变量,仍然存在线程安全问题。例如,在一个COM的DLL加载入一个进程的空间后,该进程的多个线程可能会并发访问该COM库的COM对象。为此,Windows与COM引入了线程“套间”(apartment)技术。一个进程内,应用程序与加载的各个DLL分属于不同的Module,如果DLL使用所在Module的全局变量,例如动态链接MFC的regular dll在访问自己的MFC全局变量时,应该明确声明。

DLL内部定义的全局变量

访问DLL以外定义的全局变量——使用间址技术,在DLL的data节中用一个指针数据类型的内存空间来保存一个外部全局变量的地址。

函数调用

调用DLL内部定义的函数。这不是问题。

调用DLL外部定义的函数。例如,DLL内部调用一个外部函数foo()。这个foo函数在进程1中可能实现为“四舍五入”,在进程2中实现为“下取整”。所以调用外部函数是各个进程私用的事情。解决办法是使用间址技术,在data节中用一个“函数指针”数据类型的内存空间来保存这种外部函数的入口地址。

跳转指令

DLL内部跳转,不是问题

跳转到DLL外部,解决同上述3.2

DLL代码段通常被使用这个DLL的所有进程所共享。如果代码段所占据的物理内存被收回,它的内容就会被放弃,后面如果需要的话就直接从DLL文件重新加载。

与代码段不同,DLL的数据段通常是私有的;也就是说,每个使用DLL的进程都有自己的DLL数据副本。作为选择,数据段可以设置为共享,允许通过这个共享内存区域进行进程间通信。但是,因为用户权限不能应用到这个共享DLL内存,这将产生一个安全漏洞;也就是一个进程能够破坏共享数据,这将导致其它的共享进程异常。例如,一个使用访客账号的进程将可能通过这种方式破坏其它运行在特权账号的进程。这是在DLL中避免使用共享片段的一个重要原因。

当DLL被如UPX这样一个可执行的packer压缩时,它的所有代码段都标记为可以读写并且是非共享的。可以读写的代码段,类似于私有数据段,是每个进程私有的并且被页面文件备份。这样,压缩DLL将同时增加内存和磁盘空间消耗,所以共享DLL应当避免使用压缩DLL2。

符号解析和绑定DLL输出的每个函数都由一个数字序号唯一标识,也可以由可选的名字标识。同样,DLL引入的函数也可以由序号或者名字标识。对于内部函数来说,只输出序号的情形很常见。对于大多数视窗API函数来说名字是不同视窗版本之间保留不变的;序号有可能会发生变化。这样,我们不能根据序号引用视窗API函数。

按照序号引用函数并不一定比按照名字引用函数性能更好:DLL输出表是按照名字排列的,所以对半查找可以用来在在这个表中根据名字查找这个函数。另外一方面,只有线性查找才可以用于根据序号查找函数。

将一个可执行文件绑定到一个特定版本的DLL也是可能的,这也就是说,可以在编译时解析输入函数(imported functions)的地址。对于绑定的输入函数,连结工具保存了输入函数绑定的DLL的时间戳和校验和。在运行时Windows检查是否正在使用同样版本的库,如果是的话,Windows将绕过处理输入函数;否则如果库与绑定的库不同,Windows将按照正常的方式处理输入函数。

绑定的可执行文件如果运行在与它们编译所用的环境一样,函数调用将会较快,如果是在一个不同的环境它们就等同于正常的调用,所以绑定输入函数没有任何的缺点。例如,所有的标准Windows应用程序都绑定到它们各自的Windows发布版本的系统DLL。将一个应用程序输入函数绑定到它的目的环境的好机会是在应用程序安装的过程。

运行时显式链接对每个DLL来说,Windows存储了一个全域计数器,每多一个进程使用便多额外一个。LoadLibrary与FreeLibrary指令影响每一个进程内含的计数器;动态链接则不影响。因此借由调用FreeLibrary多次,从存储器卸载一DLL是很重要的。一个进程可以从它自己的VAS注销此计数器。

DLL文件能够在运行时使用LoadLibrary(或者LoadLibraryEx)API函数进行显式调用,这个的过程微软简单地称为运行时动态调用。API函数GetProcAddress根据查找输出名称符号、FreeLibrary卸载DLL。这些函数类似于POSIX标准API中的dlopen、dlsym、和dlclose。

注意微软简单称为运行时动态链接的运行时隐式链接,如果不能找到链接的DLL文件,Windows将提示一个错误消息并且调用应用程序失败。应用程序开发人员不能通过编译链接来处理这种缺少DLL文件的隐式链接问题。另外一方面,对于显式链接,开发人员有机会提供一个完善的出错处理机制。

运行时显式链接的过程在所有语言中都是相同的,因为它依赖于Windows API而不是语言结构3。

优点扩展了应用程序的特性;

可以用许多种编程语言来编写;

简化了软件项目的管理;

有助于节省内存;

有助于资源共享;

有助于应用程序的本地化;

有助于解决平台差异;

可以用于一些特殊的目的。Windows 使得某些特性只能为 DLL 所用。

依赖项当某个程序或 DLL 使用其他 DLL 中的 DLL 函数时,就会创建依赖项。因此,该程序就不再是独立的,并且如果该依赖项被损坏,该程序就可能遇到问题。例如,如果发生下列操作之一,则该程序可能无法运行:

依赖 DLL 升级到新版本。

修复了依赖 DLL。

依赖 DLL 被其早期版本覆盖。

从计算机中删除了依赖 DLL。

这些操作通常称为 DLL 冲突。如果没有强制实现向后兼容性,则该程序可能无法成功运行。

入口点在创建 DLL 时,可以有选择地指定入口点函数。当进程或线程将它们自身附加到 DLL 或者将它们自身从 DLL 分离时,将调用入口点函数。您可以使用入口点函数根据 DLL 的需要来初始化数据结构或者销毁数据结构。此外,如果应用程序是多线程的,则可以在入口点函数中使用线程本地存储(TLS) 来分配各个线程专用的内存。下面的代码是一个 DLL 入口点函数的示例:

BOOL APIENTRY DllMain( HANDLE hModule, // DLL模块的句柄 DWORD ul_reason_for_call, // 调用本函数的原因 LPVOID lpReserved // 保留) { switch (ul_reason_for_call) { case DLL_PROCESS_ATTACH: //进程正在加载本DLL break; case DLL_THREAD_ATTACH: //一个线程被创建 break; case DLL_THREAD_DETACH: //一个线程正常退出 break; case DLL_PROCESS_DETACH: //进程正在卸载本DLL break; } return TRUE; //返回TRUE,表示成功执行本函数}当入口点函数返回 FALSE 值时,如果您使用的是加载时动态链接,则应用程序不启动。如果您使用的是运行时动态链接,则只有个别 DLL 不会加载。

入口点函数只应执行简单的初始化任务,不应调用任何其他 DLL 加载函数或终止函数。例如,在入口点函数中,不应直接或间接调用 LoadLibrary 函数或LoadLibraryEx 函数。此外,不应在进程终止时调用 FreeLibrary函数。

注意:在多线程应用程序中,请确保将对 DLL 全局数据的访问进行同步(线程安全),以避免可能的数据损坏。为此,请使用 TLS 为各个线程提供唯一的数据。

如何导出要导出 DLL 函数,您可以向导出的 DLL 函数中添加函数关键字,也可以创建模块定义文件(.def) 以列出导出的 DLL 函数。

(1)向导出的 DLL 函数中添加函数关键字

要使用函数关键字,您必须使用以下关键字来声明要导出的各个函数:

__declspec(dllexport)

要在应用程序中使用导出的 DLL 函数,您必须使用以下关键字来声明要导入的各个函数:

__declspec(dllimport)

通常情况下,您最好使用一个包含 define 语句和 ifdef 语句的头文件,以便分隔导出语句和导入语句。

(2)创建模块定义文件以列出导出的 DLL 函数

使用模块定义文件来声明导出的 DLL 函数。当您使用模块定义文件(.def)时,您不必向导出的 DLL 函数中添加函数关键字。在模块定义文件中,您可以声明 DLL 的 LIBRARY 语句和 EXPORTS 语句。

特别调用关于特定情况下的调用,比如DLL函数中使用到了 Win32 API 或者将 C++ 生成的 DLL 供标准C语言使用,则需要注意以下一些情况:

如果使用到了 Win32 API,则应该使用关键字 __stdcall

在将 C++ 生成的 DLL 供标准C语言使用时,输出文件需要用 extern "C" 修饰,否则不能被标准C语言调用。如果使用 __stdcall 调用方式,可能产生C不识别的修饰名,所以设置导出函数时要采用 .def 文件形式,而不是__declspec(dllexport) 形式。后者会进行修饰名转换,C语言无法识别函数4。

下面的代码是一个定义文件的示例。

// SampleDLL.def//LIBRARY "SampleDLL"EXPORTS示例HelloWorld 示例 DLL 和应用程序

在 MicrosoftVisual C++6.0 中,可以通过选择“Win32动态链接库”项目类型或“MFC应用程序向导(dll)”来创建 DLL。下面的代码是一个在 Visual C++ 中通过使用“Win32 动态链接库”项目类型创建的 DLL 的示例。

// SampleDLL.cpp#include "stdafx.h"#define EXPORTING_DLL#include "SampleDLL.h"BOOL APIENTRY DllMain( HANDLE hModule, DWORD ul_reason_for_call, LPVOID lpReserved) { return TRUE;}void HelloWorld() { MessageBox( NULL, TEXT("Hello World"), TEXT("In a DLL"), MB_OK);}// File: SampleDLL.h#ifndef INDLL_H #define INDLL_H #ifdef EXPORTING_DLLextern __declspec(dllexport) void HelloWorld() ; #elseextern __declspec(dllimport) void HelloWorld() ; #endif#endif下面的代码是一个“Win32应用程序”项目的示例,该示例调用 SampleDLL DLL 中的导出 DLL 函数。

// SampleApp.cpp#include "stdafx.h"#include "SampleDLL.h"int APIENTRY WinMain(HINSTANCE hInstance,HINSTANCE hPrevInstance,LPSTR lpCmdLine,int nCmdShow) { HelloWorld(); return 0;}注意:在加载时动态链接中,您必须链接在生成 SampleDLL 项目时创建的 SampleDLL.lib导入库。

在运行时动态链接中,您应使用与以下代码类似的代码来调用 SampleDLL.dll导出 DLL 函数。

//...typedef VOID (*DLLPROC) (LPTSTR);//...HINSTANCE hinstDLL;DLLPROC HelloWorld;BOOL fFreeDLL;hinstDLL = LoadLibrary("sampleDLL.dll");if (hinstDLL != NULL){ HelloWorld = (DLLPROC) GetProcAddress(hinstDLL, "HelloWorld"); if (HelloWorld != NULL) (HelloWorld); fFreeDLL = FreeLibrary(hinstDLL);}//...Windows API DLL 描述kernel32.dll

低级内核函数。包含内存管理、任务管理、资源控制等函数。

user32.dll

与 Windows 管理有关的函数。消息、菜单、光标、计时器、通信和其他大多数非现实函数都可以从这里找到。

gdi32.dll

图形设备接口库。与设备输出有关的函数:大多数绘图、显示场景、图元文件、坐标及其字体函数都可以从这里找到。

comdlg32.dll / lz32.dll / version.dll

提供一些附加函数的库,包括通用对话框、文件压缩、版本控制的支持。

comctl32.dll

一个新的 Windows 控件集合,比如 TreeView 和 RichTextBox 等等,最初这个是为了 Windows 95 而制作的,但是也使用于 NT 下。

mapi32.dll

电子邮件的专用函数。

netapi32.dll

访问和控制网络的函数。

odbc32.dll

ODBC功能的DLL。