概述
- 链接:将多个目标文件或库文件组合在一起,生成可执行文件或共享库
可以执行于编译时、加载时、运行时,使得分离编译成为可能,由链接器自动实现 - 链接器:将各个模块之间的符号引用解析为实际的内存地址,生成可执行文件或共享库。
目标文件
可执行目标文件
- 是经过链接过程后生成的最终可执行文件,可以直接加载到内存中执行,不需要进一步链接。
- 包含了程序的完整机器代码、数据和其他必要的信息。
共享目标文件
- 是一种特殊的目标文件,包含了可供多个程序共享的代码和数据。
- 通常由编译器和链接器生成,并在运行时由动态链接器加载到内存中。
- 可以被多个进程加载和使用,节省了存储空间并提供了代码的复用性。
DLL(动态链接库)和共享库都是共享目标文件,但有以下区别:
- 操作系统支持:DLL主要用于Windows,共享库主要用于类Unix系统(如Linux、MacOS等),在Linux中用.so后缀
- 文件格式:DLL采用PE(Portable Executable)文件格式,而共享库采用ELF(Executable and Linkable Format)文件格式。
- 符号解析:DLL使用的是"stdcall"或"cdecl"的调用约定,而共享库一般使用标准的C调用约定。
- 运行时链接:DLL在程序运行时动态链接到内存中,而共享库可以在程序加载时静态链接,也可以在运行时动态链接。
可重定位目标文件
- 是编译器生成的中间文件,包含了已编译的源代码的机器代码和相关的符号信息,还没有进行最终的链接和加载操作,可以被进一步链接成可执行文件或共享目标文件。
- 主要用于静态链接,通过链接器将多个目标文件合并成一个可执行文件。
ELF(Executable and Linkable Format)可重定位目标文件是一种常见的二进制文件格式,用于存储编译后的可执行程序的中间结果。
ELF文件头(ELF Header):包含了关于ELF文件的基本信息, 如文件类型、目标体系结构、入口点地址等。
节头部表:包含了每个节的描述信息
.text节:已编译程序的可执行代码,通常是只读的
.rodata节:只读数据
.data节:已初始化的全局和静态变量的数据。局部变量保存在栈中
.bss节:未初始化的全局和静态变量的数据, 这些变量在目标文件中不占据磁盘空间、在程序运行时会被初始化为零。
.symtab节:符号表,存储了程序中定义和引用的各种符号(如函数、变量)的信息。 与编译器中的符号表不同,它不包含局部变量的条目
.rel.text节和.rel.data节:代码和数据的重定位信息, 任何调用外部函数或引用全局变量的指令都需要修改
debug节:调试符号表,存储了调试信息,包括源代码行号、变量名和类型信息等。
line节:C程序的行号与.text中机器指令之间的映射
strtab节:字符串表,存储了ELF文件中使用的字符串, 包括symtab和debug的符号表、节头部表
符号
三种符号
- 由模块m定义并能被其他模块引用的全局符号
- 外部符号:由其他模块定义并被模块m引用的全局符号
- 局部符号 :只被模块m定义和引用
符号表
管理符号信息
Value:对于可重定位模块,是位置偏移;对于可执行目标文件,是绝对地址
三个伪节:
- ABS(Absolute Symbol):代表了已知的固定地址,代表一个绝对地址的符号,它的地址在链接时已经确定,不需要进行重定位。ABS符号通常用于表示具有固定地址的全局变量或常量。
- UNDEF(Undefined Symbol):代表了需要在链接时解析的未定义符号,即在当前模块中引用的符号,但其定义在其他模块中。
- COMMON:用于处理多个模块中的共享变量,用于表示在多个模块中定义的全局变量,但每个模块中的变量大小可能不同。在链接时会被合并,分配合适的内存空间,以保证各个模块中对该变量的引用都指向同一个地址。
静态链接
符号解析
解决模块之间的符号引用。将符号的引用与符号的定义进行匹配,以确定符号在内存中的地址。
强符号:函数、已初始化的全局变量
弱符号:未初始化的全局变量
规则
- 不许多个同名的强符号
- 一强多弱,则选强
- 多弱,则任选一个
若x是强int、弱double,则可能挤占下一个变量的地址
符号重定位
将目标文件中的未定义符号与其他目标文件或库文件中的定义符号进行关联。
重定位
将每个符号定义和一个内存位置关联。
合并相同类型的节,并赋予内存地址;修改对符号的引用,使之指向正确的运行时地址
符号重定位解决了符号引用的关联问题,重定位解决了代码和数据的位置调整问题
重定位条目:告诉链接器在将目标文件合并成可执行文件时如果修改引用
//重定位使用PC相对地址的引用
if(r.type==R_x86_64_PC32)
refaddr = ADDR(s) + r.offset;//ref's run-time address
*refptr = (unsigned)(ADDR(r.symbol)+ r.addend - refaddr);
//重定位使用绝对地址的引用
if (r.type==R_x86_64_32)
*refptr = (unsigned)(ADDR(r.symbol)+ r.addend);
静态库
常见的静态库文件格式包括Linux系统下的.a(Archive)文件和Windows的.lib(Library)文件。
-
独立性:静态库将目标文件打包在一个归档文件中,使得库文件与可执行文件独立,不依赖于系统环境或其他库文件。
-
静态链接:在编译时,将静态库中的目标文件直接复制到可执行文件中,形成一个完整的可执行文件。这样,程序在运行时不需要额外加载和链接库文件。
-
提高执行速度:由于静态库已经在编译时被链接到可执行文件中,因此程序的执行速度相对较快,不需要额外的库加载和链接过程。
-
简化发布和部署:将程序所需的所有依赖都包含在静态库中,使得程序的发布和部署变得更加简单和方便。
静态库的使用方式是在编译时将库文件链接到可执行文件中,使得程序能够调用库中提供的函数和功能。在静态库中可以包含多个模块或功能,程序可以选择性地链接所需的模块。
需要注意的是,静态库的使用会增加可执行文件的大小,并且不支持动态更新库文件。如果库文件发生更新或修复,需要重新编译和链接整个程序。
符号解析阶段,命令行上的库和目标文件的顺序很重要,否则引用不会被解析
加载可执行目标文件
加载:将程序复制到内存并运行
动态链接
- 将目标文件和库文件的机器码保留为独立的模块,运行时按需向内存中加载和链接库文件。
- 允许多个可执行文件共享同一个库文件的实例,节省了内存空间,使系统更加灵活和可维护。
位置无关代码(PIC)
PIC数据引用
数据段和代码段的距离总是保存不变
在数据段开始的地方创建一个表,称为全局偏移量表(GOT)
PIC函数调用
延迟绑定:将过程地址的绑定推迟到第一次调用该过程时
第一次调用过程的运行时开销很大,但其后的每次调用都只花费一条指令和一个间接的内存引用
全局偏移量表(GOT)是数据段的一部分;过程链接表(PLT)是代码段的一部分
库打桩机制
允许截取共享库函数的调用,取而代之自己的代码
- 编译时(需要能够访问源代码)
- 链接时(需要能够访问可重定位对象文件)
- 运行时(需要能够访问可重定位目标文件)