前言
C/C++程序需要经过代码编写、编译、链接等过程生成可执行文件,然后才能在目标机器上进行执行。Linux平台上可执行文件的格式为ELF (Executable Linkable Format)
,我们从ELF文件格式出发,对链接、装载过程中的一些细节进行总结。
初始ELF文件格式
从字节跳动Android PLT hook 方案 ByteHook中看到一个很好的总览ELF文件格式的图,在此直接借用下:
ELF文件格式从链接视图和执行视图两种角度对文件数据进行划分:
链接视图(Linking View)
:以 section 为单位组织数据执行视图(Execution View)
:以 segment 为单位组织数据
静态链接
链接过程
链接过程可以简单分为两步:
第一步:空间与地址分配
:虚拟地址的分配
扫描所有的输入目标文件,并且获得它们的各个段的长度、属性和位置,并且将输入目标文件中的符号表中所有的符号定义
和符号引用
收集起来,统一放到一个全局符号表
中。
这个过程还会涉及到相似段合并
,比如将所有输入文件的“.text”合并到输出文件的“.text”,接着是“.data”段、“.bss”段等。第二步:符号解析与重定位
使用第一步中收集到的信息,读取输入文件中段的数据、重定位信息,并且进行符号解析重定位
、调整代码中的地址等
对于同一个目标文件,其中各个段的起始虚拟地址不同,由相似段合并
过程决定。
符号解析
重定位过程伴随着符号的解析过程。重定位的过程中,每个重定位的入口都是对一个符号的引用,当链接器要对某个符号的引用进行重定位时,它就要确定这个符号的目标地址,此时链接器会查找由所有输入目标文件的符号表组成的全局符号表
,找到相应的符号进行重定位。
符号解析主要难点在于全局符号的解析。
强、弱符号与COMMON块
强符号和弱符号都是针对定义
来说,不是针对符号的引用
。 C/C++语言来说,编译器默认函数和初始化了的全局变量文强符号(Strong Symbol
),未初始化的全局变量为弱符号(Weak Symbol
)。也可以通过GCC的 __attribute__((weak))
来定义任何一个强符号为弱符号。
弱符号机制允许同一个符号的定义存在于多个文件中
,而变量类型对于链接器来说是透明的,链接器只知道符号名字和符号占用空间的大小,并不知道类型是否一致
。可能会多个符号定义类型不一致,主要分三种情况:
- 情况1:两个或两个以上的强符号类型不一致。
- 情况2:有一个强符号,其他都是弱符号,出现类型不一致。
- 情况3:两个或两个以上弱符号类型不一致。
针对情况1,链接器不允许强符号被多次定义,链接器会报符号多重定义错误
。
针对情况2,链接器会选择强符号,但如果有弱符号大小大于强符号
,链接器会发出警告。
针对情况3,编译器和链接器支持一种叫COMMON块(Common Block)
的机制,链接器会选择占用空间大的符号。在目标文件
中,弱符号会被标记为COMMON类型变量,并不会存在于BSS段。只有当链接器确认弱符号最终类型后,才在最终输出文件的BSS段为其分配空间。总体来看,未初始化的全局变量最终是被放在BSS段。
至此,链接器处理多次定义的全局符号的规则总结如下:
- 规则1:不允许强符号被多次定义。如果有多个强符号定义,链接器报重复定义错误。
- 规则2:如果有一个强符号和多个弱符号同名,则选择强符号。如果弱符号占用空间比强符号大,链接器发出警告。
- 规则3:如果有多个弱符号同名,则通过Common块机制选择占用空间最大的那个符号。
链接时重定位
静态链接过程中的重定位为链接时重定位(Link Time Relocation)
,而动态链接时的重定位为装载时重定位(Load Time Relocation)
。
重定位过程涉及到的重要ELF段是重定位表(重定位段)
,ELF文件中每个需要被重定位的ELF段都需要有一个对应的重定位表,代码段.text
对应的重定位段为.rel.text
,数据段.data
对应的重定位段为.rel.data
。可以通过objdump -r
命令查看目标文件中的重定位表,该命令能查看目标文件的重定位信息,readelf -r
也能查看重定位信息,而且除了能查看目标文件,还能查看可执行文件或共享对象文件
的重定位信息。
静态链接指令修正方式主要有两种:
宏定义 |值|重定位修正方法 :———–:|:—|:——- R_386_32
|1| 绝对寻址修正 S+A
R_386_PC32
|2| 相对寻址修正 S+A-P
,相对位移调用指令
S = 符号的实际地址
A = 保存在被修正位置的值
P = 被修正的位置(相对于段开始的偏移量或者虚拟地址)
相对寻址修正
修正的是调用指令的下一条指令的偏移量。推导过程如下:
P - A + X = S
- 其中
P-A
为下一条指令的虚拟地址,X
为待修正的值
动态链接
动态链接主要是为了解决静态链接引起的地址浪费、程序更新困难的问题。Linux系统中,ELF动态链接文件被称为动态共享对象(DSO,Dynamic Shared Objects)
,一般以.so为后缀。
装载时重定位
上面已提到动态链接时的重定位为装载时重定位
,其基本思路是:在链接时,对所有绝对地址的引用都不作重定位,而把这一步推迟到装载时再完成。一旦模块装载地址确定,即目标地址确定,那么系统就对程序中所有的绝对地址引用进行重定位。
但是也存在问题:指令部分被多个进程共享,但是装载时重定位需要修改指令,因此没有办法做到同一份指令被多个进程共享。为了解决这个问题,地址无关代码(PIC, Position-independent Code)
技术被提出。
共享对象模块中的地址引用可划分为四种:
- 第1种:模块内部的函数调用。
- 第2种:模块内部的数据访问,如模块中定义的全局变量、静态变量。
- 第3种:模块外部的数据访问,如其他模块中定义的全局变量。
- 第4种:模块外部的函数调用。
类型1:模块内部的函数调用
参考相对位移调用指令的地址修正方式,但是共享对象存在全局符号介入(Global Symbol Interposition)
问题,因此实际上此类型也采用和类型4一样的调用形式。
类型2:模块内部的数据访问
数据的相对寻址没有相对于当前指令地址(PC)
的寻址方式,因此ELF采用一种巧妙的方式获得当前PC值。
类型3:模块外部的数据调用
基本思想:把跟地址相关的部分放在数据段里面。具体做法:在数据段里建立一个指向全局变量的指针数组
,也称为全局偏移表(Global Offset Table,GOT)
,当代码需要引用该全局变量时,通过GOT相应的项间接引用。
类型4:模块外部的函数访问
和类型3类似,只不过GOT表中存放的项是目标函数的地址
。
各种地址引用方式小结:
模块 |指令跳转、调用|数据访问 :———–:|:—|:——- 模块内部 |(1)相对跳转和调用| (2)相对地址访问(实际上也是使用的间接访问 GOT) 模块外部 |(3)间接跳转和调用(GOT)| (4)间接访问(GOT)
地址无关代码的生成:
-fpic
: 代码相对较小,较快;在某些平台有限制,比如全局符号的数量或者代码的长度-fPIC
: 代码较大
延迟绑定(PLT)
ELF程序在静态链接下比动态链接稍微快点(约1~5%),因为:
- 动态链接下对于全局和静态的数据访问都要进行复杂的GOT定位,然后间接寻址;
- 对于模块间的调用也要先定位GOT,在间接跳转;
- 动态链接的链接过程在运行时完成:寻找并装载所需的共享对象,符号查找地址重定位等过程;
延迟绑定(Lazy Binding)
用来提升动态链接速度,基本思想:当函数第一次被用到时才进行绑定(符号查找、重定位等),如果用不到则不进行绑定。 ELF使用PLT(Procedure Linkage Table)
方法来实现。
bar@plt
的实现:
1
2
3
4
5
bar@plt:
jmp *(bar@GOT)
push n
push moduleID
jump _dl_runtime_resolve
push n
中数字时bar符号在重定位表.rel.plt
中的下标,_dl_runtime_resolve
实际执行lookp(module, function)
查找符号地址,最终将bar()
的真正地址填入到bar@GOT
中。一旦bar()
函数解析完成,再次调用bar@plt
时,第一条jmp指令就能跳转到真正的bar()
函数。
ELF将GOT拆分为.got
和.got.plt
,其中.got
保存全局变量引用的地址,.got.plt
用来保存函数引用的地址。
动态链接相关结构
.interp
段:可执行文件需要的动态链接器的地址。Linux下,几乎都是/lib/ld-linux.so.2
,软链接到真正的动态链接器.dynamic
段:保存了动态链接器所需要的基本信息,比如依赖于哪些共享对象、动态链接符号表的位置、重定位表位置、共享对象初始化代码的地址等.symtab
/dynsym
/strtab
/dynstr
:dynsym
保存了与动态链接相关的符号,.symtab
往往保存了所有符号.rel.dyn
/.rel.plt
:动态链接重定位表,.rel.dyn
实际上是对数据引用的修正,修正的位置位于.got
及数据段;.rel.plt
是对函数引用的修正,修正的位置位于.got.plt
全局符号介入(Global Symbol Interpose)
当一个符号需要被加入全局符号表时,如果相同的符号名已经存在,则后加的符号被忽略
模块内部的函数调用,也视为模块外部函数调用
SO_NAME命名机制
记录共享库的依赖关系
文件装载过程
1)创建虚拟地址空间
实际上是分配一个页目录(Page Directory),虚拟空间到物理内存的映射关系2)读取可执行文件头,并建立虚拟空间与可执行文件的映射关系
虚拟空间与可执行文件的映射关系
当发生缺页中断
时,操作系统可以知道所需页在可执行文件的什么位置,然后将可执行文件从磁盘读取到内存中3)将CPU指令寄存器设置成可执行文件入口,启动执行
参考资料
字节跳动Android PLT hook 方案 ByteHook
计算机那些事(5)——链接、静态链接、动态链接
程序员的自我修养——链接、装载与库