链接器是做什么的?

我一直很好奇。我知道编译器会把你写的代码转换成二进制文件,但是链接器是做什么的呢?他们对我来说一直是个谜。

我大概知道什么是“连接”。它是指将对库和框架的引用添加到二进制文件中。除此之外我一无所知。对我来说,它“只是工作”。我也了解动态链接的基本知识,但是不太深入。

有人能解释一下这些术语吗?

77139 次浏览

在像“ C”这样的语言中,单个的代码模块通常被单独地编译成对象代码,这些对象代码在各个方面都可以执行,除了那些模块对外部的所有引用(例如对库或其他模块的引用)尚未解决(例如,它们是空白的,等待着有人来建立所有的连接)。

链接器所做的就是一起查看所有模块,查看每个模块需要连接到外部的什么,并查看它正在导出的所有内容。然后,它修复所有这些问题,并生成最终的可执行文件,然后可以运行该文件。

当动态链接也在进行时,链接器的输出是 还是无法运行——仍然有一些对外部库的引用尚未解析,它们在加载应用程序时(或者在运行过程中可能更晚)被操作系统解析。

当编译器生成一个目标文件时,它包含在该目标文件中定义的符号的条目,以及对该目标文件中未定义的符号的引用。链接器获取这些内容并将它们放在一起,这样(当一切正常时)每个文件的所有外部引用都由在其他对象文件中定义的符号满足。

然后,它将所有这些对象文件组合在一起,并为每个符号分配地址,当一个对象文件对另一个对象文件有外部引用时,它将填充每个符号的地址,无论它被另一个对象使用到哪里。在一个典型的例子中,它还会构建一个包含所有绝对地址的表,这样加载程序可以/将在加载文件时“修复”这些地址(也就是说,它会将基本加载地址添加到每个地址,这样它们都会引用正确的内存地址)。

相当多的现代链接器也可以执行一些(在少数情况下是 很多)其他“东西”,例如优化代码的方式,只有当所有的模块都可见时才可能(例如,删除包含的函数,因为其他模块可能会调用它们,但是一旦所有的模块放在一起,很明显,没有任何东西会调用它们)。

要理解链接器,首先要了解当你将源文件(比如 C 或 C + + 文件)转换成可执行文件(可执行文件是一个可以在你的机器上或者其他运行相同机器架构的机器上执行的文件)时,“内部”会发生什么。

在底层,编译程序时,编译器将源文件转换为目标字节码。这个字节码(有时称为目标代码)是只有计算机体系结构才能理解的助记符指令。传统上,这些文件有一个。OBJ 扩展。

在创建目标文件之后,链接器开始发挥作用。通常情况下,做任何有用事情的实际程序都需要引用其他文件。例如,在 C 语言中,一个将你的名字打印到屏幕上的简单程序包括:

printf("Hello Kristina!\n");

当编译器将您的程序编译成 obj 文件时,它只是放置一个对 printf函数的引用。链接器解析此引用。大多数编程语言都有一个标准的例程库来涵盖该语言所需的基本内容。链接器将您的 OBJ 文件链接到这个标准库。链接器还可以链接您的 OBJ 文件与其他 OBJ 文件。您可以创建其他具有函数的 OBJ 文件,这些函数可以由另一个 OBJ 文件调用。链接器的工作方式几乎就像文字处理器的复制和粘贴。它“复制”出程序引用的所有必要函数,并创建单个可执行文件。有时复制出来的其他库依赖于其他 OBJ 或库文件。有时候连接器必须非常递归才能完成它的工作。

注意,并非所有操作系统都创建单个可执行文件。例如,Windows 使用 DLL 将所有这些函数放在一个文件中。这减少了可执行文件的大小,但使可执行文件依赖于这些特定的 DLL。DOS 过去常常使用称为 Overlay (。OVL 文件)。这样做有很多目的,但其中一个目的是将常用函数放在一个文件中(如果您想知道的话,另一个目的是将大型程序放入内存中)。DOS 在内存方面有一个限制,重叠可以从内存中“卸载”,其他重叠可以在内存顶部“加载”,因此得名为“重叠”)。Linux 有共享库,这基本上与 DLL (我认识的 Linux 铁杆粉丝会告诉我有很多很大的不同)的想法相同。

希望这能帮助你理解!

地址重定位最小示例

地址重定位是连接的关键功能之一。

因此,让我们看看它是如何使用一个最小的例子。

0)简介

摘要: 重定位编辑目标文件的 .text部分翻译:

  • 目标文件地址
  • 进入可执行文件的最终地址

这必须由链接器完成,因为编译器一次只能看到一个输入文件,但我们必须一次了解所有目标文件,以决定如何:

  • 解析未定义符号,如声明的未定义函数
  • 不要冲突多个目标文件的多个 .text.data

先决条件: 最低限度地理解:

  • X86-64或 IA-32组装
  • 一个 ELF 文件的全局结构

链接与 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寄存器中,rsi寄存器被传递给 write 系统调用。

但是等等! 编译器怎么可能知道当程序加载时 "Hello world!"将在内存中的哪个位置?

嗯,它不能,特别是在我们链接了一大堆 .o文件和多个 .data部分之后。

只有链接器可以做到这一点,因为只有他将拥有所有这些目标文件。

所以编译器只是:

  • 将占位符值 0x0放在已编译的输出上
  • 给链接器提供一些额外的信息,说明如何用好的地址修改已编译的代码

这个“额外信息”包含在目标文件的 .rela.text部分中

2) . rel.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位地址部分。

  • 地址指向 .data部分

  • Type = R_X86_64_64,它确切地指定了转换地址所必须进行的计算。

    这个字段实际上是依赖于处理器的,因此在 AMD64系统 V ABI 扩展第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部分的第一个地址。

3) . 文本

现在让我们看看为我们生成的可执行 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 字符串。

额外奖励

链接器将您的 OBJ 文件链接到这个标准库。