《深入理解计算机系统》读书笔记 第七章 链接
第七章链接
链接(linking)是将各种代码和数据部分收集起来并组合成为一个单一文件的过程,这个文件可被加载(或被拷贝)到存储并执行。
-
链接的时机
- 编译时,也就是在源代码被翻译成机器代码时
- 加载时,也就是在程序被加载器加载到存储器并执行时。
- 运行时,由应用程序执行。
- 在现代系统中,链接是由链接器自动执行的。
-
链接器的关键角色:使分离编译称为可能。
7.1 编译器驱动程序
驱动程序的工作:1、运行C预处理器,将C源程序(.c)翻译成一个ASCⅡ码中间文件(.i);2、运行C编译器,将.i文件翻译成汇编文件(.s);3、运行汇编器,翻译成可重定位目标文件(.o);4、运行链接器程序,将各个部分组合,创建可执行程序。
7.2静态链接
链接器的两任务:1、符号解析:目标文件定义和引用符号。将每个符号引用刚好和一;个符号定义联系起来;2、重定位:链接器通过把每个符号定义与一个存储器位置联系起来,然后修改所有对这些符号的引用。
基本事实:目标文件纯粹是字节块的集合,
7.3目标文件
三种形式:
l 可重定位目标文件:包含二进制代码和数据,可与其他可重定位目标文件合并起来,创建可执行目标文件。
l 可执行目标文件: 包含二进制代码和数据,其形式可以被直接拷贝到存储器并行。
l 共享目标文件:特殊类型的可重定位目标文件,可以在加载或者运行时被动态的加载到存储器并链接。
编译器和汇编器生成可重定位目标文件,链接器生成可执行程序。
7.4可重定位目标文件
ELF可重定位目标文件格式
. text: 已编译程序的机器代码。
.rodata: 只读数据,比如printf 语句中的格式串开关语句的跳转表
.data: 已初始化的全局C 变量
.bss: 未初始化的全局C 变量。在目标文件中这个节不占据实际的空间,它仅仅是一个占位符。
.symtab: 一个符号哀,它存放在程序中定义和引用典型的ELF 可重定位目标文件的函数和全局变量的信息。
. rel . text :一个.text 节中位置的列表,当链接器把这个目标文件和其他文件结合时,需要修改这些位置。
.rel.data: 被模块引用或定义的任何全局变量的重定位信息。
.debug: 一个调试符号表,其条目是程序中定义的局部变量和类型定义, 程序中定义和引用的全局变量,以及原始的C 源文件。
.line: 原始C 源程序中的行号和.text 节中机器指令之间的映射。
.strtab: 一个字符串表,其内容包括.symtab 和.debug 节中的符号表,以及节头部中的节名字。
7.5符号和符号表
每个可重定位目标模块m 都有一个符号表,它包含m 所定义和引用的符号的信息。在链接器的上下文中,有三种不同的符号:
·由m 定义并能被其他模块引用的全局符号。全局链接器符号对应于非静态的C以及被定义为不带C static 属性的全局变量。
·由其他模块定义并被模块m 引用的全局符号。这些符号称为外部符号,对应于定义在其他模块中的C 函数和变量。
·只被模块m 定义和引用的本地符号。有的本地链接器符号对应于带static 属性的C 函数和全局变量。这些符号在模块m 中随处可见,但是不能被其他模块引用。目标文件中对应于模块m 的节和相应的源文件的名字也能获得本地符号。
name 是字符串表中的字节偏移,指向符号的以null 结尾的字符串名字.value 是符号的地址。对于可重定位的模块来说, value 是距定义目标的节的起始位置的偏移。
每个符号都和目标文件的某个节相关联,由section 字段表示,该字段也是一个到节头部表的索引.有三个特殊的伪节(pseudo section) ,它们在节头部表中是没有条目的: ABS 代表不该被重定位的符号; UNDEF 代表未定义的符号,也就是在本目标模块中引用,但是却在其他地方定义的符号; COMMON 表示还未被分配位置的未初始化的数据目标.
7.6符号解析
链接器解析符号引用的方法是将每个引用与它输入的可重定位目标文件的符号表中的一个确定的符号定义联系起来.编译器只允许每个模块中每个本地符号只有一个定义。编译器还确保静态本地变量,也会有本地链接器符号,拥有唯一的名字。
根据强弱符号的定义, Unix 链接器使用下面的规则来处理多重定义的符号:
·规则1 :不允许有多个强符号。
·规则2 :如果有一个强符号和多个弱符号,那么选择强符号。
·规则3 :如果有多个弱符号,那么从这些弱符号中任意选择一个。
所有的编译系统都提供一种机制,将所有相关的目标模块打包成为一个单独的文件,称为静态库(static ),它可以用做链接器的输入。将所有的标准C 画数都放在一个单独的可重定位目标模块中(如libc中),应用程序员可以把这个模块链接到他们的可执行文件中:
unix> gcc main.c /usr/lib/libc.o
一个很大的缺点是系统中每个可执行文件现在都包含着一份标准函数集合的完全拷贝,这对磁盘空间是很大的浪费。我们可以通过为每个标准函数创建一个独立的可重定位文件,把它们存放在一个为大家都知道的目录中来解决其中的一些问题.
unix> gcc main.c /usr/lib/printf.o /usr/lib/scanf.o ...
在Unix 系统中,静态库以一种称为存档(archive) 的特殊文件格式存放在磁盘中。。存档文件是一组连接起来的可重定位目标文件的集合,有一个头部用来描述每个成员目标文件的大小和位置。存档文件名由后缀.a 标识。为了创建这个可执行文件,我们要编译和链接输入文件main.o
unix> gcc -02 -c main2 .c
unix> gcc -static -0 p2 main2 .o ./libvector.a
7.7重定位
重定位由两步组成:
·重定位节和符号定义。在这一步中,链接器将所有相同类型的节合并为同一类型的新的聚合节。
·重定位节中的符号引用。在这一步中,链接器修改代码节和数据节中对每个符号的引用,使得它们指向正确的运行时地址。
当汇编器生成一个目标模块时,它并不知道数据和代码最终将存放在存储器中的什么位置。它也不知道这个模块引用的任何外部定义的函数或者全局变量的位置。代码的重定位条目放在.rel.text 中。已初始化数据的重定位
条目放在.rel.data 中。
中两种最基本的重定位类型:
R_386_PC32: 重定位一个使用32 位PC 相对地址的引用.一个PC 相对地址就是距程序计数器(PC) 的当前运行时值的偏移量。当CPU 执行一条使用PC 相对寻址的指令时,它就将在指令中编码的32 位值上加上PC 的当前运行时值,得到有效地址。
R_386_32: 重定位一个使用32 位绝对地址的引用。通过绝对寻址, CPU 直接使用在指令中编码的32 位值作为有效地址,不需要进一步修改。
7.8 可执行目标文件
执行目标文件的格式类似于可重定位目标文件的格式。ELF 头部描述文件的总体格式。它还包括程序的入口点(Centry point) ,也就是当程序运行时要执行的第一条指令的地址。
.init 节定义了一个小函数,叫做init ,程序的初始化代码会调用它。因为可执行文件是完全链接的(已被重定位了),所以它不再需要.rel。
7.9 加载可执行目标文件
要运行可执行目标文件p ,可以在Unix 外壳的命令行中输入它的名字:
unix> . /p
因为p 不是一个内置的外壳命令,所以外壳会认为p 是一个可执行目标文件,通过调用某个驻留在存储器中称为加载器(loader) 的操作系统代码来运行它.加载器将可执行目标文件中的代码和数据从磁盘拷贝到存储器中,然后通过跳转到程序的第一条指令或入口点来运行该程序。这个将程序拷贝到存储器并运行的过程叫做加截(loading) 。
在32 位Linu系统中,代码段总是从地址Ox08048000 处开始。数据段是在接下来的下一个4KB 对齐的地址处。运行时堆在读/写段之后接下来的第一个4KB 对齐的地址处,并通过调用malloc 库往上增长。
7.10动态链接共事库
共享库是致力于解决静态库缺陷的一个现代创新产物。共享库是一个目标模
块,在运行时,可以加载到任意的存储器地址,并和一个在存储器中的程序链接起来。这个过程称为动态链接(dynamic linking) ,是由一个叫做动态链接器(dynamic linker) 的程序来执行的。
共享库也称为共享目标 ,在Unix 系统中通常用. s。后缀来表示。微软的操作系统大量地利用了共享库,它们称为DLL (动态链接库)。
首先,在任何给定的文件系统中,对于一个库只有一个.5。文件。所有引用该库的可执行目标文件共享这个.5。文件中的代码和数据,而不是像静态库的内容那样被拷贝和嵌人到引用它们的可执行的文件中。其次,在存储中,一个共享库的.text 节的一个副本可以被不同的正在运行的进程共享.
给链接器如下特殊指令:
unix> gcc -sbared -ÎPIC -0 libvector.so addvec.c multvec.c//-fPIC 选项指示编译器生戚与位置无关的代码 -5hared 选项指示链接器创建一个享的目标文件。
unix> gcc -0'p2 main2 .c ./libvector.so//这样就创建了一个可执行目标文件p2 ,而此文件的形式使得它在运行时
然后,动态链接器通过执行下面的重定位完成链接任务:
.重定位libc.5 。的文本和数据到某个存储器段。
·重定位lib飞Tector.5 。的文本和数据到另一个存储器段。
·重定位p2 中所有对由libc.5。和libvector.5。定义的符号的引用。
7.11 从应用程序中加载和链接共享库
#include <dlfcn.h>
void *dlopen(const char *filename , int flag);
返回若成功则为指向句柄的指针,若出错则为NULL 。
dlopen 函数加载和链接共享库filename 。用以前带RTLD GLOBAL 选项打开的库解析filename 中的外部符号。如果当前可执行文件是带rdynamic 选项编译的,那么对符号解析而言,它的全局符号也是可用的。
#include <dlfcn.h>
void *dlsym(void *handle , char *symbol);
返回若成功为指向符号的指针,若出错则为NULL 。
dlsym 函数的输入是一个指向前面已经打开共享库的句柄和一个符号名字,如果该符号存在,就返回符号的地址,否则返回NULL 。
#include <dlfcn.h>
int dlclose (void *handle);
返回:若成功为0 ,若出错则为1.
如果没有其他共享库正在使用这个共享库, dlclose 函数就卸载该共享库。
#include <dlfcn.h>
const char *dlerror(void);
返回如采前面对dlopen 、dlsym 或dlclose 的调用失败,则为错误消息,如果前面的调用成功,则为NULL.
dlerror 函数返回一个字符串,它描述的是调用dlopen 、dlsym 或者dlclose 函数时发生的最近的错误,如果没有错误发生,就返回NULL 。
7.12 与位置无关的代码
多个进程是如何共享程序的一个拷贝的呢?一种方法是给每个共享库分配一个事先预备的专用的地址空间片(chunk) ,然后要求加载器总是在这个地址加载共享库。一种更好的方法是编译库代码,使得不需要链接器修改库代码就可以在任何地址加载和执行这些代码。这样的代码叫做与位置无关的代码.
无论我们在存储器中的何处加载一个目标模块〈包括共享目标模块),数据段总是被分配成紧随在代码段后面。为了运用这个事实,编译器在数据段开始的地方创建了一个表,叫做全局偏移量.