链接(Linking)是将各种代码和数据部分收集起来并组合成为一个单一文件的过程,这个文件可被加载(或被拷贝)到存储器并执行。链接可以执行于:
- 编译时--原代码被翻译为机器代码时
- 加载时--程序被加载器(loader)加载到存储器并执行时
- 运行时--有应用程序来执行
链接使得 分离编译 成为可能。我们可以将大型应用程序分解为一些小模块,独立修改和编译。当我们修改其中一个小模块时,只需要重新编译它,并重新链接应用,而不必重新编译其他文件。链接通常是由链接器默默处理的,学习链接知识有助于:
- 构造大型程序
- 避免一些危险的编程错误
- 理解语言的作用域是如何实现的
- 其他重要的系统概念,如加载和运行程序、虚拟存储器、分页和存储器映射
- 利用共享库
静态链接
像Unix ld 程序这样的 静态连接器 以一组可重定位目标文件和命令行参数作为输入,生成一个完全链接的可以加载和运行的可执行目标文件作为输出。图7-2概括了驱动程序将源代码翻译为可执行目标文件时的行为。
为了构造可执行文件,链接器必须完成两个主要认任务:
- 符号解析。目标文件定义和引用符号,符号解析的目的是将每个符号引用刚好和一个符号定义联系起来。
- 重定位。编译器和汇编器生成从地址0开始的代码和数据节。链接器通过把每个符号定义与一个存储器位置联系起来,然后修改所有对这些符号的引用,使得它们指向这个存储器位置,从而
重定位这些节。
目标文件
目标文件有三种形式:
- 可重定位目标文件。包括二进制代码和数据,可以在链接时与其他可重定位目标文件合并起来,创建一个可执行目标文件。
- 可执行目标文件。包括二进制代码和数据,可以被直接拷贝到存储器执行。
- 共享目标文件。一种
特殊类型的可重定位目标文件,可以在加载或者运行时被动态的加载到存储器并链接。图7-3展示了一个典型的ELF可重定位目标文件的格式。
与静态库链接
目前为止,我们都假设链接器读取一组可重定位目标文件,并把它们链接起来,成为一个输出的可执行文件。实际上,所有的编译系统都提供一种机制,将所有相关的目标模块打包成为一个单独的文件,称为 静态库(static library)它可以用作链接器的输入。当链接器构造一个输出的可执行文件时,它只拷贝静态库中被应用程序引用的目标模块。
在Unix系统中,静态库以一种称为 存档(archive) 的特殊文件格式存放在磁盘中。存档文件是一组连接起来的可重定位目标文件的集合,有一个头部用来描述每个成员目标文件的大小和位置。存档文件由后缀 .a 标识。下面举个例子来说明,我们创建一个 libvector.a 的静态库。
/* vector.h */ void addvec(int*x, int* y, int* z, int n); void multvec(int*x, int* y, int* z, int n);
/* addvec.c */ void addvec(int* x, int* y, int* z, int n) { int i; for(i=0; i<n; i++){ z[i] = x[i] + y[i]; } }
/* multvec.c */ void multvec(int* x, int* y, int* z, int n) { int i; for(i=0; i<n; i++){ z[i] = x[i] * y[i]; } }
为了创建该库,我们使用AR工具,具体如下:
- gcc -c addvec.c multvec.c //编译后得到可重定位目标文件 addvec.o multvec.o
- ar rcs libvector.a addvec.o multvec.o //创建静态库 libvector.a
为了使用该库,我们编写一个应用如下:/* main.c */ #include<stdio.h> #include"vector.h" int x[2] = {1, 2}; int y[2] = {3, 4}; int z[2]; int main() { addvec(x,y,z,2); printf("z = [%d %d]\n", z[0], z[1]); return 0; }
为了创建这个可执行文件,我们还要编译和链接输入文件 main.o 和 libvector.a:
- gcc -c main.c
- gcc -static -o main main.o ./libvector.a
图7-7概括了链接器的行为。-static 参数告诉编译器驱动程序,链接器应该构建一个完全链接的可执行目标文件,它可以加载到存储器并运行,在加载时无需更进一步的链接。当链接器运行时,它判定 addvec.o 定义的 addvec 符号是被 main.o 引用的,所以它拷贝 addvec.o 到可执行文件。因为程序不引用任何由 multvec.o 定义的符号,所以链接器不会拷贝这个模块到可执行文件。链接器还会拷贝 libc.a 中的 printf.o 模块,以及许多 c 运行时系统中的其他模块。
可执行目标文件
我们已经看到链接器是如何将多个目标模块合并成一个可执行目标文件的。我们的 C 程序,开始时是一组 ASCII 文本文件,已经被转化为一个二进制文件,且这个二进制文件包含加载程序到存储器并运行它所需的所有信息。图 7-11 概括了一个典型的 ELF可执行文件中的各类信息。
可执行目标文件的格式类似于可重定位目标文件的格式。ELF头部描述文件的总体格式。它还包括程序的 入口点(entry point),也就是程序运行时要执行的第一条指令的地址。
要运行可执行目标文件 p,可以在 Unix 外壳的命令行输入它的名字:
- ./p
因为 p 不是一个内置的外壳命令,所以外壳会认为p是一个可执行目标文件,通过调用某个驻留在存储器中称为加载器(loader)的操作系统代码来运行它。任何 Unix 程序都可以通过调用 execve 函数来调用加载器。加载器将可执行目标文件中的代码和数据从磁盘拷贝到存储器中,然后通过跳转到程序的第一条指令或者 入口点(entry point)来运行该程序。 这个将程序拷贝到存储器并运行的过程叫做 加载(loading)。
动态链接共享库
上面提到的静态库解决了许多关于如何让大量相关函数对应用程序可用的问题,然而静态库有一些明显的缺点:
- 需要定期维护和更新
- 如果需要用库的最新版本,需要以某种方式了解该库的更新情况,然后显示的将程序与更新了的库重新链接
- 几乎每个c程序都是用标准 I/O 函数,如 printf 和 scanf。在运行时,这些函数的代码会被复制到每个运行进程的文本段中,这是对存储器系统资源的极大浪费。
共享库 是致力于解决静态库缺陷的一个创新产物。 共享库 是一个目标模块,在运行时,可以加载到任意的存储器地址,并和一个在存储器中的程序链接起来。这个过程成为 动态链接 。是由一个叫做 动态链接器 的程序来执行的。共享库也称 共享目标,在 Unix 系统中通常以 .so 后缀来表示。微软的操作系统大量利用了共享库,称为动态链接库DLL。
共享库是以两种不同的方式来共享的:
- 在任何给定的文件系统中,对于一个库只有一个 .so 文件, 所有引用该库的可执行目标文件共享这个 .so 文件中的代码和数据。而不是
像静态库的内容那样被拷贝和嵌入到引用它们的可执行文件中。- 在存储器中,一个共享库的 .text 节的一个副本可以被不同的正在运行的进程共享。
图7-15 概括了 main.c 程序的动态链接过程。为构造向量运算程序的共享库 libector.so,我们调用编译器,给链接器如下指令:
- gcc -shared -fPIC -o libvector.so addvec.c multvec.c // 生成共享库 libector.so
-fPIC 指编译器生成于位置无关的代码;-shared 指示链接器生成一个共享的目标文件
然后我们将这个库链接到 main.c 示例程序中:
- gcc -o main main.c ./libvector.so
这样就创建了一个可执行目标文件 main,而此文件的形式使得它在运行时可以和 libvector.so 链接。基本思路是当创建可执行文件时,静态执行一些链接,然后在程序加载时,动态完成链接过程。
此时:没有任何 libvector.so 的代码和数据节真的被拷贝到可执行文件 main 中。反之,链接器拷贝了一些重定位和符号表信息,它们使得运行时可以解析对 libvector.so 中代码和数据的引用。![]()
当加载器加载和运行可执行文件 main 时,加载 部分链接 的可执行文件 main。接着 它注意到 main 包含一个 .interp 节,这个节包含动态链接器的路径名,动态链接器本身就是一个共享目标。加载器此时不再像通常那样将控制传递给应用,而是加载和运行这个动态链接器。
然后,动态链接器通过执行下面的重定位完成链接任务:
- 重定位 libc.so 的文本和数据到某个存储器段。
- 重定位 libector.so 的文本和数据到另一个存储器段。
- 重定位 main 中所有对由 libc.so 和 libvector.so 定义的符号的引用。
最后,动态链接器将控制传递给应用程序。从这个时刻开始,共享库的位置就固定了,并且在程序执行的过程中都不会改变。
从应用程序中加载和链接共享库
到目前为止,我们已经知道了在应用程序执行之前,即应用程序被加载时,动态链接器加载和链接共享库的情景。然而,应用程序还可能
在它运行时要求动态链接器加载和链接任意共享库,而无需在编译时链接那些库到应用中。动态链接是一项强大有用的技术。下面是一些实际的例子:
- 分发软件。微软windows应用开发者常常利用共享库来分发软件更新。更新共享库后,下一次运行应用程序,应用将自动链接和加载新的共享库。
- 构建高性能Web服务器。生成web服务器的动态内容。
总结
链接可以在链接时由静态链接器来完成,也可以在加载和运行时由动态链接器来完成。
链接器处理成为目标文件的二进制文件,它有三种不同的形式:
- 可重定位的
- 可执行的
- 共享的 (一种特殊的可重定位的)
可重定位的目标文件由静态链接器合并成一个可执行的目标文件,它可以加载到存储器中并执行。共享目标文件是在运行时由动态链接器链接和加载的,或者隐含的在调用程序被加载和开始执行时,或者根据需要在程序调用dlopen库的函数时。
链接器的两个主要任务是 符号解析 和 重定位。符号解析将目标文件中的每个全局符号都绑定到一个唯一的定义,而重定位确定每个符号的最终存储器地址,并修改对那些目标的引用。
静态链接器是由像GCC这样的编译驱动程序调用的。它们将多个可重定位目标文件合并成一个单独的可执行文件。多个目标文件可以定义相同的符号,而链接器用来悄悄解析这些多重定义的规则可能在用户程序中引入微妙错误。
加载器将可执行文件的内容映射到存储器,并运行这个程序。链接器还可能生成部分链接的可执行目标文件,这样的文件中有对定义在共享库中的程序和数据的未解析的引用。在加载时,加载器将部分链接的可执行文件映射到存储器,然后调用动态链接器,它通过加载共享库和重定位程序中的引用来完成链接任务。