ELF重定位
03 Dec 2020链接重定位
在通过编译和汇编后,就生成了目标文件,链接就是把这些目标文件加工后合并成一个输出文件的过程。
链接过程可以分为两步:
- 第一步 空间与地址分配。扫描所有的输入目标文件,获得它们每个各个节的长度、属性和位置,并且将输入目标文件中的符号表中所有的符号定义和符号引用收集起来,统一放到一个全局符号表。这一步中,链接器将能够获得所有输入目标文件的节长度,并且将它们合并(相同的节互相合并,如.text和.text合并、.data和.data合并),计算出输出文件中各个段合并后的长度和位置,并建立映射关系。通过这一步,输入文件中的各个节在链接后的地址就确定了。分配并计算出可执行文件中各个节的地址后,链接器开始计算各个符号的地址。因为各个符号在节内的相对位置是固定的,所以这时候各个符号的地址已经是确定的了。举个例子,比如“main.o”中的“add”函数相对于“main.o”的“.text”段得偏移是0x33,在经过合并之后,往后移动到0x115c, 这时候需要更新符号表的
add
项(详细见下面的例子) - 第二步符号解析和重定位。使用上面第一步中收集到的所有信息,读取输入文件中段的数据、重定位信息,并且进行符号解析和重定位、调整代码中的地址等。事实上第二步是链接过程的核心,特别是重定位过程。通过objdump -d命令对目标文件进行反汇编后(能看到汇编代码),可以看到对应指令中外部引用的地址部分都是暂时用临时的假地址来代替,真正的地址计算工作由符号解析和重定位这一步来进行,也是符号解析和重定位的主要工作。通过前面的空间和地址分配可以得知,链接器在完成地址和空间分配之后就已经可以确定所有符号的虚拟地址了,那么链接器就可以根据符号的地址对每个需要重定位的指令进行地址修正。
指令修正
typedef struct {
Elf64_Addr r_offset;
Elf64_Word r_info;
} Elf64_Rel;
typedef struct {
Elf64_Addr r_offset;
Elf64_Word r_info;
Elf64_Sword r_addend;
} Elf64_Rela;
上面是重定义节区的数据结构。
- r_offset
- 需要重定位的位置。对于重定位文件,此值是从需要重定位的符号所在节区头部开始到将被重定位的位置之间的字节偏移。对于可执行文件或者共享目标文件而言,其取值是需要重定位的虚拟地址,一般而言,也就是说我们所说的 GOT 表的地址。
- r_info
- 此成员给出需要重定位的符号的符号表索引,以及相应的重定位类型。 例如一个调用指令的重定位项将包含被调用函数的符号表索引。如果索引是 STN_UNDEF,那么重定位使用 0 作为“符号值”。此外,重定位类型是和处理器相关的。
- r_addend
- 此成员给出一个常量补齐,用来计算将被填充到可重定位字段的数值。也就是我们俗称的加数,只有在rela节里会有
重定位类型表
名称 | 值 | 大小 | 计算公式 | 说明 |
---|---|---|---|---|
R_X86_64_NONE | 0 | none | none | |
R_X86_64_64 | 1 | word64 | S + A | |
R_X86_64_PC32 | 2 | word32 | S + A - P | |
R_X86_64_GOT32 | 3 | word32 | G + A | |
R_X86_64_PLT32 | 4 | word32 | L + A - P | |
R_X86_64_COPY | 5 | none | none | |
R_X86_64_GLOB_DAT | 6 | word64 | S | |
R_X86_64_JUMP_SLOT | 7 | word64 | S | |
R_X86_64_RELATIVE | 8 | word64 | B + A | |
R_X86_64_GOTPCREL | 9 | word32 | G + GOT + A - P | |
R_X86_64_32S | 11 | word32 | S + A | |
R_X86_64_16 | 12 | word16 | S + A | |
R_X86_64_PC16 | 13 | word16 | S + A - P | |
R_X86_64_8 | 14 | word8 | S + A | |
R_X86_64_PC8 | 15 | word8 | S + A - P | |
R_X86_64_DPTMOD64 | 16 | word64 | ||
R_X86_64_DTPOFF64 | 17 | word64 | ||
R_X86_64_TPOFF64 | 18 | word64 | ||
R_X86_64_TLSGD | 19 | word32 | ||
R_X86_64_TLSLD | 20 | word32 | ||
R_X86_64_DTPOFF32 | 21 | word32 | ||
R_X86_64_GOTTPOFF | 22 | word32 | ||
R_X86_64_TPOFF32 | 23 | word32 |
- A(addend)
- The addend used to compute the value of the relocatable field.
- 加数,在ELF中会显示指定
- B(base)
- 共享文件运行期的基地址。通常基地址在编译的时候是0, 但是运行的时候运行的时候会加载到一个offset之后
- G(Global)
- 在运行时重定位项的符号在全局偏移表中的偏移。
- GOT (global offset table)
- GOT中的地址
- L (linkage)
- 过程链接表项中一个符号的节区偏移或者地址。过程链接表项会把函数调用重定位到正确的目标位置。链接编辑器会构造初始的过程链接表,然后动态链接器在执行过程中会修改这些项目
- P (place)
- 表示被修正(用 r_offset 计算)的存储单元的位置(节区偏移或者地址)。
- S (symbol)
- 符号的实际地址。
我们这里讲编译中的链接过程的重定位,一般是R_X86_64_PC32
, R_X86_64_PLT32
类型
例子
下面我们以具体的例子举例并进一步的说明
/***************main.c***************/
int add(int first, int second);
int main() {
int a,b;
a = 3;
b = 4;
int ret = add(a,b);
return 0;
}
int add(int first, int second) {
return first+second;
}
我们首先编译一下main.c
✗ gcc -c main.c -o main.o
然后用objdump看一下代码段内容
➜ relocate git:(master) ✗ objdobjdump -d main.o
main.o: file format elf64-x86-64
Disassembly of section .text:
0000000000000000 <main>:
0: f3 0f 1e fa endbr64
4: 55 push %rbp
5: 48 89 e5 mov %rsp,%rbp
8: 48 83 ec 10 sub $0x10,%rsp
c: c7 45 f4 03 00 00 00 movl $0x3,-0xc(%rbp)
13: c7 45 f8 04 00 00 00 movl $0x4,-0x8(%rbp)
1a: 8b 55 f8 mov -0x8(%rbp),%edx
1d: 8b 45 f4 mov -0xc(%rbp),%eax
20: 89 d6 mov %edx,%esi
22: 89 c7 mov %eax,%edi
24: e8 00 00 00 00 callq 29 <main+0x29>
29: 89 45 fc mov %eax,-0x4(%rbp)
2c: b8 00 00 00 00 mov $0x0,%eax
31: c9 leaveq
32: c3 retq
0000000000000033 <add>:
33: f3 0f 1e fa endbr64
37: 55 push %rbp
38: 48 89 e5 mov %rsp,%rbp
3b: 89 7d fc mov %edi,-0x4(%rbp)
3e: 89 75 f8 mov %esi,-0x8(%rbp)
41: 8b 55 fc mov -0x4(%rbp),%edx
44: 8b 45 f8 mov -0x8(%rbp),%eax
47: 01 d0 add %edx,%eax
49: 5d pop %rbp
4a: c3 retq
我们可以看到,在 24: e8 00 00 00 00 callq 29 <main+0x29>
代码段,这里是调用add函数的指令,函数地址是00 00 00 00。
我们看一下main.o的重定位表
➜ relocate git:(master) ✗ readelf -r main.o
Relocation section '.rela.text' at offset 0x250 contains 1 entry:
Offset Info Type Sym. Value Sym. Name + Addend
000000000025 000a00000004 R_X86_64_PLT32 0000000000000033 add - 4
...
根据上文提到的,add符号的重定位类型是R_X86_64_PLT32
,计算公式是L + A - P
由于编译器在处理该外部函数符号的时候是不知道这个符号是定义在普通对象还是共享对象里的,所以在rela表中统一将其定义为R_X86_64_PLT32类型,在随后的链接过程会根据函数符号是定义在普通对象还是共享对象进行不同的处理。
- 该函数符号定义在普通对象中
- LD将其当做普通的R_X86_64_PC32类型进行处理,这时L+A-P = S+A-P
- 该函数符号定义在共享对象中
- LD将其作为R_X86_64_PLT32进行处理,LD会为其create一个“函数名@plt”过程和在.got.plt表中创建一个表项(用于存储函数被加载后的实际虚拟地址),并将代码中对该函数的访问改为对该过程的访问,这些操作都要在静态链接的时候完成的,这个过程(函数名@plt)的地址就是L,所以relocate计算公式变为:L+A-P。
最后动态链接的时候会将函数的实际虚拟地址更新到.got.plt表项中,这样该过程通过.got.plt表项就可以间接跳转到实际要访问的函数了。
从符号表中知道函数符号定义在当前文件中,所以,L+A-P = S+A-P
。我们再回忆一下, S表示符号的实际地址
,这个在第一步空间的重新分配中就可以得到,.text段合并重新分配后,并将分配的值写入符号表,我们假设这个值是0x115c,P表示被修正(用 r_offset 计算)的存储单元的位置
, (空间从新分配后,新指令位置写入新的重定位表中, 所以我们能够获取到修正后的指令位置), 我们假设是0x114e, 这里的加数是-4, 所以,我们计算
S+A-P = 0x115C - 0x114E + (-4) = 0xA
这里计算的其实就是调用者地址和函数(变量)地址的差值,在加上加数
我们看一下最后链接的结果(上述的假设数值都是我们从实际链接后的代码段中拿到的,所以很一致)
...
0000000000001129 <main>:
1129: f3 0f 1e fa endbr64
112d: 55 push %rbp
112e: 48 89 e5 mov %rsp,%rbp
1131: 48 83 ec 10 sub $0x10,%rsp
1135: c7 45 f4 03 00 00 00 movl $0x3,-0xc(%rbp)
113c: c7 45 f8 04 00 00 00 movl $0x4,-0x8(%rbp)
1143: 8b 55 f8 mov -0x8(%rbp),%edx
1146: 8b 45 f4 mov -0xc(%rbp),%eax
1149: 89 d6 mov %edx,%esi
114b: 89 c7 mov %eax,%edi
114d: e8 0a 00 00 00 callq 115c <add>
1152: 89 45 fc mov %eax,-0x4(%rbp)
1155: b8 00 00 00 00 mov $0x0,%eax
115a: c9 leaveq
115b: c3 retq
000000000000115c <add>:
115c: f3 0f 1e fa endbr64
1160: 55 push %rbp
1161: 48 89 e5 mov %rsp,%rbp
1164: 89 7d fc mov %edi,-0x4(%rbp)
1167: 89 75 f8 mov %esi,-0x8(%rbp)
116a: 8b 55 fc mov -0x4(%rbp),%edx
116d: 8b 45 f8 mov -0x8(%rbp),%eax
1170: 01 d0 add %edx,%eax
1172: 5d pop %rbp
1173: c3 retq
1174: 66 2e 0f 1f 84 00 00 nopw %cs:0x0(%rax,%rax,1)
117b: 00 00 00
117e: 66 90 xchg %ax,%ax
...
这时候相对应的函数调用地址已经填上的正确的数值114d: e8 0a 00 00 00 callq 115c <add>
,也就是我们上述计算出来的值(使用小端序),右边callq 115c <add>
是工具给的解释,方便我们查看调试。
callq指令后面加上了跳转的偏移值,这是callq的相对寻址方式。
什么是加数?(我猜测的)相对寻址指向目标指令的上一条指令,所以需要加上一个偏移值,现在主流的cpu架构的指令长度都是4,所以,这里的加数就是-4 —— 指向上一条指令
这里的值采用补码表示,因为原码的补码等于自身,所以这里看起来像是原码;但是一旦add函数合并的时候,跑到main函数前面了,这里的偏移值就是一个负数,必须采用补码表示
我们这里的例子是一个文件的情况,对于多个重定位文件的链接,链接过程也是一样的。