一文彻底搞懂静态库和动态库,显示链接和隐式链接

4,488 阅读9分钟

定义:运行时库 静态库 动态库

  • 运行时库:Unix中一个典型的运行时库例子就是libc,它包含标准的C函数,如,print(),exit()等等,用户能创建他们自己的运行库(在Windows中是DLL),而具体的细节依赖编译器和操作系统的。

  • 静态库:函数和数据被编译进一个二进制文件(通常扩展名为.lib),静态库实际上是在链接时被链接到EXE的,库本身不需要与可执行文件一起发行。

  • 动态库:用VC++创建的动态库包含两个文件,一个lib文件和一个dll文件,这个lib文件就是引入库,不是静态库,引入库有时也叫输入库或导入库。

注:windows操作系统下动态库和运行时库的扩展名都是.dll,COM组件的扩展名也是.dll,动态库的引入库和静态库的扩展名都是.lib。

静态库的特点和创建过程

静态库之所以称为【静态库】,是因为在链接阶段,会将汇编生成的目标文件.o与引用到的库一起链接打包到可执行文件中。因此对应的链接方式称为静态链接。

  试想一下,静态库与汇编生成的目标文件一起链接为可执行文件,那么静态库必定跟.o文件格式相似。其实一个静态库可以简单看成是一组目标文件(.o/.obj文件)的集合,即很多目标文件经过压缩打包后形成的一个文件。静态库特点总结如下:

  • 静态库对函数库的链接是放在编译时期完成的。

  • 程序在运行时与函数库再无瓜葛,移植方便。

  • 浪费空间和资源,因为所有相关的目标文件与牵涉到的函数库被链接合成一个可执行文件。

如下图是静态库创建过程:

动态库的特点和创建过程

为什么还需要动态库?

  为什么还需要动态库,其实也就是静态库的特点导致。

  • 空间浪费是静态库的一个问题。

  • 另一个问题是静态库对程序的更新、部署和发布页会带来麻烦。如果静态库libxx.lib更新了,所有使用它的应用程序都需要重新编译、发布给用户(对于玩家来说,只是一个很小的改动,却导致整个程序重新下载,全量更新)。

  动态库在程序编译时并不会被连接到目标代码中,而是在程序运行是才被载入。不同的应用程序如果调用相同的库,那么在内存里只需要有一份该共享库的实例,规避了空间浪费问题。动态库在程序运行时才被载入,也解决了静态库对程序的更新、部署和发布页会带来麻烦。用户只需要更新动态库即可,增量更新

动态库特点总结:

  • 动态库把对一些库函数的链接载入推迟到程序运行的时期。 

  • 可以实现进程之间的资源共享。(因此动态库也称为共享库)

  • 将一些程序升级变得简单。

  • 甚至可以真正做到链接载入完全由程序员在程序代码中控制(显示调用)。 

Windows与Linux执行文件格式不同,在创建动态库的时候有一些差异。

  • 在Windows系统下的执行文件格式是PE格式,动态库需要一个**DllMain****函数做初始化的入口,通常在导出函数的声明时需要有_declspec(dllexport)**关键字

  • Linux下gcc编译的执行文件默认是ELF格式,**不需要初始化入口,亦不需要函数做特别的声明,**编写比较方便。

windows下调用动态库的方法:

1 隐式加载:即在程序中包含lib文件和.h文件,隐式链接有时称为静态加载或加载时动态链接。例如:

#include "somedll.h"

#pragma comment( lib, "somedll.lib")

然后就可以直接调用此dll中的函数,注意运行时仍然需要somedll.dll。

2 显示加载:使用loadlibrary,GetProcAddress,FreeLibrary,不需要.h文件和.lib文件,但是要知道函数的原型。显式链接有时称为动态加载或运行时动态链接

3 区别:如果在进程启动时未找到 DLL,操作系统将终止使用隐式链接的进程。同样是在此情况下,使用显式链接的进程则不会被终止,并可以尝试从错误中恢复。

有关Win32 DLL,Unix共享库及普通库的详细库结构信息请参考《链接器与加载器》一书。

链接库的链接方式

1 确定要使用的链接方法:

有两种类型的链接:隐式链接和显式链接。

隐式链接

  1. 应用程序的代码调用导出 DLL 函数时发生隐式链接。当调用可执行文件的源代码被编译或被汇编时,DLL 函数调用在对象代码中生成一个外部函数引用。若要解析此外部引用,应用程序必须与 DLL 的创建者所提供的导入库(.LIB 文件)链接。

  2. 导入库仅包含加载 DLL 的代码和实现 DLL 函数调用的代码。在导入库中找到外部函数后,会通知链接器此函数的代码在DLL 中。要解析对 DLL 的外部引用,链接器只需向可执行文件中添加信息,通知系统在进程启动时应在何处查找 DLL 代码。

  3. 系统启动包含动态链接引用的程序时,它使用程序的可执行文件中的信息定位所需的 DLL。如果系统无法定位 DLL,它将终止进程并显示一个对话框来报告错误。否则,系统将 DLL 模块映射到进程的地址空间中。

  4. 如果任何 DLL 具有(用于初始化代码和终止代码的)入口点函数,操作系统将调用此函数。在传递到入口点函数的参数中,有一个指定用以指示 DLL 正在附带到进程的代码。如果入口点函数没有返回 TRUE,系统将终止进程并报告错误。最后,系统修改进程的可执行代码以提供 DLL 函数的起始地址。

  5. 与程序代码的其余部分一样,DLL 代码在进程启动时映射到进程的地址空间中,且仅当需要时才加载到内存中。因此,由 .def 文件用来在 Windows 的早期版本中控制加载的 PRELOAD 和 LOADONCALL 代码属性不再具有任何意义。

显式链接
大部分应用程序使用隐式链接,因为这是最易于使用的链接方法。但是有时也需要显式链接。下面是一些使用显式链接的常见原因:

  1. 直到运行时,应用程序才知道需要加载的 DLL 的名称。例如,应用程序可能需要从配置文件获取 DLL 的名称和导出函数名。

  2. 如果在进程启动时未找到 DLL,操作系统将终止使用隐式链接的进程。同样是在此情况下,使用显式链接的进程则不会被终止,并可以尝试从错误中恢复。例如,进程可通知用户所发生的错误,并让用户指定 DLL 的其他路径。如果使用隐式链接的进程所链接到的 DLL 中有任何 DLL 具有失败的 DllMain 函数,该进程也会被终止。同样是在此情况下,使用显式链接的进程则不会被终止。

  3. 因为Windows 在应用程序加载时加载所有的 DLL,故隐式链接到许多 DLL 的应用程序启动起来会比较慢。为提高启动性能,应用程序可隐式链接到那些加载后立即需要的 DLL,并等到在需要时显式链接到其他 DLL。

  4. 显式链接下不需将应用程序与导入库链接。如果 DLL 中的更改导致导出序号更改,使用显式链接的应用程序不需重新链接(假设它们是用函数名而不是序号值调用 GetProcAddress),而使用隐式链接的应用程序必须重新链接到新的导入库。

下面是需要注意的显式链接的两个缺点:

1. 如果 DLL 具有 DllMain 入口点函数,则操作系统在调用 LoadLibrary 的线程上下文中调用此函数。如果由于以前调用了LoadLibrary 但没有相应地调用 FreeLibrary 函数而导致 DLL 已经附加到进程,则不会调用此入口点函数。如果 DLL 使用 DllMain 函数为进程的每个线程执行初始化,显式链接会造成问题,因为调用 LoadLibrary(或AfxLoadLibrary)时存在的线程将不会初始化。服务器负载高,性能下降,导致无法及时的处理客户端的请求,可能是服务器硬件本身需要升级,另外一方面是程序自身的bug导致的吞吐量不够,性能低、还有就是可能是架构问题,比如没有分布式处理,无法动态扩容,基本上你需要查看内存,CPU,磁盘使用情况,使用top,free ,df等命令来动态查看找到异常指标的进程。

2. 如果DLL 将静态作用域数据声明为 __declspec(thread),则在显式链接时 DLL会导致保护错误。用 LoadLibrary 加载 DLL 后,每当代码引用此数据时 DLL 就会导致保护错误。(静态作用域数据既包括全局静态项,也包括局部静态项。)因此,创建DLL 时应避免使用线程本地存储区,或者应(在用户尝试动态加载时)告诉 DLL 用户潜在的缺陷。