《深入理解计算机系统》第七章学习笔记(初稿)

第七章 链接

链接是将各种代码和数据部分收集起来并组合成为一个单一文件的过程,这个文件可被加载(或拷贝)到存储器并执行。链接可以执行于编译时,也就是在源代码被翻译成机器代码时;也可以执行于加载时,也就是在程序被加载器加载到存储器并执行时;甚至执行于运行时,由应用程序来执行。在早期的计算机系统中,链接是手动执行的。在现代系统中,链接是由叫链接器的自动执行的。

  • 理解链接器将帮助构造大型程序
  • 理解链接器将帮助避免一些危险的编程错误
  • 理解链接器将帮助语言的作用域规则是如何实现的
  • 理解链接器将帮助其他重要的系统概念
  • 理解链接器便于利用共享库

一、编译器驱动程序

大多数编译系统提供编译驱动程序,它代表用户在需要时调用语言预处理器、编译器、汇编器和链接器。

gcc -O2 -g -o p main.c swap.c

以上代码将示例程序从ASCII码源文件翻译成可执行目标文件,具体行为如下图:

二、静态链接

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

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

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

2. 链接器的一些基本事实:目标文件纯粹是字节块的集合。这些块中,有些包含程序代码,有些则包含程序数据,而其他的则包含指导链接器和加载器的数据结构。链接器将这些块连接起来,确定被连接块的运行时位置,并且修改代码和数据块中的各种位置。链接器和汇编器已经完成了大部分工作。

三、目标文件

1. 目标文件的三种形式:

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

2. 编译器和汇编器生成可重定位目标文件(包括共享目标文件)。链接器生成可执行目标文件。从技术上来说,一个目标模块就是一个字节序列,而一个目标文件就是一个存放在磁盘文件中的目标模块。

四、可重定位目标文件

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

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

  • .text 已编译程序的机器代码
  • .rodata 只读数据
  • .data 已初始化的全局C变量。局部C变量在运行时保存在栈中,既不出现在.data节中 ,也不出现在.bss节中
  • .bass 未初始化的全局C变量。在目标文件中这个节不占据实际的空间,它仅仅是一个占位符。目标文件格式区分初始化和未初始化变量是为了空间效率:在目标文件中,未初始化变量不需要占据任何实际的磁盘空间
  • .symtab 一个符号表,它存放在程序中定义和引用的函数和全局变量的信息。每个可重定位目标文件在.symtab中都有一张符号表
  • .rel.text 一个.text节中位置的列表,当链接器吧这个目标文件和其他文件结合时,需要修改这些位置。一般而言,任何调用外部函数或引用全局变量的指令都需要修改。另一方面,调用本地函数的指令则不需要修改。注意,可执行目标文件中并不需要重定位信息,因此通常省略,除非用户显示第指示链接器包含这些信息
  • .rel.data 被模块引用或定义的任何全局变量的重定位信息。一般而言,任何已初始化的全局变量,如果它的初始值是一个全局变量地址或者外部定义函数的地址,都需要被修改
  • .debug 一个调试符号表,其条目是程序总定义的局部变量和类型定义,程序中定义和引用的 全局变量,以及原始的C源文件
  • .line 原始C源文件中的行号和.text节中机器指令之间的映射
  • .strtab 一个字符串表,其内容包括.symtab和.debug节中的符号表,以及节头部中的节名字。

五、符号和符号表

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

2. 链接器上下文中三种不同的符号:

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

3. 定义为带有C static属性的本地过程变量不在栈中管理。编译器在.data和.bss中为每个定义分配空间,并在符号表中创建一个唯一有名字的本地链接器符号。

4. 利用static属性隐藏变量和函数名字。任何声明带有static属性的全局变量或函数都是模块私有的。

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

六、符号解析

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

1. 在编译时,编译器向汇编器输出每个全局符号,或者是强或者是弱,而汇编器把这个信息隐含地编码在可重定位目标文件的符号表里。函数和已初始化的全局变量时强符号,未初始化的全局变量是弱符号。

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

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

6.2 与静态库链接

1. 使用标准C库和数学库中函数的程序可以用形式如下的命令行来编译和链接

gcc main.c /usr/lib/libm.a /usr/lib/libc.a

2. 在Unix系统中,静态库以一种称为存档的特殊文件格式村凡在磁盘中。存档文件是一组连接起来的可重定位目标文件的集合,有一个头部用来描述每个成员目标文件的大小和位置。存档文件名由后缀.a标识。

  1. 与静态库链接的过程

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

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

  • 对于命令行上的每个输入文件f,链接器会判断f是一个目标文件还是一个存档文件。如果f是一个目标文件,那么链接器把f添加到E, 修改U和D来反映f中的符号定义和引用,并继续下一个输入文件
  • 如果f是一个存档文件,那么链接器就尝试匹配U中未解析的符号和由存档文件成员定义的符号。如果某个存档文件成员m,定义了一个符号来解析U中的一个引用,那么就将m加到E中,并且链接器修改U和D来反映m中的符号定义和引用。对存档文件中所有的成员目标文件都反复进行这个过程,直到U和D都不再发生变化。在此时,任何不包含在E中的目标文件都简单地被丢弃,而链接器将继续处理下一个输入文件
  • 如果当链接器完成对命令行上输入文件的扫描后,U是非空的,那么链接器就好输出一个错误并终止。否则,它会合并和重定位E中的目标文件,从而构建输出的可执行文件

2. 这种算法会导致一些令人困扰的链接时错误,因为命令行上的库和目标文件的顺序非常重要。在命令行中,如果定义一个符号的库出现在引用这个符号的目标文件之前,那么引用就不能被解析,链接会失败。关于库的一般准则是将它们放在命令行的结尾。

3. 另一方面,如果库不是相互独立的,那么它们必须排序,使得对于每个被存档文件的成员外部引用的符号s,在命令行中至少有一个s的定义实在对s的引用之后的。

4. 如果需要满足依赖需求,可以在命令行上重复库。

七、重定位

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

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

7.1 重定位条目

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

2. ELF定义了11种不同的重定位类型。其中两种最基本的重定位类型:

  • R_ 386_PC32 重定位一个使用32位PC相对地址的引用。
  • R_ 386_32 重定位一个使用32位绝对地址的引用。

7.2 重定位符号引用

链接器修改代码节和数据节中对每个符号的引用,使得他们指向正确的运行时地址。

  1. 重定位PC相对引用
  2. 重定位绝对引用

八、可执行目标文件

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

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

九、加载可执行目标文件

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

unix> ./p

  因为p不是一个内置的外壳命令,所以外壳会认为p是一个可执行目标文件,通过调用某个驻留在存储器中的称为加载器(loader)的操作系统代码来运行它。任何Unix程序都可以通过调用execve函数来调用加载器。加载器将可执行目标文件中的代码和数据从磁盘拷贝到存储器中,然后通过跳转到程序的第一条指令或入口点来运行该程序。这个将程序拷贝到存储器并运行的过程叫做加载。

2. 每个Unix程序都有一个运行时存储器映像。例如:在32位Linux系统中,代码段总是从地址(0x8048000)处开始。数据段是在接下来的下一个4KB对齐的地址处。运行时堆在读/写段之后接下来的第一个4KB对齐的地址处,并童工调用malloc库往上增长。还有一个段是为共享库保留的。用户栈总是从最大的合法用户地址开始,向下增长的(向低存储器地方向增长)。从栈的上部开始的段是为操作系统驻留存储器的部分(也就是内核)的代码和数据保留的。

3. 在可执行文件中段头部表的指导下,加载器将可执行文件的相关内容拷贝到代码和数据段。接下来,加载器跳转到程序的入口点,也就是符号_ start的地址。在_ start地址处的启动代码是在目标文件ctrl.o中定义的,对所有的C程序都是一样的。在从.text和.init节中调用了初始化例程后,启动代码调用atexti例程,这个程序附加了一系列在应用程序正常中止时应该调用的程序。exit函数运行atexit注册的函数,然后通过调用_ exit将控制返回给操作系统。接着,启动代码调用应用程序的main程序,它会开始执行我们的C代码。在应用程序返回之后,启动代码调用_ exit程序,它将控制返回给操作系统。

十、动态链接共享库

1. 概念:共享库是致力与解决静态库缺陷的一个现代创新产物。共享库是一个目标模块,在运行时,可以加载到任意的存储器地址,并加一个在存储器中的程序链接起来。这个过程称为动态链接,是由一个叫做动态链接器的程序来执行的。共享库也称为共享目标,在Unix系统中通常用.so后缀来表示。

调用编译器构造向量运算示例程序的共享库libvector.so:
gcc -shared -fPIC -o libvector.so addvec.c multvec.c

将库链接到程序中,创建一个可执行目标文件p2:
gcc -o p2 main.c /libvector.so

2. 动态链接器通过执行下面的重定位完成链接任务:

  • 重定位libc.so的文本和数据到某个存储器段
  • 重定位libvector.so的文本和数据到另一个存储器段
  • 重定位p2中所有对libc.so和libvector.so定义的符号的引用
  • 最后动态链接器将控制传递给应用程序,此时共享库的位置已固定,并且在程序执行的过程中不会改变

十一、从应用程序中加载和链接共享库

思路:将生成动态内容的每个函数打包在共享库中。

十二、与位置无关的代码

概念:编译库代码,使不需要链接器修改库代码就可以在任何地址加载和执行这些代码。这样的代码叫做与位置无关的代码(PIC)。

  1. PIC数据引用
  2. PIC函数调用

十三、处理目标文件的工具

时间: 2024-10-03 09:44:43

《深入理解计算机系统》第七章学习笔记(初稿)的相关文章

《深入理解计算机系统》第一章学习笔记

信息就是位+上下文 源程序:就是一个由0和1组合的位(bit)序列,8位组成一字(byte),每个字节表示某个文本字符. 系统中所有的信息--包括磁盘文件.存储器中的程序.存储器中存放的用户数据以及网络上传送的数据,都是由一串位表示的.区分不同数据对象的唯一方法是我们读到这些数据对象时的上下文. C语言的起源: 由Dennis Ritchie在1969年~1973年创建的. 美国国家标准学会(American National Standards Institute,ANSI)在1989年颁布了

《深入理解计算机系统》第七章学习笔记

20135109 高艺桐 一.关于链接 1.连接:将各种代码和数据部分收集起来并组合成为一个单一文件的过程.这个文件可被加载或拷贝到存储器并执行. 2.连接可以执行于编译时,也就是在源代码被翻译成机器代码.也可以执行于加载时,也就是程序被加载器加载到存储器并执行时执行于运行时,有应用程序来执行. 3.连接是由链接器的程序自动执行的. 4.连接使分离编译成为可能. 二.编译器驱动程序 1.大部分编译系统提供编译驱动程序:代表用户在需要时调用语言预处理器.编译器.汇编器和链接器.    (1)C预处

《深入理解计算机系统》 第一章读书笔记

最近开始啃CSAPP,买了英文版,看得比较慢.只有先理解系统才能在更多方面学的更明其实质 Chapter1: * 一份hello.c代码的编译分为四个阶段:1.Pre-processor:编译预处理,就是把#include后面的库代码加入到程序中,生成hello.i的文件. 2.Complier:用编译器把hello.i的C代码翻译成汇编语言,并生成:hello.s文件.(汇编语言是高级语言转为机器码的一个中间过程) 3.Assembler:汇编机把汇编语言翻译成机器二进制代码,生成hello.

《Java从入门到精通》第七章学习笔记

第7章 类和对象 一.类和对象的定义及关系 1.类是同一类别对象的抽象定义:可以理解为是某一类具体对象的模子,确定对象将会拥有的特性,包括该类对象具备的属性和方法. 2.对象是对类的一种具体化的客观存在,是对类的具体实例化.要创建一个对象,必须基于类,即先要创建可描述该对象的类. 3.创建一个类 1 public class Telphone { 2 //属性(成员变量):有什么 3 float screen; 4 float cpu; 5 float mem; 6 //方法 :干什么 7 vo

《深入理解计算机系统》第四周学习笔记

一.知识点总结 1.信息存储 练习题2.4 0x503c+0x8=0x5044 0x503c-0x40=0x4ffc 0x503c+64=0x503c+0x40=0x507c 0x50ea-0x503c=0xae 1)字长:指明整数和指针数据的标称大小.一个字长为w的机器的虚拟地址范围为0~2^(w-1),程序最多访问2^w个字节 int .char 4字节,单精度float 字节,双精度double 8字节 2)小端法:最低有效字节在最前面的顺序存储 大端法:最高有效字节在最前面的顺序存储 练

深入理解计算机系统之存储器层次结构学习笔记

一.存储技术 (一)随机访问存储器 随机访问寄存器(RAM)分为静态随机访问寄存器(SRAM)和动态随机访问寄存器(DRAM).静态RAM可以作为高速缓存寄存器,动态RAM可以用作主存以及图形系统的帧缓冲区.静态RAM将每一个位存储在一个双稳态的存储器单元里,构成静态RAM 的电路可以无限期的保持在两个不同的电压配置或状态之一.动态RAM将每一个位存储为对电容的充电,所以动态RAM要比静态RAM对干扰的敏感度更高.构成动态RAM的电路被干扰后就不会恢复了. 1 传统的DRAM 常规DRAM芯片中

第七章读书笔记《深入理解计算机系统》

第七章 读书笔记<深入理解计算机系统> 链接是将各种代码和数据部分收集起来并组合成为一个单一文件的过程,这个文件可被加载(或拷贝)到存储器并执行. 链接可以执行于编译时,也就是在源代码被翻译成机器代码时:也可以执行于加载时,也就是在程序被加载器加载到存储器并执行时:甚至执行于运行时,由应用程序来执行. 在早期的计算机系统中,链接是手动执行的.在现代系统中,链接是由叫链接器的自动执行的. 7.1 编译器驱动程序 1.大部分编译系统提供编译驱动程序:代表用户在需要时调用语言预处理器.编译器.汇编器

《深入了解计算机系统》第七章读书笔记

<深入了解计算机系统>第七章读书笔记 第一部分:链接 链接定义:链接是将各种代码和数据部分收集起来并组合成为一个单一文件的过程,这个文件可被加载(或被拷贝)到存储并执行. 第二部分:目标文件 目标文件:目标文件用以存放目标代码和由编译器或汇编器生成的相关数据的计算机文件格式称为目标文件格式. 目标文件格式有许多不同的种类.最初每种类型计算机都拥有自身独特的格式,但随着Unix和其他可移植操作系统的问世,人们定义了例如COFF和ELF这些格式,并在不同的系统上使用它们.通常,链接器的输入和输出使

《深入理解计算机系统》 Chapter 7 读书笔记

<深入理解计算机系统>Chapter 7 读书笔记 链接是将各种代码和数据部分收集起来并组合成为一个单一文件的过程,这个文件可被加载(货被拷贝)到存储器并执行. 链接的时机 编译时,也就是在源代码被翻译成机器代码时 加载时,也就是在程序被加载器加载到存储器并执行时 运行时,由应用程序执行 链接器使分离编译称为可能. 一.编译器驱动程序 大部分编译系统提供编译驱动程序:代表用户在需要时调用语言预处理器.编译器.汇编器和链接器. 1.将示例程序从ASCⅡ码源文件翻译成可执行目标文件的步骤 (1)运