ChatGPT解决这个技术问题 Extra ChatGPT

共享对象 (.so)、静态库 (.a) 和 DLL (.so) 之间的区别?

我参与了一些关于 Linux 库的辩论,并想确认一些事情。

据我了解(如果我错了,请纠正我,稍后我会编辑我的帖子),在构建应用程序时有两种使用库的方法:

静态库(.a 文件):在链接时,将整个库的副本放入最终应用程序中,以便调用应用程序始终可以使用库中的函数共享对象(.so 文件):在链接时,该对象只是通过相应的标头 (.h) 文件针对其 API 进行验证。该库直到运行时才真正使用,在需要它的地方。

静态库的明显优势是它们允许整个应用程序自包含,而动态库的好处是可以替换“.so”文件(即:如果由于安全原因需要更新它bug) 无需重新编译基础应用程序。

我听说有些人区分共享对象和动态链接库(DLL),即使它们都是“.so”文件。在 Linux 或任何其他符合 POSIX 的操作系统(即:MINIX、UNIX、QNX 等)上进行 C/C++ 开发时,共享对象和 DLL 之间有什么区别吗?有人告诉我,一个关键的区别(到目前为止)是共享对象只在运行时使用,而 DLL 必须首先使用应用程序中的 dlopen() 调用打开。

最后,我还听到一些开发人员提到“共享档案”,据我了解,它本身也是静态库,但从未被应用程序直接使用。相反,其他静态库将链接到“共享档案”,以将一些(但不是全部)功能/资源从共享档案中提取到正在构建的静态库中。

预先感谢大家的帮助。

更新

在向我提供这些术语的上下文中,实际上是一组必须学习 Linux 的 Windows 开发人员使用的错误术语。我试图纠正它们,但(不正确的)语言规范卡住了。

共享对象:程序启动时自动链接到程序中的库,并作为独立文件存在。该库在编译时包含在链接列表中(即:LDOPTS+=-lmylib 用于名为 mylib.so 的库文件)。该库必须在编译时以及应用程序启动时存在。静态库:在构建时合并到实际程序本身的库,用于单个(更大)应用程序,其中包含应用程序代码和在构建程序时自动链接到程序中的库代码,以及包含两者的最终二进制文件主程序和库本身作为一个独立的二进制文件存在。该库在编译时包含在链接列表中(即:LDOPTS+=-lmylib 用于名为 mylib.a 的库文件)。该库必须在编译时存在。 DLL:本质上与共享对象相同,但不是在编译时包含在链接列表中,而是通过 dlopen()/dlsym() 命令加载库,因此库不需要在构建时出现要编译的程序。此外,在应用程序启动或编译时不需要(必然)存在该库,因为它仅在进行 dlopen/dlsym 调用时才需要。共享存档:本质上与静态库相同,但使用“export-shared”和“-fPIC”标志进行编译。该库在编译时包含在链接列表中(即:LDOPTS+=-lmylibS 用于名为 mylibS.a 的库文件)。两者之间的区别在于,如果共享对象或 DLL 想要将共享存档静态链接到它自己的代码中并且能够使共享对象中的函数对其他程序可用,而不仅仅是使用它们,则需要这个附加标志DLL 内部。这在有人为您提供静态库并且您希望将其重新打包为 SO 的情况下很有用。该库必须在编译时存在。

额外更新

DLL”和“shared library”之间的区别只是我当时工作的公司的一种(懒惰、不准确的)俗语(Windows 开发人员被迫转向 Linux 开发,并且这个术语卡住了),坚持到上面提到的描述。

此外,在“共享档案”的情况下,库名称后的尾随“S”文字只是该公司使用的惯例,而不是一般行业中的惯例。

对于 .a 文件,“a”实际上代表“archove”,它只是目标文件的存档。现代链接器应该足够好,不需要包含 while 库,只需要包含存档中需要的目标文件,甚至可能只使用引用的目标文件中的代码/数据部分。
DLL 只是 Windows 术语。它不用于unices。
@DevNull 当然是“存档”。 :)

a
aleroot

静态库(.a)是可以直接链接到链接器生成的最终可执行文件的库,它包含在其中,无需将库放入将部署可执行文件的系统中。

共享库 (.so) 是链接但未嵌入最终可执行文件中的库,因此将在启动可执行文件时加载,并且需要存在于部署可执行文件的系统中。

windows(.dll) 上的动态链接库类似于 linux 上的共享库(.so),但与操作系统(Windows 与 Linux)相关的两种实现之间存在一些差异:

DLL 可以定义两种函数:导出函数和内部函数。导出的函数旨在由其他模块调用,以及从定义它们的 DLL 中调用。内部函数通常只能从定义它们的 DLL 中调用。

Linux 上的 SO 库不需要特殊的导出语句来指示可导出符号,因为所有符号都可用于查询过程。


+1很好的简单解释。如果一个函数在 DLL 中声明为“内部”,这是否意味着它不能从库外部调用?
不一定所有符号都在 SO 库中可用。隐藏符号是可能的,建议使用,因为图书馆用户没有充分的理由看到您的所有符号。
仅供参考:g++ 具有用于选择性“导出”符号的 __attribute__ 语法:#define DLLEXPORT __attribute__ ((visibility("default"))) #define DLLLOCAL __attribute__ ((visibility("hidden")))
M
Matthew Walton

我一直认为 DLL 和共享对象只是同一事物的不同术语 - Windows 称它们为 DLL,而在 UNIX 系统上它们是共享对象,通用术语 - 动态链接库 - 涵盖两者(甚至函数在 UNIX 上打开 .so 在“动态库”之后称为 dlopen())。

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

静态库只是直接从编译器中取出的目标文件包,就像您在项目编译中自己构建的那些文件一样,因此它们以完全相同的方式被拉入并馈送到链接器,而未使用的位是以完全相同的方式下降。


为什么我在 Linux 上看到的一些项目必须使用 dlopen() 调用来访问“.so”文件中的函数,而有些根本不需要这样做?谢谢,顺便说一句!
那些不这样做的人会得到进程加载器交给他们的函数,即linux的elf加载器。如果应用程序想要打开并使用在编译时不存在的 .so 或 .dll 或者只是添加额外的功能(如插件),则 dlopen 存在。
但是,如果 .so 在构建时不存在,应用程序是否根本不会编译?是否可以强制链接器只构建最终程序而 .so 根本不存在?谢谢你。
我相信这取决于您如何使用 .so 中的功能,但在这里我对此的了解停止了:/好问题。
关于 dlopen() 及其函数系列,据我了解,它用于以编程方式打开/关闭 dll,以便在应用程序的整个运行过程中不必将其加载到内存中。否则,您必须在其命令行参数(也就是您的 makefile)中告诉链接器您希望加载该库。它将在运行时加载并一直加载到内存中,直到应用程序退出。在操作系统级别可能会发生更多事情,但就您的应用程序而言,这大致是发生的事情。
J
JoGusto

我可以详细说明 Windows 中 DLL 的详细信息,以帮助我在 *NIX 领域的朋友澄清这些谜团......

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

Windows DLL 有一个导出表。导出可以按名称或按表位置(数字)。后一种方法被认为是“老派”并且更加脆弱——重建 DLL 和更改函数在表中的位置将以灾难告终,而如果通过名称链接入口点则没有真正的问题。因此,请忘记这是一个问题,但请注意,如果您使用“恐龙”代码(例如 3rd-party 供应商库),它就在那里。

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

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

DLL 的一个有趣特性是它们可以声明一个标准的“加载/卸载”处理函数。每当加载或卸载 DLL 时,DLL 都可以执行一些初始化或清理,视情况而定。这很好地映射为将 DLL 作为面向对象的资源管理器,例如设备驱动程序或共享对象接口。

当开发人员想要使用已构建的 DLL 时,她必须引用 DLL 开发人员在创建 DLL 时创建的“导出库”(*.LIB),或者必须在运行时显式加载 DLL 并请求通过 LoadLibrary() 和 GetProcAddress() 机制按名称命名的入口点地址。大多数时候,链接到一个 LIB 文件(它只包含 DLL 的导出入口点的链接器元数据)是使用 DLL 的方式。动态加载通常保留用于在程序行为中实现“多态性”或“运行时可配置性”(访问附加组件或后来定义的功能,即“插件”)。

Windows 的做事方式有时会引起一些混乱;系统使用 .LIB 扩展名来引用普通静态库(存档,如 POSIX *.a 文件)和在链接时将应用程序绑定到 DLL 所需的“导出存根”库。因此,应始终查看 *.LIB 文件是否具有同名的 *.DLL 文件;如果不是,那么 *.LIB 文件很有可能是一个静态库存档,而不是导出 DLL 的绑定元数据。


p
prayagupa

您是正确的,因为静态文件在链接时被复制到应用程序,并且共享文件只是在链接时验证并在运行时加载。

dlopen 调用不仅适用于共享对象,如果应用程序希望在运行时代表它这样做,否则共享对象会在应用程序启动时自动加载。 DLLS 和 .so 是一回事。 dlopen 的存在是为了为进程添加更细粒度的动态加载能力。您不必自己使用 dlopen 来打开/使用 DLL,这在应用程序启动时也会发生。


使用 dlopen() 进行更多加载控制的一个例子是什么?例如,如果 SO/DLL 在启动时自动加载,dlopen() 是否会以不同的权限或限制关闭并重新打开它?谢谢你。
我相信 dlopen 是用于插件或类似功能的。权限/限制应该与自动加载相同,无论如何 dlopen 将递归加载依赖库。
DLL 和 .so不完全是一回事。请参阅this answer
R
Raymond Jennings

我怀疑这里存在某种误解,但是头文件,至少是用于编译源代码的 .h 文件,在链接时绝对不会被检查。

.h 和 .c/.cpp 文件仅在编译阶段涉及,其中包括预处理。一旦创建了目标代码,在链接器开始处理事情之前,头文件早就消失了。