第七章——链接
1、链接是将各种代码和数据部分收集起来并组合成为一个单一文件的过程,这个文件可被加载到存储器并执行。
2、链接可以执行于编译时,加载时,运行时。
7.1编译器驱动程序
1、大多数编译系统提供编译驱动程序,它代表用户在需要时调用语言预处理器、编译器、汇编器和链接器。
7.2 静态链接
1、像Unix ld程序这样的静态链接器以一组可重定位目标文件和命令行参数作为输入,生成一个完全链接的可以加载运行的可执行目标文件作为输出。
2、输入的可重定位目标文件由各种不同的代码和数据节组成。
为了构造可执行文件,链接器必须完成两个主要任务:
(1)符号解析。目标文件定义和引用符号。符号解析的目的是将每个符号引用刚好和一个符号定义联系起来。
(2)重定位。编译器和汇编器生成从地址0开始的代码和数据节。
7.3 目标文件
1、目标文件有三种形式:
(1)可重定位目标文件;
(2)可执行目标文件;
(3)共享目标文件;
2、编译器和汇编器生成可重定位目标文件(包括共享目标文件)。链接器生成可执行目标文件
3、各个系统之间,目标文件格式都不相同。直到今天,可执行文件仍然被称为a.out文件。
7.4 可重定位目标文件
1、典型的ELF可重定位目标文件的格式
7.5符号和符号表
1、每个可重定位目标模块m都有一个符号表,它包含m所定义和引用的符号的信息。
2、在链接器的上下文中,有三种不同的符号:
(1)由m定义并能被其他模块引用的全局符号;
(2)由其他模块定义并被模块m引用的全局符号(外部符号);
(3)只被模块m定义和引用的本地符号;
7.6符号解析
1、链接器解析符号引用的方法是将每个引用与它输入的可重定位目标文件的符号表中的一个确定的符号定义联系起来。
7.6.1 链接器如何解析多重定义的全局符号
1、在编译时,编译器向汇编器输出每个全局符号,或者是强或者是弱,而汇编器把这个信息隐含的编码在可重定位目标文件的符号表里。
2、函数和已初始化的全局变量是强符号,未初始化的全局变量是弱符号。
3、根据强弱符号的定义,Unix链接器使用下面的规则来处理多重定义的符号:
规则1:不允许有多个强符号。
规则2:如果有一个强符号和多个弱符号,那么选择强符号。
规则3:如果有多个弱符号,那么从这些弱符号中任意选择一个。
7.6.2 与静态库链接
1、所有的编译系统都提供一种机制,将所有相关的目标模块打包成为一个单独的文件,称为静态库。当链接器构造一个输出的可执行文件时,它只拷贝静态库里被应用程序引用的目标模块。
2、在链接时,链接器将只拷贝程序引用的目标模块。
3、在Unix系统中,静态库以一种称为存档的特殊文件格式存放在磁盘中。
4、存档文件是一组连接起来的可重定位目标文件的集合。存档文件名由后缀.a标识。
5、链接时加上-static参数告诉编译器驱动程序,链接器应该构建一个完全链接的可执行目标文件,它可以加载到存储器并执行,在加载时无需更进一步的链接。
7.6.3 链接器如何使用静态库来解析引用
1、在符号解析的阶段,链接器从左到右按照他们在编译器驱动程序命令行上出现的相同的顺序来扫描可重定位目标文件和存档文件。(驱动程序自动将命令行中所有的.c文件翻译为.o文件)
2、在这次扫描中,链接器维持一个可重定位目标文件的集合E(这个集合中的文件会被合并起来形成可执行文件),一个未解析的符号(即引用了但是尚未定义的符号)集合U,以及一个在前面输入文件中已定义的符号集合D。初始时,E、U和D都是空的。
7.7 重定位
1、重定位由两步组成:
(1)重定位节和符号定义
(2)重定位节中的符号引用
2、无论何时汇编器遇到对最终位置未知的目标引用,它就会生成一个重定位条目,告诉链接器在将目标文件合并成可执行文件时如何修改这个引用。
3、代码的重定位条目放在.rel.text中,已初始化数据的重定位条目放在.rel.data中。
4、PC值通常是存储器中下一条指令的地址。
7.8 可执行目标文件
1、C程序开始时是一组ASCII文本文件,已经被转化为一个二进制文件,且这个二进制文件包含加载程序到存储器并运行它所需的所有信息。
2、典型的ELF可执行文件中的各类信息:
3、可执行目标文件的格式类似于可重定位目标文件的格式。ELF头部描述文件的总体格式。它还包括程序的入口点,也就是当程序运行时要执行的第一条指令的地址。
7.9 加载可执行目标文件
1、加载器将可执行目标文件中的代码和数据从磁盘拷贝到存储器中,然后通过跳转到程序的第一条指令入口点来运行该程序。这个将程序拷贝到存储器并运行的过程叫做加载。
2、在32位Linux系统中,代码段总是从地址0x08048000处开始。
3、数据段是在接下来的下一个4KB对齐的地址处。运行时堆在读/写段之后接下来的第一个4KB对齐的地址处,并通过调用malloc库往上增长。
4、还有一个段是为共享库保留的。
5、用户栈总是最大的合法用户地址开始,向下增长的(向低存储器地址方向增长)。从栈的上部开始的段是为操作系统驻留存储器的部分(内核)的代码和数据保留的。
7.10 动态链接共享库
1、静态库和所有的软件一样,需要定期维护和更新。
2、共享库是致力于解决静态库缺陷的一个现代创新产物。
3、共享库是一个目标,在运行时,可以加载到任意的存储器地址,并和一个在存储器中的程序链接起来。这个过程称为动态链接,是由一个叫做动态链接器(dynamic linker)的程序来执行的。
4、共享库也称为共享目标,在Unix系统中通常用.so后缀来表示,微软的操作系统大量的利用了共享库,它们称为DLL(动态链接库)。
5、共享库是以两种不同的方式来“共享”的。首先,在任何给定的文件系统中,对于一个库只有一个.so文件。所有引用该库的可执行目标文件共享这个.so文件中的代码和数据,而不是像静态库的内容那样被拷贝和嵌入到引用它们的可执行的文件中。其次,在存储器中,一个共享库的.text节的一个副本可以被不同的正在运行的进程共享。
(1)一种共享方式就是隐式链接,基本的思路:当创建可执行文件时,静态执行一些链接,然后在程序加载时,动态完成链接过程。
(2)一种共享方式就是“显式链接”,应用程序被加载时,动态链接器加载和链接共享库的情景。应用程序还可能在它运行时要求动态链接器加载和链接任意共享库,而无需在编译时链接那些库到应用中。
5、动态链接器通过执行下面的重定位完成链接任务:
(1) 重定位libc.so的文本和数据到某个存储器段。
(2) 重定位libvector.so的文本和数据到另一个存储器段。
(3) 重定位p2中所有对由libc.so和libvector.so定义的符号的引用。
7.11 从应用程序中加载和连接共享库
1、dlopen函数加载和链接共享库filename。
2、dlsym函数的输入是一个指向前面已经打开共享库的句柄和一个符号名字,如果该符号存在,就返回符号的地址,否则返回NULL。
3、如果没有其他共享库正在使用这个共享库,dlclose函数就卸载该共享库。
4、dlerror函数返回一个字符串,它描述的是调用dlopen、dlsym或者dlclose函数时发生的最近的错误,如果没有错误发生,就返回NULL。
7.12 与位置无关的代码(PIC)
1、共享库的一个主要目的就是允许多个正在运行的进程共享存储器中相同的库代码,因而节约宝贵的存储器资源。
2、不需要链接器修改库代码就可以在任何地址加载和执行这些代码。这样的代码叫做与位置无关的代码(PIC)。
3、ELF编译系统使用一种有趣的技术,叫做延迟绑定,将过程地址的绑定推迟到第一次调用该过程时。
4、延迟绑定是通过两个数据结构之间简洁但又有些复杂的交互来实现的,这两个数据结构是:GOT和过程链接表(PLT)。GOT是.data节的一部分,PLT是.text节的一部分。
7.13 处理目标文件的工具
1、在Unix系统中有大量可用的工具可以帮助理解和处理目标文件。
AR:创建静态库,插入、删除、列出和提取成员。
STRINGS:列出一个目标文件中所有可打印的字符串。
STRIP:从目标文件中删除符号的信息。
NM:列出一个目标文件的符号表中定义的符号。
SIZE:目标文件中节的名字和大小。
READELF:显示一个目标文件的完整结构,包括ELF头中的编码的所有信息。包含SIZE和NM的功能。
OBJDUMP:所有二进制工具之母,能够显示一个目标文件中所有的信息。它最大的作用是反汇编.text节中的二进制指令。
LDD:列出一个可执行文件在运行时所需要的共享库。
7.14 小结
1、链接可以在编译时由静态编译器来完成,也可以在加载时和运行时由动态链接器来完成。
2、链接器处理称为目标文件的二进制文件,它有三种不同的形式:可重定位的、可执行的和共享的。
3、链接器的两个主要任务是符号解析和重定位。
4、静态链接器是由像GCC这样的编译驱动器调用的。
5、多个目标文件可以被连接到一个单独的静态库中。
6、加载器将可执行文件的内容映射到存储器,并运行这个程序。
7、被编译为位置无关代码的共享库可以加载到任何地方,也可以在运行时被多个进程共享。