动态程式库

动态程式库

编程相关名词

动态程式库(Dynamic Link Library 或者 Dynamic-link Library,缩写为 DLL),是微软公司在微软Windows作业系统中,实现共享函式馆概念的一种方式。这些库函式的扩展名是 ”.dll"、".ocx"(包含ActiveX控制的库)或者 ".drv"(旧式的系统驱动程式)。

    • 中文名:动态程式库
    • 外文名:Dynamic Link Library
    • 缩写:DLL
    • 类别:程式库

介绍

动态连结提供了一种方法,使进程可以调用不属于其可执行代码的函式。函式的可执行代码位于一个 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问题当作自己的目标,它允许同一个共享库的不同版本并列共存。由于现代的计算机有足够的磁碟空间和记忆体,这也可以作为一个合理的实现方法。

特徵

记忆体管理

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

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

  1. 局部变数——每个执行绪都有自己的栈,DLL内部的局部变数随所在函式被执行而在各自执行绪的调用栈上开闢存储空间。

  2. 全局变数

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

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

  5. DLL内部定义的全局变数

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

函式调用

  1. 调用DLL内部定义的函式。这不是问题。

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

跳转指令

  1. DLL内部跳转,不是问题

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

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

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

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

符号解析和绑定

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而不是语言结构。

优点

  1. 扩展了应用程式的特性;

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

  3. 简化了软体项目的管理;

  4. 有助于节省记忆体;

  5. 有助于资源共享;

  6. 有助于应用程式的本地化;

  7. 有助于解决平台差异;

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

依赖项

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

  • 依赖 DLL 升级到新版本。

  • 修复了依赖 DLL。

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

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

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

入口点

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

BOOLAPIENTRYDllMain(HANDLEhModule,//DLL模组的句柄DWORDul_reason_for_call,//调用本函式的原因LPVOIDlpReserved//保留){switch(ul_reason_for_call){caseDLL_PROCESS_ATTACH://进程正在载入本DLLbreak;caseDLL_THREAD_ATTACH://一个执行绪被创建break;caseDLL_THREAD_DETACH://一个执行绪正常退出break;caseDLL_PROCESS_DETACH://进程正在卸载本DLLbreak;}returnTRUE;//返回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语言无法识别函式。

下面的代码是一个定义档案的示例。

//SampleDLL.def//LIBRARYSampleDLLEXPORTS

示例

HelloWorld 示例 DLL 和应用程式

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

//SampleDLL.cpp#includestdafx.h#defineEXPORTING_DLL#includeSampleDLL.hBOOLAPIENTRYDllMain(HANDLEhModule,DWORDul_reason_for_call,LPVOIDlpReserved){returnTRUE;}voidHelloWorld(){MessageBox(NULL,TEXT(HelloWorld),TEXT(InaDLL),MB_OK);}//File:SampleDLL.h#ifndefINDLL_H#defineINDLL_H#ifdefEXPORTING_DLLextern__declspec(dllexport)voidHelloWorld();#elseextern__declspec(dllimport)voidHelloWorld();#endif#endif

下面的代码是一个“Win32应用程式”项目的示例,该示例调用 SampleDLL DLL 中的导出 DLL 函式。

//SampleApp.cpp#includestdafx.h#includeSampleDLL.hintAPIENTRYWinMain(HINSTANCEhInstance,HINSTANCEhPrevInstance,LPSTRlpCmdLine,intnCmdShow){HelloWorld();return0;}

注意:在载入时动态连结中,您必须连结在生成 SampleDLL 项目时创建的 SampleDLL.lib导入库。

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

//...typedefVOID(*DLLPROC)(LPTSTR);//...HINSTANCEhinstDLL;DLLPROCHelloWorld;BOOLfFreeDLL;hinstDLL=LoadLibrary(sampleDLL.dll);if(hinstDLL!=NULL){HelloWorld=(DLLPROC)GetProcAddress(hinstDLL,HelloWorld);if(HelloWorld!=NULL)(HelloWorld);fFreeDLL=FreeLibrary(hinstDLL);}//...

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。

相关词条

相关搜索

其它词条