我一直在想。我知道编译器会将您编写的代码转换为二进制文件,但链接器的作用是什么?对我来说,它们一直是个谜。
我粗略地理解'链接'是什么。当对库和框架的引用添加到二进制文件时。除此之外我什么都不懂。对我来说它“只是有效”。我也理解动态链接的基础知识,但没有太深入。
有人可以解释这些条款吗?
要理解链接器,当您将源文件(例如C或C ++文件)转换为可执行文件(可执行文件是可在您的计算机上执行的文件或文件)时,首先要了解“引擎盖下”会发生什么?别人的机器运行相同的机器架构)。
在引擎盖下,编译程序时,编译器将源文件转换为对象字节代码。此字节代码(有时称为目标代码)是只有您的计算机体系结构才能理解的助记符指令。传统上,这些文件具有.OBJ扩展名。
创建目标文件后,链接器开始起作用。通常,执行任何有用的实际程序都需要引用其他文件。例如,在C中,将名称打印到屏幕的简单程序包括:
printf("Hello Kristina!\n");
当编译器将程序编译成obj文件时,它只是引用了printf
函数。链接器解析此引用。大多数编程语言都有一个标准的例程库,涵盖了该语言所需的基本内容。链接器将OBJ文件与此标准库链接。链接器还可以将OBJ文件与其他OBJ文件链接。您可以创建其他OBJ文件,这些文件具有可由另一个OBJ文件调用的函数。链接器的工作方式几乎就像文字处理器的复制和粘贴一样。它“复制”程序引用的所有必要功能并创建单个可执行文件。有时,复制出来的其他库依赖于其他OBJ或库文件。有时链接器必须非常递归才能完成它的工作。
请注意,并非所有操作系统都创建单个可执行文件例如,Windows使用DLL将所有这些功能集中在一个文件中。这会减小可执行文件的大小,但会使您的可执行文件依赖于这些特定的DLL。 DOS过去常常使用名为Overlays(.OVL文件)的东西。这有很多用途,但其中一个目的是将常用功能保存在一个文件中(如果您想知道的话,另一个目的就是能够将大型程序放入内存中.DOS在内存和覆盖方面有局限性从内存“卸载”,其他叠加可以“加载”在该内存之上,因此名称为“叠加”。 Linux有共享库,这与DLL基本相同(我知道硬核Linux的人会告诉我有很多大的差异)。
希望这有助于您理解!
地址重定位是链接的关键功能之一。
因此,让我们看看它如何使用最小的例子。
摘要:重定位编辑目标文件的.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。
首先我们反编译对象文件的.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 extension第4.4节“重定位”中。
该文件说R_X86_64_64
做:
Field = word64
:8个字节,因此00 00 00 00 00 00 00 00
在地址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字符串。
在像'C'这样的语言中,代码的各个模块传统上分别编译成目标代码的blob,除了模块在外部(即库或其他模块)创建的所有引用之外,它可以在各方面执行。尚未解决(即他们是空白的,等待某人出现并建立所有联系)。
链接器的作用是一起查看所有模块,查看每个模块需要连接到外部的内容,并查看它导出的所有内容。然后它修复了所有内容,并生成最终的可执行文件,然后可以运行。
在动态链接也在进行的情况下,链接器的输出仍然无法运行 - 仍有一些对尚未解析的外部库的引用,并且在加载应用程序时它们会被操作系统解析(或者可能甚至在运行期间晚些时候)。
当编译器生成目标文件时,它包括在该目标文件中定义的符号的条目,以及对该目标文件中未定义的符号的引用。链接器接收它们并将它们组合在一起(当一切正常时)每个文件的所有外部引用都由其他目标文件中定义的符号满足。
然后它将所有这些目标文件组合在一起并为每个符号分配地址,并且在一个目标文件具有对另一个目标文件的外部引用的情况下,它将填充每个符号的地址,无论它在何处被另一个对象使用。在典型的情况下,它还将构建一个使用的任何绝对地址的表,因此加载器可以/将在文件加载时“修复”地址(即,它将为每个地址添加基本加载地址)地址,所以他们都引用正确的内存地址)。
相当多的现代链接器也可以执行一些(在少数情况下很多)其他“东西”,例如以只有在所有模块都可见时才可能的方式优化代码(例如,删除包含的功能)因为有些其他模块可能会调用它们,但是一旦将所有模块放在一起,很明显没有任何模块可以调用它们。