链接器做什么?


127

我一直想知道。我知道编译器会将您编写的代码转换为二进制文件,但是链接程序会做什么?他们一直是我的谜。

我大致了解什么是“链接”。这是将对库和框架的引用添加到二进制文件中的时候。除此之外,我什么都不懂。对我来说,它“有效”。我也了解动态链接的基础知识,但没有什么太深的。

有人可以解释一下这些条款吗?

Answers:


159

要了解链接器,先了解将源文件(例如C或C ++文件)转换为可执行文件(可执行文件是可以在您的计算机或别人的机器运行相同的机器架构)。

在后台,在编译程序时,编译器会将源文件转换为目标字节代码。此字节代码(有时称为目标代码)是只有计算机体系结构可以理解的助记符指令。传统上,这些文件的扩展名为.OBJ。

创建目标文件后,链接器开始起作用。通常,执行任何有用操作的真实程序都需要引用其他文件。例如,在C语言中,一个简单的程序可以在屏幕上显示您的姓名,包括:

printf("Hello Kristina!\n");

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

请注意,并非所有操作系统都创建单个可执行文件。例如,Windows使用的DLL将所有这些功能保持在一个文件中。这样可以减小可执行文件的大小,但可以使可执行文件依赖于这些特定的DLL。DOS曾经使用过称为叠加(.OVL文件)的东西。这有很多目的,但一个目的是将常用功能保存在一个文件中(如果您想知道的话,它的另一个目的是能够将大型程序放入内存中。DOS在内存和覆盖方面存在局限性,从内存中“卸载”,其他覆盖层可以“加载”到该内存的顶部,因此命名为“覆盖层”)。Linux具有共享库,该库与DLL基本上具有相同的思想(我认识的Linux核心人员会告诉我很多差异)。

希望这可以帮助您理解!


9
好答案。此外,大多数现代链接器都会删除模板实例化等冗余代码。
爱德华·斯特兰奇

1
这是解决这些差异中的一个合适的地方吗?
约翰·P

2
嗨,假设我的文件没有引用任何其他文件。假设我只是声明并初始化两个变量。此源文件也将转到链接器吗?
Mangesh Kherdekar '16

3
@MangeshKherdekar-是的,它始终通过链接程序。链接器可能不链接任何外部库,但是链接阶段仍然必须发生以生成可执行文件。
Icemanind '16

77

地址重定位的最小示例

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

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

0)简介

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

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

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

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

先决条件:至少需要了解以下内容:

  • x86-64或IA-32组件
  • ELF文件的全局结构。我为此做了一个教程

链接与C或C ++无关,编译器只生成目标文件。然后,链接器将它们作为输入,而根本不知道编译它们的语言。最好是Fortran。

因此,为了减少外壳,让我们研究一下NASM x86-64 ELF Linux hello世界:

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

首先,我们反编译.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目标文件的部分中

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此条目更改的第一个字节是什么。

    如果我们回头看一下反编译的文本,它恰好在Critical之内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 00at地址0xC

    • Calculation = S + A

      • S是要重定位的地址的,因此00 00 00 00 00 00 00 00
      • A0这里的加数。这是重定位条目的字段。

      因此S + A == 0,我们将重新定位到该.data部分的第一个地址。

3).out的.text

现在,让我们看看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

现在指向地址0x6000d8d8 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

数据部分的唯一内容是我们的世界字符串。

奖金等级


1
老兄,你真棒。指向“ ELF文件的全局结构”教程的链接已断开。
亚当·扎赫兰

1
@AdamZahran谢谢!不能处理斜线的愚蠢GitHub页面URL!
西罗Santilli郝海东冠状病六四事件法轮功

15

在诸如“ C”之类的语言中,传统上将各个代码模块分别编译为目标代码的blob,这些目标代码可以在各个方面执行,除了该模块在其自身外部进行的所有引用(即对库或对其他模块的引用)具有尚未解决(即,它们为空白,正在等待有人来进行所有连接)。

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

在动态链接仍在进行的情况下,链接器的输出仍然无法运行-仍然有一些对外部库的引用尚未解析,并且它们在加载应用程序时由OS解析。甚至在运行期间更晚)。


值得注意的是,如果编译器“看到”所有必要的东西(通常在单个源文件及其包含的任何东西中),则某些汇编程序或编译器可以直接输出可执行文件。一些通常用于小型微型计算机的编译器将其作为唯一的操作模式。
超级猫

是的,我试图给出一个中间的答案。当然,与您的情况相反,情况也是如此,因为某些类型的目标文件甚至没有完成完整的代码生成。这是由链接程序完成的(这就是MSVC整个程序优化的工作方式)。
Will Dean 2010年

据我所知,@ WillDean和GCC的链接时间优化-它将所有“代码”作为具有所需元数据的GIMPLE中间语言流式传输,使其可用于链接器,并一口气进行优化。(尽管过时的文档暗示了什么,但默认情况下现在仅默认传输GIMPLE,而不是同时使用目标代码的两种表示形式的旧“ fat”模式。)
underscore_d 2016年

10

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

然后,它将所有这些目标文件组合在一起,并为每个符号分配地址,并且在一个目标文件具有对另一个目标文件的外部引用的情况下,无论其他对象使用的位置是什么,它都会填充每个符号的地址。在典型情况下,它还会建立一个表,列出所有使用的绝对地址,因此加载程序可以/将在文件加载时“修复”地址(即,将基本加载地址添加到每个地址中)地址,因此它们都指向正确的内存地址)。

相当多的现代链接器还可以执行一些(在很多情况下很多)其他“东西”,例如以只有在所有模块可见后才能进行的方式优化代码(例如,删除包含的功能)因为它可能是一些其他的模块可能会打电话给他们,但一旦所有的模块都放在一起很明显的是,从来都没有叫他们)。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.