操作系统【00】链接

839 阅读18分钟

1. 什么是链接

链接是将代码和数据片段整合成一个可以被加载(复制)到内存中执行的文件。现代操作系统中,链接是由链接器自动执行的。链接最大的作用是分离编译,在编写大型应用程序时,不需要再编译出一个巨大的源文件,可以把代码编译成比较小的,单独的模块。文件修改时只需要重新编译单独的模块就可以了,不需要重新编译整个文件。

2. 静态链接流程

下图为通过静态链接将两个C源文件编译成可执行目标文件的过程。

静态链接:将可重定位目标文件组合成可执行目标文件。

静态链接的两个主要工作为符号解析和重定位。

3. 目标文件

在介绍符号解析和重定位之前,先介绍一下目标文件。现代linux和unix系统使用可执行可链接格式(ELF)。目标文件有三种形式:可重定位目标文件,可执行目标文件,共享目标文件(特殊类型的可重定位目标文件)。

3.1 可重定位目标文件

下图展示了可重定位目标文件的格式

ELF头以一个16字节的描述了生成该文件的系统的字的大小和字节顺序的序列开始,除此之外,ELF头中还包含帮助链接器语法分析和解释目标文件的信息。其中包括ELF头的大小,目标文件的类型,机器类型(x86-64),节头部表的文件偏移,节头部表中条目的大小和数量。

在ELF头和节头部表之间的都是节。一个典型的ELF可重定位目标文件包含下面几个节。

  • .text:已编译程序的机器代码。
  • .rodata:只读数据。
  • .data:以初始化的全局和静态C变量。
  • .bss:为初始化的全局和静态C变量,以及所有被初始化为0的全局或静态变量。不占据实际的空间,仅仅用做占位。运行时,在内存中分配这些变量。
  • .symtab:符号表,存放在程序中定义和引用的函数和全局变量的信息。
  • .rel.text:一个.text节中位置的列表,当链接器把这个目标文件和其他文件组合时,需要修改这些位置。
  • .strtab:一个字符串表,内容包括.symtab和.debug节中的符号表,以及节头部中的节名字。

linux下的readelf可以方便的阅读elf文件。mac系统下使用brew update && brew install binutils,然后用greadelf和gobjdump

下面是使用greadelf查看的so文件:

PS:这不是一个可重定位目标文件,作者暂时没有linux系统,找了一个共享目标文件。总体的结构是差不多的。

greadelf -a libapp.so
ELF Header:
  Magic:   7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00 
  Class:                             ELF32
  Data:                              2's complement, little endian
  Version:                           1 (current)
  OS/ABI:                            UNIX - System V
  ABI Version:                       0
  Type:                              DYN (Shared object file)
  Machine:                           ARM
  Version:                           0x1
  Entry point address:               0x0
  Start of program headers:          52 (bytes into file)
  Start of section headers:          5668992 (bytes into file)
  Flags:                             0x5000200, Version5 EABI, soft-float ABI
  Size of this header:               52 (bytes)
  Size of program headers:           32 (bytes)
  Number of program headers:         10
  Size of section headers:           40 (bytes)
  Number of section headers:         12
  Section header string table index: 11Section Headers:
  [Nr] Name              Type            Addr     Off    Size   ES Flg Lk Inf Al
  [ 0]                   NULL            00000000 000000 000000 00      0   0  0
  [ 1] .note.gnu.bu[...] NOTE            00001000 001000 000020 00  WA  0   0  4
  [ 2] .bss              PROGBITS        00001020 001020 00000c 00  WA  0   0  4
  [ 3] .text             PROGBITS        00002000 002000 003420 00  AX  0   0 4096
  [ 4] .rodata           PROGBITS        00006000 006000 005f60 00   A  0   0 16
  [ 5] .text             PROGBITS        0000c000 00c000 32e540 00  AX  0   0 4096
  [ 6] .rodata           PROGBITS        0033b000 33b000 22c230 00   A  0   0 16
  [ 7] .dynstr           STRTAB          00567230 567230 000085 00   A  0   0  1
  [ 8] .dynsym           DYNSYM          005672b8 5672b8 000060 10   A  7   1  4
  [ 9] .hash             HASH            00567318 567318 000038 04   A  8   0  4
  [10] .dynamic          DYNAMIC         00568000 568000 000030 08  WA  7   0  4
  [11] .shstrtab         STRTAB          00000000 568030 000050 00      0   0  1
Key to Flags:
  W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
  L (link order), O (extra OS processing required), G (group), T (TLS),
  C (compressed), x (unknown), o (OS specific), E (exclude),
  D (mbind), y (purecode), p (processor specific)
​
There are no section groups in this file.
​
Program Headers:
  Type           Offset   VirtAddr   PhysAddr   FileSiz MemSiz  Flg Align
  PHDR           0x000034 0x00000034 0x00000034 0x00160 0x00160 R   0x4
  LOAD           0x000000 0x00000000 0x00000000 0x00194 0x00194 RW  0x1000
  LOAD           0x001000 0x00001000 0x00001000 0x0002c 0x0002c RW  0x1000
  LOAD           0x002000 0x00002000 0x00002000 0x03420 0x03420 R E 0x1000
  LOAD           0x006000 0x00006000 0x00006000 0x05f60 0x05f60 R   0x1000
  LOAD           0x00c000 0x0000c000 0x0000c000 0x32e540 0x32e540 R E 0x1000
  LOAD           0x33b000 0x0033b000 0x0033b000 0x22c350 0x22c350 R   0x1000
  NOTE           0x001000 0x00001000 0x00001000 0x00020 0x00020 RW  0x4
  LOAD           0x568000 0x00568000 0x00568000 0x00030 0x00030 RW  0x1000
  DYNAMIC        0x568000 0x00568000 0x00568000 0x00030 0x00030 RW  0x4Section to Segment mapping:
  Segment Sections...
   00     
   01     
   02     .note.gnu.build-id .bss 
   03     .text 
   04     .rodata 
   05     .text 
   06     .rodata .dynstr .dynsym .hash 
   07     .note.gnu.build-id 
   08     .dynamic 
   09     .dynamicDynamic section at offset 0x568000 contains 6 entries:
  Tag        Type                         Name/Value
 0x00000004 (HASH)                       0x567318
 0x00000005 (STRTAB)                     0x567230
 0x0000000a (STRSZ)                      133 (bytes)
 0x00000006 (SYMTAB)                     0x5672b8
 0x0000000b (SYMENT)                     16 (bytes)
 0x00000000 (NULL)                       0x0There are no relocations in this file.
​
There are no unwind sections in this file.
​
Symbol table '.dynsym' contains 6 entries:
   Num:    Value  Size Type    Bind   Vis      Ndx Name
     0: 00000000     0 NOTYPE  LOCAL  DEFAULT  UND 
     1: 00001000    32 FUNC    GLOBAL DEFAULT    1 _kDartSnapshotBuildId
     2: 00002000 13344 FUNC    GLOBAL DEFAULT    3 _kDartVmSnapshot[...]
     3: 00006000 24416 FUNC    GLOBAL DEFAULT    4 _kDartVmSnapshotData
     4: 0000c000 0x32e540 FUNC    GLOBAL DEFAULT    5 _kDartIsolateSna[...]
     5: 0033b000 0x22c230 FUNC    GLOBAL DEFAULT    6 _kDartIsolateSna[...]Histogram for bucket list length (total of 6 buckets):
 Length  Number     % of total  Coverage
      0  2          ( 33.3%)
      1  3          ( 50.0%)     60.0%
      2  1          ( 16.7%)    100.0%No version information found in this file.
​
Displaying notes found in: .note.gnu.build-id
  Owner                Data size  Description
  GNU                  0x00000010 NT_GNU_BUILD_ID (unique build ID bitstring)
    Build ID: 5a1633c81abc0aff96b21be358b0c2ab

3.2 可执行目标文件

可以直接被复制到内存中执行。

3.3 共享目标文件

特殊类型的可重定位目标文件,可以被动态链接。后面的动态链接一节会详细介绍,又名共享库。

4. 链接的主要工作

链接器的两个主要工作为符号解析和重定位。在介绍他们之前我们需要先介绍一下符号表。

4.1 符号和符号表

符号表在可重定位目标模块m的.symtab节中,包含了m中定义和引用的符号的信息。主要有三种不同的符号。

  • 由模块m定义并能被其他模块引用的全局符号(非静态的C函数和全局变量)。
  • 由其他模块定义并被模块m引用的全局符号。
  • 只被模块m定义和引用的局部符号(带static属性的C函数和全局变量)。

符号表中包含了一个对象的数据,对象格式如下。

分别解释一下每个字段的意义:

  • name是字符串表中的字节偏移,指向符号的以null结尾的字符串名字。
  • value是符号的地址,对于可重定位模块来说,value是距定义目标的节的起始位置的偏移。对于可执行目标文件来说,该值是一个绝对运行时地址。
  • size是目标的大小,以字节为单位。
  • type是符号的类型,通常是函数或者数据。
  • binding用来表示符号是本地的还是全局的。
  • section字段是一个到节头部表的索引,表示符号被分配到目标文件的哪一个节。

下面是使用greadelf查看的so文件的符号表:

Symbol table '.dynsym' contains 6 entries:
   Num:    Value  Size Type    Bind   Vis      Ndx Name
     0: 00000000     0 NOTYPE  LOCAL  DEFAULT  UND 
     1: 00001000    32 FUNC    GLOBAL DEFAULT    1 _kDartSnapshotBuildId
     2: 00002000 13344 FUNC    GLOBAL DEFAULT    3 _kDartVmSnapshot[...]
     3: 00006000 24416 FUNC    GLOBAL DEFAULT    4 _kDartVmSnapshotData
     4: 0000c000 0x32e540 FUNC    GLOBAL DEFAULT    5 _kDartIsolateSna[...]
     5: 0033b000 0x22c230 FUNC    GLOBAL DEFAULT    6 _kDartIsolateSna[...]

_kDartSnapshotBuildId是一个位于.note.gnu.bu节中00001000处的32字节全局函数。

readelf用一个整数索引来表示每个节,Ndx=1表示.note.gnu.bu节,Ndx=3表示.text节。可以查阅上面的3.1节的Section Headers:

4.2 符号解析

4.2.1 什么是符号解析

链接器解析符号引用的方法是将每个引用与可重定位目标文件的符号表中的定义关联起来。简单来说就是将引用和定义关联起来。我们来看一下引用和定义分别是什么。

c语言中是没有类的概念的,使用其他文件中定义的全局变量可以理解为引用,本文件创建的变量可以理解为定义。

符号解析将目标文件中的每个全局符号都绑定到一个唯一的定义。

4.2.2 全局符号和局部符号的解析

局部符号的解析比较容易,因为编译器只允许一个文件中每个局部符号有一个定义。

全局符号的解析要复杂一些,涉及到强弱符号。

强符号:已初始化的全局变量。

弱符号:未初始化的全局变量。

全局符号解析的原则:

  • 不允许有多个同名的强符号。
  • 如果有强符号和弱符号同名,选择强符号。
  • 有多个弱符号同名,任意选择一个。

4.2.3 静态库

静态库:将一组目标文件组合成一个文件,可以用作链接器的输入。当链接器构造可执行文件时,只复制静态库里被应用程序引用的目标模块。

4.3 重定位

重定位就是合并输入模块,为每个符号分配运行时地址,主要分为两部。

  • 重定位节和符号定义:

    链接器将所有相同类型的节合并为同一类型的可执行目标文件中的新的聚合节。

  • 重定位节中的符号引用:

    链接器根据可重定位目标模块中的名为重定位条目的数据结构,修改代码节和数据节中对每个符号的引用,使他们指向正确的运行时地址。

4.3.1 重定位条目

当汇编器生成一个目标模块时,它并不知道数据和代码最终将放在什么位置。

汇编器遇到对最终位置未知的目标引用,就会生成一个重定位条目,用来告诉链接器在将目标文件合并成可执行文件时如何修改这个引用。

代码的重定位条目放在.rel.text节中,已初始化数据的重定位条目放在.rel.data中。

重定位条目的数据结构:

  • offset是需要被修改的引用的节偏移。
  • symbol标识被修改引用应该指向的符号。
  • type代表了重定位类型。
  • addend是一个符号常数,一些类型的重定位要使用它对被修改引用的值做偏移调整。

两种基本的重定位类型:

  • R_X86_64_PC32:重定位一个使用32位PC相对地址的引用。
  • R_X86_64_32:重定位一个使用32位绝对地址的引用。

4.3.2 重定位符号引用

链接器的重定位算法伪代码:

5. 加载

加载器将可执行目标文件中的代码和数据从磁盘复制到内存中,通过跳转到程序的第一条指令或入口点来运行该程序。

6. 动态链接

6.1 静态库的缺点

  • 需要定期维护和更新。
  • 静态库中的代码会被复制到每个运行进程的文本段中,是对内存系统资源的浪费。

6.2 共享库

是一个目标模块,在运行或加载时,可以加载到任意的内存地址,并和一个在内存中的程序链接起来。

6.3 动态链接

在运行或加载时,将共享库加载到内存,并和一个在内存中的程序链接起来。

6.4 动态链接器

执行动态链接。在linux系统中通常用.so后缀。

6.5 共享库与静态库的区别

静态库的内容会被复制和嵌入到引用它们的可执行文件中。

.so文件只有一份,所有引用该库的可执行目标文件共享此文件。

6.6 动态链接过程

当创建可执行文件时,静态执行一些链接,在程序加载时,动态完成链接过程。链接器复制了一些重定位和符号表信息,使得他们可以在运行时解析对libvector.so中代码和数据的引用。

加载器发现prog21包含一个.interp节,这个节中包含动态链接器的路径名。加载器加载和运行动态链接器。然后动态链接器执行以下重定位完成链接任务:

  • 重定位libc.so的文本和数据到某个内存段。
  • 重定位libvector.so的文本和数据到另一个内存段。
  • 重定位prog21中对libc.solibvector.so定义的符号的引用。

6.7 在运行时动态链接

动态链接不仅可以在编译时进行,也可以在应用程序运行时进行。linux系统为动态链接器提供了一个dlopen方法,允许应用程序在运行时加载和链接共享库。

6.8 共享库实现原理

共享库的主要功能为允许多个正在运行的进程共享内存中相同的库代码。

实现多个进程共享程序中一个副本有两种方式:

  • 给每个共享库分配一个实现预备的专用的地址空间片,要求加载器总是在这个地址加载共享库。
  • 现代操作系统以这样一种方式编译共享模块的代码块,使得可以把它们加载到内存的任意位置而无需链接器修改。无限多个进程可以共享一个模块的代码段的单一副本。

位置无关代码(PIC):

可以加载而无需重定位的代码。

如何生成PIC不展开说了。

7.小结

文章结构看似有些杂乱,其实是根据操作系统的链接工作流程展开的,在介绍每一种技术前都会先介绍前置技术。

以一段《深入理解计算机系统》中的链接总结作为结尾:

链接可以在编译时由静态编译器来完成,也可以在加载时和运行时由动态链接器来完成。链接器处理称为目标文件的二进制文件,它有三种不同的形式:可重定位的,可执行的和共享的。可重定位的目标文件由静态链接器合并成一个可执行的目标文件,它可以加载到内存中并执行。共享目标文件(共享库)是在运行时由动态链接器链接和加载的,或者隐含地在调用程序被加载和开始执行时,或者根据需要在程序调用dlopen库的函数时。

链接器的两个主要任务是符号解析和重定位,符号解析将目标文件中的每个全局符号都绑定到一个唯一的定义,而重定位确定每个符号的最终内存地址,并修改对那些目标的引用。

静态链接器是由像GCC这样的编译驱动程序调用的。它们将多个可重定位目标文件合并成一个单独的可执行目标文件。多个目标文件可以定义相同的符号。

加载器将可执行文件的内容映射到内存,并运行这个程序。链接器还可能生成部分链接的可执行目标文件,这样的文件中有对定义在共享库中的例程和数据的未解析的引用。在加载时,加载器将部分链接的可执行文件映射到内存,然后调用动态链接器,它通过加载共享库和重定位程序中的引用来完成链接任务。

被编译为位置无关代码的共享库可以加载到任何地方,也可以在运行时被多个进程共享。为了加载,链接和访问共享库的函数和数据,应用程序也可以在运行时使用动态链接器。

8.感受

这篇文章的战线真的很长,而且写到现在也不是最终版,不过自己确实写不动了,没有什么动力继续了。原计划还要补齐Android linker的代码解析,通过代码来描述android操作系统是如何链接程序的。

机缘巧合来写这篇文章,不得不说在一开始这些知识对我来说是巨大的挑战,在最初学习的时候自己非常诧异,字和字组合在一起,怎么就看不懂呢?不过自己还是没有放弃,一遍一遍的读,一遍一遍的看,书读百遍其意自现,从一开始的迷茫,到逐渐理解,到最后融会贯通。“山重水复疑无路,柳暗花明又一村”。花了多少时间来读,真正掌握的那一刻就有多大的快乐。

这段时间的收获很大,除了掌握了操作系统如何链接程序之外。对如何阅读经典计算机书籍也有了新的理解,读书要拆解,先对总体的概念和流程有模糊的了解,在细化的去学习具体的步骤和过程,具体的步骤学习过了一个阶段之后,再来读总体的概念和流程,再重复这一步骤补齐所有的盲区,知识也就融汇于心了。

我觉得这世界上的所有知识都这样,正常的理解力加上耐心和持续的动力解决所有问题。 重要的是了解自己哪里不会,也就是找到方向,剩下的耐心还是很重要的,这篇文章我就没写完,哈哈。人的潜力果然是无限的。

不过怎么说呢,离了文章也还是记不起来。。。计算机基础知识如果不用,真的记不住。。。以后慢慢探索如何记住。。。或许学习更重要的是潜移默化的影响?

“我想要什么?”,永远明确这一点。