为什么共享库和静态库不同?

Why are shared and static libraries different things?

对于应用程序开发人员来说,共享 (.so) 和静态 (.a) 库之间的区别完全在于您如何使用它们 - 粗略地说,您需要的库代码是否被复制到您的程序中,或者只是从您的程序中引用然后在 运行 时间加载。

从概念上(天真地)看来可能只有一种图书馆。在构建您自己的应用程序时,静态链接与动态链接是您 select 的一个选择。 .so 和 .a 之间的技术差异是什么,需要在构建 时而不是构建 应用程序时做出此选择?

打个比方:在餐厅,您可以点菜留下或离开,但这是选择如何"use"食物;厨师给你做同样的汉堡包。

它是特定于操作系统的。

在 Linux 上,共享库有一些静态库没有的特性

  • 共享库是一个 ELF 共享对象文件。

  • 共享库的某些 linking 发生在运行时(在 ld-linux.so 中)

  • 使用 same API 将共享库更新到较新的版本(用于错误修复)非常简单且对应用程序透明(在升级共享库,只需重新启动使用它的应用程序即可。

  • 共享库由几个进程共享(它们在磁盘上使用相同的文件,并且大部分内存,特别是它们的code segment,是共享的)
  • 你可以 link 一个共享库到另一个共享库(但你不能真的 link 一个静态库到另一个静态库;你可以将它的成员复制到另一个静态库)。
  • 您可以使用 visibility 属性来限制已定义名称对共享库​​的可见性
  • 您可以 dynamically load 使用 dlopen
  • 共享库(作为插件)
  • 你应该建立一个共享库position-independent code
    (理论上你不必建立PIC共享库. 但在实践中,出于性能和技术原因,你应该这样做)。
    This PIC thing is why you need to build shared库不同于静态库.
  • 共享库的 ELF 符号版本控制很重要。

静态库变得几乎无用(至少在原则上)。在实践中,它们主要用于构建您不想依赖外部资源(例如 libc.sold-linux.so)的少数可执行文件(例如 /bin/sash),或者当您想要避免时dependency hell.

开发人员还应注意不要加载相同的共享库 - 即mmap-ed - 两次(但 dlopenld-linux.so 通常对此非常关心)。当发生这种情况时,数据段可能会重复并发生混乱。

更好的提问方式是 "when should I avoid shared libraries"?答案几乎是 "never"(有少数例外)。

阅读Program Library HowTo, C++ dlopen mini HowTo, Drepper's paper: How To Write Shared Libraries

顺便说一句,它主要是一件历史文物。在过去 -1990 年到 1995 年?- Linux 1(或 Linux 0.99)内核,内核还不支持 ELF,a.out 共享库非常痛苦(在那个时候,没有 PIC,你必须全局决定使用的地址段)。此外,当时的处理器比现在慢数百倍,因此运行时 linking 启动时间可能会有所不同。

它根本不像食物,因为 共享 图书馆是,嗯,共享。你几乎不会点一个任何给定数量的人也会和你同时吃的汉堡包。这就是为什么你需要不同的库实现,它们必须驻留在其他人可以访问它们的内存中,或者加载以供你独占使用。

在构建库时(或更准确地说,在安装库时,或者可能在构建二进制包时),可以选择构建静态库、共享库或两者。如果依赖项仅以一种形式存在,则选择是仅构建(和安装)该形式,或者 rebuild/reinstall 所需格式的依赖项。应用程序的 builder/installer 面临着同样的选择。影响安装选择的库之间的唯一技术差异是已安装依赖项的状态。或者,换句话说,不存在影响建造哪个的决定的技术差异。 (磁盘 space、运行时延迟等问题除外,但这些决定会推迟到应用程序构建完成。)

换句话说,选择在您构建应用程序时做出的。

IMO 的主要区别在于动态库可以由已构建的应用加载。这意味着如果 dll 中存在错误,则可以通过仅重建库来修复该错误(只要您不弄乱符号)。 此外,运行 时间链接允许 dll 成为扩展应用程序功能的插件。该应用程序将在目录中搜索它们并加载所有它们,只要它们的界面相同即可。

所以我看到很多答案都在谈论为什么你想使用共享库而不是静态库,但我认为你的问题是为什么它们现在甚至是不同的东西,即为什么不能使用将共享库作为静态库并在构建时从中提取您需要的内容?

这里有一些原因。其中一些是历史性的 - 请记住,像二进制格式这样基本的东西在计算机系统中变化非常缓慢。

编译方式不同

代码可以编译为依赖于它所在的地址(位置相关)或独立(位置无关)。这会影响全局常量的加载、函数调用等。位置相关的代码如果没有加载到它期望的地址就需要修复,即加载器必须遍历代码并实际更改偏移量。

对于 executables,这不是问题。一个 executable 是第一个被加载到地址 space 的东西,所以它总是被加载到同一个地址。您通常不需要任何修正。但是共享库由不同的 executables,不同的进程使用。多个库可能会发生冲突:如果它们希望位于重叠的地址范围内,则必须做出让步。当它这样做时,它是位置相关的,它需要由加载程序修复。但是现在您在库代码中进行了特定于进程的更改,这意味着代码不能再与其他进程共享(在运行时)。您失去了共享库的一大好处。

如果共享库使用位置无关代码 (PIC),则不需要修正。所以 PIC 适用于共享库。另一方面,PIC 在某些架构上速度较慢(特别是 x86,但不是 x64),因此将 executables 编译为 PIC 是一种资源浪费。

因此,

Executables 通常被编译为位置相关代码,而共享库被编译为位置无关代码。如果您使用共享库作为直接拉入 executables 的代码源,您将获得 PIC。如果你想要PDC,你需要一个单独的代码仓库,那就是一个静态库。

当然,在大多数现代架构上,PIC 的效率并不比 PDC 低,地址 space 随机化等安全技术也使得将 executables 编译为 PIC 也很有用,所以这更多是历史原因,而不是当前原因。

包含不同的东西

但是还有另一个更当前的原因将静态库和共享库分开,那就是 link 时间优化。

基本上,优化器掌握的关于程序的信息越多,就越能对其进行推理。经典优化器在每个模块的基础上工作:编译 .c 文件,优化它,生成目标代码。 linker 获取了所有目标文件并将它们合并在一起。这意味着优化器一次只能推理一个模块。它无法查看模块外部的被调用函数以对它们进行推理,甚至无法简单地将它们内联。

然而,在现代工具链中,编译器的工作方式通常有所不同。它不是编译和优化一个模块然后生成目标代码,而是获取一个模块,生成一个中间形式,可能对其进行一些优化,然后将中间形式放入目标文件中。 linker 不只是合并目标文件和解析引用,实际上合并了中间表示,然后在合并后的形式上调用优化器和代码生成器。有了更多可用信息,优化器可以做得更好。

这种中间表示比机器代码更详细,更忠实于原始代码。你想要这个用于你的编译过程。您不想将它运送给客户,因为它要大得多,而且如果您使用闭源模型,还因为它更容易进行逆向工程。此外,发送它没有意义,因为加载程序不理解它,而且您不想在启动时重新优化和重新编译您的程序(JIT 语言除外)。

因此,共享库包含真实的目标代码。另一方面,静态库是中间代码的良好容器,因为它由 linker 使用。这是静态库和共享库之间的主要区别。

联动模型

最后,我们还有一个半历史原因:linkage.

链接定义符号(变量或函数名称)在代码单元外的可见方式。 C语言定义了两个linkages:internal(在编译单元外不可见,即static)和external(对整个程序可见,即extern)。你通常有很多外部可见的符号。

但是,共享库会在加载时解析它们的符号,这应该很快。更少的符号意味着在符号 table 中查找更快。当然,当计算机速度较慢时,这更有意义,但它仍然可以产生明显的效果。它还会影响库的大小

因此,操作系统使用的目标文件规范(*nix 的 ELF,Windows 的 PE/COFF)为共享库定义了单独的可见性。您可以选择显式指定可见函数,而不是使 C 中的所有外部内容都可见。 (在 Windows 中,只有注释为 __declspec(dllexport) 或列在 .def 文件中的内容才会从 DLL 中导出。在 Linux 中,默认情况下导出所有外部内容,但您可以使用 __attribute__((visibility("hidden"))) 不这样做,或者您可以指定 -fvisibility=hidden 命令行开关或可见性 pragma 来覆盖默认值。)

最终结果是共享库丢弃了除导出函数之外的所有符号信息。

静态库不需要丢弃任何符号信息。更重要的是,您不想那样做,因为仔细指定哪些函数被导出,哪些函数不导出是一项工作,除非必要,否则您不想做这项工作。如果您使用的是静态库,则没有必要。

因此,可交付的共享库应该尽量减少其导出的符号,以便快速和小巧。这使得它作为静态 linking 的代码存储库不太有用,您可能希望在其中 link 选择更多的函数,特别是一旦接口函数被内联(参见 link-上面的时间优化)。

  1. 静态库是对象模块的集合。 模块的任何子集 都可以链接到生成的应用程序中(如果所有依赖项都已解析)。与静态不同,共享库作为 一个实体 .

  2. 加载到应用程序的 space 中
  3. 共享库由OS加载,因此它们必须具有系统utilities/kernel可识别的特殊格式。静态库由 linker/librarian 个应用程序处理。尽管有一些规范,但可以开发自己的格式 静态库(以及能够处理它的工具)。