链接器

作者:左少华
时间:2015-05-24
转载请注明出处:
http://blog.csdn.net/shaohuazuo/article/details/45957971


连接器的功能

  1. 链接器是将各种代码和数据部分收集起来并合成一个单一文件的过程,
    这个文件可以被加载到存储器中执行.

链接器的执行时机

  1. 可以执行于编译时,也就是在源代码被翻译成机器代码的时候.
  2. 可以执行于加载时,也就是程序被加载器加载到存储器,并执行时.
  3. 可以执行于运行时,由应用程序来执行.
  4. 链接是通过链接器程序自动执行的.

为什么使用连接器

  1. 链接器可以是的分离编译称为可能.
  2. 可以使得程序分模块,各个模块可以分开编译.
  3. 是的程序修改,和维护程序变得更加方便.

理解连接器的好处

  1. 理解链接器将帮助你构造大型程序.
    构造大型程序经常会遇到缺少模块,缺少库或者不兼容的版本引起的链接错误.
    所以理解链接链接器是如何解析引用,什么时库,以及链接器是如何使用库来解析引用
  2. 理解连接器将帮助你避免一些危险的编程错误.
  3. 理解链接器,可以帮助你理解语言的作用域规则是如何实现的.
    如全局变量和局部变量的区别,使用static修饰的属性和方法意味着什么等.
  4. 理解链接器能帮忙理解其他重要的系统概念
  5. 链接器可以使你能够使用动态库.

链接的方式

  1. 传统的静态链接静态库.
  2. 加载时动态链接动态库.
  3. 运行时动态链接动态库.

目标文件格式的类型

  1. Linux使用的是标准的ELF文件格式.

编译器驱动程序

  1. 预处理器
  2. 编译器
  3. 汇编器
  4. 链接器
  5. 使用-v 参数连运行gcc可以看到上面的四个构成)
  6. 过程如下
    一: 使用预处理器,将c源程序main.c翻译成一个asscii码的中间文件main.i
    cpp main.c mian.i
    二:接下来,驱动程序运行c编译器,将它main.i翻译成一asscii汇编语言文件main.s
    ccl main.i main.c -o2 -o main.s
    三:驱动程序运行汇编as, 将main.s翻译成一个可重定位目标文件. main.o
    as -o main.o main.s
    四:驱动程序通过相同的方式生成其他相关的 .o文件.最后通过连接器ld,将所有的.o文件组合起来,
    创建一个可执行文件.
    ld -o p main.o swap.o …
    最终生成p可执行文件.

静态链接

  • 静态链接器以一组可重定位目标文件和命令行参数作为输入,生成一个完成可以加载和运行的可执行目标文件作为输出.
  • 输入可重定向目标文件由各种不同的代码和数据节组成,指令在一节中,初始化全局变量在一节中,为初始化变量又在另一节中.

构建可执行文件,链接器必须要完成的两个主要任务

  • 符号解析
     目标文件定义和引用符号,符号解析的目的是将每个符号的引用刚好和一个符号定义联系起来.
     
  • 重定位
     编译器和汇编器生成从零地址开始的代码和数据节,链接器通常把每个符号定义和存储器的位置联系起来.然后修改所有对这些符号的引用,从而重定位这些节.

目标文件

  • 目标文件由三种.
     
     可定位目标文件
     包含二进制代码和数据,其形式可在编译时与其他可重定位目标文件合并起来.创建一个可以执行我的目标文件.
      
     可执行目标文件
     包含二进制代码和数据,其形式可以被执行拷贝到存储器并执行.
     
     共享目标文件
      一种特殊类型的可重定位目标文件,可以再加载或者运行时动态加载到存储器并链接.

符号和符号表

每个可重定位的目标模块多有一个符号表,它包含模块所定义和引用的符号信息

再链接器上下文中,有三种不同的符号:

  • 有模块定义并可以被其他模块引用的全局符
    全局连接器符号对应于非静态的c函数和不带
    static变量修饰全局变量.

  • 由其他模块定义,并被本模块引用全局符号
    这些符号称为外部符号,对应于定义C中的extends修饰的全局变量的函数.

  • 只被本模块m定义和引用的本地符号,有的本地链接器符号对应带static属性的c函数的全局变量,

    本地链接器符号和本地程序变量不同,.symtab中符号不包括对应于本地非静态程序变量和符号,c中的static变量的位置在.data段或者是在.bss段

符号表

符号表是由汇编器构造的,使用汇编器输出到汇编语言.s文件中的符号. symtab节中包含elf符号表,符号表是一个条目数组.每个条目项格式如下:

 typedef struct{
        int name;      name是字符串表中的字节偏移.指向符号的以null结尾的字符串名字.
        int value;   value是符号的地址.
        int size;    目标的大小.(单位字节)
        char type:4,   要么是数据,要么是函数.
            binding:4; 表示是全局的,还是本地的.
        char reserved;    保留字段.
        char section;     到节头部.
    }Elf_Symbol;

符号解析

  • 链接器解析符号引用的方法
    是将每个引用与它输入的可重定位目标文件的符号表中的一个确定的符号定义联系起来.
    对那些引用在本地模块中的本地符号引用,符号解析非常简单明了.编译器只允许每个模块中每个本地
    符号只有一个定义.编译器还确保静态本地变量.它们也会由本链接符号.拥有唯一的名字.

  • 编译c代码是如果main.c的main函数中应用一个printfx()函数.当编译时遇到一个不是本模块的函数.
    他会假设这个函数在其他的模块中被定义,他会生成一个符号链接表条目.
    并把它交给连接器处理.但是如果链接器再所有输入的目标文件中没由找到他的定义
    链接器将会出错.

链接器如何解析多重定义的全局符号

  1. 不允许多个强符号.
  2. 如果由一个强符号和多个弱符号,那么选择强符号.
  3. 如果有多个弱符号,那么从弱符号中选择一个.
  4. 可以使用gcc -fno-common 这样的选项调用连接器这个选项会告示连接器,遇到多重定义的全局符号时.输出一条警告信息.

与静态库链接

  在早期库的组织形式相对简单,里面的目标代码只能够进行静态链接,所以我们称为“静态库”,静态库的结构比较简单,其实就是把原来的目标代码放在一起,链接程序根据每一份目标代码的符号表查找相应的符号(函数和变量的名字),找到的话就把该函数里面需要定位的进行定位,然后将整块函数代码放进可执行文件里,若是找不到需要的函数就报错退出。
静态库的两个特点:

  • 链接后产生的可执行文件包含了所有需要调用的函数的代码,因此占用磁盘空间较大。
  • 如果有多个(调用相同库函数的)进程在内存中同时运行,
    内存中就存有多份相同的库函数代码,因此占用内存空间较多。

  • 链接方法如下 gcc main.c /usr/lib/libm.a /usr/lib/libc.a

重定位

  • 一旦链接器完成了符号解析这一步,它就把代码中的每个符号引用和确定的一个符号定义联系起来.
    链接器就知道它输入目标模块中的代码节和数据节的确切大小,现在就可以开始重定位了.
    这个步骤中,将合并输入模块,并为每个符号分配运行时地址,

重定位由两步组成:

  • 重定位节和符号定义,连接器将所有相同类型的节合并为同一类型的聚合节,然后连接器将运行时存储器地址赋值给新的聚合节.
  • 重定位节中的符号引用.
    链接器修改代码节和数据节中对每个符号的引用,使得他们指向正确的运行地址.为执行这一步
    连接器依赖于称为重定向条目. 

重定位条目

  • 当汇编器生成一个目标模块时,它并不知道数据和代码最终将存放在存储器中的什么位置。
    它也不知道这个模块引用的任何外部定义的函数和全局变量。
    所以,无论何时汇编器遇到对最终位置未指定目标引用,它就会生成一个重定位条目,
    告诉链接器在将目标文件合并可执行文件时如何修改这个引用。
    代码重定位条目放在.rel.text中。已经初始化数据的重定位条目放在.rel.data中。

重定位符号引用

以下来自:http://docs.oracle.com/cd/E26926_01/html/E25910/chapter3-29.html
运行时链接程序在装入应用程序所需的全部依赖项之后,将会处理每个目标文件并执行所有必需的重定位。

在目标文件的链接编辑过程中,随输入可重定位目标文件提供的任何重定位信息均会应用于输出文件。但是,在创建动态可执行文件或共享目标文件时,许多重定位无法在链接编辑时完成。这些重定位需要仅在目标文件装入内存时才知道的逻辑地址。在这种情况下,链接编辑器将在输出文件映像中生成新的重定位记录。然后,运行时链接程序必须处理这些新的重定位记录。

有关许多重定位类型的更详细说明,请参见SPARC: 重定位。重定位存在两个基本类型。

非符号重定位

符号重定位

使用 elfdump(1) 可以显示目标文件的重定位记录。在以下示例中,文件 libbar.so.1 包含两条重定位记录,用于指示必须更新全局偏移表或 .got 节。

$ elfdump -r libbar.so.1

Relocation Section: .rel.got:
type offset section symbol
R_SPARC_RELATIVE 0x10438 .rel.got
R_SPARC_GLOB_DAT 0x1043c .rel.got foo

第一个重定位是一个简单的相对重定位,这可通过重定位类型以及没有符号引用看出。此重定位需要使用将目标文件装入内存的基本地址来更新关联的 .got 偏移。

第二个重定位需要符号 foo 的地址。要完成此重定位,运行时链接程序必须从动态可执行文件或其依赖项之一查找该符号。
重定位符号查找

运行时链接程序负责搜索目标文件在运行时所需的符号。通常,用户应该熟悉应用于动态可执行文件及其依赖项的缺省搜索模型,以及应用于通过 dlopen(3C) 获取的目标文件的缺省搜索模型。但是,目标文件的符号属性,或是具体的绑定要求会导致符号查找结果具有更复杂的特性。

目标文件有两个属性会影响符号查找。第一个属性是请求目标文件的符号搜索作用域。第二个属性是进程中每个目标文件提供的符号可见性。

装入目标文件时,可将这些属性作为缺省属性应用。此外,也可将这些属性作为 dlopen(3C) 的特定模式提供。在某些情况下,可在生成目标文件时将这些属性记录在目标文件中。

目标文件可以定义一个 world 搜索作用域和/或一个 group 搜索作用域。

world

目标文件可以在进程的任何其他全局目标文件中搜索符号。

group

目标文件可以在同一组的任何目标文件中搜索符号。通过使用 dlopen(3C) 获取的目标文件或使用链接编辑器的 -B group 选项生成的目标文件创建的依赖项树构成一个唯一的组。

目标文件可以定义目标文件的导出符号是全局可见还是局部可见。

global

可在具有 world 搜索作用域的任何目标文件中引用该目标文件的导出符号。

local

只能在构成同一组的其他目标文件中引用该目标文件的导出符号。

运行时符号搜索也可由符号可见性指定。指定了 STV_SINGLETON 可见性的符号不受任何符号搜索作用域的影响。所有的单件符号引用都绑定到进程中第一次出现的单件定义。请参见表 12-20。

符号查找的最简单形式将在下一节缺省符号查找中进行概述。通常,符号属性由多种形式的 dlopen(3C) 使用。这些情况将在符号查找中进行讨论。

动态目标文件使用直接绑定时,将提供替代的符号查找模型。此模型指示运行时链接程序直接在链接编辑时提供符号的目标文件中搜索符号。请参见第 9 章。
缺省符号查找

动态可执行文件及随其装入的所有依赖项都被指定了 world 搜索作用域和 global 符号可见性。针对动态可执行文件或随其装入的任何依赖项的缺省符号查找会导致搜索每个目标文件。运行时链接程序将从动态可执行文件开始,并按目标文件的装入顺序搜索每个依赖项。

ldd(1) 将按依赖项的装入顺序列出动态可执行文件的依赖项。例如,假定动态可执行文件 prog 将 libfoo.so.1 和 libbar.so.1 指定为其依赖项。

$ ldd prog
libfoo.so.1 => /home/me/lib/libfoo.so.1
libbar.so.1 => /home/me/lib/libbar.so.1

需要符号 bar 来执行重定位,运行时链接程序首先在动态可执行文件 prog 中查找 bar。如果找不到符号,运行时链接程序随后会在共享目标文件 /home/me/lib/libfoo.so.1 中查找,最后在共享目标文件 /home/me/lib/libbar.so.1 中查找。

注 - 符号查找操作的开销可能很大,尤其是在符号名称大小和依赖项数目增加的情况下。这方面的性能将在性能注意事项中详细介绍。有关替代查找模型,请参见第 9 章。

缺省重定位处理模型还允许转换为延迟装入 (lazy loading) 环境。如果在当前装入的目标文件中找不到某符号,则会处理所有暂挂的延迟装入目标文件,以尝试查找该符号。此装入是对尚未完整定义其依赖项的目标文件的补偿。但是,该补偿可能会破坏延迟装入的优点。
运行时插入

缺省情况下,运行时链接程序首先在动态可执行文件中搜索符号,然后在每个依赖项中进行搜索。使用此模型时,第一次出现的所需符号满足搜索要求。因此,如果同一符号存在多个实例,则会在所有其他实例中插入第一个实例。

简单解析中概述了插入如何影响符号解析。缩减符号作用域中提供了有关更改符号可见性,从而减少意外插入几率的机制。

注 - 指定了 STV_SINGLETON 可见性的符号提供了一种插入形式。所有的单件符号引用都绑定到进程中第一次出现的单件定义。请参见表 12-20。

如果目标文件被显式标识为插入项,则可以对每个目标文件强制执行插入。使用环境变量 LD_PRELOAD 装入或通过链接编辑器的 -z interpose 选项创建的任何目标文件都会标识为插入项。运行时链接程序搜索符号时,将在应用程序之后、任何其他依赖项之前搜索标识为插入项的任何目标文件。

仅当在进行任何进程重定位之前装入了插入项的情况下,才能保证可以使用插入项提供的所有接口。在重定位处理开始之前,将装入使用环境变量 LD_PRELOAD 提供的插入项,或作为应用程序的非延迟装入依赖项建立的插入项。启动重定位之后,引入进程中的插入项会降级为正常依赖项。如果插入项是延迟装入的,或者是由于使用 dlopen(3C) 而装入的,则插入项可能会降级。可使用 ldd(1) 来检测前一种类别。

$ ldd -Lr prog
libc.so.1 => /lib/libc.so.1
foo.so.2 => ./foo.so.2
libmapmalloc.so.1 => /usr/lib/libmapmalloc.so.1
loading after relocation has started: interposition request \
(DF_1_INTERPOSE) ignored: /usr/lib/libmapmalloc.so.1

注 - 如果链接编辑器在处理延迟装入的依赖项时遇到显式定义的插入项,则插入项将被记录为非延迟可装入依赖项。

可以使用 INTERPOSE mapfile 关键字将动态可执行文件中的单个符号定义为插入项。该机制使用 -z interpose 选项,因而更具选择性,并且针对随着依赖项发展而发生的逆向插入提供更好的隔离。请参见定义显式插入。
执行重定位的时间

根据重定位的执行时间,重定位可分为两种类型。产生这种区别是由对已重定位偏移进行的引用的类型所致。

即时引用

延迟引用

即时引用指的是必须在装入目标文件后立即确定的重定位。这些引用通常是目标文件代码使用的数据项、函数指针,甚至是通过与位置相关的共享目标文件进行的函数调用。这些重定位无法向运行时链接程序提供有关何时引用重定位项的信息。因此,必须在装入目标文件时,并在应用程序获取或重新获取控制权之前执行所有即时重定位。

延迟引用指的是在目标文件执行时可确定的重定位。这些引用通常是通过与位置无关的共享目标文件进行的全局函数调用,或者是通过动态可执行文件进行的外部函数调用。在对提供这些引用的任何动态模块进行编译和链接编辑的过程中,关联的函数调用将成为对过程链接表项的调用。这些项构成 .plt 节。每个过程链接表项都成为包含关联重定位的延迟引用。

在首次调用过程链接表项时,控制权会移交给运行时链接程序。运行时链接程序将查找所需符号,并重写关联目标文件中的项信息。将来调用此过程链接表项时,将直接转至相应函数。使用此机制,可以推迟此类型的重定位,直到调用函数的第一个实例。此过程有时称为延迟绑定。

运行时链接程序的缺省模式是在每次提供过程链接表重定位时执行延迟绑定。通过将环境变量 LD_BIND_NOW 设置为任意非空值,可以覆盖此缺省模式。此环境变量设置将导致运行时链接程序在装入目标文件时,同时执行即时引用和延迟引用重定位。这些重定位在应用程序获取或重新获取控制权之前执行。例如,根据以下环境变量来处理文件 prog 及其依赖项中的所有重定位。在将控制权转交给应用程序之前处理这些重定位。

$ LD_BIND_NOW=1 prog

此外,也可使用 dlopen(3C) 来访问目标文件,并将模式定义为 RTLD_NOW。还可使用链接编辑器的 -z now 选项来生成目标文件,以指示该目标文件需要在装入时进行完整的重定位处理。此重定位要求还将在运行时传播至所标记目标文件的所有依赖项。

注 - 前面的即时引用和延迟引用示例都很典型。但是,过程链接表项的创建最终受用作链接编辑输入的可重定位目标文件提供的重定位信息控制。R_SPARC_WPLT30 和 R_386_PLT32 等重定位记录指示链接编辑器创建过程链接表项。这些重定位由与位置无关的代码公用。

但是,通常会通过与位置相关的代码创建动态可执行文件,该代码可能不会指示需要过程链接表项。由于动态可执行文件具有固定位置,因此链接编辑器可在将引用绑定到外部函数定义时创建过程链接表项。无论原始重定位记录如何,都会创建此过程链接表项。
重定位错误

如果找不到符号,则会发生最常见的重定位错误。此情况将会产生相应的运行时链接程序错误消息并终止应用程序。在以下示例中,找不到在文件 libfoo.so.1 中引用的符号 bar。

lddproglibfoo.so.1=>./libfoo.so.1libc.so.1=>/lib/libc.so.1libbar.so.1=>./libbar.so.1libm.so.2=>/lib/libm.so.2 prog
ld.so.1: prog: fatal: relocation error: file ./libfoo.so.1: \
symbol bar: referenced symbol not found
$

在对动态可执行文件进行链接编辑的过程中,此类别的任何潜在重定位错误都会标记为致命未定义符号。有关示例,请参见生成可执行输出文件。但是,如果运行时找到的依赖项与链接编辑过程中引用的原始依赖项不兼容,则可能会发生运行时重定位错误。在前面的示例中,根据包含 bar 的符号定义的 libbar.so.1 共享目标文件的版本生成了 prog。

在链接编辑过程中使用 -z nodefs 选项,将抑制验证目标文件运行时重定位要求。抑制验证还可能会导致运行时重定位错误。

如果由于找不到用作即时引用的符号而发生重定位错误,则会在进程初始化期间立即出现该错误状态。对于延迟绑定的缺省模式,如果找不到用作延迟引用的符号,则会在应用程序获取控制权后出现该错误状态。后一种情况可能需要几分钟、几个月,也可能从不发生,具体情况视整个代码中使用的执行路径而定。

为防止发生此类错误,可使用 ldd(1) 来验证任何动态可执行文件或共享目标文件的重定位要求。

如果在使用 ldd(1) 时指定 -d 选项,将显示每个依赖项并处理所有即时引用重定位。如果无法解析引用,则会生成诊断消息。在前面的示例中,-d 选项将导致以下错误诊断。

$ ldd -d prog
libfoo.so.1 => ./libfoo.so.1
libc.so.1 => /lib/libc.so.1
libbar.so.1 => ./libbar.so.1
libm.so.2 => /lib/libm.so.2
symbol not found: bar (./libfoo.so.1)

你可能感兴趣的:(C语言)