我经常听到术语“静态链接”和“动态链接”,通常指的是用C、c++或c#编写的代码。它们是什么,它们到底在谈论什么,它们连接了什么?
静态链接库在编译时被链接。动态链接库在运行时加载。静态链接将库位烘焙到可执行文件中。动态链接仅在对库的引用中烘烤;动态库的位存在于其他地方,以后可以交换出来。
从源代码(您编写的代码)到可执行代码(您运行的代码)有两个阶段(在大多数情况下,不考虑解释代码)。
第一个是编译,它将源代码转换为对象模块。
第二种方法是链接,将目标模块组合在一起以形成可执行文件。
其中的区别在于,允许第三方库包含在可执行文件中而不需要看到它们的源代码(例如用于数据库访问、网络通信和图形用户界面的库),或者用于用不同语言编译代码(例如C语言和汇编代码),然后将它们链接在一起。
当你静态链接一个文件到一个可执行文件,该文件的内容包括在链接时。换句话说,文件的内容被物理地插入到您将要运行的可执行文件中。
当你链接动态时,指向被链接的文件的指针(例如文件的文件名)包含在可执行文件中,而该文件的内容在链接时不包括在内。只有当你稍后运行可执行文件时,这些动态链接的文件才会被引入,而且它们只会被带到可执行文件的内存副本中,而不是磁盘上的副本。
它基本上是一种延迟链接的方法。甚至还有一个更多的延迟方法(在某些系统上称为晚绑定),它不会引入动态链接的文件,直到你真正尝试调用其中的函数。
静态链接文件在链接时被“锁定”到可执行文件,因此它们永远不会改变。可执行文件引用的动态链接文件可以通过替换磁盘上的文件进行更改。
这允许更新功能,而无需重新链接代码;每次运行加载器时,加载器都会重新链接。
这有利有弊——一方面,它允许更容易的更新和错误修复,另一方面,如果更新不兼容,它可能导致程序停止工作——这有时是可怕的“DLL地狱”的原因。有些人在书中提到,如果你用一个不兼容的动态链接库替换一个动态链接库,应用程序就会被破坏(顺便说一下,这样做的开发人员应该会被追捕和严厉惩罚)。
作为例子,让我们看看用户编译他们的main.c文件用于静态和动态链接的情况。
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,以提供更新或错误修复。如前所述,这既有优点也有缺点。
(我不懂c#,但对于虚拟机语言来说,有一个静态链接的概念是很有趣的)
动态链接包括知道如何找到所需的功能,而您只能从程序中引用该功能。语言运行库或操作系统在文件系统、网络或已编译代码缓存中搜索一段代码,匹配引用,然后采取一些措施将其集成到内存中的程序映像中,比如重定位。它们都是在运行时完成的。它可以手动完成,也可以由编译器完成。可以在有搞乱风险(即DLL地狱)的情况下进行更新。
静态链接是在编译时完成的,你告诉编译器所有功能部分在哪里,并指示它集成它们。没有搜索,没有歧义,不能在不重新编译的情况下更新。所有依赖项都与程序映像在物理上是一体的。
我认为这个问题的一个很好的答案应该解释是。
当你编译一些C代码时(例如),它会被翻译成机器语言。只是一个字节序列,当它运行时,会导致处理器进行加、减、比较、“goto”、读内存、写内存,诸如此类的事情。这些东西存储在object (.o)文件中。
很久以前,计算机科学家发明了这种“子程序”的东西。Execute-this-chunk-of-code-and-return-here。没过多久,他们就意识到最有用的子程序可以存储在一个特殊的地方,供任何需要它们的程序使用。
在早期,程序员必须输入这些子程序所在的内存地址。类似CALL 0x5A62。如果这些内存地址需要更改,那么这是非常繁琐和有问题的。
CALL 0x5A62
所以,这个过程是自动化的。你编写了一个调用printf()的程序,而编译器不知道printf的内存地址。因此,编译器只写入CALL 0x0000,并在目标文件中添加一个注释,说“必须将此0x0000替换为printf的内存位置”。
printf()
printf
CALL 0x0000
静态链接意味着链接器程序(GNU的称为ld)将printf的机器代码直接添加到可执行文件中,并将0x0000更改为printf的地址。这发生在创建可执行文件时。
动态链接意味着上面的步骤不会发生。可执行文件仍然有一个注释,说“必须用printf的内存位置替换0x000”。操作系统的加载器需要找到printf代码,将其加载到内存中,并纠正CALL地址每次运行程序时。
程序调用一些静态链接的函数(像printf这样的标准库函数通常是静态链接的)和其他动态链接的函数是很常见的。当可执行文件运行时,静态的“成为”可执行文件的“一部分”,动态的“加入”。
这两种方法各有优缺点,操作系统之间也存在差异。但既然你没问,我就说到这里吧。
因为上面的帖子都没有实际上展示了来静态链接一些东西,看看你做对了,所以我将解决这个问题:
一个简单的C程序
#include <stdio.h> int main(void) { printf("This is a string\n"); return 0; }
动态链接C程序
gcc simpleprog.c -o simpleprog
并在二进制文件上运行file:
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
运行这个静态链接的二进制文件将显示:
现在的结果将是
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库。
libtool
幸运的是,许多嵌入式C库(如musl)为其库的几乎所有如果不是全部提供了静态链接选项。
musl
现在strace是你创建的二进制文件,你可以看到在程序开始之前没有库被访问:
strace
strace ./simpleprog
现在与动态链接程序上strace的输出进行比较,你会发现静态链接版本的strace要短得多!