Posts 链接与装载总结
Post
Cancel

链接与装载总结

前言

C/C++程序需要经过代码编写、编译、链接等过程生成可执行文件,然后才能在目标机器上进行执行。Linux平台上可执行文件的格式为ELF (Executable Linkable Format),我们从ELF文件格式出发,对链接、装载过程中的一些细节进行总结。

初始ELF文件格式

字节跳动Android PLT hook 方案 ByteHook中看到一个很好的总览ELF文件格式的图,在此直接借用下:
ELF file format overview

ELF文件格式从链接视图和执行视图两种角度对文件数据进行划分:

  • 链接视图(Linking View):以 section 为单位组织数据
  • 执行视图(Execution View):以 segment 为单位组织数据

静态链接

链接过程

链接过程可以简单分为两步:

  • 第一步:空间与地址分配:虚拟地址的分配
    扫描所有的输入目标文件,并且获得它们的各个段的长度、属性和位置,并且将输入目标文件中的符号表中所有的符号定义符号引用收集起来,统一放到一个全局符号表中。
    这个过程还会涉及到相似段合并,比如将所有输入文件的“.text”合并到输出文件的“.text”,接着是“.data”段、“.bss”段等。
  • 第二步:符号解析与重定位
    使用第一步中收集到的信息,读取输入文件中段的数据、重定位信息,并且进行符号解析重定位、调整代码中的地址等
    Object File and Process Memory Space

对于同一个目标文件,其中各个段的起始虚拟地址不同,由相似段合并过程决定。

符号解析

重定位过程伴随着符号的解析过程。重定位的过程中,每个重定位的入口都是对一个符号的引用,当链接器要对某个符号的引用进行重定位时,它就要确定这个符号的目标地址,此时链接器会查找由所有输入目标文件的符号表组成的全局符号表,找到相应的符号进行重定位。

符号解析主要难点在于全局符号的解析。

强、弱符号与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值。
Dynamic linker data access inside module

类型3:模块外部的数据调用
基本思想:把跟地址相关的部分放在数据段里面。具体做法:在数据段里建立一个指向全局变量的指针数组,也称为全局偏移表(Global Offset Table,GOT),当代码需要引用该全局变量时,通过GOT相应的项间接引用。
Dynamic linker data access external module

类型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用来保存函数引用的地址。
Dynamic linker GOT PLT structure

动态链接相关结构

  • .interp段:可执行文件需要的动态链接器的地址。Linux下,几乎都是/lib/ld-linux.so.2,软链接到真正的动态链接器
  • .dynamic段:保存了动态链接器所需要的基本信息,比如依赖于哪些共享对象、动态链接符号表的位置、重定位表位置、共享对象初始化代码的地址等
  • .symtab/dynsym/strtab/dynstrdynsym保存了与动态链接相关的符号,.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)——链接、静态链接、动态链接
程序员的自我修养——链接、装载与库

This post is licensed under CC BY 4.0 by the author.

C++对象模型实践探索

-

Comments powered by Disqus.