第七章连接css
连接(linking)是将各类代码和数据部分收集起来并组合成为一个单一文件的过程,这个文件可被加载(或被拷贝)到存储并执行。
连接的时机程序员
连接器的关键角色:使分离编译称为可能。swift
7.1 编译器驱动程序函数
驱动程序的工做:一、运行C预处理器,将C源程序(.c)翻译成一个ASCⅡ码中间文件(.i);二、运行C编译器,将.i文件翻译成汇编文件(.s);三、运行汇编器,翻译成可重定位目标文件(.o);四、运行连接器程序,将各个部分组合,建立可执行程序。ui
7.2静态连接编码
连接器的两任务:一、符号解析:目标文件定义和引用符号。将每一个符号引用恰好和一;个符号定义联系起来;二、重定位:连接器经过把每一个符号定义与一个存储器位置联系起来,而后修改全部对这些符号的引用。spa
基本事实:目标文件纯粹是字节块的集合,操作系统
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
重定位由两步组成:
·重定位节和符号定义。在这一步中,连接器将全部相同类型的节合并为同一类型的新的聚合节。 ·重定位节中的符号引用。在这一步中,连接器修改代码节和数据节中对每一个符号的引用,使得它们指向正确的运行时地址。
当汇编器生成一个目标模块时,它并不知道数据和代码最终将存放在存储器中的什么位置。它也不知道这个模块引用的任何外部定义的函数或者全局变量的位置。代码的重定位条目放在.rel.text 中。已初始化数据的重定位
条目放在.rel.data 中。
中两种最基本的重定位类型:
R_386_PC32: 重定位一个使用32 位PC 相对地址的引用.一个PC 相对地址就是距程序计数器(PC) 的当前运行时值的偏移量。当CPU 执行一条使用PC 相对寻址的指令时,它就将在指令中编码的32 位值上加上PC 的当前运行时值,获得有效地址。 R_386_32: 重定位一个使用32 位绝对地址的引用。经过绝对寻址, CPU 直接使用在指令中编码的32 位值做为有效地址,不须要进一步修改。
执行目标文件的格式相似于可重定位目标文件的格式。ELF 头部描述文件的整体格式。它还包括程序的入口点(Centry point) ,也就是当程序运行时要执行的第一条指令的地址。
.init 节定义了一个小函数,叫作init ,程序的初始化代码会调用它。由于可执行文件是彻底连接的(已被重定位了),因此它再也不须要.rel。
要运行可执行目标文件p ,能够在Unix 外壳的命令行中输入它的名字:
unix> . /p
由于p 不是一个内置的外壳命令,因此外壳会认为p 是一个可执行目标文件,经过调用某个驻留在存储器中称为加载器(loader) 的操做系统代码来运行它.加载器将可执行目标文件中的代码和数据从磁盘拷贝到存储器中,而后经过跳转到程序的第一条指令或入口点来运行该程序。这个将程序拷贝到存储器并运行的过程叫作加截(loading) 。
在32 位Linu系统中,代码段老是从地址Ox08048000 处开始。数据段是在接下来的下一个4KB 对齐的地址处。运行时堆在读/写段以后接下来的第一个4KB 对齐的地址处,并经过调用malloc 库往上增加。
共享库是致力于解决静态库缺陷的一个现代创新产物。共享库是一个目标模
块,在运行时,能够加载到任意的存储器地址,并和一个在存储器中的程序连接起来。这个过程称为动态连接(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。定义的符号的引用。
#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 。
多个进程是如何共享程序的一个拷贝的呢?一种方法是给每一个共享库分配一个事先预备的专用的地址空间片(chunk) ,而后要求加载器老是在这个地址加载共享库。一种更好的方法是编译库代码,使得不须要连接器修改库代码就能够在任何地址加载和执行这些代码。这样的代码叫作与位置无关的代码.
不管咱们在存储器中的何处加载一个目标模块〈包括共享目标模块),数据段老是被分配成紧随在代码段后面。为了运用这个事实,编译器在数据段开始的地方建立了一个表,叫作全局偏移量.