计算机系统第七章——链接

链接器:为什么需要链接器 链接器如何工作
目标文件:可重定位目标文件
符号与符号解析:符号与符号表 符号解析过程 静态链接库
hello.c(text)->hello.i(把include的头文件插入源文件中,形成一个完整的源文件text)->hello.s(汇编文件,text)->可重定位目标程序(binary会引用一些库函数,如printf.o)->通过链接器,最终生成可执行文件hello(binary)
(1)预处理(cpp):在高级语言源程序中插入所有有#include命令指定的文件和用#define声明指定的宏
(2)编译(cc1):将预处理后的源程序文件编译生成相应的汇编语言程序
(3)汇编(as)由汇编程序将汇编语言源程序文件转换为可重定位目标文件
(4)链接(ld)由链接器将多个可重定位目标文件及库例程(如printf.o)链接起来,生成可执行文件

为什么要链接器
原因1:模块化
程序不用写成一个巨大的源文件,而是可以分成多个更小,更好管理的源文件(模块)
可以创建一些公用的函数库:例如数学库,标准C库等

原因2:效率
时间上:分别编译
一个源文件发生变化,则单独编译,然后重新链接
不必重编译其他无变化的源文件
可以同时编译多个文件
空间上:使用库,无需包含共享库所有代码
常用的函数可以聚合到一个文件中
选项一:静态链接
可执行文件和正在运行的内存映像仅包含它们实际使用的库代码
选项二:动态链接
可执行文件不包含库代码
在执行期间,可以在所有正在执行的进程中共享库代码的单个副本

一旦理解了链接:帮助编程者构建大型程序,帮助编程者避免危险的编程错误,帮助理解语言的作用域规则如何实现,帮助理解其他重要的系统概念:加载和运行程序,虚拟存储器,分页和存储器映射,能够利用共享库

静态链接:
静态链接器(UNIX ld程序)输入:一组可重定位的目标文件(.o)
输出:一个完全链接的可以加载运行的可执行目标文件

可重定位目标文件:由各种不同的代码和数据节组成,指令节,数据节(全局变量),BSS节(未初始化的全局变量,节省目标文件的体积)

链接器做了什么
1.符号解析:程序中有定义和引用的符号(包括变量和函数)
void swap(){…}//定义符号swap
swap();//引用符号swap
int *xp=&x//定义符号xp,引用符号x
编译器将符号的定义储存在一个符号表中
符号表是一个条目数组,每个条目包含符号的名称,长度和保存位置
在符号解析的步骤期间,链接器将每个符号的引用都与一个符号定义相关联

gcc单步编译:-E:生成预处理文件
-S:把预处理文件,源文件变成汇编文件
-c:生成可重定位目标文件”.o"
-o:指定输出文件file
-v:显示gcc内部编译各过程的命令行信息和编译器的版本
-l dir:在头文件的搜索路径列表中添加dir目录
-L dir:在库文件的搜索路径列表中添加dir目录
-static:强制使用静态链接库
-l library:连接名为library的库文件

2.重定位:将每一个符号定义与一个存储器的位置联系起来,将符号引用定位到对应的存储器的位置
把多个代码段与数据段分别合并为一个统一的代码段和数据段(从地址0开始)
将.o文件中的每个符号从相对位置重新定位到可执行文件中的最终绝对存储器地址
将这些符号在符号表中的位置信息更新为重定位后的位置信息

三种目标文件
可重定位目标文件(.o file):包含的代码和数据可以在编译时与其他可重定位目标文件合并起来,创建一个可执行目标文件
每个.o文件由对应的.c文件生成,代码和数据地址都从0开始

可执行目标文件(a.out file)
包含的代码和数据可以直接复制到内存并被执行,代码和数据地址为虚拟地址空间中的地址

共享的目标文件(.so file)
特殊的可重定位目标文件,可以在装入或运行时被动态加载到存储器并链接
Windows中叫做动态链接库

可重定位目标文件vs可执行目标文件
可重定位目标文件的地址是从0开始的偏移地址
可执行目标文件的地址是真实的虚拟内存地址

可执行和可链接格式(ELF)
目标文件的标准二进制格式
统一的格式:可重定位目标文件(.o)
可执行目标文件(a.out\COFF\PE\ELF)
共享目标文件(.so)
通用名:ELF二进制文件

可执行和可链接格式(ELF)两种视图
链接视图:可重定位目标文件
执行视图:可执行目标文件

计算机系统第七章——链接_第1张图片计算机系统第七章——链接_第2张图片

节是ELF文件中具有相同特征的最小可处理单位
.text节:代码
.data节:数据
.rodata:只读数据
.bss:未初始化数据

可执行目标文件由不同的段组成,描述节如何映射到存储段中,可多个节映射到同一段
例如:可合并.data节和.bss节,并映射到一个可读可写数据段中

可重定位目标文件格式:
计算机系统第七章——链接_第3张图片
ELF header:字长,字节序(大端/小端),文件类型(.o,exec,.so),机器类型等
.text section:已编译的机器代码(不允许修改)
.rodata section:只读数据,如printf格式串,switch跳转表等
.data section:已初始化的全局变量,注意局部变量运行时保存在栈中
.bss section:未初始化的全局变量,仅是占位符,不占用实际磁盘空间,目标文件格式区分初始化和非初始化是为了空间效率
.symtab section:符号表,函数和静态变量名,不含局部变量条目,节名和存放位置
.rel.text section:.text节的重定位信息,用于重新修改代码段的指令中的地址信息
.rel.data section:.data节的重定位信息,用于对被模块引用或定义的全局变量进行重定位的信息
.debug section(.line section):调试符号表(gcc -g)
.strtab section:包含.symtab和.debug节中符号及节名
Section header table(节头表):每个section的名称,偏移和大小

可执行目标文件格式:
计算机系统第七章——链接_第4张图片
与.o文件稍有不同
ELF头中字段e_entry给出执行程序时第一条指令的地址,而在可重定位文件中,此字段为0
多一个程序头表,也称段头表,是一个结构数组:页大小,虚地址内存段,段大小
多一个.init节,用于定义_init函数,该函数用来进行可执行目标文件开始执行时的初始化工作
少两个.rel节(无需重定位)

符号和符号表
每个可重定位目标模块m都有一个符号表,它包含了在m中定义和引用的所有符号。
有三种链接器符号
Global symbols(模块内部定义的全局符号,不带static)在C++中的public
由模块m定义并能被其他模块引用的符号,例如,非static的C函数和非static的C全局变量(指不带static的全局变量)
External symbols(外部定义的全局符号)
由其他模块定义并被模块m引用的全局符号
Local symbols(本模块的本地符号,带static)在c++中的private
仅由模块m定义和引用的本地符号。例如,在模块m中定义的带static的C函数和全局变量

链接器本地符号不是指程序中的局部变量(分配在栈中的临时性变量),链接器不关心这种局部变量
static等价于private,模块内可见
将变量加上static,就会将变量放入BSS节中,初始化为0
static局部变量就更改了存储模式,初始化一次,修改了之后,以后的引用用修改的值
static全局变量,只初始化一次,修改了之后,其他模块引用不变

目标文件的符号表
符号表(.symtab节)中每个条目的结构如下
typedef struct{
int name;//指向符号对应字符串在strtab表中的偏移
int value;//在对应section中的偏移量,可执行文件中是虚拟地址
int size;//符号对应目标所占字节数
char type:4,//符号对应目标的类型:数据,函数,节,源文件名
binding:4//符号对应目标是本地符号还是全局符号
char reserved;
char section;//符号对应目标所在的section的节头表索引,或伪节
}Elf_Symbol;
三个伪节:ABS表示不该被重定位;UNDEF表示未定义;COMMON表示未初始化数据(.bss),此时,value表示对齐要求,size给出最小大小

readelf可以读取ELF文件的信息
-s可以看符号表
-S可以看节头表
readelf -s main.o//显示可重定位目标文件符号表的信息
计算机系统第七章——链接_第5张图片
符号解析:
目的:将每个模块中引用的符号与某个目标模块中的符号定义建立关联
每个符号定义在代码段或数据段中都被分配了存储空间,将引用符号与对应符号定义建立关联后,就可在重定位时将引用符号的地址重定位为相关联的符号定义的地址
本地符号在本模块内定义并引用,因此,其解析较简单,只要与本模块内唯一的符号定义关联即可
全局符号(外部定义的,内部定义的)的解析涉及多个模块,故较复杂
符号的定义其实质是什么?是指符号被分配了虚拟地址空间。符号位函数名即指其代码所在区;符号为变量即指其占的静态数据区
全局符号处理的问题
编译器遇到一个不在当前模块定义的符号:
生成一个链接器符号表,交给链接器处理
链接器如果在所有输入模块中找不到这个被引用的符号,链接器输出错误信息并终止
多个目标文件可能定义相同的符号:
链接器标志一个错误,输出错误信息并终止
或按某种方法选择一个定义,抛弃其他定义
编译器,汇编器和链接器之间协作进行处理
程序员不清楚这个流程会很麻烦

全局符号的符号解析:编译时,编译器向汇编器输出的每个全局符号,不是强符号就是弱符号
函数名和已初始化的全局变量名是强符号
未初始化的全局变量名是弱符号

链接器的符号解析规则:
规则一:强符号只能被定义一次,否则链接错误
规则二:强符号覆盖同名的弱符号(若一个符号被定义为一次强符号和多次弱符号,则按强符号定义为准)
对弱符号的引用被解析为其同名的强符号
规则三:若有多个弱符号定义,则选择其中任意一个:使用命令gcc -fno -common链接时,会告诉链接器在遇到多个弱符号定义的全局符号时输出一条警告信息
计算机系统第七章——链接_第6张图片
多重定义全局符号的问题
尽量避免使用全局变量
一定要使用的话,就按以下规则使用:
尽量使用本地变量:static
定义全局变量要初始化
引用外部全局变量:extern
多重定义全局变量会造成一些意想不到的错误,而且是默默发生的,编译系统不会警告,并会在程序执行很久后才能表现出来,且远离错误引发处。特别是在一个具有几百个模块的大型软件中,这类错误很难修正
在头文件中使用宏,C预处理程序解析#include,插入.h文件的内容到源文件中(gcc -E)

重定位与库:
重定位:重定位条目,符号引用
目标文件:可执行目标文件格式,加载可执行目文件
库:静态链接,动态链接

重定位:
符号解析完成后,可进行重定位工作,由两步组成:
对节和符号定义进行重定位:将所有目标模块中相同的节合并成新的聚合节,并将运行时的虚拟地址赋给每个新节中所有定义的符号
例如,所有.text节合并作为可执行文件中的.text节,并为每个.text节确定在新.text节中的绝对地址,从而为其中定义的函数确定首地址(含有多个函数时),进而确定每条指令的地址
完成这一步后,每条指令和每个全局变量都有唯一的运行时地址

对节中的符号引用进行重定位
修改.text节和.data节中对每个符号的引用(指向正确的运行时地址)
需要用到在.rel_data和.rel_text节中保存的重定位信息

用命令readelf -r main.o可显示main.o中的重定位表项

objdump -t prom显示所有的符号

重定位条目:汇编器遇到对位置未知的目标引用时,生成一个重定位条目
数据引用的重定位条目在.rel_data节中
指令中引用的重定位条目在.rel_text节中
ELF中重定位条目的格式
typedef struct
{
int offset;//需重定位的引用的节偏移
int symbol:24,//需重定位的引用所指向的符号
type:8;//重定位类型(即修改方式)
}Elf32_Rel;
有两种最基本的重定位类型(11种重定位类型)
:R_386_PC32:使用32位PC相对地址的引用(PC为下条指令地址)
R_386_32:使用32位绝对地址
例如:在rel_text节中有重定位条目
offset:0x7
symbol:swap
type:R_386_PC32
说明
在.text节中偏移为0x7的地方需重定位
引用的符号为swap
按PC相对地址引用方式修改
objdumo -d(反汇编)r(加上重定位条目) -j .text(对代码段进行反汇编) main.o
readelf -Ss main.o
main的定义在.text节中偏移为0处开始,占18B
查看在rel_text节中的重定位条目为:
r_offset=0x7,r_sym=swap,r_type=R_386_PC32
dump出来后为"7:R_386_PC32 swap"
main.o中的符号表
swap是main.o的符号表中第十项,是未定义符号,类型和大小未知,并是全局符号,故在其他模块中定义
objdump -r -j .text main.o(只出来重定位条目)
readelf -r main.o命令也可以查看目标文件中的重定位信息

R_386_PC32的重定位方式(相对引用)
假定:可执行文件中main函数对应机器代码从0x80483ed开始
swap()紧跟main()后
则swap起始地址为多少?
0x80483ed+0x12=0x80483ff
则 重定位后call指令的机器代码是什么?
call转移目标地址=PC+偏移地址(重定位值)
PC=addr(main.text)+7-(fc ff ff ff)
PC=0x80483ed+0x07-(-4)=0x80483f8
重定位值=转移目标地址-PC=0x80483ff-0x80483f8=0x7
重定位后call指令的机器代码为"e8 07 00 00 00"
PC相对地址方式下,重定位值计算公式为
*refptr=ADDR(r_sym)-((ADDR(.text)+r_offset)-init)
引用目标处(swap) pc(call指令下条指令地址

重定位:
把指向PC的初始值:-4替换成从PC到swap实际地址的偏移值:7
计算机系统第七章——链接_第7张图片

你可能感兴趣的:(计算机系统基础)