静态链接库和动态链接库有何不同?

30
如果两者都包含编译代码,为什么我们不能在运行时加载“静态”文件,或者在编译时与动态库链接呢?为什么需要不同的格式来存储“独立”代码?两者之间需要存储什么内容才需要这样的差异呢?

-fPIC 这里是一个小提示 - alternative
6个回答

23

静态库是真正意义上的库,因为它们包含了一系列目标文件。每个目标文件通常都是从单个源文件创建而来,它包含了机器代码以及有关代码所需数据的信息。在链接步骤中,链接器会选择必要的目标文件并将它们组合成一个可执行文件。

机器代码的一个重要部分是跳转、调用和数据指针必须包含实际的内存地址。然而,如果一个目标文件需要调用另一个目标文件中的函数,则只能使用符号来引用该函数。当链接器将目标文件组合成可执行代码时,符号引用将被解析并转换为实际的内存地址。

动态库是可执行代码,可以加载到内存中并立即执行。在某些操作系统上,可能需要进行额外的步骤,通过将可执行代码移动到另一个位置来重新定位代码,这需要将代码中的所有绝对地址向左或右移动一定数量的字节。这个操作仍然比链接器组合目标文件和解析符号要快得多。

总之:

  • 静态库包含使用符号引用其他可执行代码的代码片段
  • 动态库(和可执行文件)包含现在位于固定位置的可执行代码,使得符号可以被替换为实际的内存地址

如果你曾经尝试链接一个相当大的项目,你会注意到这需要相当长的时间,可能比你愿意等待启动应用程序的时间更长。这就解释了为什么不能执行静态库。而动态库已经被优化和削减,不包含除了可执行代码之外的任何内容,因此它们不适合用作静态库。


1
“Re:“*. 这就解释了为什么无法执行静态库*”,这并不是很清楚吧?” - Pacerier
@Pacerier - 这是编译器和链接器之间的一个重要区别。为了使编译后的代码能够调用其他文件中的函数,它需要跳转(或者说调用)到内存地址。然而,当编译器执行时,它并不知道这些内存地址;它使用符号代替。实际上,将符号替换为地址是链接器的工作。有关更多信息,请参见此问题。 简而言之:您可以执行静态库,但只有在运行链接器之后才能这样做。动态库已经完成了这个过程。 - Tyrannosaur

7
对象文件中的代码没有被链接,其中包含对尚未解决的外部函数的隐式引用。
当对象文件链接以创建动态链接库(DLL)时,链接器会查找所有这些外部引用,并找到可以满足它们的其他库(静态或动态)。对静态库中名称的引用通过将该函数的实现体(或其他内容)包含到DLL中来解决。如果它引用了一个动态库,则该DLL和所引用的函数的名称都包含在DLL中。
理论上讲,这不一定是必须的。你可以编写加载器,在每次加载文件时完成所有这些操作。这基本上只是一种优化:链接器执行相对较慢的工作。 DLL的引用保留了下来,但它们已经被解决到足以让加载器快速找到和加载目标文件(如果需要),并解决所引用的函数。当链接器在执行其工作时,它通过扫描长列表来查找你关心的定义,这要慢得多。

5
注意:以下答案不是平台无关的,而是针对基于ELF的系统和一些类似系统。其他系统的细节由其他人填写。
什么是静态库?
静态库是一个归档文件中的*.o文件集合。每个文件可以包含对未定义符号的引用,这些符号必须由链接器解决,例如,您的库可能会引用printf。库没有提供任何关于printf在哪里找到的指示,期望链接器将在其要求链接的其他库中找到它。
假设您的库包含以下代码:
read_png.o
write_png.o
read_jpg.o
write_jpg.o
resize_image.o
handle_error.o

如果一个应用程序只使用read_pngwrite_png,那么其他代码片段不会被加载到可执行文件中(除了read_pngwrite_png调用的handle_error)。
我们不能在运行时加载静态库,因为:
  • 链接器不知道如何找到外部对象,例如printf

  • 这样会很慢。动态库优化了快速加载。

  • 静态库没有命名空间的概念。我无法定义自己的handle_error,因为它会与库的定义冲突。

什么是动态库? 在ELF系统上,动态库与可执行文件是相同类型的对象。它还导出更多符号,而可执行文件只需要导出_start。动态库被优化,使整个库可以直接映射到内存中。
如果你在动态库中调用printf,除了静态库的要求之外,还有一些额外的要求:
  • 你必须指定哪个库有printf

  • 你必须以特殊的方式调用函数,让链接器插入printf的地址。在静态库中,链接器可以修改你的代码并直接插入地址,但这在共享库中不可能。

我们不想使用动态库来静态链接,因为:
  • 我们不能链接动态库的一部分。即使我们的可执行文件从未调用read_jpg,它也会被包含,因为动态库是全有或全无的。

  • 即使它很小,函数调用的额外开销是浪费的。

总结 编译过程大致如下:
Source  ==compile==>  Object  ==link==>  Executable / Shared Library

静态库是一个未进行链接的对象集合,它们被存档在一起。还有很多工作要做。

共享库则是已经链接完成并可直接加载到内存中的最终产品。

静态库先于共享库被发明出来。如果两者同时被发明,它们可能会更加相似。


你是不是真的想说:“我们无法在运行时加载静态库,因为链接器不知道如何找到外部对象,例如printf。”?还是有打字错误?你实际上想说的是“加载器”而不是“链接器”? - Nawaz
@Nawaz:不,链接器是正确的。在Linux上是动态链接器ld.so,在OS X上是dyld,它负责在运行时解析符号。(不,我并不是想说“加载时间”。) - Dietrich Epp

3

理论上,您可以将静态库(.a文件)用作动态库,在程序启动时或甚至在运行时动态加载和链接它们。但是,它们并没有经过特殊编译和准备以在任意地址上运行(甚至对齐也不一定正确!),因此加载器代码必须分配内存,从.a文件中读取必要的对象到这个内存中,并进行重大修改。当然,所有这些内存都无法共享。因此,这可能是一个非常糟糕的想法...


1

静态库 == tarball 而动态库 == 连续图像部分链接

静态库只是一个1包含`.o或.obj`文件的tarball。当可执行文件被链接时,引用的模块(以及它们引用的模块,然后是它们引用的模块,等等)从静态库中复制出来,并附加到主程序的末尾。整个过程作为单个OS“对象”分页加载到内存中。

动态库只是将静态库的所有元素链接在一起(解决内部关系),然后将整个内容映射到内存中。(按需分页可能会使部分内存存在。)在启动动态程序时,需要进行一定程度的调整,以将主程序(其将在自己的模块中“静态”链接)与共享系统范围内的库连接起来。有时,这种调整会被推迟到每个链接元素直到进行给定的调用。在非常广泛、过于概念化的概述中,可以将静态链接归类为急切加载,而将动态链接归类为惰性加载

静态库有优点和缺点。

没有DLL地狱(也称为依赖地狱
对于小型运行时进程混合,内存占用更小
对于大型运行时进程混合的不同程序,内存占用更大
除非它们正在运行相同的程序,否则无法在进程之间共享任何库代码内存
大量程序集(如Linux / Windows / Mac足迹)占用大量空间,因为printf等在每个映像中重复出现
很难甚至不可能修复源自库的安全漏洞
单独更新库很困难,甚至不可能
单独更新库并且不破坏程序很困难,甚至不可能


1. 实际上,它们不是tar(1)格式,但与之相关。


0

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