在大多数情况下(不考虑解释代码),从源代码(你所编写的)到可执行代码(你所运行的)需要经过两个阶段。
第一个是编译,将源代码转换为目标模块。
第二个是链接,将目标模块组合在一起形成一个可执行文件。
这个区别的存在是为了允许第三方库被包含在你的可执行文件中而你不必看到它们的源代码(例如用于数据库访问、网络通信和图形用户界面的库),或者编译不同语言的代码(例如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以提供更新或错误修复。正如前面所述,这既有优点又有缺点。
我认为对这个问题的一个好答案应该解释一下链接是什么。
当你编译一些C代码(例如),它会被翻译成机器语言。只是一系列字节的序列,当运行时,会导致处理器进行加法、减法、比较、"跳转"、读取内存、写入内存等操作。这些东西存储在目标(.o)文件中。
现在,很久以前,计算机科学家发明了这个“子程序”东西。执行这段代码并返回到这里。不久之后,他们意识到最有用的子程序可以存储在一个特殊的位置,允许任何需要它们的程序使用。
早期的程序员必须输入这些子程序所在的内存地址。类似于CALL 0x5A62
。这很繁琐,并且如果这些内存地址需要更改,会带来问题。
所以,这个过程是自动化的。你编写一个调用printf()
的程序,编译器不知道printf
的内存地址。因此,编译器只会写入CALL 0x0000
,并在目标文件中添加一条注释,说明“必须将此0x0000替换为printf的内存位置”。
静态链接意味着链接器程序(GNU的链接器称为ld)直接将printf
的机器码添加到可执行文件中,并将0x0000更改为printf
的地址。这发生在创建可执行文件时。
动态链接意味着上述步骤不会发生。可执行文件仍然有一条注释,说明“必须将0x000替换为printf的内存位置”。操作系统的加载程序需要找到printf代码,将其加载到内存中,并在每次运行程序时纠正CALL地址。
程序通常会调用一些将被静态链接的函数(标准库函数如printf
通常是静态链接的),以及其他动态链接的函数。静态链接的函数成为了可执行文件的一部分,而动态链接的函数则在可执行文件运行时加入。
这两种方法都有优缺点,并且操作系统之间存在差异。但既然您没有提问,我就到此为止了。
ld
文档。 - Artelius因为以上帖子没有一个显示如何静态链接并查看您是否正确地执行此操作,所以我将解决这个问题:
一个简单的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要短得多!
我不了解C#,但为虚拟机语言提供静态链接概念很有趣。
动态链接涉及到如何找到所需的功能,你的程序只有一个引用。你的语言运行时或操作系统在文件系统、网络或已编译代码缓存中搜索匹配引用的代码段,然后采取多项措施将其集成到程序镜像中的内存中,例如重定位。这些都是在运行时完成的。可以手动或由编译器完成。有更新的能力,但也有出现问题的风险(即DLL地狱)。
静态链接是在编译时完成的,你告诉编译器所有功能部件的位置,并指示它将它们集成起来。没有搜索、没有歧义、不能更新而不重新编译。所有依赖关系都与程序镜像物理上相连。
.dll
或.so
扩展名)- 将答案视为解释概念而不是精确描述。正如文本所述,这是一个示例,仅显示C运行时文件的静态和动态链接,因此,是的,这就是所有这些中crt
的含义。 - paxdiablo