链接(linking)是将各种代码和数据部分收集起来并组合成一个单一文件的过程,这个文件可被加载到存储器并执行。链接可以执行于编译时,也就是在源代码被翻译成机器码时;也可以执行于加载时,也就是在程序被加载器加载到存储器并执行时;甚至执行于运行时,由应用程序来执行。链接是由叫做链接器(linker)的程序自动执行的。
链接器在软件开发中扮演着一个关键的角色,因为它们使得分离编译成为可能。我们不用将一个大型的应用程序组织为一个巨大的源文件,而是可以把它分解为更小、更好管理的模块,可以独立地修改和编译这些模块。当我们改变这些模块中的一个时,只需要简单地重新编译它,并重新链接应用,而不必重新编译其他文件。
// main.c
void swap();
int buf[2] = {1, 2};
int main(){
swap();
return 0;
}
// swap.c
extern int buf[];
int *bufp0 = &buf[0];
int *bufp1;
void swap(){
int temp;
bufp1 = &buf[1];
temp = *bufp0;
*bufp0 = *bufp1;
*bufp1 = temp;
}
$ gcc -o p main.c swap.c
$ ./p
GCC将实例程序从ASCII源码文件翻译成可执行目标文件的行为概括如下图所示:

像 ld 程序这样的静态链接器以一组可重定位目标文件和命令行参数作为输入,生成一个完全链接的可以加载和运行的可执行目标文件作为输出。输入的可重定位目标文件由各种不同的代码和数据节组成。指令在一个节中,初始化的全局变量在另一个节中,而未初始化的变量又在另外一个节中。
目标文件纯粹是字节块的集合。这些块中,有些包含程序代码,有些则包含程序数据,而其它的则包含指导链接器和加载器的数据结构。链接器将这些块连接起来,确定被连接块的运行时位置,并且修改代码和数据块中的各种位置。
目标文件有三种类型格式:
- 可重定位目标文件。包含二进制代码和数据,其形式可以在编译时与其它可重定位目标文件合并起来,创建一个可执行目标文件。
- 可执行目标文件。包含二进制代码和数据,其形式可以被直接拷贝到存储器并执行。
- 共享目标文件。一种特殊类型的可重定位目标文件,可以在加载或者运行时被动态地加载到存储器并链接。
可重定位目标文件

ELF头以一个16字节的序列开始,这个序列描述了生成该文件的系统的字的大小和字节顺序。ELF头剩下的部分包含帮助链接器语法分析和解释目标文件的信息。其中包括ELF头的大小、目标文件的类型、机器类型、节头部表的文件偏移,以及节头部表中的条目大小和数量。不同节的位置和大小是由节头部表描述的,其中目标文件中每个节都有一个固定大小的条目(entry)。
夹在ELF头和节头部表之间的都是节:
- .text:已编译程序的机器代码。
- .rodata:只读数据,比如 printf 语句中的格式串和 switch 语句的跳转表。
- .data:已初始化的全局C变量。局部C变量在运行时保存在栈中。
- .bss:未初始化的全局C变量。在目标文件中这个节不占据实际的空间,它仅仅是一个占位符。目标文件格式区分初始化和未初始化变量是为了空间效率:在目标文件中,未初始化变量不需要占据任何实际的磁盘空间。
- .symtab:一个符号表,它存放那些在程序中定义和引用的函数与全局变量的信息。
- .rel.text:一个.text节中位置的列表,当链接器把这个目标文件和其它文件结合时,需要修改这些位置。
- .rel.data:被模块引用或定义的任何全局变量的重定位信息。一般而言,任何已初始化的全局变量,如果它的初始值是一个全局变量地址或者外部定义函数的地址,都需要被修改。
- .debug:一个调试符号表,其条目是程序中定义的局部变量和类型定义,程序中定义和引用的全局变量,以及原始的C源文件。只有以 -g 选项调用GCC时才会得到这张表。
- .line:原始C源程序中的行号和.text节中机器指令之间的映射。只有以 -g 选项调用GCC时才会得到这张表。
- .strtab:一个字符串表,其内容包括.symtab和.debug节中的符号表,以及节头部中的节名字。字符串表就是以null结尾的字符串序列。
所有的编译系统都提供一种机制,将所有相关的目标模块打包成一个单独的文件,称为静态库(static library),它可以用做链接器的输入。当链接器构造一个输出的可执行文件时,它只拷贝静态库里被应用程序引用的目标模块。
在Unix系统中,静态库以一种称为存档(archive)的特殊文件格式存放在磁盘中。存档文件是一组连接起来的可重定位目标文件的集合,有一个头部用来描述每个成员目标文件的大小和位置。存档文件名由后缀 .a 标识。
可执行目标文件

可执行目标文件的格式与可重定位目标文件的格式类似。ELF头描述文件的总体格式,它还包括程序的入口点(entry point),也就是当程序运行时要执行的第一条指令的地址。.text、.rodata、.data节和可重定位目标文件中的节是相似的,除了这些节已经被重定位到它们最终的运行时存储器地址以外。.init节定义了一个小函数,叫做 _init,程序的初始化代码会调用它。因为可执行文件是完全链接的(已被重定位了),所以它不再需要.rel节。
ELF可执行文件被设计得很容易加载到存储器,可执行文件的连续的片(chunk)被映射到连续的存储器段。段头部表(segment header table)描述了这种映射关系。
动态链接共享库
静态库解决了许多关于如何让大量相关函数对应用程序可用的问题,但是仍然存在一些缺陷。静态库和所有软件一样,需要定期维护和更新。如果应用程序员想要使用一个库的最新版本,他们必须以某种方式得到该库的更新情况,然后显示地将他们的程序与更新了了的库重新链接。另一个问题是,几乎每个C程序都使用标准I/O函数,如 printf 和 scanf 。在运行时,这些函数的代码会被复制到每个运行进程的文本段中。在一个运行50~100个进程的典型系统上,这将是对稀缺的存储器资源的极大浪费。
共享库(shared library)就是致力于解决静态库缺陷的一个产物。共享库是一个目标模块,在运行时,可以加载到任意的存储器地址,并和一个在存储器中的程序链接起来。这个过程称为动态链接(dynamic linking),是由一个叫做动态链接器(dynamic linker)的程序来执行的。
共享库也被称为共享目标(shared object),在Unix系统中通常用 .so 后缀来表示。微软的操作系统大量地利用了共享库,它们称为DLL(动态链接库)。
应用程序还可以在它运行时要求动态链接器加载和链接任何共享库,而无需在编译时链接那些库到应用中。动态链接是一项强大有用的技术,可以用来分发软件更新或构建高性能的Web(动态内容)服务器。
网友评论