从源代码(你写的)到可执行代码(你运行的)有两个阶段(在大多数情况下,不考虑解释代码)。
第一个是将源代码转换为目标模块的编译。
第二个,链接,是将对象模块组合在一起以形成可执行文件。
区别在于,除其他外,允许第三方库包含在您的可执行文件中,而您无需查看其源代码(例如用于数据库访问、网络通信和图形用户界面的库),或用于编译不同语言的代码(例如 C 和汇编代码),然后将它们链接在一起。
当您将文件静态链接到可执行文件时,该文件的内容将包含在链接时。换句话说,文件的内容被物理插入到您将运行的可执行文件中。
当您动态链接时,指向正在链接的文件的指针(例如文件的文件名)包含在可执行文件中,并且在链接时不包含所述文件的内容。只有当您稍后运行可执行文件时,才会购买这些动态链接的文件,并且它们只被购买到可执行文件的内存副本中,而不是磁盘上的那个。
它基本上是一种延迟链接的方法。还有一种更延迟的方法(在某些系统上称为后期绑定),在您实际尝试调用其中的函数之前,它不会引入动态链接文件。
静态链接的文件在链接时被“锁定”到可执行文件,因此它们永远不会改变。可执行文件引用的动态链接文件只需替换磁盘上的文件即可更改。
这允许更新功能而无需重新链接代码;每次运行时加载程序都会重新链接。
这既好又坏 - 一方面,它允许更轻松的更新和错误修复,另一方面,如果更新不兼容,它可能导致程序停止工作 - 这有时是某些人可怕的“DLL地狱”的原因提到如果您将动态链接库替换为不兼容的库,则应用程序可能会被破坏(顺便说一句,这样做的开发人员应该期望受到追捕和严厉惩罚)。
作为一个示例,让我们看看用户编译他们的 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
,并在目标文件中添加一个注释,说“必须用 printf 的内存位置替换这个 0x0000”。
静态链接是指链接器程序(GNU 称为 ld)将 printf
的机器代码直接添加到您的可执行文件中,并将 0x0000 更改为 printf
的地址。创建可执行文件时会发生这种情况。
动态链接意味着上述步骤不会发生。可执行文件仍然有一个注释,上面写着“必须将 0x000 替换为 printf 的内存位置”。每次程序运行时,操作系统的加载程序都需要找到 printf 代码,将其加载到内存中,并更正 CALL 地址。
程序通常会调用一些静态链接的函数(像 printf
这样的标准库函数通常是静态链接的)和其他动态链接的函数。当可执行文件运行时,静态的“成为”可执行文件的一部分,而动态的“加入”。
两种方法各有利弊,操作系统之间也存在差异。不过既然你没问,那我就到此结束吧。
ld
文档。
静态链接库在编译时链接。动态链接库在运行时加载。静态链接将库位烘焙到您的可执行文件中。动态链接仅包含对库的引用;动态库的位存在于其他地方,以后可以换掉。
因为上述帖子都没有真正展示如何静态链接某些东西并看到你做对了,所以我将解决这个问题:
一个简单的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 位 LSB 可执行文件,x86-64,版本 1 (SYSV),动态链接(使用共享库),适用于 GNU/Linux 2.6.26,BuildID[sha1]=0xf715572611a8b04f686809d90d1c0d75c6028f0f,未剥离”
这次让我们静态链接程序:
gcc simpleprog.c -static -o simpleprog
在这个静态链接的二进制文件上运行文件将显示:
file simpleprog
“simpleprog:ELF 64 位 LSB 可执行文件,x86-64,版本 1 (GNU/Linux),静态链接,用于 GNU/Linux 2.6.26,BuildID[sha1]=0x8c0b12250801c5a7c7434647b7dc65a644d6132b,未剥离”
你可以看到它是愉快地静态链接的。但遗憾的是,并非所有库都可以简单地以这种方式静态链接,并且可能需要使用 libtool
或手动链接目标代码和 C 库进行扩展。
幸运的是,许多嵌入式 C 库(如 musl
)为几乎所有如果不是全部库提供静态链接选项。
现在 strace
您创建的二进制文件,您可以看到在程序开始之前没有访问任何库:
strace ./simpleprog
现在与动态链接程序的 strace
的输出进行比较,您会发现静态链接版本的 strace 要短得多!
(我不知道 C#,但有一个 VM 语言的静态链接概念很有趣)
动态链接涉及知道如何找到您仅从程序中引用的所需功能。您的语言运行时或操作系统在文件系统、网络或编译代码缓存上搜索一段代码,匹配引用,然后采取多种措施将其集成到内存中的程序映像中,例如重定位。它们都是在运行时完成的。它可以手动完成,也可以由编译器完成。有能力进行更新,但有搞砸的风险(即 DLL 地狱)。
静态链接在编译时完成,您告诉编译器所有功能部分在哪里并指示它集成它们。没有搜索,没有歧义,没有重新编译就无法更新。您的所有依赖项在物理上与您的程序映像是一体的。
.dll
或.so
扩展名) - 将答案视为解释 concepts 而不是作为一个准确的描述。而且,根据正文,这是一个仅显示 C 运行时文件的静态和动态链接的示例,所以,是的,这就是 `crt 在所有文件中所表示的。