3. 静态链接【《链接、装载与库》学习笔记】
空间与地址分配
两步链接,分成两步:
- 空间与地址分配:扫描所有输入目标文件,获得他们各个段的长度属性和位置,并且将输入目标文件中的符号表的所有符号定义和引用收集起来统一放在一个全局符号表,合并每个段建立映射关系。
- 符号解析与重定位:读取输入文件段中数据、重定位信息,进行符号解析与重定位,调整代码中的地址。
使用ld链接器链接a.o和b.o:
1 | |
-e main表示将main函数作为程序入口,ld链接器默认程序入口为_start。-o ab表示输出文件名为ab。
符号解析与重定位
重定位
编译器把不能知道符号真正的地址暂时使用一些临时地址代替,把真正地址计算留给链接器。
在ELF文件中,重定位表专门用来保存于重定位相关的信息。可以使用objdump查案目标文件的重定位表:
1 | |
每一个要重定位的地方叫重定位入口,重定位入口的偏移表示该入口要被重定位的段中的位置。重定位表是一个Elf32_Rel结构的师叔祖,每个数组元素对应一个重定位入口。
指令修正方式
对于32为x86平台的ELF只有两种指令寻址方式:
- 绝对近址32位寻址
- 相对近址32位寻址
每个被修正的位置的长度都为4字节。重定位入口的r_info成员低8位表示重定位入口类型: R_386_32: 绝对寻址修正 S+AR_386_PC32:相对寻址修正 S+A-P
其中,- S:符号的实际地址(
r_info的高24位指定的符号实际地址) - A:保存在被修正位置的值
- P:被修正的位置
COMMON块
现代的链接机制在处理弱符号的时候,采用的是于COMMON块一样的机制。在链接过程中如果有弱符号大小大于强符号,ld链接器会报出警告。
C++ 相关问题
对于模板,GNU GCC 和MSVC都采用“将每个模板的示实例代码都单独存放在一个段里,每个段只包含一个模板实例。当别的编译单元生成同样的名字,链接器在最终链接的时候可以区分这些享用的模板实例段,合并入最终的代码段。
msvc提供了函数级别链接的编译选项,让所有的函数都像模板函数一样单独保存在一个段里面,当链接器用到某个函数就合并到输出文件中,抛弃没有用的函数。
C++会在main之前构造全局对象,在main后析构全局对象。
Linux系统下一般的程序入口是_start,这个函数是Linux系统库(Glibc)的一部分。为了在main之前和结束后执行一些操作,在ELF文件定义了两种特殊的段:
.init保存可执行指令,构成进程的初始化代码。当一个程序开始运行时,在main函数被调用之前执行这里的代码。.fini保存进程种植代码指令。
API是源代码级别的接口,ABI是二进制层面的接口,兼容性比API更为严格。
静态库链接
一种语言的开发环境往往附带有语言库(Language Library),这些库是对操作系统的API的封装。
一个静态库可以简单地看成一组目标文件的集合,即所有目标文件经过压缩打包形成的一个文件。Linux中最常用的C语言静态库libc位于/usr/lib/libc.a,属于glibc项目的一部分。glibc本身使用C语言开发的,可以使用ar工具查看一个静态库文件包含了哪些目标文件:
1 | |
使用如下命令把libc.a中所有目标文件“解压”至当前目录:
1 | |
链接过程控制
默认的ld链接脚本存放在/usr/lib/ldscripts下。不同机器平台、输出文件格式都有相应链接脚本。为了更加精确控制链接过程可以自己写脚本:
1 | |
一般链接脚本名以.lds作为扩展名,ENTRY制定程序入口,SECTIONS是链接脚本的主体,制定了各种输入段到输出段的变换。
BFD库
BFD库(Binary File Descriptor Library)是 binutils 项目的一个子项目。 BFD把目标文件抽象成一个统一的模型。现在GCC、 ld、 gdb等都通过BFD库处理目标文件,而不是直接操作目标文件。