计算机系统篇之链接(5):静态链接(下)——重定位
Author: stormQ
Created: Wednesday, 15. April 2020 04:35PM
Last Modified: Sunday, 10. January 2021 02:56PM
本文描述了 Linux 系统中重定位的工作原理,并介绍了 X86_64 中常见的重定位类型。
重定位的目的是确定每个符号定义的运行时内存地址,并修改对这些符号的引用,使之指向符号定义的运行时内存地址。
重定位的整体过程可以分为两个步骤:
重定位节和符号定义。链接器将输入目标文件的相同节合并成一个节,合并的节将作为可执行目标文件中此类型的节。随后,链接器确定每个合并节的运行时内存地址,并确定合并节中符号定义的运行时内存地址。这一步骤完成后,可执行目标文件中的所有指令和符号定义的运行时内存地址就唯一确定了。
重定位节中的符号引用。链接器修改所有的符号引用,使之指向符号定义的运行时内存地址。链接器要执行此步骤依赖于目标文件中的重定位信息。
ELF 中的重定位条目分为两种格式:Rel和Rela。每个重定位条目表示一个必须被重定位的符号引用,并指明如何计算被修改的符号引用。重定位条目由汇编器生成。
查看可重定位目标文件中的重定位信息:
$ 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
注:
Offset是 Relocation Entry 结构体中的第 1 个字段,占用 8 字节,表示需要修改的符号引用的位置。对于可重定位目标文件,该字段表示需要修改的符号引用的起始位置在目标 section (.rela.text中的重定位条目对应的目标 section 为.text,.rela.data中的重定位条目对应的目标 section 为.data,以此类推)中的偏移量(字节)。对于可执行目标文件和可共享目标文件,该字段表示需要修改的符号引用的起始位置所对应的虚拟内存地址。
Info是 Relocation Entry 结构体中的第 2 个字段,占用 8 字节,表示符号表索引和重定位类型(符号表索引占用高 32 位,重定位类型占用低 32 位)。
符号表索引表示需要修改的符号引用在.symtabsection中的索引。这里的Sym. Value和Sym. Name列只是打印了所对应符号表条目中Value和Name列的值。
重定位类型指示链接器如何修改该符号引用的值。重定位类型因不同的处理器而异。
Addend是 Relocation Entry 结构体中的第 3 个字段,占用 8 字节,表示一个有符号常数,一些重定位类型要使用它对被修改符号引用的值做偏移调整。
重定位条目Rel和Rela之间的唯一区别:Rel中没有Addend字段。
源码:
$ 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.o的symtabsection 中索引为 0xb 的符号表条目为_Z4funcv,即可验证此结论。
查看可重定位目标文件main.o的symtabsection:
$ readelf -s main.o
Symbol table '.symtab' contains 12 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND
1: 0000000000000000 0 FILE LOCAL DEFAULT ABS main.cpp
2: 0000000000000000 0 SECTION LOCAL DEFAULT 1
3: 0000000000000000 0 SECTION LOCAL DEFAULT 3
4: 0000000000000000 0 SECTION LOCAL DEFAULT 4
5: 0000000000000000 0 SECTION LOCAL DEFAULT 6
6: 0000000000000000 0 SECTION LOCAL DEFAULT 7
7: 0000000000000000 0 SECTION LOCAL DEFAULT 5
8: 0000000000000000 4 OBJECT GLOBAL DEFAULT 4 g_val_1
9: 0000000000000000 4 OBJECT GLOBAL DEFAULT 3 g_val_2
10: 0000000000000000 16 FUNC GLOBAL DEFAULT 1 main
11: 0000000000000000 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: 8b 05 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: 8b 05 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 1f 80 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: 8b 05 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: 8b 05 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 中此符号所有的引用。