概述
链接(Linking)是把各种代码和数据片段组合成一个单一文件的过程,此文件可被加载到内存中执行,链接可发送于各种时刻:
- 编译时(compile time):源码被翻译为机器码时
- 加载时(load time):程序被loader加载到内存时
- 运行时(run time):由程序来执行(动态链接)
静态链接
示例
以下两个.c文件说明链接的过程
在linux下使用gcc -Og -o prog main.c sum,c
命令,调用GCC驱动程序在适当时候使用预处理器,编译器等对源码进行编译,过程如下
最后一步中,链接器(linker,ld)把main.o和sum.o以及一些必要的系统文件组可起来创建可执行目标文件prog
静态链接:以一组可重定位目标文件和命令行参数位输入,生成一个完全链接的,可加载和运行的可执行目标文件作为输出的过程
⭐链接器在其中的主要任务:符号解析(symbol resolution) & 合并同类型模块 & 重定位(relocate)
目标文件
目标文件有三种形式:
- 可重定位目标文件(Linux下.o,Win下.obj):二进制代码和数据,编译时可于其他可重定位目标文件组合
- 可执行目标文件(.exe):二进制代码和数据,可直接被加载到内存执行
- 共享目标文件:特殊可重定位目标文件
可重定位目标文件
| 右侧位一个ELF可重定位目标文件格式,分为多个部分,介绍重点section:
- .text:已编译程序的机器代码
- .data:已初始化的全局和static变量
- .bss:未初始化的全局和static变量(包括被初始化为0的全局和static变量)
- .symtab:符号表
- .debug:调试符号表
| | | —- | :—-: |
符号解析⭐
目标文件定义和引用了符号,其中每个符号对应函数/全局变量/静态变量,符号解析就是把引用和定义关联,具体实现依靠把引用和可重定位目标文件符号表中确定的符号定义关联
符号&符号表
可重定位模块m都有符号表,其中包含m定义和引用的符号信息,符号有以下三种:
- 由m定义可被其他模块引用的全局符号:非static函数和全局变量
- 由其他模块定义但被m引用的全局符号:称为外部(external)符号,对应其他模块的非static函数和全局变量
- 只在m中定义和引用的局部符号:对应m中的static函数和static全局变量
ELF符号表格式
下图为一个Linux下ELF符号表的格式:
属性 | 描述 |
---|---|
value | 符号地址 - 对可重定位文件:表示距定义目标的节的起始位置偏移 - 对可执行文件:绝对运行地址 |
size | 目标字节数 |
type | 目标类型,函数或者数据 |
binding | 表示符号为本地或全局 |
section | 指示符号被分配到目标文件的哪一节 |
伪节 | 在section中出现,包含3类: - ABS:不该被重定位的符号 - UNDEF:未定义的符号,即本模块中引用但在别处定义 - COMMON:未分配位置的未初始化数据目标(代码中的未初始化全局变量) - *现代GCC有如下规则: - COMMON:未初始化全局变量 - .bss:未初始化静态变量,以及初始化为0的全局或静态变量 |
示例 | 以下为一个main.o符号表的最后三条 main条目:Ndx=1说明为.text节,是.text中偏移量为0的24字节全局函数 array条目:Ndx=3说明为.data节,是.data中偏移量为0的8字节全局变量 sum条目:Ndx=UND说明未在本模块定义,是对外部符号sum的引用 |
符号表生成
给定以下两个.c文件,填表说明swap.o符号表情况
符号 | 是否为symtab条目 | 符号类型 | 何处定义 | 节 |
---|---|---|---|---|
buf | 是(外部符号) | External | m.o | .data(m.o中) |
bufp0 | 是(全局变量) | Global | swap.o | .data |
bufp1 | 是(未初始全局变量) | Global | swap.o | COMMON |
swap | 是(非static函数) | Global | swap.o | .text |
temp | 否(局部变量) |
*注意定义和声明不同swap()虽然声明在m.o但定义在swap.o
符号解析
有了符号表后,linker可以把每个引用和其定义关联起来,对于局部静态变量的关联很简单,但全局符号的引用较复杂,linker需要在所有输入模块中找到其定义,如果定义不存在或定义不明确都会报错
编译时,compiler向as输出所有全局符号,其中强符号:函数和已初始化全局变量;弱符号:未初始化全局变量
- 不允许有多个同名强符号
- 一个强符号和多个弱符号同名,选择强符号
- 多个弱符号同名,从中任选一个
| | 使用
gcc foo1.c bar1.c
,报错,违反规则1:
两个main(),同名强符号 | | :—-: | —- | | | 报错,违反规则1:
两个int x=15213,同名强符号 | | | 不会报错,应用规则2:
强符号int x =15213和弱符号int x中选择强符号,右侧f()中改变的为foo3.c中定义的全局x
输出x=15212 | | | 不会报错,应用规则3:
两个弱符号选择一个,无论是哪一个都被f()改变
输出x=15212 |
合并&重定位⭐
完成符号解析后,ld得到目标模块中.text和.data的大小,此时可以开始重定位,将合并输入模块,并为每个符号分配运行时地址,分为两步:
- 重定位节和符号定义:ld把所有相同类型的节合并,如把所有模块的.data合并成为输出exe的.data节,之后ld把运行时内存地址赋给新的节/输入模块节/输入模块符号==>最终程序每条指令和全局变量都有唯一运行时地址
- 重定位节中符号引用:ld修改.text和.data中每个符号的引用,使其指向正确的运行时地址,依赖于可重定位目标文件的rel节(重定位条目)
重定位条目
as生成.o/.object模块时,不知道数据或代码最终在内存的什么位置,as在遇到任何一个最终位置未知的目标引用时就会生成一个重定位条目,告诉ld在合并生成exe时如何修改,对代码和数据的重定位条目在.rel.text和.rel.data中
ELF重定位条目格式
属性 | 描述 |
---|---|
offset | 需要被修改的引用的节偏移 |
symbol | 标识被修改引用应该指向的符号 |
type | 重定位类型,告知ld如可修改新引用 |
addend | 有符号常数,用于偏移调整 |
⭐两类最基本重定位类型
- EFL-R_X86_64_32/PE-DIR32:重定位一个使用32bits绝对地址的引用.
通过绝对寻址,指令中编码的32btis值=有效地址,无需修改
- EFL-R_X86_64_PC32/PE-REL32:重定位一个使用32bitsPC相对地址的引用.
PC相对地址是距离PC的当前运行时值的偏移量.
通过PC相对寻址指令,指令中编码的32bits值+PC当前运行时值=得到有效地址
算法如下:
⭐举例:给定以下main.o的反汇编代码(已注明重定位条目) 已知节地址,符号绝对地址 |
|
---|---|
重定位PC相对引用(call sum()) | 重定位绝对引用(array) |
1. 通过 f: R_X86_64_PC32 sum-0x4 可知offset=0xf, type=REL32, symbol=sum, addend=0x(-4)1. 由👆算法: |
引用的运行时地址/当前地址=节地址+节内偏移
refaddr=ADDR(S)+r.offset
=0x4004d0+0xf
=0x4004df
3. 修改引用地址/求相对地址=
符号绝对地址-当前地址+调整量
refptr=ADDR(r.symbol)+r.addend-refaddr
=0x4004e8-0x4-0x4004df
=0x5
3. 最终生成的exe文件中.text节中,call指令更新为
call指令存在4004de处,CPU执行时,PC的值为0x4004e3,为了执行指令
- CPU把PC压入栈
- PC<-PC+0x5=0x4004e3+0x5=0x4004e8,即sum例程的入口地址
|
1. 通过a:R_X86_64_32 array
可知offset=0xa, type=DIR32, symbol=array, addend=0x0
1. 由👆算法,直接修改引用地址
refptr=ADDR(r.symbol)+r.addend
=0x601018+0=0x601018
3. 在exe文件.data节中,有如下重定位结果
|
可执行文件
右图为典型ELF可执行文件,结构类似可重定位目标文件 - .text/.rodata/.data都已经被重定位到最终的运行内存地址外 - .init:定义函数_init()用于初始化代码 - 因为不再需要链接,没有.rel节 - ELF可执行文件可以被加载到内存,能够被映射到连续的内存段 |
|
---|---|
加载
loader把可执行文件中代码和数据从磁盘放入内存中,然后跳转到程序的入口执行,此过程为加载
右侧为程序的内存映像(image) - loader运行时,创建image - loader把可执行文件的片(chunk)复制到image的代码段和数据段,然后跳转到入口点 - 每个程序都有自己的内存映像,即活动记录 |
|
---|---|
选择题知识点
- loader可以完成:从磁盘中加载&映射exe到内存中
- linker可以完成:符号解析(resolution),重定位(relocation),合并同类型section
- 说明可重定位目标文件使用大端/小端的域为ELF header
- link可发生在compile,load,run time
- 与解析有关的section:只有symtab
- 与重定位有关的section:只有.rel.text和.rel.data
- 当没有特别强调时,所有未初始化全局/静态变量都在.bss中,不考虑COMMON伪节
- ⭐可执行目标文件的格式有:PE,COFF,ELF,a.out