链接是 gcc 编译过程的最后阶段。

在链接过程中,目标文件被链接在一起,所有对外部符号的引用都被解析,最终地址被分配给函数调用等。
在本文中,我们将主要关注 gcc 链接过程的以下几个方面:
目标文件以及它们如何链接在一起
代码重定位
在阅读本文之前,请确保您了解 C 程序在成为可执行文件之前必须经过的所有 4 个阶段(预处理、编译、汇编和链接)。
链接对象文件
让我们通过一个例子来理解这第一步。首先创建以下 main.c 程序。
$ vi main.c #include extern void func(void); int main(void) { printf("\n Inside main()\n"); func(); return 0; }
接下来创建以下 func.c 程序。在 main.c 文件中,我们通过关键字“extern”声明了一个函数 func(),并在一个单独的文件 func.c 中定义了这个函数
$ vi func.c void func(void) { printf("\n inside func()\n"); }
为 func.c 创建目标文件,如下所示。这将在当前目录中创建文件 func.o。
$ gcc -c func.c
同样为 main.c 创建目标文件,如下所示。这将在当前目录中创建文件 main.o。
$ gcc -c main.c
现在执行以下命令来链接这两个目标文件以生成最终的可执行文件。这将在当前目录中创建文件“main”。
$ gcc func.o main.o -o main
当您执行此“主”程序时,您将看到以下输出。
$ ./main Inside main() Inside func()
从上面的输出中可以清楚地看出,我们能够成功地将两个目标文件链接到最终的可执行文件中。
当我们将函数 func() 从 main.c 中分离出来并写在 func.c 中时,我们得到了什么?
答案是如果我们也将函数 func() 写在同一个文件中,这可能无关紧要,但想想我们可能有数千行代码的非常大的程序。对一行代码的更改可能会导致重新编译整个源代码,这在大多数情况下是不可接受的。因此,非常大的程序有时会分成小块,这些小块最终链接在一起以生成可执行文件。
在大多数情况下,适用于 makefile的make 实用程序都会发挥作用,因为该实用程序知道哪些源文件已被更改以及哪些目标文件需要重新编译。相应源文件未被更改的目标文件按原样链接。这使得编译过程非常容易和易于管理。
所以,现在我们明白了,当我们链接两个目标文件 func.o 和 main.o 时,gcc 链接器能够解析对 func() 的函数调用,并且当最终的可执行文件 main 执行时,我们会看到 printf()在正在执行的函数 func() 内部。
链接器在哪里找到函数 printf() 的定义?由于链接器没有给出任何错误,这肯定意味着链接器找到了 printf() 的定义。printf() 是在 stdio.h 中声明并定义为标准“C”共享库 (libc.so) 的一部分的函数
我们没有将此共享对象文件链接到我们的程序。那么,这是如何工作的呢?使用ldd工具一探究竟,它会打印出每个程序所需的共享库或命令行指定的共享库。
在“main”可执行文件上执行 ldd,它将显示以下输出。
$ ldd main linux-vdso.so.1 => (0x00007fff1c1ff000) libc.so.6 => /lib/libc.so.6 (0x00007f32fa6ad000) /lib64/ld-linux-x86-64.so.2 (0x00007f32faa4f000)
上面的输出表明主可执行文件依赖于三个库。上述输出中的第二行是“libc.so.6”(标准“C”库)。这就是 gcc 链接器能够解析对 printf() 的函数调用的方式。
第一个库是进行系统调用所必需的,而第三个共享库是加载可执行文件所需的所有其他共享库的库。该库将出现在每个依赖于任何其他共享库执行的可执行文件中。
在链接过程中,gcc 内部使用的命令很长,但对于用户而言,我们只需要编写即可。
$ gcc