C++ 链接在实践中是如何工作的?我正在寻找的是有关链接如何发生的详细说明,而不是链接的命令。
已经有一个关于编译的类似问题没有详细说明:编译/链接过程如何工作?
编辑:我已将此答案移至副本:https ://stackoverflow.com/a/33690144/895245
这个答案侧重于地址重定位,这是链接的关键功能之一。
将使用一个最小示例来阐明这个概念。
摘要:重定位编辑.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
这应该将 hello world 字符串的地址移动到rsi
寄存器中,然后传递给 write 系统调用。
可是等等!当程序被加载时,编译器怎么可能知道"Hello world!"
在内存中的最终位置?
好吧,它不能,特别是在我们将一堆.o
文件与多个.data
部分链接在一起之后。
只有链接器才能做到这一点,因为只有他才能拥有所有这些目标文件。
所以编译器只是:
0x0
在编译的输出上放置一个占位符值此“额外信息”包含在.rela.text
目标文件的部分中
.rela.text
代表“.text 部分的重定位”。
使用重定位一词是因为链接器必须将地址从对象重定位到可执行文件中。
我们可以使用以下方法分解该.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 扩展第 4.4 节“重定位”中。
该文件说R_X86_64_64
:
Field = word64
: 8 个字节,因此是00 00 00 00 00 00 00 00
at 地址0xC
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 字符串。
实际上,可以说链接相对简单。
在最简单的意义上,它只是将目标文件1捆绑在一起,因为它们已经包含了每个函数/全局变量/数据的发出程序集......包含在它们各自的源中。链接器在这里可能非常愚蠢,只是将所有内容视为符号(名称)及其定义(或内容)。
显然,链接器需要生成一个尊重某种格式(通常在 Unix 上的 ELF 格式)的文件,并将各种类别的代码/数据分成文件的不同部分,但这只是分派。
我知道的两个并发症是:
删除重复符号的需要:一些符号存在于多个目标文件中,并且只有一个符号应该出现在正在创建的结果库/可执行文件中;仅包含其中一个定义是链接器的工作
链接时优化:在这种情况下,目标文件不包含发出的程序集,而是包含中间表示,链接器将所有目标文件合并在一起,应用优化通道(例如内联),将其编译为程序集并最终发出其结果.
1 : 编译不同翻译单元的结果(大致是预处理的源文件)