关于链接的相关重点描述
¶链接的两个步骤
- 符号解析:将每一个符号引用和符号定义联系起来
- 程序中 定义和引用的符号(包括变量和函数)
- 编译器(编译过程)将定义的符号存放在
符号表
中 - 编译器(编译过程)将符号的引用存放在重定位节(.rel.text rel.data)中
- 重定位
- 将多个代码段与数据段合并
- 计算定义的符号在虚拟地址空间中的绝对地址
¶符号解析(符号绑定)
¶符号类型
- 全局符号(global symblos)
- 非 static 函数及非 static 全局变量
- g++编译器将未初始化全局变量初始化为0,将之变为强符号
- 外部符号(external symbols)
- 仅仅declear的函数,带有extern 声明的变量
- 也就是从语法角度来看函数声明总是extern,变量不带extern的话g++就会将之视为全局符号
- 局部符号(local symbols)
- 带有static修饰的静态符号
- 不是局部变量,局部变量编译时不会放到任何节中去
¶多重定义符号处理
- 强符号不能多次定义
- 若一个符号被定义为弱及强,以强为准
- 存在多个弱定义则选择一个
¶概述
1.编译器会将静态局部变量解析为独一无二的符号,分配在.data/.bss中,因此链接器对于static修饰局部符号不做修改
2.对于外部符号,在符号表中表示为NDE,链接器会在其他输入单元中寻找,如果没有输出异常信息
3.对于全局符号来说,链接要判断的工作就是在其他输入单元中不能存在相同的符号名
- 符号解析的最终结果
- 将全局符号唯一化,也就是说最终的符号表中的全局符号都是唯一的
¶静态链接和符号解析
静态库(static library) .a(linux) .lib(window)
解析方式:链接器维护一个obj集合E,该集合最终被合并成可执行文件,未解析的符号(即外部符号)U,以及在前一个输入文件中定义的符号D
- 对于命令行中个每一个输入文件f,若为obj则加入E,修改U和D来反应f的符号定义和引用,并输入下一个f
- 若f为.a,则尝试匹配U中未定义的符号和.a中定义的符号.若a中存在obj文件m则将m加入,并且修改U和D来反映m的符号情况,依次对其他文件进行,知道U和D都不发生改变.
- 当链接器完成工作后,若U为非空则输出一个错误.否则则进行和并及重定向E中的目标文件,构建可执行文件.
解释:我之前一直在想所谓的符号解析到底对elf文件本身做了那些改变,这么来看实际上就是:
- 保证全局变量唯一性:即保证合并后的符号表每个符号唯一
- 处理未定义符号:即去在符号表中去除未定义符号,并合并
- 静态符号不改变
符号表就是给链接器进行符号解析处理的一个信息,它并不参与到程序代码的任意一处去,如此图中的代码段,对于汇编代码来说,这些 符号就是符号引用
,也就是要在链接过程得到真正的地址
,编译器会将这些引用作为所谓的重定向信息添加到.rel text / .rel data中
¶重定向
¶概述
- 重定位节和符号定义:
合并不同的节,如上图第二部分,当该聚合结构体构建后,那么在text,data段中的所有变量(data节) 函数(指令)都有了唯一的地址 - 重定位节中的符号引用:
即处理上图指令中的符号引用
,通过重定位条目处理
¶重定位条目
汇编器在汇编时并不知道数据和代码最终在内存中的实际位置,因此当遇到对最终位置位置的引用时,就会产生可重定位条目
.函数引用对应.rel.text 全局变量对应.rel.data.
1 | typedef struct |
r_offset表示该条目据离其所在text节偏移
r_addend表示该条目到下一条指令的附加
可重定位条目类型有32种,这里仅仅研究2种
- R_X86_64_PC32: 使用相对pc定位,具体参照
深入计算机原理
3.6.3节,关于跳转部分.
原理:相对跳转方式 call xx xx=下一条指令地址 - 目标地址
1 | int sum(int *a,int n); |
1 | 0000000000000000 <main>: |
12: e8 00 00 00 00 就是xx的占位符
在符号解析完毕后目标函数
的地址时确定的,下一条指令
=重定向地址+r_addend,两者之差就是xx所要填上的
- R_X86_64_32:绝对定位
如全局变量在符号定位后其位置就是绝对的,直接改变并调用就行了.
由于引用函数调用会改变pc数,才会采用相对pc跳转,否则也按照绝对就行了.
至此就完成了链接过程,在可执行文件
中不会带有.rel节,并且生成了program header
来描述每个section
所占段位置.
¶与动态库链接
静态链接弊端:
- 更新时要和要更新的库进行主动链接
- 所有使用了静态链接的程序都会将重复的代码 数据加载到内存中,浪费了内存空间
共享库(shared library):
- 在文件系统中,仅仅存在一个so文件,所有引用该库的
可执行目标文件
都共享该库的代码和数据,节省磁盘. - 在内存运行中,所有不同的进程都可以共享该库的.text代码,节省内存.
¶加载时链接
- 创建动态库 gcc -shared -fpic - o xx.so xx.c xx.c
- gcc -o program xx.c ./xx.so
此时ld(链接器)执行了部分链接工作,复制了so中的重定位和符号表信息,并没有进行text和data的复制工作,处理符号解析工作,确定了唯一全局符号. - 当加载器加载
可执行文件
时,将会调用动态链接器(在program header
中声明的.interp节表示的位置),该链接器也是一个共享库文件,此时动态链接器完成一下工作:- 重定位.so文件中的文本及数据到某个内存段
- 重定位program中的所有对动态库的引用
- 之后共享库的内存位置就固定了,并在程序执行过程中都不会改变
动态链接的情况下,不同的模块装载地址一样是不行的。对于一个单个程序,我们可以指定各个模块的地址,但是对于某个模块被多个程序使用,或者是多个模块被多个程序使用,那么就会产生冲突的情况,比如1个人指定A模块为0x1000-0x2000,另一个人不使用B模块,而且指定B模块地址为0x1000-0x2000,那么很明显,A与B两个模块无法同时存在,任何人不能再同一个程序内使用模块A与B。
¶运行时动态链接
位置无关代码(PIC)
- 共享库代码在加载时位置是可以不确定的
- 即使代码库长度改变,也不影响调用它的程序
- 无需修改程序代码就可将共享库加载到任意地址运行
引用情况: 程序对共享库的引用情况,以及共享库自身的引用情况
- 模块内部调用,采用pc相对偏移寻找
- 当动态链接时,动态模块会被分配到确定的内存位置
- 模块内部代码调用pc偏移量实际就是pc下一条位置和函数指令间距,所以即使该模块在任何地方,该偏移量固定.
- 无需重定位
- 模块内部数据访问
- 动态库文件是由编译器构成,因此对于动态库文件的汇编代码实现和一般的可重入文件是不同
- 这里也说明动态库内部文件没有进行节合并
3 模块外数据调用 (PIC)
4 模块外函数调用 (PIC)
此图中GOT[2]存放的是动态链接延迟绑定代码的地址,该代码做了重定位工作