C ++链接如何在实践中工作?
这个问题在这里已经有了答案:
编辑 :我已经将这个答案移动到重复:https://stackoverflow.com/a/33690144/895245
这个答案着重于地址重定位 ,这是链接的关键功能之一。
一个最小的例子将被用来澄清这个概念。
0)介绍
简介:重定位编辑要翻译的对象文件的.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 -o hello_world.o hello_world.asm
ld -o hello_world.out hello_world.o
与NASM 2.10.09。
1).o。的文字
首先我们反编译对象文件的.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
寄存器中。
可是等等! 编译器如何知道"Hello world!"
程序加载时会在内存中结束?
那么,它不能,特别是在我们将一堆.o
文件与多个.data
部分连接起来之后。
只有链接器才能做到这一点,因为只有他将拥有所有这些目标文件。
所以编译器只是:
0x0
这个“额外信息”包含在目标文件的.rela.text
部分
2).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字节,因此地址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
部分的第一个地址。
3).out。的文字
现在让我们看看为我们生成的可执行文件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
(little-endian中的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:编译不同翻译单元的结果(粗略地说,预处理源文件)
除了已经提到的“连接器和装载机”,如果你想知道一个真实和现代的连接器是如何工作的,你可以从这里开始。
链接地址: http://www.djcxy.com/p/12735.html上一篇: How does C++ linking work in practice?
下一篇: Why are these constructs (using ++) undefined behavior in C?