例子
我们先写一个简单的程序:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 __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 ; }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; Elf64_Xword r_info; Elf64_Sxword r_addend; } Elf64_Rela;
使用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 > [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
符号的位置是正确的,这验证了上面的说法。