链接是将各种代码和数据片段收集并组合成为一个单一文件的过程,这个文件可被加载(复制)到内存并执行。
1. 编译器驱动程序
看一下链接的整个过程
-
驱动程序运行c预处理器(cpp),将main.c翻译成一个ASCII码的中间文件main.i
-
驱动程序运行c编译器(cll),将main.i翻译成一个ASCII汇编语言文件main.s
-
驱动程序运行汇编器(as),将main.s翻译成一个可重定位目标文件main.o
sum.o和main.o同理
-
运行链接器程序ld,将main.o和sum.o以及一些必要的系统目标文件组合起来,创建一个可执行目标文件
-
可执行程序的运行是shell通过调用一个叫做加载器(loder)的函数来实现的。它将可执行文件的代码和数据复制到内存,然后将cpu的控制权转到程序的开头
2. 可重定位目标文件
一个典型的ELF可重定位目标文件格式如下
ELF头(ELF header),节(section),节头部表(section header)
1. elf header
通过这个命令查看elf header的具体内容
readelf -h main.o //-h表示只显示header信息
ELF Magic(魔数)用来确定文件类型,操作系统加载可执行文件时会判断魔数是否正确,若不正确则拒绝加载
最后那个01表示ELF的版本号,通常都是1
2. section
每个节的含义如下图
一般只需要知道.text 是代码段,.data是已初始化的数据段,.bss是未初始化的段,.symtab是符号表
3. 符号和符号表
对于每一个全局变量和从外部库引用的函数都是一个符号
看一下这个main.c的例子
#include<stdio.h>
int sum(int *a, int n);
int array[2] = {1,2};
int num;
int main()
{
int x = 1;
int val = sum(array,2);
printf("%d\n",val);
return val;
}
main.o的符号表如下所示
两个细节 :
-
sum,array,num,printf甚至就连main都在符号表中,但没有局部变量x和val。他们在栈里
-
Ndx 表示符号所在的section
查看section header table。array在序号为3的 .data段,num在序号为4的.bss段...
因为sum和printf只是在该程序引用,但是在别的地方定义,所以Ndx为UND(undefine)
4. 符号解析
当多个可重定位文件定义了相同的全局符号
-
若相同的符号为强符号,则报错
-
若一个为强符号,一个弱符号
如下图,函数f会将x从15213变为15212
5. 与静态库链接
静态库:
我们为每一个标准函数创建一个独立的可重定位文件(比如printf对应的文件为printf.o),并将相关的的文件封装成一个单独的静态库文件(比如libc.a)
与静态库链接:
在两个.c文件定义两个函数
用下面这条指令将两个.c 文件 生成的.o 文件归档成一个.a文件的静态库
linux>ar rcs libvector.a addvec.o multvec.o
下图中vector.h定义了libvector.a 中的函数
运行下面指令。链接器会判定main.o引用了addvec.o中定义的addvec符号,所以复制addvec.o到可执行文件。但不会复制multvec.o
linux>gcc -static -o prog main.o ./libvector.a
静态库的解析过程
链接时会维护三个集合: E(可重定位目标文件),U(引用了但是尚未定义的符号),D(已定义的符号)
linux>gcc -static -o prog main.o ./libvector.a libc.a
链接器会根据这条指令从左到右依次扫描
- 扫描到main.o时,集合如下
-
扫描到libvector.a时,集合如下
链接器会发现libvector.a是一个静态库文件,则尝试从这个链接库文件寻找集合U中符号的定义。
这样会将addvec.o加到E集合,并删除U集合中之前未找到定义的符号addvec,并加上在addvec.o中被定义的符号addcnt
-
扫描到libc.a , 执行逻辑和上面扫描到libvector.a 的逻辑类似。最后集合如下
当扫描完成后,若集合U为空。则链接器会进行E集合中目标文件的合并和重定位。若不为空,则会输出一个错误并终止
6. 重定位
重定位主要做两件事情
-
重定位节和符号定义
链接器将所有相同类型的节合并为同一类型的新的聚合节。比如将所有.data节合并为一个.data节
这步执行完后,程序中的每条指令和全局变量都有一个唯一的运行时内存地址
-
重定位节中的符号引用
链接器修改代码节和数据节中对每个符号的引用,使他们指向正确的运行时地址
重定位节中的符号引用分为相对引用和绝对引用
重定位条目
在汇编器生成目标模块时,并不知道数据和代码最终会在内存中的什么位置。
所以当汇编器遇到对最终位置不确定的引用,就会生成一个重定位条目
重定位条目格式如下
下面代码中有两个重定位条目 : 对array 和 sum的引用
重定位pc相对引用
我们先来看sum,sum在模块sum.o中定义。
call sum 指令开始于0xe,操作码为0xe8,后面四个'00'是对sum pc相对引用(相当于下一条指令的地址)的占位符
sum的重定位条目如下
链接器会根据重定位条目修改对应的符号的引用,使他们指向正确的运行时地址
具体过程如下:
- 计算引用的运行时地址 :
- 更新该引用,使它在运行时指向sum函数
重定位后,当cpu执行call指令时,pc的值为0x4004e3
若将pc值+更新后的引用的运行时地址,则正好为0x4004e3+0x5=0x4004e8,为sum函数的第一条指令。这正是我们重定位后想达到的目的
重定位pc绝对引用
mov指令将array的地址复制到寄存器edi中
计算步骤
\[*refptr = ADDR(array)+r.addend=0x601018 \]执行完后,结果如图
array的地址由0x0变为0x601018
7. 可执行目标文件
可执行目标文件的elf header table多了对程序入口点(entry point)的描述
以及在.init节中定义了一个_init函数,用来初始化
加载可执行目标文件
- 加载器运行时创建一个内存映像,将可执行文件的chunk复制到映像的代码段和数据段
- 加载器跳转到程序入口点,_start函数的地址
- _start函数调用系统启动函数__libc_start_main(定义在lib.so中)。它初始化执行环境,调用用户层的main函数
8. 动态链接
几乎每个c程序都会使用'printf'这种标准I/O函数。
如果每次都像静态链接将这些函数代码复制到每个运行进程的文本段是对内存系统资源的浪费
共享库
和静态库不同,共享库可以在运行或加载时,加载到任意的内存地址,并和一个在内存中的程序链接。
这也就是动态链接,由动态链接器(静态链接器ld是一个编译器,动态链接器则是一个程序)执行。
共享库也称为共享目标,linux系统用后缀.so表示,win系统用ddl