计算机系统篇之链接(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 位)。
符号表索引表示需要修改的符号引用在.symtab
section中的索引。这里的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.text
section 中。该 section 只包含一个重定位条目,其中Offset
字段的值为 0x5,表示在目标 section (即.text
)中起始位置为 0x5 的内容需要被修改。如果该内容的值在可执行目标文件main
中被修改了,即可验证此结论。
查看可重定位目标文件main.o
的.text
section:
$ 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
的.text
section 中偏移量(相对于.text
的起始位置,即相对于main
函数的起始位置)为 0x5 的内容的当前值为 0x0。这里有两点需要注意:1)由于偏移量为 0x5 的位置与下一条指令的开始位置之间有 4 个字节,所以该内容占用 4 字节;2)由于main.o
的字节序是小端,所以,00 00 00 00
中最右边的00
对应的地址为偏移量 0x5。
查看可执行目标文件main
的.text
section:
$ 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
的.text
section 的main
函数中偏移量(相对于main
函数的起始位置)为 0x5 的内容的当前值为 0x7。即该内容的值由原来的 0x0 被修改为了 0x7。因此,验证了结论对于可重定位目标文件,Offset 字段表示需要修改的符号引用的起始位置在目标 section 中的偏移量(字节)
。
2)验证Info 字段的高 32 位的值表示需要修改的符号引用在
.symtabsection中的索引
可重定位目标文件main.o
的.rela.text
section 只包含一个重定位条目,其中Info
字段的值为 0x000b00000002(高 32 位的值为 0xb),符号名称(即Sym. Name
列的值)为_Z4funcv
。如果main.o
的symtab
section 中索引为 0xb 的符号表条目为_Z4funcv
,即可验证此结论。
查看可重定位目标文件main.o
的symtab
section:
$ 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
从上面结果中可以看出,symtab
section 中索引为 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
的.text
section:
$ 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
的.text
section:
$ 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 中此符号所有的引用。