链接是如何工作的?

8
我理解的编译过程如下:
1)预处理:所有宏都会被替换为实际值,所有注释都会被删除等。将 #include 语句替换为所包含文件的文字内容。
2)编译:不深入探讨此过程,结果是针对特定架构的汇编文件。
3)汇编:将汇编文件转换为二进制指令即机器码。
4)链接:这就是我困惑的地方。在这一步中,你已经有了一个可执行文件。但如果你真正运行该可执行文件会发生什么?问题在于你可能已经包含了 *.h 文件,而这些文件只包含函数原型。因此,如果你从这些文件调用其中一个函数,它将没有定义并且你的程序将崩溃。
如果是这种情况,那么链接到底是做什么的呢?它是如何找到与所包含的 .h 文件相关联的 .c 文件,并将其注入到你的机器码中的?难道它不必再次经历整个编译过程吗?
现在,我已经了解到有两种类型的链接,动态链接和静态链接。静态链接是当你为每个可执行文件重新编译库源代码时发生的吗?我不太理解动态链接如何工作。所以你编译一个可执行库,所有使用它的进程都共享它?这究竟是如何实现的呢?它不会在试图访问它的进程的地址空间之外吗?此外,对于动态链接,你不仍然需要在某个时间点编译库吗?它只是一直在内存中等待使用吗?它是在何时编译的?
请你阅读上述内容并澄清所有误解、错误假设,并用正确的解释代替。

可能是实践中C++链接的工作原理是什么?的重复问题。 - Ciro Santilli OurBigBook.com
1个回答

17
此时,您拥有一个可执行文件。 不,此时您拥有的是目标文件,它们本身并不可执行。 但是,如果您实际运行该可执行文件会发生什么呢? 类似于这样的情况:
h2co3-macbook:~ h2co3$ clang -Wall -o quirk.o quirk.c -c
h2co3-macbook:~ h2co3$ chmod +x quirk.o
h2co3-macbook:~ h2co3$ ./quirk.o
-bash: ./quirk.o: Malformed Mach-o file

我告诉过你了,这不是一个可执行文件。

问题在于你可能包含了 *.h 文件,而这些文件只包含函数原型吗?

相当接近。一个翻译单元(.c 文件)一般被转换为代表它所做的汇编/机器代码。如果它调用一个函数,那么文件中会有对该函数的引用,但没有定义。

那么,如果你实际上从这些文件中调用其中一个函数,它将没有定义,你的程序将崩溃?

正如我所说,它甚至无法运行。让我重复一遍:一个目标文件是不可执行的。

链接究竟是做什么的?在幕后,它是如何找到与您包含的 .h 相关的 .c 文件的呢?

它不会。它会查找由 .c 文件生成的其他对象文件,最终还会查找库(本质上只是其他对象文件的集合)。

它之所以能找到它们,是因为你告诉它去找。假设你有一个项目,其中包括两个调用彼此函数的 .c 文件,这是行不通的:

gcc -c file1.c -o file1.o
gcc -c file2.c -o file2.o
gcc -o my_prog file1.o

它将会因为链接错误而失败:链接器找不到在file2.c(和file2.o)中实现的函数定义。但是这样做可以解决问题:

gcc -c file1.c -o file1.o
gcc -c file2.c -o file2.o
gcc -o my_prog file1.o file2.o

对象文件包含存根引用(通常以函数入口点地址或明确的、可读的名称形式)调用它们的函数。然后,链接器查看每个库和对象文件,找到引用(如果找不到函数定义,则抛出错误),然后用实际的“调用此函数”的机器代码指令替换存根引用。(是的,这大大简化了,但如果您没有询问特定的体系结构和特定的编译器/链接器,很难更精确地说明...)
不是。静态链接意味着库的对象文件的机器代码实际上被复制/合并到最终的可执行文件中。动态链接意味着一个库只加载一次到内存中,然后在启动可执行文件时由操作系统解析前面提到的存根函数引用。库中的任何机器代码都不会被复制到最终的可执行文件中。(因此,在工具链中的链接器只完成了部分工作。)
以下内容可能有助于您实现启蒙:如果您静态链接可执行文件,它将是自包含的。它可以在任何地方运行(无论如何,在兼容的架构上)。如果您动态链接它,它只会在机器上运行,如果该特定机器安装了程序引用的所有库。
“那么,您编译一个可由使用它的所有进程共享的可执行库?这到底是怎么做到的呢?它不会超出试图访问它的进程的地址空间吗?”
操作系统的动态链接器/加载器组件会处理所有这些问题。
“此外,对于动态链接,您仍然需要在某个时刻编译库吗?”
正如我已经提到的那样:是的,它已经编译好了。然后在某个时间点(通常是第一次使用时)加载到内存中。
“它是什么时候编译的?”
在它被使用之前的某个时间。通常,库被编译,然后安装到系统上的某个位置,以便操作系统和编译器/链接器知道它的存在,然后您可以开始编译(嗯,链接)使用该库的程序。不早于此。

1
太棒了!谢谢。当然,如果您愿意,我现在有跟进问题:
  • 您说如果它调用一个函数,那么将会有一个对函数的引用。因此,目标文件只包含可以在链接时解析的某种符号?它不是实际的机器指令吗?就像它只是说“printf”之类的东西?因为它无法预先知道该函数在内存中的位置...
- ordinary
在动态链接范例中,根据您的解释,最终的可执行文件似乎会有未解决的符号。那么当您将程序加载到内存中时,这是否意味着在实际加载程序之前,操作系统会进行中间步骤,在其中查看所有未解决的符号并为这些函数提供字面地址?如果调用不存在的函数会发生什么?故障点在哪里? - ordinary
1
@ordinary 是的,它包含存根符号(“符号”--这就是它们被称为的原因之一)。这也是为什么这些文件实际上不能被执行的部分原因。 - user529758
1
是的,动态链接可执行文件包含未解析的符号。这些符号在 CPU 实际跳转到您的 main() 函数之前由操作系统(通过其动态链接器/动态加载器组件)进行解析。 - user529758
1
@ordinary 它是预编译的。它在标准库实现中,通常随操作系统或编译器一起提供。 - user529758
显示剩余4条评论

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