链接总结

时间:2024-03-01 14:18:29

1、链接概述和目标文件格式

1.1 使用GCC生成C语言程序的可执行文件的过程: 

第一步预处理,对#include、#define、#ifdef等预处理命令进行处理;

第二步编译,将预处理结果编译转换为ASCII形式的汇编语言程序代码;

第三步汇编,将汇编语言代码汇编转换为机器指令表示的机器语言代码; 

第四步链接,将多个模块的机器语言代码链接生成可执行目标程序文件。
 

 

 

 

 

 

对应的Linux指令:

gcc –E hello.c –o hello.i//预处理
gcc –S hello.i –o hello.s//编译
gcc –c hello.s –o hello.o//汇编
gcc –static –o myproc main.o test.o//静态链接

1.2 使用链接的好处:

链接带来的好处1:模块化
  (1)一个程序可以分成很多源程序文件
  (2)可构建公共函数库,如数学库,标准C库等(代码重用,开发效率高)
链接带来的好处2:效率高
  (1)时间上,可分开编译
  只需重新编译被修改的源程序文件,然后重新链接
  (2)空间上,无需包含共享库所有代码
  源文件中无需包含共享库函数的源码,只要直接调用即可(如,只要直接调用printf()函数,无需包含其源码)
可执行文件和运行时的内存中只需包含所调用函数的代码,而不需要包含整个共享库 

 

子程序(函数)起始地址和变量起始地址是符号定义definition) 注意函数的声明不属于函数的定义,void foo(void);局部变量分配在栈中,不属于符号定义。

调用子程序(函数或过程)和使用变量即是符号引用reference

链接的本质就是合并相同的节。

记住2^10 = 1024。1TB = 1024GB,1GB = 1024MB,1MB = 1024KB,1KB = 1024B;

1.3 ELF可执行可链接格式

ELF有两种视图,链接视图和执行视图。

1.3.1 链接视图——可重定位目标文件

可被链接(合并)生成可执行文件或共享目标文件
静态链接库文件由若干个可重定位目标文件组成
包含代码、数据(已初始化.data和未初始化.bss
包含重定位信息(指出哪些符号引用处需要重定位)
文件扩展名为.o(相当于Windows中的 .obj文件)

重要的部分是.text节,.data节(已初始化的全局和static局部变量),.bss节(未初始化的全局和static局部变量,初始化为0),

         .rodata节,.rel.text节,.rel.data节,节头表(important)

 将未初始化变量(.bss节)与已初始化变量(.data节)分开的好处:
.data节中存放具体的初始值,需要占磁盘空间
.bss节中无需存放初始值,只要说明.bss中的每个变量将来在执行时占用几个字节即可,因此,.bss节实际上不占用磁盘空间,提高了磁盘空间利用率,不占任何磁盘空间,仅是占位符。

ELF头位于ELF文件开始,包含文件结构说明信息。分32位系统对应结构和64位系统对应结构(32位版本、64位版本) ;

readelf -h main.o//查看可重定位目标文件的ELF头

除ELF头之外,节头表是ELF可重定位目标文件中最重要的部分内容

描述每个节的节名、在文件中的偏移、大小、访问属性、对齐方式等
以下是32位系统对应的数据结构(每个表项占40B) ,可重定位目标文件中,每个可装入节的起始地址总是0 

1.3.2 执行视图——可执行目标文件

包含代码、数据(已初始化.data和未初始化.bss
定义的所有变量和函数已有确定地址(虚拟地址空间中的地址)
符号引用处已被重定位,以指向所引用的定义符号
没有文件扩展名或默认为a.out(相当于Windows中的 .exe文件)
可被CPU直接执行,指令地址和指令给出的操作数地址都是虚拟地址

 

可执行目标文件有段的概念,而可重定位目标文件是节的概念。

程序头表描述可执行文件中的节与虚拟空间中的存储段之间的映射关系,一个表项(32B)说明虚拟地址空间中,一个连续的段一个特殊的节

readelf -h main//查看可执行目标文件的ELF头
readelf –l main //可执行文件中的程序头表

 可执行文件的存储器映像 :

 

2、链接操作的步骤

1)确定符号引用关系(符号解析)
2)合并相关.o文件            ]
3)确定每个符号的地址    ==}  重定位
4)在指令中填入新地址    ]
 

Step 1. 符号解析(Symbol resolution)
程序中有定义和引用的符号 (包括变量和函数等)
编译器将定义的符号存放在一个符号表( symbol table)中.
链接器将每个符号的引用都与一个确定的符号定义建立关联
Step 2. 重定位
将多个代码段与数据段分别合并一个单独的代码段和数据段
计算每个定义的符号在虚拟地址空间中的绝对地址
将可执行文件中符号引用处的地址
修改为重定位后的地址信

2.1 符号及符号解析

2.1.1 链接符号的类型 

1) Global symbols(模块内部定义的全局符号
如,指不带static的全局变量,main.c 中的全局变量名buf
2) External symbols(外部定义的全局符号
– 由其他模块定义并被模块m引用的全局符号
3) Local symbols(本模块的局部符号
– 仅由模块m定义和引用的本地符号。例如,在模块m中定义的带static的C函数和全局变量
链接器局部符号不是指程序中的局部变量(分配在栈中的临时性变量),链接器不关心这种局部变量 

2.1.2 符号解析(symbol resolution)

目的:将每个模块中引用的符号与某个目标模块中的定义符号建立关联。
每个定义符号在代码段或数据段中都被分配了存储空间,将引用符号定义符号建立关联后,就可在重定位时将引用符号的地址重定位为相关联的定义符号的地址

“符号的定义”其实质是什么? 指被分配了存储空间。为函数名即指其代码所在区;变量名即指其所占的静态数据区。

                                              所有定义符号的值就是其目标所在的首地址 。

全局符号的强弱:

1)函数名和已初始化的全局变量名是强符号
2)未初始化的全局变量名是弱符号

链接器对符号的解析规则:

多重定义符号的处理规则
Rule 1: 强符号不能多次定义
强符号只能被定义一次,否则链接错误
Rule 2: 若一个符号被定义为一次强符号和多次弱符号,则按强定义为准
对弱符号的引用被解析为其强定义符号
Rule 3: 若有多个弱符号定义,则任选其中一个
使用命令 gcc –fno-common链接时,会告诉链接器在遇到多个弱定义的全局符号时输出一条警告信息。
符号解析时只能有一个确定的定义(即每个符号仅占一处存储空间)

尽量避免使用全局变量
• 一定需要用的话,就按以下规则使用
– 尽量使用本地变量(static)
– 全局变量要赋初值
– 外部全局变量要使用extern

2.2 静态链接和符号解析

 静态链接对象:
多个可重定位目标模块 + 静态库(标准库、自定义库)
(.o文件) (.a文件,其中包含多个.o模块)

静态库 (.a archive files)
将所有相关的目标模块(.o)打包为一个单独的库文件(.a),称为静态库文件 ,也称存档文件(archive)
使用静态库,可增强链接器功能,使其能通过查找一个或多个库文件中定义的符号来解析符号
在构建可执行文件时,只需指定库文件名,链接器会自动到库中寻找那些应用程序用到的目标模块,并且只把用到的模块从库中拷贝出来
在gcc命令行中无需明显指定C标准库libc.a(默认库)

gcc –c myproc1.c myproc2.c
ar rcs mylib.a myproc1.o myproc2.o//静态库文件/存档文件

gcc –c main.c
gcc –static –o myproc main.o ./mylib.a//静态链接

2.2.1 链接器中符号解析的全过程

E 将被合并以组成可执行文件的所有目标文件集合
U 当前所有未解析的引用符号的集合
D 当前所有定义符号的集合

注意链接顺序,一般静态库文件放在链接命令的最后。

首先,扫描mylib,因是静态库,应根据其中是否存在U中未解析符号对应的定义符号来确定哪个.o被加入E。因为开始U为空,故其中两个.o模块都不被加入E中而被丢弃。

举例子:开始E、U、D为空,首先扫描main.o,把它加入E,同时把myfun1加入U,main加入D。接着扫描到
mylib.a,将U中所有符号(本例中为myfunc1)与mylib.a中所有目标模块(myproc1.o和myproc2.o
)依次匹配,发现在myproc1.o中定义了myfunc1,故myproc1.o加入E,myfunc1从U转移到D。
myproc1.o中发现还有未解析符号printf,将其加到U。不断在mylib.a的各模块上进行迭代以匹配U中的
符号,直到U、D都不再变化。此时U中只有一个未解析符号printf,而D中有main和myfunc1。因为模块
myproc2.o没有被加入E中,因而它被丢弃 。

接着,扫描默认的库文件libc.a,发现其目标模块printf.o定义了printf,于是printf也从U移到D,并将
printf.o加入E,同时把它定义的所有符号加入D,而所有未解析符号加入U。处理完libc.a时,U一定是空的。

链接器对外部引用的解析算法要点如下:
按照命令行给出的顺序扫描.o 和.a 文件
扫描期间将当前未解析的引用记录到一个列表U中
每遇到一个新的.o 或 .a 中的模块,都试图用其来解析U中的符号
如果扫描到最后,U中还有未被解析的符号,则发生错误

得到可执行文件之后,就可以装入只读代码段和读写数据段中。

3、重定位及动态链接

3.1  重定位

符号解析完成后,可进行重定位工作,分三步
合并相同的节
将集合E的所有目标模块中相同的节合并成新节
例如,所有.text节合并作为可执行文件中的.text节
定义符号进行重定位(确定地址)
确定新节中所有定义符号在虚拟地址空间中的地址
例如,为函数确定首地址,进而确定每条指令的地址,为变量确定首地址
完成这一步后,每条指令和每个全局或局部变量都可确定地址
引用符号进行重定位(确定地址)
修改.text节和.data节中对每个符号的引用(地址)需要用到在.rel_data和.rel_text节中保存的重定位信息

IA-32有两种最基本的重定位类型:1)R_386_32:绝对地址

                                                        2)R_386_PC32:PC相对地址

符号解析后得到的结果是E中有main.oswap.o两个模块!D中有所有定义的符号!

3.2 可执行文件的加载

可执行目标文件保存在磁盘中,程序头表描述了可执行文件中代码和数据的存储映像

 

• 通过调用execve系统调用函数来调用加载器;
加载器(loader)根据可执行文件的程序(段)头表中的信息,将可执行文件的代码和数据从磁盘“拷
 贝”到存储器中(实际上不会真正拷贝,仅建立一种映射,这涉及到许多复杂的过程和一些重要概念,将在后续课上学习);
加载后,将PC(EIP)设定指向Entry point (即符号_start处),最终执行main函数,以启动程序执行 ;

 

 execve()函数的功能是在当前进程上下文中加载并运行一个新程序。

//execve()函数的用法如下:
int execve(char *filename, char *argv[], *envp[]);
/*filename是加载并运行的可执行文件名(如./hello),可带参数列表argv和环境变量列表envp。若错误(如找不到指定文件filename)
,则返回-1,并将控制权交给调用程序; 若函数执行成功,则不返回,最终将控制权传递到可执行目标中的主函数main。 */

程序的加载和运行 :

3.3 共享库和动态链接

 静态库有一些缺点:
库函数(如printf)被包含在每个运行进程的代码段中,对于并发运行上百个进程的系统,造成极大的主存资源浪费
库函数(如printf)被合并在可执行目标中,磁盘上存放着数千个可执行文件,造成磁盘空间的极大浪费
程序员需关注是否有函数库的新版本出现,并须定期下载、重新编译和链接,更新困难、使用不便
解决方案: Shared Libraries (共享库)
是一个目标文件,包含有代码和数据
从程序中分离出来,磁盘和内存中都只有一个备份
可以动态地在装入时运行时被加载并链接
Window称其为动态链接库(Dynamic Link Libraries,.dll文件)
Linux称其为动态共享对象( Dynamic Shared Objects, .so文件)

 动态链接可以按以下两种方式进行:
在第一次加载并运行时进行 (load-time linking).
Linux通常由动态链接器(ld-linux.so)自动处理
标准C库 (libc.so) 通常按这种方式动态被链接
在已经开始运行后进行(run-time linking).
在Linux中,通过调用 dlopen()等接口来实现
分发软件包、构建高性能Web服务器等
在内存中只有一个备份,被所有进程共享,节省内存空间
一个共享库目标文件被所有程序共享链接,节省磁盘空间
共享库升级时,被自动加载到内存和程序动态链接,使用方便
共享库可分模块、独立、用不同编程语言进行开发,效率高
第三方开发的共享库可作为程序插件,使程序功能易于扩展

 

 

位置无关代码(PIC
好处:

1)保证共享库代码的位置可以是不确定的;
2)即使共享库代码的长度发生变化,也不会影响调用它的程序 。

 

gcc –c myproc1.c myproc2.c
gcc –shared –fPIC –o mylib.so myproc1.o myproc2.o
//位置无关的共享代码库文件

动态链接用到一个重要概念:
位置无关代码(Position-Independent Code,PIC
GCC选项-fPIC指示生成PIC代码
共享库代码是一种PIC
共享库代码的位置可以是不确定的
即使共享库代码的长度发生变化,也不影响调用它的程序
引入PIC的目的
链接器无需修改代码即可将共享库加载到任意地址运行
所有引用情况
(1) 模块内的过程调用、跳转,采用PC相对偏移寻址

调用或跳转源与目的地都在同一个模块,相对位置固定,只要用相对偏移寻址即可
无需动态链接器进行重定位

(2) 模块内数据访问,如模块内的全局变量和静态变量

.data节与.text节之间的相对位置确定,任何引用局部符号的指令与该符号之间的距离是一个常数

(3) 模块外的过程调用、跳转

引用其他模块的全局变量,无法确定相对距离
在.data节开始处设置一个指针数组(全局偏移表,GOT),指针可指向一个全局变量
GOT与引用数据的指令之间相对距离固定

(4) 模块外的数据访问,如外部变量的访问要实现动态链接,必须生成PIC代码 

 方法一:

类似于(3),在GOT中加一个项(指针),用于指向目标函数的首地址(如&ext)

动态加载时,填入目标函数的首地址

方法二:延迟绑定
可用“延迟绑定(lazy binding)”技术减少指令条数:不在加载时重定位,而延迟到第一次函数调用时,需要用GOT和PLT(Procedure linkage Table, 过程链接表)

 

处理目标文件的常用Linux工具命令:

AR:创建静态库、插入、删除、列出和提取成员。

READELF:显示一个目标文件的完整结构。

OBJDUMP:所有二进制工具之母,能够显示一个目标文件的所有信息,它最大的作用是可以反汇编.text节中的二进制指令。

LDD:列出一个可执行文件在运行时所需要的共享库。