计算机系统篇之链接(5):静态链接(下)——重定位

Author: stormQ

Created: Wednesday, 15. April 2020 04:35PM

Last Modified: Sunday, 10. January 2021 02:56PM



摘要

本文描述了 Linux 系统中重定位的工作原理,并介绍了 X86_64 中常见的重定位类型。

重定位的整体过程

重定位的目的是确定每个符号定义的运行时内存地址,并修改对这些符号的引用,使之指向符号定义的运行时内存地址。

重定位的整体过程可以分为两个步骤:

重定位条目

ELF 中的重定位条目分为两种格式:RelRela。每个重定位条目表示一个必须被重定位的符号引用,并指明如何计算被修改的符号引用。重定位条目由汇编器生成。

查看可重定位目标文件中的重定位信息:

$ readelf -r main.o

Relocation section '.rela.text' at offset 0x208 contains 1 entries:
  Offset          Info           Type           Sym. Value    Sym. Name + Addend
000000000005  000b00000002 R_X86_64_PC32     0000000000000000 _Z4funcv - 4

Relocation section '.rela.eh_frame' at offset 0x220 contains 1 entries:
  Offset          Info           Type           Sym. Value    Sym. Name + Addend
000000000020  000200000002 R_X86_64_PC32     0000000000000000 .text + 0

源码:

$ cat test.cpp 
extern int g_val_1;
extern int g_val_2;

void func()
{
  g_val_1 *= 2;
  g_val_2 *= 2;
}

$ cat main.cpp 
int g_val_1;
int g_val_2 = 3;

void func();

int main()
{
  func();
  return 0;
}

1)验证对于可重定位目标文件,Offset 字段表示需要修改的符号引用的起始位置在目标 section 中的偏移量(字节)

可重定位目标文件main.o中代码的重定位条目放在.rela.textsection 中。该 section 只包含一个重定位条目,其中Offset字段的值为 0x5,表示在目标 section (即.text)中起始位置为 0x5 的内容需要被修改。如果该内容的值在可执行目标文件main中被修改了,即可验证此结论。

查看可重定位目标文件main.o.textsection:

$ objdump -d main.o

main.o:     file format elf64-x86-64


Disassembly of section .text:

0000000000000000 <main>:
   0:    55                      push   %rbp
   1:    48 89 e5                mov    %rsp,%rbp
   4:    e8 00 00 00 00          callq  9 <main+0x9>
   9:    b8 00 00 00 00          mov    $0x0,%eax
   e:    5d                      pop    %rbp
   f:    c3                      retq   

可以看出,main.o.textsection 中偏移量(相对于.text的起始位置,即相对于main函数的起始位置)为 0x5 的内容的当前值为 0x0。这里有两点需要注意:1)由于偏移量为 0x5 的位置与下一条指令的开始位置之间有 4 个字节,所以该内容占用 4 字节;2)由于main.o的字节序是小端,所以,00 00 00 00中最右边的00对应的地址为偏移量 0x5。

查看可执行目标文件main.textsection:

$ objdump -d main

main:     file format elf64-x86-64

# 省略...

Disassembly of section .text:

# 省略...

00000000004004d6 <main>:
  4004d6:    55                      push   %rbp
  4004d7:    48 89 e5                mov    %rsp,%rbp
  4004da:    e8 07 00 00 00          callq  4004e6 <_Z4funcv>
  4004df:    b8 00 00 00 00          mov    $0x0,%eax
  4004e4:    5d                      pop    %rbp
  4004e5:    c3                      retq   

# 省略...

可以看出,main.textsection 的main函数中偏移量(相对于main函数的起始位置)为 0x5 的内容的当前值为 0x7。即该内容的值由原来的 0x0 被修改为了 0x7。因此,验证了结论对于可重定位目标文件,Offset 字段表示需要修改的符号引用的起始位置在目标 section 中的偏移量(字节)

2)验证Info 字段的高 32 位的值表示需要修改的符号引用在.symtabsection中的索引

可重定位目标文件main.o.rela.textsection 只包含一个重定位条目,其中Info字段的值为 0x000b00000002(高 32 位的值为 0xb),符号名称(即Sym. Name列的值)为_Z4funcv。如果main.osymtabsection 中索引为 0xb 的符号表条目为_Z4funcv,即可验证此结论。

查看可重定位目标文件main.osymtabsection:

$ readelf -s main.o

Symbol table '.symtab' contains 12 entries:
   Num:    Value          Size Type    Bind   Vis      Ndx Name
     00000000000000000     0 NOTYPE  LOCAL  DEFAULT  UND 
     10000000000000000     0 FILE    LOCAL  DEFAULT  ABS main.cpp
     20000000000000000     0 SECTION LOCAL  DEFAULT    1 
     30000000000000000     0 SECTION LOCAL  DEFAULT    3 
     40000000000000000     0 SECTION LOCAL  DEFAULT    4 
     50000000000000000     0 SECTION LOCAL  DEFAULT    6 
     60000000000000000     0 SECTION LOCAL  DEFAULT    7 
     70000000000000000     0 SECTION LOCAL  DEFAULT    5 
     80000000000000000     4 OBJECT  GLOBAL DEFAULT    4 g_val_1
     90000000000000000     4 OBJECT  GLOBAL DEFAULT    3 g_val_2
    100000000000000000    16 FUNC    GLOBAL DEFAULT    1 main
    110000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND _Z4funcv

从上面结果中可以看出,symtabsection 中索引为 0xb 的符号表条目正是_Z4funcv。因此,验证了结论Info 字段的高 32 位的值表示需要修改的符号引用在.symtabsection中的索引

重定位类型

X86_64 中的重定位类型 含义 地址在指令中占用的字节数 符号引用的值如何计算
R_X86_64_PC32 32 位 PC 相对地址 4 计算方式详见下文
R_X86_64_64 64 位绝对地址 8 计算方式详见下文
R_X86_64_32 32 位绝对地址 4 计算方式详见下文

1)重定位类型 R_X86_64_PC32 如何计算符号引用的值

重定位类型R_X86_64_PC32表示重定位符号引用的结果是一个 32 位的 PC 相对地址。

一个 PC 相对地址就是距程序计数器(即PC寄存器)的当前运行时值的偏移量。当 CPU 执行一条使用 32 位 PC 相对寻址的指令时,它会将指令中编码的 32 位值加上 PC 寄存器的当前运行值,作为目的地址(如call指令的目标地址),PC 寄存器中的值通常是下一条指令在内存中的地址。

因此,可以得出公式1:ADDR(PC) + VALUE(reference) = ADDR(defined_symbol)

注:ADDR(PC)表示符号引用所在指令的下一条指令的运行时地址;VALUE(reference)表示要修改的符号引用的值,重定位符号引用的目的就是计算出该值的大小;ADDR(defined_symbol)表示被引用的符号定义的运行时地址。

另外,公式2:ADDR(PC) + VALUE(r.addend) = ADDR(s) + VALUE(r.offset) = ADDR(reference)。(该公式的最左侧部分是根据下文中 VALUE(reference) 的计算方式反向推导而来的,即最左侧部分为 ADDR(PC) + VALUE(r.addend),而不是 ADDR(PC) - VALUE(r.addend))

注:VALUE(r.addend)表示重定位条目中addend字段的值;ADDR(s)表示符号引用的目标 section 的运行时地址(即符号引用所在 section 中第一个字节的虚拟地址);VALUE(r.offset)表示重定位条目中offset字段的值。

根据公式 1 和公式 2,可以得出VALUE(reference)的计算方式。推导过程为:

VALUE(reference) = ADDR(defined_symbol) - ADDR(PC)
                 = ADDR(defined_symbol) - ( ADDR(s) + VALUE(r.offset) - VALUE(r.addend) )
                 = ADDR(defined_symbol) + VALUE(r.addend) - ( ADDR(s) + VALUE(r.offset) )
                 = ADDR(defined_symbol) + VALUE(r.addend) - ADDR(reference) 

注:ADDR(reference)表示需要修改的符号引用的运行时地址。

最终,得出需要修改的符号引用的值等于ADDR(defined_symbol) + VALUE(r.addend) - ADDR(reference)

接下来,通过示例验证上述过程。

查看可重定位目标文件main.o的重定位信息:

$ readelf -r main.o

Relocation section '.rela.text' at offset 0x208 contains 1 entries:
  Offset          Info           Type           Sym. Value    Sym. Name + Addend
000000000005  000b00000002 R_X86_64_PC32     0000000000000000 _Z4funcv - 4

Relocation section '.rela.eh_frame' at offset 0x220 contains 1 entries:
  Offset          Info           Type           Sym. Value    Sym. Name + Addend
000000000020  000200000002 R_X86_64_PC32     0000000000000000 .text + 0

从上面结果中可以看出,VALUE(r.offset)的值为 0x5,VALUE(r.addend)的值为 -0x4。

查看可重定位目标文件main.o和可执行目标文件main.textsection:

$ objdump -d main.o

main.o:     file format elf64-x86-64


Disassembly of section .text:

0000000000000000 <main>:
   0:    55                      push   %rbp
   1:    48 89 e5                mov    %rsp,%rbp
   4:    e8 00 00 00 00          callq  9 <main+0x9>
   9:    b8 00 00 00 00          mov    $0x0,%eax
   e:    5d                      pop    %rbp
   f:    c3                      retq   

$ objdump -d main

main:     file format elf64-x86-64

# 省略...

Disassembly of section .text:

# 省略...

00000000004004d6 <main>:
  4004d6:    55                      push   %rbp
  4004d7:    48 89 e5                mov    %rsp,%rbp
  4004da:    e8 07 00 00 00          callq  4004e6 <_Z4funcv>
  4004df:    b8 00 00 00 00          mov    $0x0,%eax
  4004e4:    5d                      pop    %rbp
  4004e5:    c3                      retq   

00000000004004e6 <_Z4funcv>:
  4004e6:    55                      push   %rbp
  4004e7:    48 89 e5                mov    %rsp,%rbp
  4004ea:    805 48 0b 20 00       mov    0x200b48(%rip),%eax        # 601038 <__TMC_END__>
  4004f0:    01 c0                   add    %eax,%eax
  4004f2:    89 05 40 0b 20 00       mov    %eax,0x200b40(%rip)        # 601038 <__TMC_END__>
  4004f8:    805 32 0b 20 00       mov    0x200b32(%rip),%eax        # 601030 <g_val_2>
  4004fe:    01 c0                   add    %eax,%eax
  400500:    89 05 2a 0b 20 00       mov    %eax,0x200b2a(%rip)        # 601030 <g_val_2>
  400506:    90                      nop
  400507:    5d                      pop    %rbp
  400508:    c3                      retq   
  400509:    0f 180 00 00 00 00    nopl   0x0(%rax)

# 省略...

从上面结果中可以看出,ADDR(s)的值为 0x4004d6,ADDR(defined_symbol)的值为 0x4004e6。

因此,VALUE(reference)的值等于 0x4004e6 + (-0x4) - (0x4004d6 + 0x5) = 0x7。即链接器需要将符号引用的值修改为 0x7。查看main中该符号引用的值也确实是 0x7。

2)重定位类型 R_X86_64_64 如何计算符号引用的值

重定位类型R_X86_64_64表示重定位符号引用的结果是一个 64 位的绝对地址。

首先,编译源码时使用大型代码模型,即编译时添加选项-mcmodel=large,从而出现重定位类型为R_X86_64_64的重定位条目。

生成并查看可重定位目标文件main_large.o的重定位信息:

$ g++ -c main.cpp -mcmodel=large -o main_large.o
$ readelf -r main_large.o 

Relocation section '.rela.text' at offset 0x210 contains 1 entries:
  Offset          Info           Type           Sym. Value    Sym. Name + Addend
000000000006  000b00000001 R_X86_64_64       0000000000000000 _Z4funcv + 0

Relocation section '.rela.eh_frame' at offset 0x228 contains 1 entries:
  Offset          Info           Type           Sym. Value    Sym. Name + Addend
000000000020  000200000002 R_X86_64_PC32     0000000000000000 .text + 0

生成并查看可执行目标文件main_large.textsection:

$ g++ -o main_large main_large.o test.o
$ objdump -d main_large

main_large:     file format elf64-x86-64

# 省略...

Disassembly of section .text:

# 省略...

00000000004004d6 <main>:
  4004d6:    55                      push   %rbp
  4004d7:    48 89 e5                mov    %rsp,%rbp
  4004da:    48 b8 ed 04 40 00 00    movabs $0x4004ed,%rax
  4004e1:    00 00 00 
  4004e4:    ff d0                   callq  *%rax
  4004e6:    b8 00 00 00 00          mov    $0x0,%eax
  4004eb:    5d                      pop    %rbp
  4004ec:    c3                      retq   

00000000004004ed <_Z4funcv>:
  4004ed:    55                      push   %rbp
  4004ee:    48 89 e5                mov    %rsp,%rbp
  4004f1:    805 41 0b 20 00       mov    0x200b41(%rip),%eax        # 601038 <__TMC_END__>
  4004f7:    01 c0                   add    %eax,%eax
  4004f9:    89 05 39 0b 20 00       mov    %eax,0x200b39(%rip)        # 601038 <__TMC_END__>
  4004ff:    805 2b 0b 20 00       mov    0x200b2b(%rip),%eax        # 601030 <g_val_2>
  400505:    01 c0                   add    %eax,%eax
  400507:    89 05 23 0b 20 00       mov    %eax,0x200b23(%rip)        # 601030 <g_val_2>
  40050d:    90                      nop
  40050e:    5d                      pop    %rbp
  40050f:    c3                      retq

# 省略...

从上面可以看出,链接器需要将符号引用的值修改为 0x4004ed,正是_Z4funcv符号定义的运行时地址。

3)重定位类型 R_X86_64_32 如何计算符号引用的值

重定位类型R_X86_64_32表示重定位符号引用的结果是一个 32 位的绝对地址。与重定位类型R_X86_64_64的验证过程类似,此处不再赘述。

重定位符号引用

重定位符号引用的过程为遍历所有的重定位条目,每个重定位条目根据其重定位类型修改其目标 section 中此符号所有的引用。


下一篇:计算机系统篇之链接(6):动态链接

上一篇:计算机系统篇之链接(4):静态链接(中)——符号解析

首页