共享对象(.so)、静态库(.a)和动态链接库(.so)之间的区别是什么?

371
我在Linux的库方面参与了一些辩论,并希望确认一些事情。
据我了解(如果我错了,请纠正我,我会在之后编辑我的帖子),在构建应用程序时,有两种使用库的方式:
1. 静态库(.a文件):在链接时,整个库的副本被放入最终的应用程序中,以便库中的函数始终对调用应用程序可用。 2. 共享对象(.so文件):在链接时,对象只是通过相应的头文件(.h文件)对其API进行验证。直到运行时需要时,库才会被实际使用。
静态库的明显优势是它们使整个应用程序成为自包含的,而动态库的好处是“.so”文件可以被替换(例如:如果由于安全漏洞需要更新),而无需重新编译基本应用程序。
我听说有些人在共享对象和动态链接库(DLL)之间做了区分,尽管它们都是“.so”文件。在Linux或其他POSIX兼容的操作系统(如MINIX、UNIX、QNX等)上进行C/C++开发时,共享对象和DLL之间是否有区别?据我所知,一个关键的区别(至少目前是这样)是共享对象仅在运行时使用,而DLL必须首先在应用程序中使用dlopen()函数进行打开。
最后,我还听说一些开发人员提到了“共享归档”,据我理解,它们本身也是静态库,但不会直接被应用程序使用。相反,其他静态库将链接到“共享归档”,从中提取一些(但不是全部)函数/资源到正在构建的静态库中。
更新
在这些术语被提供给我的背景下,这实际上是一组Windows开发人员在学习Linux时使用的错误术语。我试图纠正他们,但是(错误的)语言规范仍然被坚持使用。
共享对象:在程序启动时自动链接到程序中的库,作为一个独立的文件存在。该库在编译时包含在链接列表中(例如:对于名为mylib.so的库文件,LDOPTS+=-lmylib)。该库必须在编译时和应用程序启动时存在。 静态库:在构建时将库合并到实际程序本身中,用于包含应用程序代码和自动链接到程序的库代码的单个(较大)应用程序。最终的二进制文件包含主程序和库本身,作为一个独立的二进制文件存在。该库在编译时包含在链接列表中(例如:对于名为mylib.a的库文件,LDOPTS+=-lmylib)。该库必须在编译时存在。 DLL:与共享对象基本相同,但不是在编译时包含在链接列表中,而是通过dlopen()/dlsym()命令加载库,以便程序在编译时不需要该库。此外,该库不需要在应用程序启动或编译时存在,因为它只在dlopen/dlsym调用时需要。 共享存档:与静态库基本相同,但使用了“export-shared”和“-fPIC”标志进行编译。该库在编译时包含在链接列表中(例如:对于名为mylibS.a的库文件,LDOPTS+=-lmylibS)。两者之间的区别在于,如果共享对象或DLL希望将共享存档静态链接到自己的代码中,并能够使共享对象中的函数对其他程序可用,而不仅仅在DLL内部使用它们,则需要此附加标志。当有人向您提供一个静态库,并且您希望将其重新打包为SO时,这非常有用。该库必须在编译时存在。

额外更新

在我当时工作的公司中,“DLL”和“共享库”之间的区别只是一种(懒散、不准确的)口头说法(当时的Windows开发人员被迫转向Linux开发,这个术语就流传开来),遵循上述所述的描述。

此外,在“共享存档”中,库名称后面的“S”字面量只是该公司使用的一种约定,并不普遍存在于整个行业中。


21
对于 .a 文件,"a" 实际上代表 "archive"(归档文件),它只是一组目标文件的归档。现代链接器应该足够智能,不需要包含整个库,只需包含所需的归档中的目标文件,并且可能仅使用引用的目标文件中的代码/数据部分。 - Some programmer dude
7
"DLL" 只是 Windows 的术语,它在 Unix 系统中并不使用。 - R.. GitHub STOP HELPING ICE
2
可能是为什么静态链接库和动态链接库不同?的重复问题。 - Tamás Szelei
3
请访问http://tldp.org/HOWTO/Program-Library-HOWTO/,该文档将介绍如何设计和使用程序库(libraries),并且可以帮助您更好地组织和管理您的代码。 - Basile Starynkevitch
7
“archive” 当然是指“存档”。 :) - Some programmer dude
显示剩余3条评论
5个回答

272
静态库(.a)是一个可以直接链接到链接器生成的最终可执行文件中的库;它包含在其中,不需要将库放在可执行文件部署的系统中。
共享库(.so)是一个被链接但不嵌入到最终可执行文件中的库,因此它会在可执行文件启动时加载,并且需要存在于可执行文件部署的系统中。
Windows上的动态链接库(.dll)类似于Linux上的共享库(.so),但两种实现之间存在一些与操作系统(Windows vs Linux)相关的差异:
DLL可以定义两种类型的函数:导出函数和内部函数。导出函数旨在被其他模块调用,以及在定义它们的DLL内部调用。内部函数通常只能从定义它们的DLL内部调用。
Linux上的SO库不需要特殊的导出语句来指示可导出的符号,因为所有符号对于询问进程都是可用的。

3
如果一个函数在动态链接库(DLL)中被声明为“Internal”,那么这是否意味着它无法从库外部调用?+1 很好的简明解释。 - Mike
36
并非所有的符号都可以在SO库中获得。出现隐藏符号是可能的,也是建议的,因为库用户没有充分的理由需要看到你所有的符号。 - Zan Lynx
8
FYI: g++使用__attribute__语法来选择性地“导出”符号: #define DLLEXPORT __attribute__ ((visibility("default"))) #define DLLLOCAL __attribute__ ((visibility("hidden"))) - Brian Cannard

125

我一直认为DLL和共享对象只是相同的东西的不同称呼——Windows称之为DLL,在UNIX系统中它们被称为共享对象,而“动态链接库”这个通用术语涵盖了两者(即使在UNIX上打开.so文件的函数也被称为dlopen(),其中“dynamic library”指动态库)。

它们确实只在应用程序启动时链接,但是你对校验头文件的观念是错误的。头文件定义了原型,编译使用库的代码需要这些原型,但是链接器在链接时查看库本身,以确保它所需的函数实际存在。在链接时,链接器必须找到函数体,否则会引发错误。它还在运行时执行此操作,因为正如你正确指出的那样,库本身可能已经与编译程序时不同。这就是平台库中ABI稳定性非常重要的原因,因为ABI改变会破坏针对旧版本编译的现有程序。

静态库只是编译器生成的一组对象文件的捆绑包,类似于项目编译过程中生成的对象文件,因此它们以完全相同的方式被拉入并提供给链接器,并以完全相同的方式丢弃未使用的部分。


3
为什么我在Linux上看到的一些项目需要使用dlopen()调用来访问“.so”文件中的函数,而有些则根本不需要这样做?顺便说一下,谢谢! - Cloud
13
不这样做的话,进程加载器(Linux 的 ELF 加载器)会将函数传递给它们。如果应用程序想要打开和使用在编译时不存在的 .so 或 .dll,或者添加额外的功能(如插件),则可以使用 dlopen。 - rapadura
1
但是,如果在构建时没有.so文件,应用程序不会编译通过,对吗?是否可能强制链接器在完全没有.so的情况下仅构建最终程序?谢谢。 - Cloud
2
我相信这取决于您如何使用.so中的函数,但是我的知识在此停止:/ 很好的问题。 - rapadura
3
关于dlopen()及其相关函数,我的理解是它们用于以编程方式打开/关闭动态链接库,以便在整个应用程序运行期间不必将其加载到内存中。否则,您必须在链接器的命令行参数(也称为makefile)中告诉链接器要加载该库。它将在运行时加载并一直保留在内存中,直到应用程序退出。在操作系统层面可能会发生更多事情,但就您的应用程序而言,大致就是这样。 - Taylor Price
显示剩余3条评论

49

我可以详细说明Windows中的DLL来帮助解释这些在*NIX系统中的神秘之处...

DLL类似于共享对象文件。两者都是图像,由各自操作系统的程序加载器准备好加载到内存中。这些图像伴随着各种元数据以帮助链接器和加载器建立必要的关联并使用代码库。

Windows DLL具有导出表。导出可以按名称或表位置(数字)进行。后一种方法被认为是“老派”的方法,并且更加脆弱——重建DLL并在表中更改函数的位置将导致灾难性后果,而如果入口点链接是按名称进行的,则不会有实质性问题。所以,忘记它作为问题,但要知道它是存在的,如果你使用“恐龙”代码(例如第三方供应商库),就需要注意。

Windows DLL是通过编译和链接构建的,就像对于可执行应用程序EXE一样,但是DLL不是独立存在的,就像SO是被应用程序使用的,通过动态加载或链接时间绑定(引用SO嵌入在应用程序二进制文件的元数据中,操作系统程序加载器将自动加载所引用的SO)。DLL可以引用其他DLL,就像SO可以引用其他SO一样。

在Windows中,DLL只会提供特定的入口点。这些被称为“导出”。开发人员可以使用特殊的编译器关键字使符号成为外部可见的(对其他链接器和动态加载器),或者将导出列在模块定义文件中,该文件在创建DLL本身时在链接时间使用。现代做法是用关键字装饰函数定义以导出符号名称。还可以创建带有关键字的头文件,它将声明该符号作为要从当前编译单元外部的DLL中导入的符号之一。请查询__declspec(dllexport)和__declspec(dllimport)关键字以获取更多信息。

DLLs的一个有趣特性是它们可以声明一个标准的“加载/卸载”处理函数。每当DLL被加载或卸载时,DLL都可以进行一些初始化或清理工作。这非常适合将DLL作为面向对象的资源管理器,例如设备驱动程序或共享对象接口。
当开发人员想要使用已构建的DLL时,她必须引用由DLL开发者创建的“导出库”(*.LIB),或者在运行时显式加载DLL,并通过LoadLibrary()和GetProcAddress()机制按名称请求入口点地址。大多数情况下,链接到LIB文件(其中仅包含DLL导出的入口点的链接器元数据)是使用DLL的方法。动态加载通常用于在程序行为中实现“多态”或“运行时可配置性”(访问插件或后定义的功能)。
Windows做事情的方式有时会引起一些困惑;系统使用.LIB扩展名来引用正常的静态库(类似于POSIX *.a文件的归档),以及在链接时绑定应用程序到DLL所需的“导出存根”库。因此,应始终查看*.LIB文件是否具有同名的*.DLL文件;如果没有,则很有可能*.LIB文件是静态库档案,而不是DLL的导出绑定元数据。

7
您的观点是正确的,静态文件在链接时被复制到应用程序中,而共享文件只在链接时进行验证,并在运行时加载。
dlopen调用不仅适用于共享对象,如果应用程序希望代表它在运行时执行此操作,否则共享对象将在应用程序启动时自动加载。DLLS和.so是相同的东西。dlopen存在的目的是为进程添加更精细的动态加载能力。您不必自己使用dlopen来打开/使用DLL,这也会在应用程序启动时发生。

1
使用dlopen()进行更多的加载控制的一个例子是什么?如果SO/DLL在启动时自动加载,那么dlopen()是否会以不同的权限或限制关闭和重新打开它?谢谢。 - Cloud
2
我认为dlopen是用于插件或类似功能的。权限/限制应与自动加载相同,无论如何,dlopen将递归加载依赖库。 - rapadura
1
DLL和.so并不完全相同,请参见此答案 - Basile Starynkevitch

-1
我怀疑这里有某种误解,但头文件,至少是用于编译源代码的.h类型,绝对不会在链接时进行检查。
.h文件,以及.c/.cpp文件,仅在编译阶段中涉及,其中包括预处理。一旦对象代码被创建,头文件就已经消失了,在链接器开始处理事情之前就已经不存在了。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接