4.1.2 符号解析与重定位
(1)重定位
在完成空间和地址的分配步骤之后,链接器就进入了符号解析和重定位的步骤,这是静态链接的核心部分。
先看看 a.o 的反汇编文件: objdump -d a.o:
程序代码里面都是使用的虚地址,main 起始地址为 0 ,这是因为在未进行空间分配之前,目标文件代码段中的起始地址以 0x00000000 开始,等到空间分配完成之后,各个函数才会确定自己在虚拟地址空间中的位置。
从反汇编来看,a.o 中定义了一个函数 main,这个函数占 0x55 个字节,共 21 条指令。冒号前的代表每条指令的偏移量。
上面的 eax,esi 等为寄存器,在参数很少的情况下编译器会选择让寄存器来传递参数,但这并不是一个通用的方法,通用的方法是将参数压入栈。
在第36偏移处,callq 调用了 swap 函数,swap 的地址是 0x00000000。对于shared 的引用,在第 29 偏移处,从寄存器中直接读取,当前 shared 的地址是 0x00000000。因为 a.c 并不知道 swap 和 shared 的地址,所以都为0。
再看下 ab 文件:
40056f 处的偏移有了地址 0x601038,即shared 分配出来了地址;同样的,40057c 偏移处,也有了地址 0x40059c,这个是 swap 的地址。这两个地址都是经过链接器修正的地址。
(2)重定位表
链接器通过重定位表(Relocation Table)中保存的与重定位相关的信息来确定哪些指令需要被调整以及指令的哪些部分需要被调整。
对于可重定位的目标文件来说,它必须包含有重定位表,用来描述如何修改相应的段里的内容。对于每个要重定位的 ELF 段都有一个对应的重定位表,而一个重定位表往往就是 ELF 文件中的一个段,所以重定位表也可以叫做重定位段。一般表示为'.rel.text'等等。可以使用 objdump 来查看重定位表: objdump -r a.o,这个命令可以用来查看'a.o'里面要重定位的地方,即 'a.o'所有引用到外部符号的地址。
每个要被重定位地方叫一个重定位入口(Relocation Entry),'a.o'中有三个重定位入口,第三个我们不关心,只关心前面两个。重定位入口的偏移表示该入口在要被重定位的段中的位置,'RELOCATION RECORDS FOR [.text]'表示这个重定位表是代码段的重定位表,所以偏移表示代码段中需要被调整的位置。
从前面的反汇编可以知道,0x2a 和 0x37 分别就是代码中的 'mov' 和 'callq'指令。
重定位表的结构如下:
r_offset |
重定位入口的偏移。对于可重定位文件来说,这个值是该重定位入口所要修正的位置的第一个字节相对于段起始的偏移;对于可执行文件或共享对象文件来说,这个值是该重定位入口索要修正的位置的第一个字节的虚拟地址。 |
r_info |
重定位入口的类型和符号。这个成员你的低 8 位,表示重定位入口的类型,高 24 位表示重定位入口的符号在符号中的下标。 因为各种处理器的指令格式不一样,所以重定位所修正的指令地址格式也不一样。每种处理器都有自己一套重定位入口的类型。对于可执行文件和共享目标文件来说,它们的重定位入口是动态链接类型。 |
(3)符号类型
重定位过程伴随着符号解析的过程,每个目标文件都可能定义一些符号,也可能引用到定义在其他目标文件的符号。重定位过程中,每个重定位的入口都是对一个符号的引用,那么当链接器需要对某个符号的引用进行重定位时,它就要确定这个符号的目标地址。这时候链接器就会去查找由所有输入目标文件的符号表组成的全局符号表,找到相应的符号后进行重定位。
'GLOBAL'类型的符号,除了 main 定义在代码段之外,其他两个 'shared' 和 'swap' 都是'UND',即'undefined'未定义类型,这种未定义的符号都是因为该目标文件中有关于它们的重定位项。所以在链接器扫描完所有的输入目标文件后,所有这些未定义的符号都应该能够在全局符号表中找到,否则链接器就报符号未定义错误。
(4)指令修正方式
不同的处理器指令对于地址的格式和方式都不一样。主要的寻址方式有如下区别:
近址寻址或远址寻址
绝对寻址或相对寻址
寻址长度为 8 位、16位、32位或 64 位
但对于 32 位 x86 平台下的 ELF 文件的重定位入口所修正的指令寻址方式只有两种:
绝对近址 32 位寻址
相对近址 32 位寻址
这两种重定位方式指令修正方式每个被修正的位置的长度都为 32 位,即 4 个字节。而且都是近址寻址。
x86 重定位入口的 r_info 成员低 8 位表示重定位入口类型,如下表:
x86基本重定位类型 | ||
宏定义 |
值 |
重定位修正方法 |
R_X86_64_32 |
10 |
绝对寻址修正 S + A |
R_X86_64_PC32 |
2 |
相对寻址修正 S + A – P |
A = 保存在被修正位置的值 P = 被修正的位置(相对于段开始的偏移量或者虚拟地址),注意,该值可以通过 r_offset 计算得到 S = 符号的实际地址,即由 r_info 的高 24 位指定的符号的实际地址 |
修正方式简单介绍即可,如需要深入研究,看看 GCC 方面的书籍。
4.1.3 静态库链接
一个静态库就是一组目标文件的集合,即很多目标文件打包后形成的一个文件。
一个 C 语言的运行库中,包含了很多跟系统功能相关的代码,如输入输出、文件操作等。当我们编译完成后,这些代码生成各种 .o 文件,我们通常使用'ar'压缩程序将这些目标文件压缩到一起,并且对齐进行编号和索引,以便于查找和检索,形成了 libc.a 类似的静态库文件。我们也可以使用 ar 命令来查看静态库文件包含哪些目标文件:ar -t libc.a
可以使用 objdump 工具查看 libc.a 符号: objdump -t libc.a
合理运用 grep 和 objdump 可以查找到我们需要的符号:objdump -t libc.a | grep -abw 'printf.o'
编译程序:gcc -c -fno-builtin hello.c(-fno-builtin 禁止GCC 的自动优化功能)
解压静态库:ar -x libc.a
链接:ld hello.o printf.o
4.1.4 链接控制
链接器一般提供多种控制整个链接过程的方法,以用来产生用户需要的文件。一般连接器有如下三种方法:
使用命令行来给链接器指定参数
将链接指令存放在目标文件当中,编译器会通过这种方法像链接器传递指令
使用链接控制脚本
ld 在用户没有指定链接脚本的时候会使用默认链接脚本。可以使用如下命令查看链接器的默认链接脚本:ld -verbose
如果看过 u-boot 的代码的话,可以知道 u-boot 中的 ARM 的链接控制脚本就是 u-boot.lds 这个文件。
我们可以自己编写自己的链接脚本,然后通过命令指定编译的链接脚本 ld -T link.script
具体的链接脚本在分析 u-boot 的时候再介绍。