链接

参考的博客

链接(linking)是将代码和数据片断收集并组合成为一个单一文件的过程,这个文件可以被加载到内存中运行。

链接过程可以发生在在代码编译时、加载时、甚至是运行时。在现代系统中链接是通过“链接器”程序自动执行的,但是我们还是有学习链接的必要。

链接器在软件开发中扮演者很关键的角色,他们使得“分离编译”成为了可能,我们不用把一个大型的应用组织成一个巨大的源文件,而是可以把它分解为更小更好管理的模块。

理解链接器的重要性:

  1. 帮助你构造大型的程序。
  2. 避免一些危险的编程错误。
  3. 帮助你理解语言的作用域是怎么实现的。
  4. 理解其他重要的系统概念。

1.1链接器的功能

为了构造可执行文件,链接器必须完成两个主要任务。

  1. 符号解析:每个符号对应一个函数、全局变量、静态变量(C语言中使用static修饰的元素),所以符号对应文件定义和引用。符号解析的目的就是保证每个符号引用和符号相关联。
  2. 重定位:编译器和汇编器生成从地址零开始的代码和数据块。链接器将原先分开的代码和数据片段汇总成一个文件,重新修改对符号的引用,使它们指向正确的内存位置。

编译器生成的目标文件有三种形式

  1. 可重定位目标文件(汇编器生成的文件):包含二进制代码和数据,可以和其他可重定位目标文件合并,创建一个可执行目标文件。
  2. 可执行目标文件:包含二进制和数据,可以直接复制到内存中执行。
  3. 共享目标文件:一种特殊的可重定位目标文件,可以在加载或者运行时动态的加紧内存并链接。

编译器和汇编器生成可重定位文件,链接器生成可执行目标文件,目标文件纯粹是字节块的集合。

1.2可重定位目标文件

现代x86-64 Linux和Nuix系统使用ELF格式表示可重定位目标文件。

下图展示了一个典型的ELF格式的可重定位目标文件。

ELF头以一个16字节的序列开始,描述生成该文件系统的字的大小和字节顺序。

1.2.1符号和符号表

每个可重定位目标模块m都有一个符号表.symtab,它包含m的定义和引用的符号信息,在链接器的上下文存储着三种不同的符号。

  • 由模块m定义并被其他模块引用的全局符号信息,对应于函数中定义的非静态的C函数和全局变量。
  • 由其他模块定义并被模块m引用的全局符号,对应于在其它函数中非静态的C函数和全局变量。
  • 只被模块m定义和引用的局部符号。static属性的函数和全局变量。

链接器符号和本地程序变量是不同的,.symtab中只包含函数中定义的非静态的C函数和全局变量,局部变量在运行时的栈中保存。

另一方面可重定位目标中 static属性的本地过程变量保存在编译器分配的.data或者.ssh的分配空间而不是保存在栈中。并在符号表中创建具有唯一名字的本地链接器符号。

1.2.2符号解析

链接器解析符号引用的方法是将每个引用与它输入的可重定向目标文件的符号表中一个确定的符号表达式关联起来。编译器只允许每个模块中每个局部符号有一个定义。静态局部变量也会有本地链接器符号,编译器确保他们拥有一个唯一的名字。

对于全局符号的引用就比较棘手,如何保证全局变量空间的变量定义不重复。

  1. 但编译器遇到一个不是在当前模块定义的符号(全局变量或者函数名),他会假设该符号在其他模块定义,生成一个连接表条目,并把它交给链接器处理。如果编译器在其他模块找不到被引用的符号定义,就输出一条错误信息终止运行。
  2. 在多个目标文件中可能会定义相同的全局符号。在这种情况下,连接器要么标识一个错误或者抛弃其他优先级低的定义。联想到(命名空间)

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

如果多个模块定义同名的全局符号,会发生什么?链接器怎么解析?

  • 强符号:函数和初始化的全局变量
  • 弱符号:未初始化的全局变量

根据强弱符号的定义,linux使用下面的规则处理多重定义的符号名。

  1. 不能出现多个同名的强符号,不然就会出现链接错误
  2. 如果有同名的强符号和弱符号,选择强符号。
  3. 如果有多个弱符号,随便选择一个。

实例

不能出现多个同名的强符号

如果有同名的强符号和弱符号,选择强符号。

修改err.c的定义改为弱类型并且添加输出语句,err2.c去掉main函数.

1.2.4静态库连接

所有的编译器都提供一种机制,将所有相关的目标模块打包成一个单独的文件,成为静态库。当链接器构造一个输出的可执行文件并在代码中使用静态库的函数时,它只复制静态库里被引用的模块。

使用链接器解析引用

在符号解析阶段,链接器从左到右按照他们在编译器命令行上的顺序来扫描可重定位目标文件和存档文件。

链接器维护三个集合:一个可重定位目标文件的集合E(所以文件合并成一个可执行文件)、一个未解析的符号集合U(引用了但是还没有定义的符号)、一个前面输入文件已经定义的集合D。

链接器这样的处理方式:会导致一些令人烦恼的连接时错误因为命令行上的库和目标文件的顺序非常重要。

1.3可执行目标文件

我们已经看到链接器是如何将多个目标文件合并成可执行文件的。我们书写地C语言程序,从开始的ASCLL程序转化为一个二进制文件,这个二进制文件包含加载程序到内存并运行它所需要的信息。

可执行目标文件的格式类型类似于可重定位的目标文件的格式。ELF头描述文件的总体格式,因为是可执行文件,所以(.rel.text和.rel.data)块不能存在啦,另外它还包括程序的入口点,也就是第一条指令的地址,init节定义了一个小函数,叫做_init,程序的初始化命令会调用它。

ELF可执行文件被设计的很容易加载到内存,执行文件的连续的片被映射成连续的内存段。程序的头部表描述了这种关系,通过objdump 反汇编指令输出程序头部表。输出err的头部表使用命令objdump -x err;

程序头部表,表示根据可执行目标文件的内容初始化了两个内存段。

第一二行初始化了代码段的内存空间,开始于内存地址0x400000处,总共的内存大小为0x69c字节,具有读和执行的权限。

第三四行初始化了数据段的内存空间,开始于内存地址0x600df8处,总共的内存大小为0x230字节,具有读和写的权限。

1.4加载可执行目标文件 

通过shell命令执行可执行文件,通过调用一个称为加载器(loader)的操作系统代码来运行它。

加载器将可执行文件加载到内存中,然后跳到程序的第一条指令或入口点运行该程序。

每个linux程序都有一个运行时的内存映像,代码段总是从0x400000初开始,后面是数据段。运行时堆栈段位于数据段上

1.5动态链接共享库

静态库解决了很多关于如何让大量函数库对程序可用的问题。但是静态库也存在明显的缺点。例如几乎每个程序都使用标准的I/O函数scanf()函数,这些函数的代码都恢复知道每个运行的进程的文本段中。在运行了上百个进程的典型系统上,这是对内存的极大的浪费。

共享库是一个目标模块,在运行和加载时,可以加载到任意的内存地址,并和一个内存上的程序链接起来。这个过程叫做动态链接。共享库也称为共享目标,在linux上以.so后缀来表示。

共享库使用两种不同的方式来共享。

在给定的文件系统中,对于一个库只有一个.so文件。所有引用该库的可执行文件共享该库的代码和数据。(具体参考虚拟内存的实现)

在内存中,一个共享库的.text(已编译的机器代码)的副本可以被不同的正在运行的进程共享。

posted @ 2017-09-03 20:09  _春华秋实  阅读(44)  评论(0)    收藏  举报