《Linux内核设计与实现》第7章读书笔记

第七章 链接 

一、 链接的概念


 

  链接是将各种代码和数据部分收集起来并组合成为一个单一文件的过程。可以执行于编译、加载和运行时,由叫做链接器(可实现分离编译)的程序自动执行。

二、静态链接


 

  为了创建静态链接,链接器完成两个主要任务:

  • 符号解析:将每个符号引用和一个符号定义联系起来。
  • 重定位:编译器和汇编器生成从0地址开始的代码和数据节。链接器通过把每个符号定义与一个存储器位置联系起来,然后修改所有对这些符                号的引用,使得它们指向这个存储器位置,从而重定位这些节。

三、目标文件


 

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

四、可重定位目标文件


 

  ELF头(ELF header)以一个16字节的序列开始,这个序列描述了生成该文件的系统的字的大小和字节顺序。ELF头剩下的部分包含帮助链接器语法分析和解释目标文件的信息。其中包括ELF头的大小、目标文件的类型(如可重定位、可执行或是共享的)、机器类型(如IA32)、节头部表的文件偏移,以及节头部表中的条目大小和数量。不同的节的位置和大小是由节头部表描述的,其中目标文件中每个节都有一个固定大小的条目。

 

五、符号和符号表


 

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

  • 由m定义并能被其他模块引用的全局符号。全局链接器符号对应于非静态的C函数以及被定义为不带C static属性的全局变量
  • 由其他模块定义并被模块m引用的全局符号。这些符号称为外部符号,对应于定义在其它函数中的C函数和变量
  • 只被模块m定义和引用的本地符号。有的本地链接器符号对应于带static属性的C函数和全局变量。这些符号在模块m中随处可见,但是不能被其他模块引用。目标文件中对应于模块m的节和相应的源文件的名字也能获得本地符号

六、符号解析


 

   C++和Java中能使用重载函数,是因为编译器将每个惟一的方法和参数列表组合编码成一个对链接器来说惟一的名字。这种编码过程叫做毁坏,而相反的过程叫恢复

   C++和Java使用的是兼容的毁坏策略。一个已毁坏类的名字是由名字中字符的整数数量,后面跟原始名字组成的。比如类Foo被编译成3Foo;方法被编译成:原始方法名 + __ + 已毁坏的类名+ 再加上每个类参数的一个字母。如Foo::bar(int, long)被编译成bar__3Fooil。毁坏全局变量和模板名字的策略是相似的。

1. 链接器如何解析多处定义的全局符号

  函数和已初始化的全局变量是强符号(strong);未初始化的全局变量是弱符号(weak)。

  根据强弱符号的定义,Unix链接器使用下面的规则来处理多重定义的符号:

  • 规则1:不允许有多个强符号
  • 规则2:如果有一个强符号和多个弱符号,那么选择强符号
  • 规则3:如果有多个弱符号,那么从这些弱符号中任意选择一个

2.与静态库链接

  静态库:有的编译系统都提供一种机制,将所有相关的目标模块打包成为的一个单独的文件。

  unix中,静态库以一种称为存档(archive)的特殊文件格式存放在磁盘中。存档文件是一组连接起来的可重定位目标文件的集合,有一个头部描述每个成员目标文件的大小和位置;后辍名为.a。

 

3.链接器如何使用静态库来解析引用

  (1)在符号解析的阶段,链接器从左到右按照它们在编译器驱动程序命令行上出现的相同顺序来扫描可重定位目标文件和存档文件。在这次扫描中,链接器维持一个可重定位目标文件的集合E(这个集合中的文件会被合并起来形成可执行文件),一个未解析的符号(即引用了但是尚未定义的符号)集合U,以及一个在前面输入文件中已定义的符号集合D。初始时,E、U和D都是空的。

  (2)命令行上的库和目标文件的顺序非常重要。在命令行中,如果定义一个符号的库出现在引用这个符号的目标文件之前,那么引用就不能被解析,链接会失败。

  (3)关于库的一般准则:

  将它们放在命令行的结尾;
  另一方面,如果库不是相互独立的,那么它们必须排序,使得对于每个被存档文件的成员外部引用的符号s,在命令行中至少有一个s的定义实在对s的引用之后的;
  如果需要满足依赖需求,可以在命令行上重复库。
七、重定位


 

   重定位由两步组成:

  重定位节和符号定义。在这一步中,链接器将所有相同类型的节合并为同一类型的新的聚合节。然后,链接器将运行时存储器地址赋给新的聚合节,赋给输入模块定义的每个节,以及赋给输入模块定义的每个符号。当这一步完成时,程序中的每个指令和全局变量都有唯一的运行时存储器地址。
  重定位节中的符号引用。在这一步中,链接器修改代码节和数据节中对每个符号的引用,使得它们指向正确的运行时地址。为了执行这一步,链接器依赖于称为重定位条目的可重定位目标模块中的数据结构。

八、可执行目标文件


 

  可执行目标文件的格式类似于可重定位目标文件的格式。ELF头部描述文件的总体格式。它还包括程序的入口点,也就是当程序运行时要执行的第一条指令的地址。.text 、.rodata和.data 节和可重定位目标文件中的节是相似的,除了这些节已经被重定位到它们最终的运行时存储器地址以外。.init节定义了一个小函数,叫做_init,程序的初始化代码会调用它。因为可执行文件是完全链接的(已被重定位了),所以它不再需要.rel节。

  ELF可执行文件被设计得很容易加载到存储器,可执行文件的连续的片被映射到连续的存储器段。段头部表描述了这种映射关系。

九、加载可执行目标文件


 

  要运行可执行目标文件p,可以在Unix外壳的命令行中输入它的名字:unix> ./p

  任何Unix程序都可以通过调用execve函数来调用加载器。加载器将可执行目标文件中的代码和数据从磁盘拷贝到存储器中,然后通过跳转到程序的第1条指令,即入口点(Entry Point),来运行该程序。将程序拷贝到存储器并运行的过程叫做加载(loading)。

 

十、动态链接共享库


 

   共享库是一个目标模块,在运行时,可以加载到任意的地存储器地址,并在存储器中和一个程序链接起来。这个过程称为动态链接,是由一个叫做动态链接器的程序执行的。

   共享库也称为共享目标,在Unix系统中通常用.so后缀来表示。(在MS OS 中为DLL文件)


  【注意:静态链接与动态链接的区别:静态链接是把程序所需要的库代码和数据拷贝和嵌入到引用它们的可执行文件中;而动态链接是所有引用该库的可执行文件文件共享这个.so(dll)文件中的代码和数据。】

十一、与位置无关的代码


 

   共享库的一个主要目的就是允许多个正在运行的进程共享存储器中相同的库代码,因而节约存储器资源。

   PIC:编译库代码,不需要链接器修改库代码,就可以在任何地址加载和执行这些代码。

   在一个IA32系统中,对同一个目标模块中过程的调用不需要特殊处理的,因为引用是PC相关的,已知偏移量,就是PIC了。然而,对外部定义的过程调用和对全局变量的引用通常不是PIC,因为它们都要求在链接时重定位。

   如何对全局变量生成PIC引用呢?基于以下事实:无论我们在存储器中的何处加载一个目标模块(包括共享模块),数据段总是分配为紧随在代码段后面。因此,代码段中任何指令和数据段中任何变量之间的距离都是一个运行时常量。

 

posted @ 2016-04-07 09:46  5219hsw  阅读(126)  评论(0编辑  收藏  举报