Home
updated:

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


例子

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

// 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反汇编的结果:

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的反汇编结果如下:

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的段,这个段是一个名叫重定位表结构体数组。结构体的定义如下所示:

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段的内容:

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,再看符号表:

> # 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

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