170

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

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

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

4

4 回答 4

207

要了解链接器,首先要了解当您将源文件(例如 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 硬核人士会告诉我有很多很大的不同)。

希望这可以帮助您理解!

于 2010-07-23T23:04:48.383 回答
118

地址重定位最小示例

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

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

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

首先我们反编译.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:这个条目改变的第一个字节是什么。

    如果我们回头看反编译的文本,它恰好在关键的内部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

数据部分唯一的就是我们的 hello world 字符串。

奖金等级

于 2015-11-13T10:04:02.467 回答
18

在像“C”这样的语言中,单个代码模块传统上被单独编译成目标代码块,除了模块在自身外部所做的所有引用(即对库或其他模块)具有的所有引用之外,这些代码块已准备好在各个方面执行尚未解决(即它们是空白的,等待有人出现并建立所有连接)。

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

在动态链接也在进行的情况下,链接器的输出仍然无法运行 - 仍然有一些对外部库的引用尚未解析,它们在加载应用程序时由操作系统解析(或者可能甚至在运行的后期)。

于 2010-07-23T22:48:43.410 回答
13

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

然后它将所有这些目标文件组合在一起并为每个符号分配地址,并且在一个目标文件具有对另一个目标文件的外部引用的情况下,它会填充每个符号的地址,无论它被另一个对象使用。在典型情况下,它还会构建一个使用的任何绝对地址的表,因此加载器可以/将在加载文件时“修复”地址(即,它将基本加载地址添加到每个地址,所以它们都引用正确的内存地址)。

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

于 2010-07-23T23:01:27.117 回答