将一个程序所有关联模块对应的可重定位目标文件合并成一个可执行文件的过程叫做链接。
1.作用
- 分离编译与链接:每个源文件独立编译生成目标文件,然后在链接阶段将这些目标文件结合起来,形成最终的可执行程序。当修改某个源文件时,只需重新编译该文件而不是整个项目,从而节省时间。
- 重用代码库:代码库在编译时不会直接包含进目标文件中,而是在链接阶段根据需要被引入。这样可以避免重复编译相同的代码,同时确保代码的一致性和易于维护。
2.可执行文件的生成
- 预处理
- 头文件包含、宏定义展开、条件编译选择等
- 产生.i文件
- 编译
- 词法、语法、语义分析,并将结果进行代码优化和存储分配
- 产生.s文件
- 汇编
- 将汇编代码转换机器代码
- 产生.o文件
- 链接
- 静态链接
- 动态链接
- 整体流程:
3.目标文件
1. 区别
- 可执行目标文件和可重定位目标文件
1. 可重定位目标文件:
1. 单个模块生成
2. 汇编代码从0开始
3. .o文件
2. 可执行目标文件:
1. 多个模块组合而成
2. 汇编代码地址在虚拟地址空间产生
2. ELF可重定位目标文件格式
-
ELF:可执行可链接格式
-
ELF头
1. 文件结构信息
2. 其中e_entry用于指定系统将控制权转移到的起始虚拟地址,开始执行程序
3. readelf -h main.o查看信息 -
节
- .text:目标代码
- .rodata:只读数据
- data:已初始化全局变量+静态变量
- .bss:未初始化全局变量+静态变量
- symtab:符号表,存放函数名,全局变量名
- .rel.text:.text节可重定位信息
- .rel.data:.data节可重定位信息
- .debug:调试信息
- .line:行号
- .strtab:字符串表
-
节头表
1. 表中每个表项描述一个节的节名、在文件中偏移、大小等信息
2. 使用readelf -S test.o查看
3. -
格式:
可执行目标文件格式
-
程序头表
- 描述可执行文件数据段,代码段在虚拟空间中的映射关系
- readelf -l main查看
- 注意,load表示需装载的段,virtAddr是该段的虚拟地址
-
结构
- 可执行目标文件中所有代码位置连续,所有只读数据位置连续,所有可读可写数据位置连续。
- 在可执行文件中, ELF 头、程序头表、.init 节、.fini 节、.text 节和. rodata 节合起来可构成一个只读代码段(read-only code segment)。
- .data 节和.bss 节合起来可构成一个可读写数据段(read/write data segment)。
- 显然, 在可执行文件启动运行时, 这两个段必须装人内存且需要为之分配存储空间, 因而称为可装入段。
-
存储映射
1. 内存中常驻的加载器(通过execve系统调用启动)根据可执行目标文件的程序头表信息,将对应节,代码段,数据段和页表建立映射。
2. 加载过程中并没有将磁盘上的代码和数据放入主存,而是创建了对应的页表项,当执行时发生缺页异常时才会加载到主存。
3.
4.符号表与符号解析
todo
5.静态链接
- 符号解析
- 将符号引用和符号定义关联
- 符号包括全局变量,静态变量名和函数名,非静态局部变量不是
- 符号存放在可重定位目标文件的符号表中
- 重定位
- 可重定位目标文件代码区和数据区都是从0开始,链接器需要将不同模块中相同的节合并成新的节,并按虚拟地址空间重新确定位置。
6.动态链接
-
优势
- 共享性:是指共享库中的代码段在内存只有一个副本, 当应用程序在其代码中需要引用共享库中的符号时, 在引用处通过某种方式确定指向共享库中对应定义符号的地址即可。例如, 对于动态共享库 libc.so 中的 printf 模块, 内存中只有一个 printf 副本, 所有应用程序都可以通过动态链接 printf 模块来使用它。因为内存中只有一个副本, 磁盘中也只有共享库中一份代码,所以能节省主存资源和磁盘空间。
- 动态性:是指共享库只在使用它的程序被加载或执行时才加载到内存, 因而在共享库更新后并不需要重新对程序进行链接, 每次加载或执行程序时所链接的共享库总是最新的。可以利用共享库的这个特性来实现软件分发或生成动态 Web 网页等。
-
加载
- 在程序加载过程中加载和链接共享库
- 在程序执行过程中加载和链接共享库
-
使用
- dlsym返回该共享库中指定符号的地址
- dlopen中RTLD_LAZY是延迟绑定,为了提高启动效率
#include <stdio.h>
#include <dlfcn.h>
int main()
void *handle;
void (*myfunc1)();
char *error;
handle = dlopen("./my11b.so", RTLD_LAZY);
if (!handle) {
fprintf(stderr, "%s\n", dlerror());
exit(1);
}
myfunc1 = dlsym(handle, "myfunc1");
if ((error = dlerror()) != NULL) {
fprintf(stderr, "%s\n", error);
exit(1);
}
myfunc1();
if (dlclose(handle) < 0) {
fprintf(stderr, "%s\n", dlerror());
exit(1);
}
return 0;
- 位置无关代码
- 共享库由于共享且动态的原因,其中代码与地址无关,在生成时使用fPIC选项。
- 如何实现
- 符号间引用分为模块内和模块间
- 对于模块内过程调用和跳转,数据引用,通过相对偏移的方式实现
- 对于模块间,则通过在数据段起始处设置全局偏移量表,存放对应符号地址。
- 编译器为 GOT 中每一个表项生成一个重定位项, 指示动态链接器在加载并进行动态链接时必须对这些 GOT 表项中的内容进行重定位, 也即在动态链接时需要对这些表项绑定一个符号定义, 并填人所引用的符号的地址。