《程序员的自我修养》- 重定位


#例子

我们先写一个简单的程序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// try.c
__attribute__ ((weak)) extern int shared;
void swap(int *a, int *b) __attribute__ ((weak));
int main(void)
{
int a = 100;
if (swap)
swap(&a, &shared);
return 0;
}
// lib.c
int shared = 1;
void swap(int *a, int *b)
{
*a ^= *b ^= *a ^= *b;
}

在程序try.c中,我们引用了外部的一个整形变量和一个函数,将其编译成.o文件后可以在.text段中看到main函数对应的汇编指令。 在看到汇编指令之前,我们先提出一个问题:当我们调用一个并不在本文件中定义的变量时,编译器是如何处理的? 下面我们来看看objdump反汇编的结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Disassembly of section .text:
0000000000000000 <main>:
0: 55 push %rbp
1: 48 89 e5 mov %rsp,%rbp
4: 48 83 ec 10 sub $0x10,%rsp
8: c7 45 fc 64 00 00 00 movl $0x64,-0x4(%rbp)
f: 48 8b 05 00 00 00 00 mov 0x0(%rip),%rax # 16 <main+0x16>
16: 48 85 c0 test %rax,%rax
19: 74 16 je 31 <main+0x31>
1b: 48 8d 45 fc lea -0x4(%rbp),%rax
1f: 48 8b 15 00 00 00 00 mov 0x0(%rip),%rdx # 26 <main+0x26>
26: 48 89 d6 mov %rdx,%rsi
29: 48 89 c7 mov %rax,%rdi
2c: e8 00 00 00 00 callq 31 <main+0x31>
31: b8 00 00 00 00 mov $0x0,%eax
36: c9 leaveq
37: c3 retq

对照源代码,我们可以看到0x1f处应该是使用变量shared的位置,而0xf和0x2c处则是调用函数swap的位置。然而这两个位置的指令却让人摸不着头脑,看起来关键的数据都被使用00填充掉了。然而将try.c与lib.c放在一起编译时,最终可执行文件中main的反汇编结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Disassembly of section .text:
0000000000401000 <main>:
401000: 55 push %rbp
401001: 48 89 e5 mov %rsp,%rbp
401004: 48 83 ec 10 sub $0x10,%rsp
401008: c7 45 fc 64 00 00 00 movl $0x64,-0x4(%rbp)
40100f: 48 c7 c0 38 10 40 00 mov $0x401038,%rax
401016: 48 85 c0 test %rax,%rax
401019: 74 16 je 401031 <main+0x31>
40101b: 48 8d 45 fc lea -0x4(%rbp),%rax
40101f: 48 c7 c2 00 40 40 00 mov $0x404000,%rdx
401026: 48 89 d6 mov %rdx,%rsi
401029: 48 89 c7 mov %rax,%rdi
40102c: e8 07 00 00 00 callq 401038 <swap>
401031: b8 00 00 00 00 mov $0x0,%eax
401036: c9 leaveq
401037: c3 retq

需要注意,这里的地址是指令的虚拟地址。对照来看,原本调用函数swap的指令操作数发生了变化,变成了swap函数的相对地址,而变量shared的地址也变成了0x404000。经验告诉我们,shared将会被存放在.data段,我们来看一看是否是这样: 事实的确如此,看来在编译的过程中,某一步操将这些原本用于填充的“假数据”替换成了真正的数据。这一步操作便是所谓的重定位。

#重定位

在重定位这一步操作前,我们需要知道链接器将多个目标文件(即初步编译形成的.o文件)的相同类型段合并到最终的可执行文件中。在这个过程中,数据的地址被重组。接下来重定位的任务就是把合并后数据的正确地址填充到相应位置。 那么链接器是怎么知道上面的“相应位置”是指哪里呢?这里就需要用到重定位表了。当我们查看原来的目标文件的段时,我们可以看到一个名为.text.rela的段,这个段是一个名叫重定位表结构体数组。结构体的定义如下所示:

1
2
3
4
5
6
7
typedef struct elf64_rela {
Elf64_Addr r_offset; /* Location at which to apply the action */
Elf64_Xword r_info; /* index and type of relocation */
Elf64_Sxword r_addend; /* Constant addend used to compute value */
} Elf64_Rela;
// Elf64_Xword -> __u64
// Elf64_Sxword -> __s64

使用readelf -r命令查看.rela.text段的内容:

1
2
3
4
5
Relocation section '.rela.text' at offset 0x228 contains 3 entries:
Offset Info Type Sym. Value Sym. Name + Addend
000000000012 000a0000002a R_X86_64_REX_GOTP 0000000000000000 swap - 4
000000000022 000b0000002a R_X86_64_REX_GOTP 0000000000000000 shared - 4
00000000002d 000a00000004 R_X86_64_PLT32 0000000000000000 swap - 4

注意info的内容被分为了两段,高32位的内容是重定位符号在符号表中的位置,而低32位则是重定位类型。 下面我们来进行验证:由info成员可以得知swap和shared在符号表中索引分别为0xa和0xb,再看符号表:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
> # rabin2 -s try.o
[Symbols]
nth paddr vaddr bind type size lib name
------------------------------------------------------
1 0x00000000 0x08000000 LOCAL FILE 0 try.c
2 0x00000040 0x08000040 LOCAL SECT 0 .text
3 0x00000078 0x08000078 LOCAL SECT 0 .data
4 0x00000078 0x08000078 LOCAL SECT 0 .bss
5 0x0000009f 0x0800009f LOCAL SECT 0 .note.GNU-stack
6 0x000000a0 0x080000a0 LOCAL SECT 0 .eh_frame
7 0x00000078 0x08000078 LOCAL SECT 0 .comment
8 0x00000040 0x08000040 GLOBAL FUNC 56 main
9 0x00000000 0x08000000 GLOBAL NOTYPE 16 imp._GLOBAL_OFFSET_TABLE_
10 0x00000000 0x08000000 WEAK NOTYPE 16 imp.swap
11 0x00000000 0x08000000 WEAK NOTYPE 16 imp.shared

符号的位置是正确的,这验证了上面的说法。


← Prev BJDCTF 2nd - 部分WP | 数据结构 - Lesson 2.1 Next →