C++链接在实践中是如何工作的?我要寻找的是关于链接发生的详细解释,而不是执行链接所需的命令。
已经有一个类似的关于编译的问题,但没有详细说明:编译/链接过程是如何工作的?
C++链接在实践中是如何工作的?我要寻找的是关于链接发生的详细解释,而不是执行链接所需的命令。
已经有一个类似的关于编译的问题,但没有详细说明:编译/链接过程是如何工作的?
编辑: 我已将此答案移至重复问题: https://dev59.com/aHA75IYBdhLWcg3wYYBQ#33690144
本答案重点讨论了地址重定位,这是链接的关键功能之一。
我们将使用一个最小的示例来澄清概念。
总结:重定位会编辑目标文件的.text
部分,以将:
必须由链接器完成,因为编译器一次只看到一个输入文件,但我们必须同时知道所有目标文件,以决定如何:
.text
和.data
部分冲突前提条件:对以下内容有最低限度的理解:
链接与C或C++无关:编译器只生成目标文件。然后,链接器将它们作为输入接受,而不会知道编译它们的语言是什么。也可以是Fortran。
因此,为了简化,让我们研究一个NASM x86-64 ELF Linux hello world示例:
section .data
hello_world db "Hello world!", 10
section .text
global _start
_start:
; sys_write
mov rax, 1
mov rdi, 1
mov rsi, hello_world
mov rdx, 13
syscall
; sys_exit
mov rax, 60
mov rdi, 0
syscall
使用以下工具进行编译和汇编:
nasm -felf64 hello_world.asm # creates hello_world.o
ld -o hello_world.out hello_world.o # static ELF executable with no libraries
使用 NASM 2.10.09 版本。
首先,我们将对目标文件的 .text
部分进行反编译:
objdump -d hello_world.o
这将会给出:
0000000000000000 <_start>:
0: b8 01 00 00 00 mov $0x1,%eax
5: bf 01 00 00 00 mov $0x1,%edi
a: 48 be 00 00 00 00 00 movabs $0x0,%rsi
11: 00 00 00
14: ba 0d 00 00 00 mov $0xd,%edx
19: 0f 05 syscall
1b: b8 3c 00 00 00 mov $0x3c,%eax
20: bf 00 00 00 00 mov $0x0,%edi
25: 0f 05 syscall
关键行是:
a: 48 be 00 00 00 00 00 movabs $0x0,%rsi
11: 00 00 00
将“which should move the address of the hello world string into the rsi
register, which is passed to the write system call.”翻译为中文之后,编译器将“Hello World!”字符串的地址移动到传递给写入系统调用的rsi
寄存器中。但是问题来了!当程序加载到内存中时,“Hello World!”字符串在内存中的位置如何得知?由于我们将许多.o
文件链接在一起,并具有多个.data
段,因此编译器无法知道,只有链接器才能知道只有链接器才有所有这些目标文件。因此,编译器会在已编译的输出上放置占位符值0x0
,并向链接器提供一些额外信息,以便修改编译代码的好地址。这些“额外信息”包含在对象文件的.rela.text
部分中。readelf -r hello_world.o
其中包含:
Relocation section '.rela.text' at offset 0x340 contains 1 entries:
Offset Info Type Sym. Value Sym. Name + Addend
00000000000c 000200000001 R_X86_64_64 0000000000000000 .data + 0
本节的格式在此处固定记录:http://www.sco.com/developers/gabi/2003-12-17/ch4.reloc.html
每个条目告诉链接程序需要重新定位的一个地址,这里我们只有一个字符串的条目。
简单来说,对于这一行,我们有以下信息:
Offset=C
:这个条目改变的.text
的第一个字节是什么。
如果我们回到反编译的文本中,它恰好在关键的movabs $0x0,%rsi
内部,那些了解x86-64指令编码的人会注意到,这对指令的64位地址部分进行编码。
Name = .data
:该地址指向.data
节
Type = R_X86_64_64
,指定要执行哪个计算以转换地址。
此字段实际上是处理器相关的,因此在AMD64 System V ABI extension的第4.4节“Relocation”中有记录。
该文档表示R_X86_64_64
执行以下操作:
Field = word64
:8字节,因此地址0xC
处为00 00 00 00 00 00 00 00
Calculation = S + A
S
是要重新定位的地址处的值,因此为00 00 00 00 00 00 00 00
A
在此处为0
。这是一个重定位条目的字段。因此,S + A == 0
,我们将被重新定位到.data
部分的第一个地址。
现在让我们看一下链接器ld
为我们生成的可执行文件的文本区域:
objdump -d hello_world.out
提供:00000000004000b0 <_start>:
4000b0: b8 01 00 00 00 mov $0x1,%eax
4000b5: bf 01 00 00 00 mov $0x1,%edi
4000ba: 48 be d8 00 60 00 00 movabs $0x6000d8,%rsi
4000c1: 00 00 00
4000c4: ba 0d 00 00 00 mov $0xd,%edx
4000c9: 0f 05 syscall
4000cb: b8 3c 00 00 00 mov $0x3c,%eax
4000d0: bf 00 00 00 00 mov $0x0,%edi
4000d5: 0f 05 syscall
因此,从对象文件中发生变化的唯一事物是关键行:
4000ba: 48 be d8 00 60 00 00 movabs $0x6000d8,%rsi
4000c1: 00 00 00
现在这些指针指向地址 0x6000d8
(小端序表示为 d8 00 60 00 00 00 00 00
),而不是 0x0
。
这是 hello_world
字符串的正确位置吗?
为了判断,我们需要检查程序头部,它告诉 Linux 加载每个段的位置。
我们可以使用以下方式反汇编:
readelf -l hello_world.out
这将会得到:
Program Headers:
Type Offset VirtAddr PhysAddr
FileSiz MemSiz Flags Align
LOAD 0x0000000000000000 0x0000000000400000 0x0000000000400000
0x00000000000000d7 0x00000000000000d7 R E 200000
LOAD 0x00000000000000d8 0x00000000006000d8 0x00000000006000d8
0x000000000000000d 0x000000000000000d RW 200000
Section to Segment mapping:
Segment Sections...
00 .text
01 .data
这告诉我们,第二个部分是.data
部分,它从VirtAddr
= 0x06000d8
开始。
而数据段上唯一的内容就是我们的hello world字符串。
objdump -dr
并使用两个字符串、两个函数和两个编译单元,以便可以看到不同类型的重定位和不同的偏移量(此时它变成了一个值得发布的博客文章)。 - Employed Russian-dr
,它非常棒。我可能会分析两个编译单元,以查看是否正确理解了事情。这个问题实在是太“广泛”了,我喜欢它。 - Ciro Santilli OurBigBook.com