连接器是做什么的?

我一直在想。 我知道编译器会将你编写的代码转换成二进制文件,但是连接器是做什么的? 他们一直是我的一个谜。

我大致了解“连接”是什么。 这是对库和框架的引用被添加到二进制文件。 除此之外,我什么都不明白。 对我来说,它“只是工作”。 我也理解dynamic链接的基础知识,但没有深入。

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

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

在编译时,编译器将源文件转换为对象字节码。 这个字节码(有时称为目标码)是只有你的计算机体系结构能理解的助记符指令。 传统上,这些文件具有.OBJ扩展名。

在创build目标文件之后,链接器进入游戏。 通常情况下,一个真正有用的程序需要引用其他文件。 例如,在C中,一个简单的程序将你的名字打印到屏幕上,包括:

printf("Hello Kristina!\n"); 

当编译器将你的程序编译成一个obj文件时,它只是简单地引用printf函数。 链接器parsing这个引用。 大多数编程语言都有一个标准的例程库来涵盖该语言的基本内容。 链接器将您的OBJ文件与此标准库链接。 链接器也可以链接您的OBJ文件与其他OBJ文件。 您可以创build其他具有可以由另一个OBJ文件调用的函数的OBJ文件。 链接器的工作原理与文字处理器的复制和粘贴相似。 它“复制”你程序引用的所有必要function并创build一个可执行文件。 有时候复制出来的其他库依赖于其他OBJ或库文件。 有时一个链接器必须得到相当的recursion来完成它的工作。

请注意,并非所有操作系统都创build一个可执行文件 例如,Windows使用将所有这些函数集中在一个文件中的DLL。 这会减小可执行文件的大小,但会使您的可执行文件依赖于这些特定的DLL。 DOS用于使用称为覆盖(.OVL文件)的东西。 这有很多目的,但是一个是将常用的函数放在一个文件中(另一个目的是为了让大型程序能够适应内存,DOS的内存限制和覆盖从存储器“卸载”,其他覆盖层可以被“加载”在存储器的顶部,因此名称“覆盖”)。 Linux有共享库,这和dll基本上是一样的(我知道硬核Linux的人会告诉我有很多不同之处)。

希望这可以帮助你理解!

地址重定位最小的例子

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

那么让我们来看看它是如何工作的一个最小的例子。

0)介绍

总结:重定位编辑对象文件的.text节来翻译:

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

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

  • 解决未定义的符号像声明未定义的函数
  • 不会冲突多个对象文件的多个.text.data部分

先决条件:对以下内容了解最less:

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

链接与C或C ++没有任何关系:编译器只是生成目标文件。 然后链接器将它们作为input,而无需知道编译它们的语言。 它可能是Fortran。

那么为了减less地壳,我们来研究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 Worldstring的地址移动到传递给写入系统调用的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

每个条目告诉链接器需要重新定位一个地址,这里我们只有一个string。

简化一下,对于这个特定的行我们有以下信息:

  • Offset = C :该条目改变的.text的第一个字节是什么。

    如果我们回顾一下反编译的文本,它正好在关键的movabs $0x0,%rsi ,那些知道x86-64指令编码的文件将会注意到,这个编码指令的64位地址部分。

  • Name = .data :地址指向.data

  • Type = R_X86_64_64 ,它指定了转换地址到底需要做什么计算。

    该字段实际上取决于处理器,因此logging在AMD64 System 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).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_worldstring的正确位置吗?

为了决定我们必须检查程序头文件,它告诉Linux在哪里加载每个部分。

我们用以下方式拆卸它们:

 readelf -l hello_world.out 

这使:

 Program Headers: Type Offset VirtAddr PhysAddr FileSiz MemSiz Flags Align LOAD 0x0000000000000000 0x0000000000400000 0x0000000000400000 0x00000000000000d7 0x00000000000000d7 RE 200000 LOAD 0x00000000000000d8 0x00000000006000d8 0x00000000006000d8 0x000000000000000d 0x000000000000000d RW 200000 Section to Segment mapping: Segment Sections... 00 .text 01 .data 

这告诉我们,第二个.data节开始于VirtAddr = 0x06000d8

而数据部分唯一的事情就是我们的hello世界string。

免责声明 :我已经回答了这个重复的问题,现在才发现这个。 我投票结束这个骗局: C ++如何在实际中链接工作?

在'C'这样的语言中,单独的代码模块传统上被分别编译成目标代码块,除了模块自身以外的所有引用(即库或其他模块)还没有解决(即他们是空白的,等待有人来和做所有的连接)。

链接器所做的就是一起查看所有的模块,看看每个模块需要连接到外部,看看所有的输出。 然后修复这一切,并产生一个最终的可执行文件,然后可以运行。

在dynamic链接的情况下,链接器的输出仍然无法运行 – 还有一些对外部库的引用还没有解决,并且在加载应用程序时由操作系统解决(或者可能甚至在运行后期)。

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

然后,它将所有这些目标文件合并在一起,并为每个符号分配地址,并且在一个目标文件具有对另一个目标文件的外部引用的情况下,它填充每个符号的地址,而不pipe它被另一个目标使用。 在一个典型的情况下,它也会build立一个使用绝对地址的表,所以当文件被加载时,加载程序可以“修正”地址(即,它会将基本加载地址添加到每个地址地址,以便他们都参考正确的内存地址)。

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