《深度探索Linux系统:系统构建和原理解析》笔记——2.工具链

1. 工具链的工作过程

如下是从源文件到二进制文件的构建过程,需要注意的是 链接阶段,链接和包含起始程序的目标文件(crti.o ...)

实际情况我们只需要 gcc main.c即可完成编译链接,实际上,gcc只是驱动程序,控制 编译器cc1,汇编器as,连接器ld 工作。
使用 gcc -v main.c 可以看到工作的具体流程。

2. 构建阶段

2.1 预编译

gcc -E main.c -o main.i
有预编译器cpp,但编译器通常实现了预编译功能,所以gcc直接使用cc1进行预编译
主要完成:

  • 文件包含:将包含的文件的内容复制到源文件中
  • 宏展开
  • 条件编译

2.2 编译

gcc -S main.c
通过词法分析,语法分析,语义分析,并生成中间代码,并对中间代码进行优化,以生成效率更高,更小的可执行代码,
最终生成汇编代码,实际生成的汇编代码中会很多汇编伪指令,目的是为接下来的链接做准备和避免代码优化导致的调试信息丢失。

2.3 汇编

将汇编指令翻译成机器指令,
创建链接时需要的信息,包括符号表和重定位表等。

和其他阶段不同,汇编的输出为二进制格式文件,在Linux中为 ELF格式文件。包括目标文件,静态库,动态库,可执行文件都是ELF格式。

2.3.1 ELF格式

文件头部信息:描述整个文件的基本属性,包括:本文件运行在什么操作系统,什么硬件体系,程序入口地址等。最重要的两个表的信息,表的位置和条目数。
一张表是 Section Header Table, 链接时使用,记录各段的位置,长度等。
一张表是 Program Header Table, 供内核和动态加载器加载ELF文件到内存时使用。

所以,对于目标文件,只需要链接,不需要执行,所以只有Section Header Table,没有 Program Header Table

查看 文件头信息
readelf -h main.o
查看 Section Header Table信息
readelf -S main.o

在文件头信息后就是各个段:
.text 代码
.data 初始化的数据
.bss 未初始化的数据,由于未初始化的数据默认为0,所以不需要在elf文件中占用空间,只需要告诉系统分配对应大小的空间,并初始化为0即可。

一个目标文件的elf如下

.symtab :符号表
read -s main.o 查看符号表

2.3.2 重定位表

在汇编一个模块时,由于无法知道导入符号的运行地址,所以给这些符号虚拟一个地址(通常为空),在链接时,确定了这些符号的运行地址,再修订这些符号的位置,这个过程称为重定位。
除了编译时重定位还有加载时重定位和运行时重定位,这里只讨论前者。

链接其会为目标文件找到其外部符号的定义,为了方便链接器工作,目标文件需要建立一个表,每个表项为外部符号,这个表称为重定位表。
重定位表的表项为如下两种格式:

r_offset 符号值在目标文件中的偏移值
r_info 符号名称在符号表的索引
r_addend 用于辅助计算修订值

查看重定位表
readelf -r hello.o
下面说明了哪些符号需要重定位

2.3.3 符号表

因为链接器需要重定位符号,所以需要知道这些符号定义在哪里,所以目标文件需要建立一个表,记录自己的导出符号。

value:符号运行地址,链接时才分配运行时地址,所以这里值都为空
size:符号对应实体占用内存大小
type:符号类型,object指变量类型
bind:符号绑定信息,local为内部符号,global为全局符号
ndx:符号在哪个段,对于main在1即.text段,对于foo2,foo2_func是导入符号,未定义,所以是UND

在链接时会用符号表进行重定位,若删除符号表,链接会找不到符号定义而报错

2.4 链接

链接将多个目标文件,静态库,动态库,合成一个文件
链接分为两个阶段:

  1. 将多个文件合成一个文件,若输出可执行文件,还需要为指令和符号分配运行时地址
  2. 进行符号重定位

2.4.1 验证合并

合成多个文件就是将多个目标文件中的相同类型段合并到一个文件

下面的示例可以看出,可执行文件被分配了运行时地址,
多个目标文件的.text的大小和,远小于可执行文件的.text大小,因为链接器还链接了包含启动程序的目标文件(crt1.o...)

手动链接,-e指定入口地址为main,发现大小为 0x48,但我们希望为0x46。

更换文件顺序,大小成功为0x46,原因是,32机上链接需要4字节对齐,hello.o大小为26,需要补2个字节,foo.o,foo2.o都是4字节对齐的,所以若hello.o做首位需要对齐,若hello.o做末尾,不需要对齐

2.4.2验证重定位

重定位公式
重定位类型为 R_386_32,公式为 S+A
重定位类型为 R_386_PC32, 公式为 S+A+P
S:运行时地址
A:Addend
P:修订处运行时地址或偏移,对于目标文件,P为修订处在段内偏移,对于可执行文件或动态库,P为修订处的运行时地址。

首先查看hello.o需要重定位的符号,记住其在.text段中的偏移值为0x0b, 0x1b

运行时地址在链接后确定,查看hello的符号段,获得foo2, foo2_func 地址为0x0804a020, 0x08048414

根据偏移值,确定Addend,分别是0, -4

可以计算foo2的重定位地址为:
S + A = 0x0804a020 + 0 = 0x0804a020

计算foo2_func还需要确定P,即引用符号的运行时地址,
0x080483f6 + 1 = 0x080483f7
计算为:
S + A + P = 0x08048414 -4 + 0x080483f7 = 0x19

链接器合并段,确定了各个符号的运行地址,并在重定位时,直接修改了需要重定位的符号的相关引用部分的代码(修改地址对应符号的运行地址),所以链接器也称为 link editor

2.4.3 链接静态库

静态库为目标文件的打包,链接静态库和链接目标文件一样,不过合并的段只有真正需要的部分。

构建静态库,并使用

查看静态库

查看可执行文件,发现的确只有真正使用的符号被链接了

2.4.5 链接

链接动态库,不会将动态库代码合并,但是需要在被链接文件中创建动态库信息,加载器才能确定其依赖的动态库是什么

动态库信息在可执行文件的 dynamic段

动态链接时还要创建 重定位表,这样加载时,加载器才能根据重定位表重定位外部引用符号,重定位表记录在ELF文件的重定位段中,可能包含多个重定位段。

.rel.dyn:需要重定位的变量
.rel.plt:需要重定位的函数

总结:
虽然链接时不会进行重定位,但是 需要记录依赖的动态库,和记录需要重定位的条目。加载时,加载器根据这些信息完成重定位。

posted on 2021-12-15 12:48  开心种树  阅读(159)  评论(0编辑  收藏  举报