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

43

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

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


7
试试一本书,例如《Linkers and Loaders》 - Joachim Isaksson
3个回答

73

编辑: 我已将此答案移至重复问题: https://dev59.com/aHA75IYBdhLWcg3wYYBQ#33690144

本答案重点讨论了地址重定位,这是链接的关键功能之一。

我们将使用一个最小的示例来澄清概念。

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 -felf64 hello_world.asm            # creates hello_world.o
ld -o hello_world.out hello_world.o     # static ELF executable with no libraries

使用 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
将“which should move the address of the hello world string into the rsi register, which is passed to the write system call.”翻译为中文之后,编译器将“Hello World!”字符串的地址移动到传递给写入系统调用的rsi寄存器中。但是问题来了!当程序加载到内存中时,“Hello World!”字符串在内存中的位置如何得知?由于我们将许多.o文件链接在一起,并具有多个.data段,因此编译器无法知道,只有链接器才能知道只有链接器才有所有这些目标文件。因此,编译器会在已编译的输出上放置占位符值0x0,并向链接器提供一些额外信息,以便修改编译代码的好地址。这些“额外信息”包含在对象文件的.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 extension的第4.4节“Relocation”中有记录。

    该文档表示R_X86_64_64执行以下操作:

    • Field = word64:8字节,因此地址0xC处为00 00 00 00 00 00 00 00

    • Calculation = S + A

      • S是要重新定位的地址处的值,因此为00 00 00 00 00 00 00 00
      • A在此处为0。这是一个重定位条目的字段。

      因此,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

现在这些指针指向地址 0x6000d8(小端序表示为 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字符串。


2
“在大端序中”--你肯定是指“在小端序中”吧?写得不错,但可能会受益于查看objdump -dr并使用两个字符串、两个函数和两个编译单元,以便可以看到不同类型的重定位和不同的偏移量(此时它变成了一个值得发布的博客文章)。 - Employed Russian
1
@EmployedRussian 感谢您的纠正!很高兴像您这样的ELF专家来看看它 :-) 我不知道-dr,它非常棒。我可能会分析两个编译单元,以查看是否正确理解了事情。这个问题实在是太“广泛”了,我喜欢它。 - Ciro Santilli OurBigBook.com
如果一个目标文件有一个对“print”的函数调用,那么另一个目标文件提供“print”函数,链接器将用实际的偏移量替换对print的调用,这样说怎么样? - CoffeDeveloper
@sherrellbc 首先请查看ELF文件的全局结构:http://www.cirosantilli.com/elf-hello-world/#global-file-structure 一切都是基于偏移量的,所以顺序并不重要。然后我不确定映射是如何存储的,这个问题已经在这里提出了:https://dev59.com/82Ag5IYBdhLWcg3w7enx - Ciro Santilli OurBigBook.com
好的回答。如果这是一个愚蠢的离题问题,我很抱歉;为什么输出将地址“4000c1: 00 00 00”的最后三个字节分开,而它们明显属于前一条指令? - 2501
显示剩余6条评论

10
实际上,可以说链接相对简单。简单来说,它只是将目标文件捆绑在一起,因为这些文件已经包含了各自源代码中的每个函数/全局变量/数据的汇编代码。链接器可以非常愚蠢,并将所有内容都视为符号(名称)及其定义(或内容)。当然,链接器需要产生一个符合特定格式的文件(通常是Unix上的ELF格式),并将不同类别的代码/数据分开放置在文件的不同部分,但这只是分配的问题。我知道的两个复杂点是:需要去重符号,有些符号存在于多个目标文件中,只有一个应该包含在正在创建的库/可执行文件中;链接器的工作是仅包含一个定义。另一个是链接时优化:在这种情况下,目标文件不包含已发出的汇编代码,而是包含中间表示形式,链接器将所有目标文件合并在一起,应用优化(例如内联)进行编译,然后将其编译为汇编语言,最后发出结果。

+1个好答案。第一个要搜索的术语是“名称修饰”。 - Marco van de Voort
1
@MarcovandeVoort:并不完全是这样。例如,在C语言中没有管理(manging)的概念,但仍然有链接器。重复符号通常是符号,例如标记为“inline”的函数、模板函数实例、模板静态符号实例等等。这些符号会为每个翻译单元生成,但只应将一个符号放入最终库中,这是链接器的工作。 - Matthieu M.
1
当你忽略链接的所有复杂部分时,链接只是相对简单的(当然,这是一个重言)。如果你想知道什么使真正的链接器变得复杂,你可以从这里开始:http://www.airs.com/blog/archives/38 - Employed Russian
@Matthieu 这取决于你的意思... C在某些平台上确实有名称改编(尤其是Windows)。请参见Raymond Chen - Nicholas Wilson
1
@MarcovandeVoort:这与符号名称的形成有关,但是是否进行名称混淆与是否存在重复(在不同对象中)无关。 - Matthieu M.
显示剩余2条评论

9
除了之前提到的 "链接器和装载器",如果您想了解一个真正的、现代的连接器是如何工作的,您可以从这里开始。

2
感谢提供博客链接,这个系列确实非常有趣。通常情况下,仅包含链接的答案会受到反对(如果该博客突然关闭怎么办?),因此我鼓励您完善这个答案。例如,提供该系列的概述(链接+每个部分的摘要)? - Matthieu M.
4
我一直认为SO的这个规则很愚蠢,试图在简短的SO回答中总结一个领先专家关于链接器的整个系列帖子是浪费时间。这个问题不适合在SO上提问,因为答案无法用几段话来概括,提问者获取答案的正确方式是在其他地方阅读详细信息,而不是在这里阅读摘要。何况如果SO关闭了怎么办?Ian使用airs.com的时间比SO存在的时间还要长,也许他也能超越它 ;) - Jonathan Wakely

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接