1.目标文件格式
程序的链接概述
链接的作用
模块化
- 一个程序可以分成很多源程序文件
- 可构建公共函数库,如数学库,标准I/O库
效率高
- 一个程序可以分成很多的源程序文件,然后重新链接
- 可执行文件和运行时的内存中只需包含所调用函数的代码,而不需要包含整个共享库
符号定义
符号定义:全局静态变量名、函数名、全局变量
链接
链接本质:合并相同的“节“
step1:符号解析
编译器将定义的符号存放在一个符号表
链接器将每个符号的引用都与一个确定的符号定义建立关联
step2:重定位
将多个代码段与数据段分别合并为一个单独的代码段和数据段
计算每个定义的符号在虚拟地址空间中的绝对地址
将可执行文件中符号引用处的地址修改为重定位后的地址信息
ELF目标文件、重定位目标文件格式、可执行目标文件格式
可重定位目标文件 (.o) :其代码和数据可和其他可重定位文件合并为可执行文件;每个.o 文件由对应的.c文件生成;每个.o文件代码和数据地址都从0开始
可执行目标文件 (默认为a.out) 包含的代码和数据可以被直接复制到内存并被执行;代码和数据地址为虚拟地址空间中的地址;
共享的目标文件 (.so) 特殊的可重定位目标文件,能在装入或运行时被装入到内存并自动被链接,称为共享库文件
- .text:代码节
- .rodata:只读数据节
- .data:已初始全局数据节
- .bss:未初始全局数据节
可重定位目标文件
静态链接库文件由若干个可重定位目标文件组成
- ELF头:ELF魔数(用来确定文件的类型和格式)、版本、小端/大端,目标文件的类型,节头表的起始位置和长度等
- rodata:只读数据,如 printf 格式串,switch-case的跳转表
- bss节:未初始化全局变量,仅是占位符,不占据任何实际磁盘空间
让我们来看一个具体例子
- Size of this header:ELF头长度52字节
- Entry point address:因为是可重定位的文件所以地址从零开始
- Start of program headers:程序头表(可选)
- Start of section headers:节头表离文件起始处的偏移516字节
- Size of section headers:每个表项40字节
- Number of section headers:表项数15个
节头表
通过上述表我们可以看到.bss是没有分配空间的
可执行目标文件
- 定义的所有变量和函数已有确定地址(虚拟地址空间中的地址)
- 符号引用处已被重定位,以指向所引用的定义符号
- 指令地址和指令给出的操作数地址都是虚拟地址
- 多一个程序头表,也称段头表
- 多一个.init节,用于定义_init函数,该函数用来进行可执行目标文件开始执行时的初始化工作
- 少两个.rel节(无需重定位)
程序头表
描述可执行文件中的节与虚拟空间中的存储段之间的映射关系
表项中包含可装入段(即Type=LOAD)
第一可装入段:包括ELF头、程序头表、.init、.text和.rodata节,是只读代码段
第二可装入段:.data节,是可读写数据段
2.符号解析与重定位
符号和符号表、符号解析
符号
每个可重定位目标模块都有一个符号表:Global symbols(模块内部定义的全局符号) 指不带static的全局变量;External symbols(外部定义的全局符号),由其他模块定义并被模块m引用的全局符号,Local symbols(本模块的局部符号),带static的函数和全局变量
符号表(symtab)
函数名在text节中;变量名在data节或bss节中
让我们来看一个符号表最后三个条目的例子
Name为buf的全局变量,大小是8B,类型是变量,是第3节.data偏移为0的符号
swap是main.o中未定义全局(在其他模块定义)符号,类型和大小未知
符号解析
将每个模块中引用的符号与某个目标模块中的定义符号建立关联
符号定义的实质:被分配了存储空间,所有定义符号的值就是其目标所在的首地址
全局符号的强弱
- 函数名和已初始化的全局变量名是强符号
- 未初始化的全局变量名是弱符号
多重定义符号的处理规则
- Rule 1: 强符号不能多次定义 强符号只能被定义一次,否则链接错误
- Rule 2: 若一个符号被定义为一次强符号和多次弱符号,则按强定义为准,对弱符号的引用被解析为其强定义符号
- Rule 3: 若有多个弱符号定义,则任选其中一个,使用命令 gcc –fno-common链接时,会告诉链接器在遇到多个弱定义的全局符号时输出一条警告信息。
与静态库的链接
静态库
将所有相关的目标模块(.o)打包为一个单独的库文件(.a),称为静态库文件 ,也称存档文件(archive) 增强了链接器功能,使其能通过查找一个或多个库文件中的符号来解析符号,在构建可执行文件时只需指定库文件名,链接器会自动到库中寻找那些应用程序用到的目标模块,并且只把用到的模块从库中拷贝出来 在gcc命令行中无需明显指定C标准库libc.a(默认库)
链接器中符号解析的全过程
- E 将被合并以组成可执行文件的所有目标文件集合
- U 当前所有未解析的引用符号的集合
- D 当前所有定义符号的集合
让我们还是来看一个例子
上述是创建了一个myproc1.c然后让它和mypro2.c打包生成一个mylib的库
开始EDU为空,首先扫描到main.c,把它放入E,同时myfunc1放入U,main加入D。接着扫描mylib,将U的符号与目标模块(myproc1.c、mypro2.c)匹配,找到myfunc1。于是将myproc1.c放入E,将myfunc1放入D,同时将printf放入U。接着扫描默认库文件(libc.a),找到printf.o中的printf,于是将printf.o放入E,将printf从U移到D
重定位信息、重定位过程
- 合并相同的节
将集合E的所有目标模块中相同的节合并成新节:例如,所有.text节合并作为可执行文件中的.text节
- 对定义符号进行重定位(确定地址)
确定新节中所有定义符号在虚拟地址空间中的地址:例如,为函数确定首地址,进而确定每条指令的地址,为变量确定首地址 完成这一步后,每条指令和每个全局变量都可确定地址
- 对引用符号进行重定位(确定地址)
修改.text节和.data节中对每个符号的引用(地址) 需要用到在.rel_data和.rel_text节中保存的重定位信息
重定位信息
- 汇编器遇到引用时,生成一个重定位条目
- 数据引用的重定位条目在.rel_data节中
- 指令中引用的重定位条目在.rel_text节中
两种最基本的重定位类型 R_386_32: 绝对地址;R_386_PC32: PC相对地址
由该图可知,swap在重定义条目rel_text节中:
r_offset=0x7, r_sym=10, r_type=R_386_PC32
其中 r_sym=10是指在符号表中第十位符号swap
R_386_32的重定位方式
假设合并后buf的存储地址ADDR(buf)=0x8049620,buf定义在.data节中偏移为0处,占8B
bufp0紧接在buf后,故地址为0x8049620+8= 0x8049628
因是R_386_32方式,故bufp0内容为buf的绝对地址0x8049620,即“20 96 04 08”
R_386_PC32的重定位方式
已知swap函数紧跟在main后,main函数对应的机器代码从0x8048380开始。
则swap函数开始地址=0x8048380+0x12(main函数大小)=0x8048392,为了4字节对齐,swap函数开始地址是0x8048394
转移目标地址=PC+偏移地址,也就是偏移地址(相对PC地址)=转移目标地址-PC。这里的转移目标地址就是0x8048394,PC=0x8048380+0x7(call指令的偏移量)+4=0x804838b,所以偏移地址=0x8048394-0x804838b=0x9
3.动态链接
可以动态地在装入时或运行时被加载并链接
动态链接可以按以下两种方式进行:
- 在第一次加载并运行时进行
- 在已经开始运行后进行
位置无关代码(PIC)
- 保证共享库代码的位置可以是不确定的
- 即使共享库代码的长度发生变化,也不会影响调用它的程序
可通过动态链接器接口提供的函数在运行时进行动态链接,如dlopen, dlsym, dlerror, dlclose等,其头文件为dlfcn.h
#include <stdio.h>
#include <dlfcn.h>
int main()
{void *handle;void (*myfunc1)();char *error; /* 动态装入包含函数myfunc1()的共享库文件 */handle = dlopen("./mylib.so", RTLD_LAZY);if (!handle) {fprintf(stderr, "%s\n", dlerror());exit(1);}/* 获得一个指向函数myfunc1()的指针myfunc1*/myfunc1 = dlsym(handle, "myfunc1");if ((error = dlerror()) != NULL) {fprintf(stderr, "%s\n", error);exit(1);}/* 现在可以像调用其他函数一样调用函数myfunc1() */myfunc1();/* 关闭(卸载)共享库文件 */if (dlclose(handle) < 0) {fprintf(stderr, "%s\n", dlerror());exit(1);}return 0;
}