连接器有什么作用?

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

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

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


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

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

在创建目标文件后,链接器开始发挥作用。 通常情况下,一个真正有用的程序需要引用其他文件。 例如,在C中,一个简单的程序可以将您的姓名打印到屏幕上,其中包括:

printf("Hello Kristina!n");

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

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

希望这可以帮助你理解!


地址重定位最简单的例子

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

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

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部分:

    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寄存器中。

    可是等等! 编译器如何知道"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字节,因此地址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。的文字

    现在让我们看看为我们生成的可执行文件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_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字符串。

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


    在像'C'这样的语言中,代码的单独模块传统上分别编译成目标代码块,除了模块自身之外的所有引用(即库或其他模块),它们都可以在各个方面执行尚未解决(即他们是空白的,等待有人前来,并建立所有连接)。

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

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

    链接地址: http://www.djcxy.com/p/73281.html

    上一篇: What do linkers do?

    下一篇: Why does the smallest int, −2147483648, have type 'long'?