什么是“静态链接”和“动态链接”?

299

我经常听到“静态链接”和“动态链接”这两个术语,通常是指使用CC++C#编写的代码。它们是什么,究竟在谈论什么,以及它们连接了什么?

5个回答

541

在大多数情况下(不考虑解释代码),从源代码(你所编写的)到可执行代码(你所运行的)需要经过两个阶段。

第一个是编译,将源代码转换为目标模块。

第二个是链接,将目标模块组合在一起形成一个可执行文件。

这个区别的存在是为了允许第三方库被包含在你的可执行文件中而你不必看到它们的源代码(例如用于数据库访问、网络通信和图形用户界面的库),或者编译不同语言的代码(例如C和汇编代码),然后将它们全部链接在一起。

当你将一个文件以静态方式链接到可执行文件中时,该文件的内容将在链接时被包含进去。换句话说,该文件的内容实际上被插入到你将要运行的可执行文件中。

当你进行动态链接时,在可执行文件中会包含指向被链接文件的指针(例如文件名),而该文件的内容不会在链接时被包含进去。只有当你稍后运行可执行文件时,这些动态链接文件才会被带入,并且只会被带入内存中的可执行文件副本,而不是磁盘上的副本。

这基本上是一种延迟链接的方法。还有一种更加延迟的方法(在某些系统上称为晚期绑定),它只会在你尝试调用其中的函数时才会带入动态链接文件。

静态链接文件在链接时与可执行文件“锁定”,因此它们永远不会改变。被可执行文件引用的动态链接文件可以通过替换磁盘上的文件来进行更改。

这允许更新功能而不必重新链接代码;每次运行时加载器都会重新链接。

这既有好处又有坏处-一方面,它允许更容易地更新和修复错误,另一方面,如果更新不兼容,它可能会导致程序停止工作-这有时是人们提到的可怕的“DLL hell”的原因,即如果你用不兼容的库替换一个动态链接库,应用程序可能会崩溃(顺便说一句,这样做的开发人员应该预料到被严重惩罚)。


例如,让我们看看用户将他们的main.c文件编译成静态和动态链接的情况。

Phase     Static                    Dynamic
--------  ----------------------    ------------------------
          +---------+               +---------+
          | main.c  |               | main.c  |
          +---------+               +---------+
Compile........|.........................|...................
          +---------+ +---------+   +---------+ +--------+
          | main.o  | | crtlib  |   | main.o  | | crtimp |
          +---------+ +---------+   +---------+ +--------+
Link...........|..........|..............|...........|.......
               |          |              +-----------+
               |          |              |
          +---------+     |         +---------+ +--------+
          |  main   |-----+         |  main   | | crtdll |
          +---------+               +---------+ +--------+
Load/Run.......|.........................|..........|........
          +---------+               +---------+     |
          | main in |               | main in |-----+
          | memory  |               | memory  |
          +---------+               +---------+

在静态情况下,主程序和C运行时库在链接时(由开发人员)被链接在一起。由于用户通常无法重新链接可执行文件,他们将被困在库的行为中。

在动态情况下,主程序与C运行时导入库链接(声明动态库中的内容但不实际定义)。这使得链接器能够链接即使实际代码缺失。

然后,在运行时,操作系统加载器会将主程序与C运行时DLL(动态链接库或共享库或其他命名法)进行晚期链接。

C运行时的所有者可以随时放置新的DLL以提供更新或错误修复。正如前面所述,这既有优点又有缺点。


14
如果我说错了,请纠正我,但在Windows上,软件安装时通常会包含自己的库,即使它们是动态链接的。在许多带有软件包管理器的Linux系统中,许多动态链接库(“共享对象”)实际上是在软件之间共享的。 - Paul Fisher
6
@PaulF:像Windows公共控件、DirectX、.NET等这样的东西通常会随着应用程序一起打包,而在Linux上,你倾向于使用apt或yum之类的工具来管理依赖项 - 所以在这方面你是正确的。那些将自己的代码作为DLL文件打包送出的Windows应用程序不太共享它们。 - paxdiablo
40
那些更新DLL并破坏向后兼容性的人将会被预留在地狱第九层的一个特殊位置。是的,如果接口消失或被修改,那么动态链接将会崩溃。这就是为什么不应该这样做的原因。如果您想在DLL中添加一个function2()函数,但有人正在使用function()函数,请不要更改function()函数。处理的最佳方式是以这种方式重新编码function()函数,使其调用function2()函数,但不要更改function()函数的签名。 - paxdiablo
3
@Paul Fisher,我知道现在有点晚了,但是……随着Windows DLL一起提供的库并不是完整的库,它只是一堆存根,告诉链接器DLL包含什么。然后,链接器可以自动将信息放入.exe中以加载DLL,并且符号不会显示为未定义。 - Mark Ransom
3
@Santropedro,你对库、导入和DLL名称的含义都是正确的。后缀只是惯例,不要过于深究(例如,DLL可能具有.dll.so扩展名)- 将答案视为解释概念而不是精确描述。正如文本所述,这是一个示例,仅显示C运行时文件的静态和动态链接,因此,是的,这就是所有这些中crt的含义。 - paxdiablo
显示剩余17条评论

257

我认为对这个问题的一个好答案应该解释一下链接是什么。

当你编译一些C代码(例如),它会被翻译成机器语言。只是一系列字节的序列,当运行时,会导致处理器进行加法、减法、比较、"跳转"、读取内存、写入内存等操作。这些东西存储在目标(.o)文件中。

现在,很久以前,计算机科学家发明了这个“子程序”东西。执行这段代码并返回到这里。不久之后,他们意识到最有用的子程序可以存储在一个特殊的位置,允许任何需要它们的程序使用。

早期的程序员必须输入这些子程序所在的内存地址。类似于CALL 0x5A62。这很繁琐,并且如果这些内存地址需要更改,会带来问题。

所以,这个过程是自动化的。你编写一个调用printf()的程序,编译器不知道printf的内存地址。因此,编译器只会写入CALL 0x0000,并在目标文件中添加一条注释,说明“必须将此0x0000替换为printf的内存位置”。

静态链接意味着链接器程序(GNU的链接器称为ld)直接将printf的机器码添加到可执行文件中,并将0x0000更改为printf的地址。这发生在创建可执行文件时。

动态链接意味着上述步骤不会发生。可执行文件仍然有一条注释,说明“必须将0x000替换为printf的内存位置”。操作系统的加载程序需要找到printf代码,将其加载到内存中,并在每次运行程序时纠正CALL地址。

程序通常会调用一些将被静态链接的函数(标准库函数如printf通常是静态链接的),以及其他动态链接的函数。静态链接的函数成为了可执行文件的一部分,而动态链接的函数则在可执行文件运行时加入。

这两种方法都有优缺点,并且操作系统之间存在差异。但既然您没有提问,我就到此为止了。


4
我也是,不过我只能选一个答案。 - UnkwnTech
3
Artelius,我想更深入地了解你对这些疯狂低级别的东西是如何工作的解释。请回复我们必须阅读哪些书籍才能获得关于上述事物的深入知识。谢谢。 - Mahesh
3
抱歉,我无法建议任何书籍。你应该先学习汇编语言。然后,维基百科可以提供这些主题的良好概述。你可能想看一下GNU ld文档。 - Artelius
1
这应该是最佳答案。非常清晰简洁。谢谢@Artelius和Peter。 - Abdel Shokair

43
静态链接库在编译时链接,动态链接库在运行时加载。静态链接将库位元素嵌入到可执行文件中。动态链接只嵌入对库的引用;动态链接库的位元素存在于其他位置,并可稍后交换。

26

因为以上帖子没有一个显示如何静态链接并查看您是否正确地执行此操作,所以我将解决这个问题:

一个简单的C程序

#include <stdio.h>

int main(void)
{
    printf("This is a string\n");
    return 0;
}

动态链接C程序

gcc simpleprog.c -o simpleprog

然后在二进制文件上运行file

file simpleprog 

这将表明它是动态链接的某种形式,类似于:

simpleprog: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.26, BuildID[sha1]=0xf715572611a8b04f686809d90d1c0d75c6028f0f, not stripped

这次我们尝试将程序进行静态链接:

gcc simpleprog.c -static -o simpleprog

在这个静态链接二进制文件上运行该文件将显示:

file simpleprog 

现在的结果将会是

simpleprog: ELF 64-bit LSB executable, x86-64, version 1 (GNU/Linux), statically linked, for GNU/Linux 2.6.26, BuildID[sha1]=0x8c0b12250801c5a7c7434647b7dc65a644d6132b, not stripped

你可以看到它被愉快地静态链接了。不幸的是,并非所有的库都能这样简单地进行静态链接,可能需要使用 libtool 或手动链接目标代码和C库来进行扩展努力。

幸运的是,许多嵌入式C库(如musl)为几乎所有的库提供了静态链接选项。

现在,strace这个你创建的二进制文件,你会发现程序开始之前没有访问任何库:

strace ./simpleprog

现在将静态链接版本的strace输出与动态链接程序的输出进行比较,你会发现静态链接版本的strace要短得多!


2

我不了解C#,但为虚拟机语言提供静态链接概念很有趣。

动态链接涉及到如何找到所需的功能,你的程序只有一个引用。你的语言运行时或操作系统在文件系统、网络或已编译代码缓存中搜索匹配引用的代码段,然后采取多项措施将其集成到程序镜像中的内存中,例如重定位。这些都是在运行时完成的。可以手动或由编译器完成。有更新的能力,但也有出现问题的风险(即DLL地狱)。

静态链接是在编译时完成的,你告诉编译器所有功能部件的位置,并指示它将它们集成起来。没有搜索、没有歧义、不能更新而不重新编译。所有依赖关系都与程序镜像物理上相连。


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