深入理解链接过程
如果追忆会荡起涟漪,那么今天的秋红落叶和晴空万里都归你
本文来自微信公众号:技术乱舞
艾恩凝
大家晚上好,赶在月底又来了,没更新的日子都在偷偷卷!
想要更加深入的理解程序链接的过程,那么这篇文章就是我们需要的。
链接,是将各种代码和数据片段收集并组合成一个单一文件的过程,这个文件可以被加载到内存并执行。
首先来看下代码编译过程如下
本文重点关注链接过程。
静态链接
静态链接器以一组可重定位目标文件和命令行参数作为输入,生成一个完全链接、可以加载和运行的可执行文件作为输出。其中可重定位目标文件由各种不同的代码和数据节组成,如data段bss段等。
上图.o文件就是可执行目标文件
目标文件有三种形式:
1、可重定位目标文件,包含二进制代码和数据,可与其他可重定位目标文件生成一个可执行文件
2、可执行目标文件,包含二进制代码和数据,可以直接复制到内存执行
3、共享目标文件,一种特殊类型的可重定位目标文件,可以在加载或者运行时被动态的加载进内存并链接
回归正题,链接器主要完成两个任务
1、符号解析,这个符号其实就是目标文件中的一个函数、一个全局变量、一个静态变量。解析的目的就是把符号定义与符号引用联系起来。简单说,就是定义 int a = 1; 在另一处 用了 ++a; 那么我们就需要把定义a和引用a的位置关联起来。
2、重定位,链接器把符号关联起来后,要对这些节进行重定位,指向节内存的位置。简单说,就是几个文件或者一个文件中的各个符号关联以后,但是只是关联有联系了,如果要执行的话,但又不知道符号的具体位置,也就是在内存中的地址位置,重定位就是做了这件事。
可重定位目标文件
这部分用一个图片表达的最清楚。
符号和符号表
每个可重定位目标都有一个符号表,就是上文说的.symtab,符号表中有三种不同的符号
1、模块m定义并能被其他模块引用的全局符号,模块m的非static的函数和全局变量
2、由其它模块定义并被模块m引用的全局符号,这叫做外部符号,其他模块的非static的函数和全局变量
3、被模块m定义或 引用的局部符号,也就是static的函数和全局变量
tips:一个文件内,尽可能的用static来保护自己的函数,类似类中的私有函数
要特别注意的是,section字段中有三个特殊的伪节,
1、ABS 不该被重定位的符号
2、UNDEF,未定义的符号
3、COMMON,代表还未被分配位置的未初始化的数据
其中要特别注意COMMON与bss段的区别
COMMON:未初始化的全局变量
.bss:未初始化的静态变量,以及初始化为0的全局或静态变量
这种细微差别只出现在可重定位文件中,链接以后,生成可执行文件后,.bss段就是未初始化的全局或静态变量,以及初始化为0的全局或静态变量,至于为什么会这样,下文会给出解释。
符号解析
简单说,就是那些引用的变量或者函数要与它真正的地方关联起来。最重要的就是全局符号的解析。
每个符号都有强弱之分,比如函数和初始化的全局变量就是强符号,未初始化的全局变量就是弱符号。
它发规则是:
1、不允许有多个同名的强符号
2、如果有一个强符号与多个弱符号,选择强符号
3、如果是多个弱符号,任意选择一个
那么这里也就是为什么会有COMMON段了,我们看到了编译器如何按照一个看似绝对的规则来把符号分配为COMMON和.bss。实际上,采用这个惯例是由于在某些情况中链接器允许多个模块定义同名的全局符号。当编译器在翻译某个模块时,遇到一个弱全局符号,比如说x,它并不知道其他模块是否也定义了x,如果是,它无法预测链接器该使用x的多重定义中的哪一个。所以编译器把×分配成COMMON,把决定权留给链接器。另一方面,如果x初始化为0,那么它是一个强符号(因此根据规则2必须是唯一的),所以编译器可以很自信地将它分配成.bss。类似地.静态符号的构造就必须是唯一的,所以编译器可以自信地把它们分配成.data或者.bss段。
另外与静态库链接就不多做介绍了。
重定位
重定位主要做两件事
1、重定位节和符号定义
2、重定位节中的符号引用
简单说就是多个可重定位文件合成一个可执行文件,要重新把data bss段等组合成一个新的,另外就是重定位每个符号的地址,让它指向真正的地址。
在每个重定位的位置,都有个重定位条目
ELF中定义了32种重定位类型,但我们只需要关注其中的两种
1、相对地址的引用,
2、绝对地址的引用
接下来看一看重定位算法
refptr是指向引用的指针,refaddr是引用的运行时地址,ADDR(r.symbol)是sum的运行时地址,r.addend是引用和下一条指令的相对位置。
我们可以用一个例子来做说明
对于绝对引用比较简单,这里用相对地址的例子,上图可知
1)offset --> f,f指立即数0x0位置的偏移量,即要修改的引用位置离main的偏移量
2)type --> RX86_64PC32*,*重定位条目一共有32种type,R_X86_64_PC32表明重定位的引用使用32位PC相对地址
3)symbol --> sum,表明调用的符号是sum,在本例中是个函数
4)addend --> -0x4,这个地方就是这个立即数占用了4个字节,偏移了4字节
main的运行时地址 ADDR(S) = 0x04004d0
sum的运行时地址 ADDR(r.symbol) = 0x4004e8
引用的运行时地址 refaddr = ADDR(S) + r.offset = 0x4004df(0x5的位置)
callq下一指令运行时地址 = 0x4004e3 = refaddr - r.addend
根据图4的重定位算法,我们将修改0x0为0x5,这正是图1引用处的最终值
现在让我们利用处理器进行相对寻址的过程来理解和验证上述重定位算法
当CPU执行callq指令时,PC的值为0x4004e3,即callq的下一指令地址(在处理器中,callq进入执行阶段,下一指令进入取址阶段),为了执行callq指令,CPU将执行以下步骤:
1)将PC压入栈中
2)更新 PC <-- PC + 0x5 = 0x4004e3 + 0x5 = 0x4004e8
显然0x5是我们的重定位算法最后放在引用处的值,它对应于 sum 的运行时地址 减去 callq 下一指令的运行时地址,即:ADDR(r.symbol) - (refaddr - r.addend)
这就是链接中断重定位
可执行目标文件
经过上文,我们已经知道经过链接器,多个可重定位目标文件合成了一个可执行文件,格式同样是elf格式,再经过压缩裁剪成可以在嵌入式设备中直接运行的bin文件,或者直接运行。
关于动态链接库位置无关码等知识点便不详述了,行文至此,本文主要解释了链接的两个主要任务,符号解析和重定位。
最后本文导图:
公众号'技术乱舞'
个人微信
-
C
啊 这
一脸懵逼的进来,一脸懵逼的出去
啊 这
一脸懵逼的进来,一脸懵逼的出去
牛批,这个钻的很深
virginOS
他在讲什么????
virgin
你是吗