ELF重定位
Posted rtoax
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了ELF重定位相关的知识,希望对你有一定的参考价值。
在工作和学习中,越发发现ELF文件格式的重要性,今天简单谈谈ELF重定位。重定位技术实际上是给二进制打补丁的机制,如果使用了动态连接器,可以使用重定位在内存打热补丁。
--问题引入--
因为32位系统和64位系统差异不大,我们采用32位架构进行分析,已知一个重定向条目定义为:
typedef struct
{
Elf32_Addr r_offset;
Elf32_Word r_info;
} Elf32_Rel;
上述结构中,加数为隐式的,还有个结构体,添加了显式加数:
typedef struct
{
Elf32_Addr r_offset;
Elf32_Word r_info;
Elf32_Sword r_addend;
} Elf32_Rela;
总共有以下四种结构体:
typedef struct {
Elf32_Addr r_offset;
Elf32_Word r_info;
} Elf32_Rel;
typedef struct {
Elf32_Addr r_offset;
Elf32_Word r_info;
Elf32_Sword r_addend;
} Elf32_Rela;
typedef struct {
Elf64_Addr r_offset;
Elf64_Xword r_info;
} Elf64_Rel;
typedef struct {
Elf64_Addr r_offset;
Elf64_Xword r_info;
Elf64_Sxword r_addend;
} Elf64_Rela;
下面给出一个简单的示例C代码:
//$ cat obj1.c
_start()
{
foo();
}
上面的代码的函数foo并没有实现,将在另一个C文件中给出。对上面的C程序进行编译,生成二进制目标文件,当然,我们这里指定生成32位目标文件:
$ gcc -nostdlib obj1.c -m32 -c
对目标文件进行反汇编为:
$ objdump -d obj1.o
obj1.o: file format elf32-i386
Disassembly of section .text:
00000000 <_start>:
0: f3 0f 1e fb endbr32
4: 55 push %ebp
5: 89 e5 mov %esp,%ebp
7: 53 push %ebx
8: 83 ec 04 sub $0x4,%esp
b: e8 fc ff ff ff call c <_start+0xc>
10: 05 01 00 00 00 add $0x1,%eax
15: 89 c3 mov %eax,%ebx
17: e8 fc ff ff ff call 18 <_start+0x18>
1c: 90 nop
1d: 83 c4 04 add $0x4,%esp
20: 5b pop %ebx
21: 5d pop %ebp
22: c3 ret
Disassembly of section .text.__x86.get_pc_thunk.ax:
00000000 <__x86.get_pc_thunk.ax>:
0: 8b 04 24 mov (%esp),%eax
3: c3 ret
在这个目标文件中,存在重定向条目:
$ readelf -r obj1.o
Relocation section '.rel.text' at offset 0x22c contains 3 entries:
Offset Info Type Sym.Value Sym. Name
0000000c 00000c02 R_386_PC32 00000000 __x86.get_pc_thunk.ax
00000011 00000d0a R_386_GOTPC 00000000 _GLOBAL_OFFSET_TABLE_
00000018 00000e04 R_386_PLT32 00000000 foo
Relocation section '.rel.eh_frame' at offset 0x244 contains 2 entries:
Offset Info Type Sym.Value Sym. Name
00000020 00000202 R_386_PC32 00000000 .text
00000044 00000502 R_386_PC32 00000000 .text.__x86.get_pc_thu
只关注下面这一行:
Offset Info Type Sym.Value Sym. Name
00000018 00000e04 R_386_PLT32 00000000 foo
那么,foo对应的重定向条目为:
r_offset = 00000018
r_info = 00000e04
类型为:R_386_PLT32 ,这将在后面进行介绍。在objdump的结果中:
17: e8 fc ff ff ff call 18 <_start+0x18>
e8表示call指令,fc ff ff ff 为隐式加数(因为是小端,所以这个数值为0xfffffffc,对应-4)。下图红色位置是我们需要注意的:
在另一源文件obj2.c中,定义了函数foo:
//$ cat obj2.c
void foo()
{}
同样,进行编译和反汇编:
$ gcc -nostdlib obj2.c -m32 -c
$ objdump -d obj2.o
obj2.o: file format elf32-i386
Disassembly of section .text:
00000000 <foo>:
0: f3 0f 1e fb endbr32
4: 55 push %ebp
5: 89 e5 mov %esp,%ebp
7: e8 fc ff ff ff call 8 <foo+0x8>
c: 05 01 00 00 00 add $0x1,%eax
11: 90 nop
12: 5d pop %ebp
13: c3 ret
Disassembly of section .text.__x86.get_pc_thunk.ax:
00000000 <__x86.get_pc_thunk.ax>:
0: 8b 04 24 mov (%esp),%eax
3: c3 ret
这个文件中其实也存在重定向条目,但是,我们不需要关心。
接着,趁热,编译出可执行文件:
$ gcc -nostdlib obj1.o obj2.o -m32 -o a.out
再次反汇编查看:
$ objdump -d a.out a.out: file format elf32-i386Disassembly of section .text:00001000 <_start>: 1000: f3 0f 1e fb endbr32 1004: 55 push %ebp 1005: 89 e5 mov %esp,%ebp 1007: 53 push %ebx 1008: 83 ec 04 sub $0x4,%esp 100b: e8 13 00 00 00 call 1023 <__x86.get_pc_thunk.ax> 1010: 05 e4 2f 00 00 add $0x2fe4,%eax 1015: 89 c3 mov %eax,%ebx 1017: e8 0b 00 00 00 call 1027 <foo> 101c: 90 nop 101d: 83 c4 04 add $0x4,%esp 1020: 5b pop %ebx 1021: 5d pop %ebp 1022: c3 ret 00001023 <__x86.get_pc_thunk.ax>: 1023: 8b 04 24 mov (%esp),%eax 1026: c3 ret 00001027 <foo>: 1027: f3 0f 1e fb endbr32 102b: 55 push %ebp 102c: 89 e5 mov %esp,%ebp 102e: e8 f0 ff ff ff call 1023 <__x86.get_pc_thunk.ax> 1033: 05 c1 2f 00 00 add $0x2fc1,%eax 1038: 90 nop 1039: 5d pop %ebp 103a: c3 ret
为了清晰,我简化一下:
Disassembly of section .text:
00001000 <_start>:
1017: e8 0b 00 00 00 call 1027 <foo>
00001027 <foo>:
这和编译出来的两个目标文件中的地址显然不同了,如下:
$ objdump -d obj1.o
Disassembly of section .text:
00000000 <_start>:
17: e8 fc ff ff ff call 18 <_start+0x18>
$ objdump -d obj2.o
Disassembly of section .text:
00000000 <foo>:
--分析--
我们的问题正式引入,
重定位是怎么将obj1+obj2转化为可执行文件的?
在上面我们简单提到foo在obj1中的重定位类型是 R_386_PLT32 ,重定位类型有很多,可以参见
32-bit x86: ELF Relocation Types
Name | Value | Field | Calculation |
---|---|---|---|
R_386_NONE | 0 | None | None |
R_386_32 | 1 | word32 | S + A |
R_386_PC32 | 2 | word32 | S + A - P |
R_386_GOT32 | 3 | word32 | G + A |
R_386_PLT32 | 4 | word32 | L + A - P |
R_386_COPY | 5 | None | Refer to the explanation following this table. |
R_386_GLOB_DAT | 6 | word32 | S |
R_386_JMP_SLOT | 7 | word32 | S |
R_386_RELATIVE | 8 | word32 | B + A |
R_386_GOTOFF | 9 | word32 | S + A - GOT |
R_386_GOTPC | 10 | word32 | GOT + A - P |
R_386_32PLT | 11 | word32 | L + A |
R_386_16 | 20 | word16 | S + A |
R_386_PC16 | 21 | word16 | S + A - P |
R_386_8 | 22 | word8 | S + A |
R_386_PC8 | 23 | word8 | S + A - P |
R_386_SIZE32 | 38 | word32 | Z + A |
https://docs.oracle.com/cd/E19120-01/open.solaris/819-0690/chapter6-26/index.html
上表中,字母的含义为:
-
A:用于计算可重定位字段值的加数。
-
B:共享对象在执行过程中加载到内存的基地址。
一般情况下,共享对象文件是用0的虚拟基地址构建的,但是共享对象的执行地址是不同的。
见程序头。
-
G:执行时重定位项的符号地址所在的全局偏移表中的偏移量。
-
GOT:全局偏移表的地址。
-
L:符号的过程链接表项的段偏移量或地址。
-
P:被重定位的存储单元的段偏移或地址,使用r_offset计算。
-
S:索引位于重定位项中的符号的值。
-
Z:索引位于重定位项的符号的大小。
对应类型 R_386_PLT32 可以找到计算方式为 L + A - P,他的意思是:计算符号的过程链接表条目的地址并指示链接编辑器创建过程链接表。仿佛一头雾水,那么我们结合汇编代码看就显得不那么灰色难懂了。再次给出简化的反汇编代码:
那么就需要根据以上信息计算“L + A - P”即可:
L = 0x1027
A = 0xfffffffc
P = 0x1018
为了清晰,我把他们在图上标注出来:
计算:
L + A - P
= 0x1027 + 0xfffffffc - 0x1018
= 0x1027 - 4 - 0x1018
= 0xb
计算出来的0xb 是什么?就是指令:e8 0b 00 00 00 也就是call 1027 <foo> 。
0x1027的计算过程是:
CALL指令地址 + offset + CALL指令长度
= 0x1017 + 0xb + 5
= 0x1027
至此,重定位过程就结束了。
--结论--
重定位技术实际上是给二进制打补丁的机制,如果使用了动态连接器,可以使用重定位在内存打热补丁。
--参考链接--
-
https://docs.oracle.com/cd/E19120-01/open.solaris/819-0690/6n33n7fct/index.html
-
源码:https://gitee.com/rtoax/test-linux
我的分析流程见下图:
以上是关于ELF重定位的主要内容,如果未能解决你的问题,请参考以下文章