【读书笔记】深入理解计算机系统(第七章)

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

 

一、编译器驱动程序

编译系统提供的调用预处理器、编译器、汇编器和链接器来构造目标文件的程序。

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

 

二、静态链接

【读书笔记】深入理解计算机系统(第七章)

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

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

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

 

三、目标文件

目标文件的三种形式

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

编译器和汇编器生成可重定位目标文件(包括共享目标文件)。

链接器生成可执行目标文件。从技术上来说,一个目标模块就是一个字节序列,而一个目标文件就是一个存放在磁盘文件中的目标模块。

 

四、可重定位目标文件

一个典型的ELF可重定位目标文件的格式P451。

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

ELF头剩下的部分包含帮助链接器语法分析和解释目标文件的信息。其中包括ELF头的大小、目标文件的类型(如可重定位、可执行或是共享的)、机器类型(如IA32)、节头部表的文件偏移,以及节头部表中的条目大小和数量。

不同的节的位置和大小是由节头部表描述的,其中目标文件中每个节都有一个固定大小的条目。

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

【读书笔记】深入理解计算机系统(第七章)

 

五、符号和符号表

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

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

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

 

 

六、符号解析

 

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

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

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

 

所有的编译系统都提供一种机制,将所有相关的目标模块打包成为一个单独的文件,称为静态库(Linux下是存档文件,Windows下是lib),只拷贝静态库里被应用程序引用的目标模块。

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

 

 

 

七、重定位

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

 

重定位由两步组成:

 

  • 重定位节和符号定义。在这一步中,链接器将所有相同类型的节合并为同一类型的新的聚合节。然后,链接器将运行时存储器地址赋给新的聚合节,赋给输入模块定义的每个节,以及赋给输入模块定义的每个符号。当这一步完成时,程序中的每个指令和全局变量都有唯一的运行时存储器地址。

  • 重定位节中的符号引用。在这一步中,链接器修改代码节和数据节中对每个符号的引用,使得它们指向正确的运行时地址。为了执行这一步,链接器依赖于称为重定位条目的可重定位目标模块中的数据结构

 

重定位条目

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

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

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

重定位符号引用

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

  • 重定位PC相对引用
  • 重定位绝对引用

八、可执行目标文件

可执行目标文件的格式类似于可重定位目标文件的格式。ELF头部描述文件的总体格式。它还包括程序的入口点,也就是当程序运行时要执行的第一条指令的地址。

.text 、.rodata和.data 节和可重定位目标文件中的节是相似的,除了这些节已经被重定位到它们最终的运行时存储器地址以外。

.init节定义了一个小函数,叫做_init,程序的初始化代码会调用它。因为可执行文件是完全链接的(已被重定位了),所以它不再需要.rel节。

【读书笔记】深入理解计算机系统(第七章)

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

 

九、加载可执行目标文件

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

unix> ./p

 

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

【读书笔记】深入理解计算机系统(第七章)

 

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

十、动态链接共享库

共享库是致力与解决静态库缺陷的一个现代创新产物。

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

这个过程称为动态链接,是由一个叫做动态链接器的程序来执行的。

共享库也称为共享目标,在Unix系统中通常用.so后缀来表示。

 

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

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

 

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

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

void *dlopen( const char *file, int mode );//将共享目标文件打开并且映射到内存中,并且返回句柄 
void *dlsym( void *restrict handle, const char *restrict name );//回一个指向被请求入口点的指针
char *dlerror();//返回 NULL 或者一个指向描述最近错误的 ASCII 字符串的指针
char *dlclose( void *handle );//关闭句柄并且取消共享目标文件的映射

 

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

在Unix系统中有大量可用的工具可以帮助你理解和处理目标文件。

  • AR:创建静态库,插入、删除、列出和提取成员
  • STRINGS:列出一个目标文件中所有可打印的字符串
  • STRIP:从目标文件中删除符号表信息
  • NM:列出一个目标文件的符号表中定义的符号
  • SIZE:列出目标文件中节的名字和大小
  • READELF:显示一个目标文件的完整大小,包括ELF头中编码的所有信息。包含SIZE和NM的功能
  • OBJDUMP:所有二进制工具之母。能够显示一个目标文件中的所有信息。它最大的作用是反汇编.text节中的二进制指令

Unix系统为操作共享库还提供了LDD程序:

  • LDD:列出一个可执行文件在运行时所需要的共享