C ++链接如何在实践中工作?

C ++链接如何在实践中工作? 我正在寻找的是关于链接如何发生的详细解释,而不是链接的命令

关于编译已经有一个类似的问题,这个问题没有太详细的说明: 编译/链接过程是如何工作的?

编辑 :我已经将这个答案移到重复: https : //stackoverflow.com/a/33690144/895245

这个答案着重于地址重定位 ,这是链接的关键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。

其实,可以说连接是比较简单的。

从最简单的意义上讲,它仅仅是把目标文件1捆绑在一起,因为那些目标文件已经包含了包含在它们各自源代码中的每个函数/全局variables/数据…的发射组件。 链接器在这里可能是非常愚蠢的,只是把所有东西当作符号 (名称)及其定义(或内容)来处理。

显然,链接器需要生成一个尊重某种格式的文件(Unix上通常使用ELF格式),并将不同类别的代码/数据分成文件的不同部分,但这只是调度。

我所知道的两个问题是:

  • 需要去重复的符号:一些符号出现在几个对象文件中,只有一个符号应该在创build的结果库/可执行文件中; 链接器工作只包含其中一个定义

  • 链接时间优化:在这种情况下,目标文件不包含发射的程序集,而是包含中间表示,链接器将所有目标文件合并在一起,应用优化传递(例如内联),将其编译为汇编,最终发送结果。

1 :编译不同翻译单元的结果(大致是预处理源文件)

除了已经提到的“ 连接器和装载机 ”,如果你想知道一个现实和现代的连接器是如何工作的,你可以从这里开始。