CSAPP学习笔记 -- 第七章 链接

链接是将各种代码和数据片段搜集并组合成为一个单一文件的过程。
链接器使得分离编译成为可能。
 
学习链接的目的
  • 理解链接器将帮助你构造大型程序
  • 理解链接器将帮助你避免一些危险的编程错误
  • 理解链接器将帮助你理解语言的作用域规则是如何实现的
  • 理解链接器将帮助你理解其他重要的系统概念
  • 理解链接器将使你能够利用共享库
 
环境:一个运行于Linux的x86-64系统,使用标准的ELF-64目标文件格式
 
 
7.1 编译器驱动程序

我直接推荐一手博客,老会偷懒了:https://blog.csdn.net/u012662731/article/details/78520349
 
 
7.2 静态链接

静态链接器:以一组可重定位目标文件和命令行参数作为输入,生成一个完全链接的、可以加载和运行的可执行目标文件作为输出。
 
链接器主要任务
  • 符号解析:日标文件定义和引用符号,每个符号对应于一个函数、一个全局变量或一个静态变量(即C语言中任何以static属性声明的变量)。符号解析的目的是将每个符号引用正好和一个符号定义关联起来。
  • 重定位:编译器和汇编器生成从地址0开始的代码和数据节,链接器通过把每个符号定义与一个内存位置关联起来,从而重定位这些节,然后修改所有对这些符号的引用,使得它们指向这个内存位置。链接器使用汇编器产生的重定位条目的详细指令,不加甄别地执行这样的重定位。
 
 
7.3 目标文件

目标文件形式
  • 可重定位目标文件:包含二进制代码和数据,其形式可以在编译时与其他可重定位目标文件合并起来,创建一个可执行目标文件。
  • 可执行目标文件:包含二进制代码和数据,其形式可以被直接复制到内存并执行。
  • 共享目标文件:一种特殊类型的可重定位目标文件,可以在加载或者运行时被动态地加载进内存并链接。
 
编译器和汇编器生成可重定位目标文件(包括共享目标文件)。
链接器生成可执行目标文件。
目标文件是按照特定的目标格式来组织的,各个系统的目标文件格式都不同。
 
 
7.4 可重定位目标文件

  • .text:已编译程序的机器代码
  • .rodata:只读数据,比如printf语句中的格式串和开关语句的跳转表
  • .data:已初始化的全局和静态C变量。局部C变量在运行时被保存在栈中,既不出现在.data节中,也不出现在.bss节中。
  • .bss:未初始化的全局和静态C变量,以及所有被初始化为0的全局或静态变量。在目标文件中这个节不占据实际的空间,它仅仅是一个占位符。目标文件格式区分已初始化和未初始化变量是为了空间效率:在目标文件中,未初始化变量不需要占据任何实际的磁盘空间。运行时,在内存中分配这些变量,初始值为0。
  • .symtab:一个符号表,它存放在程序中定义和引用的函数和全局变量的信息。一些程序员错误地认为必须通过-g选项来编译一个程序,才能得到符号表信息。实际上,每个可重定位目标文件在.symtab中都有一张符号表(除非程序员特意用STRIP命令去掉它)。然而,和编译器中的符号表不同,.symtab符号表不包含局部变量的条目。
  • .rel.text:一个.text节中位置的列表,当链接器把这个目标文件和其他文件组合时,需要修改这些位置。一般而言,任何调用外部函数或者引用全局变量的指令都需要修改。另一方面,调用本地函数的指令则不需要修改。注意,可执行目标文件中并不需要重定位信息,因此通常省略,除非用户显式地指示链接器包含这些信息。
  • .rel.data:被模块引用或定义的所有全局变量的重定位信息。一般而言,任何已初始化的全局变量,如果它的初始值是一个全局变量地址或者外部定义函数的地址,都需要被修改。
  • .debug:一个调试符号表,其条目是程序中定义的局部变量和类型定义,程序中定义和引用的全局变量,以及原始的C源文件。只有以-g选项调用编译器驱动程序时,才会得到这张表。
  • .line:原始C源程序中的行号和.text节中机器指令之间的映射。只有以-g选项调用编译器驱动程序时,才会得到这张表。
  • .strtab:一个字符串表,其内容包括.symtab和.debug节中的符号表,以及节头部中的节名字。字符串表就是以nu11结尾的字符串的序列。
 
 
7.5 符号和符号表

每个可重定位目标模块m都有一个符号表
  • 由模块m定义并能被其他模块引用的全局符号
    • 全局链接器符号对应于非静态的C函数和全局变量
  • 有其他模块定义并被模块m引用的全局符号
    • 这些符号称为外部符号,对应于在其他模块中定义的非静态C函数和全局变量
  • 只被模块m定义和引用的局部符号
    • 他们对应于带static属性的C函数和全局变量。这些符号在模块m中任何位置可见,但是不能被其他模块引用。
 
认识到本地链接器符号和本地程序变量不同是很重要的
 
.symtab中的符号表不包含对应于本地非静态程序变量的任何符号。这些符号在运行时再栈中管理。链接器对此类符号不感兴趣。
 
定义为带有C static属性的本地过程变量是不在栈中管理的,相反,编译器在.data或.bss中为每个定义分配空间,并在符号表中创建一个有唯一名字的本地链接器符号
 
COMMON    未初始化的全局变量
.bss    未初始化的静态变量,和初始化为0的全局或静态变量
 
7.6 符号解析

链接器解析符号引用的方法是将每个引用与它输入的可重定位目标文件的符号表中的一个确定的符号定义关联起来。
编译器只允许每个模块中每个局部符号有一个定义。
静态局部变量也会有本地链接器符号,编译器还要确保他们拥有唯一的名字。
 
对全局符号的引用解析就棘手得多。当编译器遇到一个不是在当前模块中定义的符号(变量或函数名)时,会假设该符号是在其他某个模块中定义的,生成一个链接器符号表条目,并把它交给链接器处理。如果链接器在它的任何输入模块中都找不到这个被引用符号的定义,就输出一条(通常很难阅读的)错误信息并终止。
 
7.6.1 链接器如何解析多重定义的全局符号
  • 强符号:函数和已初始化的全局变量
  • 弱符号:未初始化的全局变量
 
处理多重定义符号的规则
  • 规则一:不允许有多个同名的强符号
  • 规则二:一强多弱选强
  • 规则三:无强多弱随机选取
 
GCC-fno-common:告诉链接器,在遇到多重定义的全局符号时报错。
-Werror:将所有警告变成错误
 
多重定义的区分规则使得编译器可以将一个非静态全局变量分类分配到COMMON/.bss
静态符号唯一的规则使得编译器可以将一个静态全局变量分类分配到.data/.bss
 
7.6.2 与静态库链接
静态库:所有相关的文件模块打包成一个单独的文件。在Linux系统中,静态库以一种称为存档的特殊文件格式存放在磁盘中。
 
-static参数告诉编译器驱动程序,链接器应该构建一个完全链接的可执行目标文件,他可以加载到内存并运行,在加载时无需更进一步的链接。
 
 
7.6.3 链接器如何使用静态库来解析引用
在符号解析阶段,链接器从左到右按照它们在编译器驱动程序命令行上出现的顺序来扫描可重定位目标文件和存档文件。(驱动程序自动将命令行中所有的.c文件翻译为.o文件。)在这次扫描中,链接器维护一个可重定位目标文件的集合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中的目标文件,构建输出的可执行文件。
 
链接算法问题以及解决方法
  • 问题
    • 在命令行中,如果定义一个符号的库出现在引用这个符号的目标文件之前,那么引用就不能被解析,链接会失败。
  • 解决方法
    • 关于库的一般准则是将它们放在命令行的结尾。如果各个库的成员是相互独立的(也就是说没有成员引用另一个成员定义的符号),那么这些库就可以以任何顺序放置在命令行的结尾处。另一方面,如果库不是相互独立的,那么必须对它们排序,使得对于每个被存档文件的成员外部引用的符号s,在命令行中至少有一个s的定义是在对s的引用之后的。
    • 另一种方法是将需要的存档文件合并成一个
 
 
7.7 重定位

重定位将合并输入模块,并为每个符号分配运行时地址
  • 重定位节和符号定义
    • 链接器将所有相同类型的节合并为同一类型的新的聚合节
    • 链接器将运行时内存地址赋给新的聚合节,赋给输入模块定义的每个节,以及赋给输入模块定义的每个符号
    • 程序中的每条指令和全局变量都有唯一的运行时内存地址
  • 重定位节中的符号引用
    • 链接器修改代码节和数据节中对每个符号的引用,依赖于可重定位目标模块中称为重定位条目的数据结构
 
7.7.1 重定位条目
重定位条目告诉链接器在将目标文件合并成可执行文件时如何修改这个引用
代码重定位条目放在.rel.text中,已初始化数据的重定位条目放在.rel.data中
 
7.7.2 重定位符号引用
  • 第三行计算的是需要被重定位的4字节引用的数组s中的地址
    • 如果这个引用使用的是PC相对寻址,那就用5-9行来重定位
    • 如果这个引用使用的是绝对寻址,那就用11-13行来重定位
  • 对sum的引用使用32位PC相对地址进行重定位(使用图7-10的重定位算法
    • 相应重定位条目r有4个字段
      • r.offset = 0xf
      • r.symbol = sum
      • r.type = R_X86_64_PC32
      • r.addend = -4
    • 假设链接器已经确定 ADDR(s) = ADDR(.text) = 0x4004d0 和 ADDR(r.symbol) = ADDR(sum) = 0x4004e8
    • refaddr = ADDR(s) + r.offset = 0x4004df
    • *refptr = (unsigned)(ADDR(r.symbol) + r.addend - refaddr) = (unsigned)(0x5)
    • 于是在得到的可执行目标文件中,call指令有如下的重定位的形式
      • 4004de: e8 05 00 00 00             callq 4004e8 <sum>
  • 对array的引用要使用32位绝对地址进行重定位
    • 对应的占位符条目r包括4个字段
      • r.offset = 0xa
      • r.symbol = array
      • r.type = R_X86_64_32
      • r.addend = 0
    • 假设链接器已经确定 ADDR(r.symbol) = ADDR(array) = 0x601018
    • *refptr = (unsigned)(ADDR(r.symbol) + r.addend) = (unsigned)(0x601018)
    • 在得到的可执行目标文件中,该引用有下面的重定位形式
      • 4004d9: bf 18 10 60 00          mov $0x601018, %edi
  • 综合到一起,图7-12给出了最终可执行目标文件中已重定位的.text节和.data节。在加载的时候,加载器会把这些节中的字节直接复制到内存,不再进行任何修改地执行这些指令。
 
 
7.8 可执行目标文件

它还包括程序的入口点,也就是当程序运行时要执行的第一条指令的地址。
 
.init节定义了一个小函数_init,程序初始化代码会调用它。
 
因为可执行文件是完全链接的,所以不再需要.rel节。
 
程序头部表描述了可执行文件的连续的片被映射到连续的内存段的映射关系。
 
 
7.9 加载可执行目标文件

加载:加载器将可执行目标文件中的代码和数据从磁盘复制到内存中,然后通过跳转到程序的第一条指令或入口点来运行该程序。
任何Linux程序都可以通过execve函数来调用加载器。
 
 
7.10 动态链接共享库

共享库是一个目标模块,在运行或加载时,可以加载到任意的内存地址,并和一个在内存中的程序链接起来。这个过程称为动态链接,是由一个叫做动态链接器的程序来执行的。
 
共享库也称为共享目标,在Linux系统中通常用.so后缀来表示。
 
微软的操作系统大量地使用了共享库,它们称为DLL(动态链接库)。
 
动态链接器通过执行下面的重定位完成链接任务
  • 重定位libc.so的文本和数据到某个内存段。
  • 重定位libvector.so的文本和数据到另一个内存段。
  • 重定位prog21中所有对由1ibc.so和1ibvector.so定义的符号的引用。
最后,动态链接器将控制传递给应用程序。从这个时刻开始,共享库的位置就固定了,并且在程序执行的过程中都不会变。
 
 
7.11 从应用程序中加载和链接共享库

动态链接是一项强大有用的技术。下面是一些现实世界中的例子
  • 分发软件。微软Windows应用的开发者常常利用共享库来分发软件更新。他们生成一个共享库的新版本,然后用户可以下载,并用它替代当前的版本。下一次他们运行应用程序时,应用将自动链接和加载新的共享库。
  • 构建高性能Web服务器。许多Web服务器生成动态内容,比如个性化的Web页面、账户余额和广告标语。早期的Web服务器通过使用fork和execve创建一个子进程,并在该子进程的上下文中运行CGI程序来生成动态内容。然而,现代高性能的Web服务器可以使用基于动态链接的更有效和完善的方法来生成动态内容。
其思路是将每个生成动态内容的函数打包在共享库中。当一个来自Web浏览器的请求到达时,服务器动态地加载和链接适当的函数,然后直接调用它,而不是使用fork和execve在子进程的上下文中运行函数。
 
Linux为动态链接器提供了一个简单的接口
 
 
7.12 位置无关代码(略)

可以加载而无需重定位的代码称为位置无关代码PIC
用户对GCC使用-fpic选项只是GNU编译系统生成PIC代码,共享库的编译必须总是使用该选项。
  • PIC数据引用
    • 无论我们在内存中的何处加载一个目标模块(包括共享目标模块),数据段和代码段的距离总是保持不变。
    • 想要生成对全局变量PIC引用的编译器在数据段开始的地方创建了全局偏移量表GOT
  • PIC函数调用
    • 延迟绑定:将过程地址的绑定延迟到第一次调用该过程时
    • 延迟绑定通过两个数据结构之间简洁但又有些复杂的交互实现
      • 全局偏移量表GOT
      • 过程链接表PLT
 
 
713 库打桩机制(重点)

库打桩(library interpositioning),它允许你截获对共享库函数的调用,取而代之执行自己的代码。
 
基本思想:给定一个需要打桩的目标函数,创建一个包装函数,它的原型与目标函数完全一样。使用某种特殊的打桩机制,你就可以欺骗系统调用包装函数而不是目标函数了。包装函数通常会执行它自己的逻辑,然后调用目标函数,再将日标函数的返回值传递给调用者。
 
打桩可以发生在编译时、链接时或当程序被加载和执行的运行时。
 
7.13.1 编译时打桩
  • 编译与链接程序
    • 有 -I. 参数,所以会进行打桩,他告诉C处于处理器在搜索通常的系统目录之前,先在当前目录中查找malloc.h
  • 运行
 
7.13.2 链接时打桩
Linux静态链接器支持用 --wrap f 标志进行链接时打桩。这个标志告诉链接器,把对符号 f 的引用解析成__wrap_f(前缀是两个下划线),还要把对符号__real_f(前缀是两个下划线)的引用解析为 f 。
  • 编译
  • 链接
    • -Wl,option 标志把 option 传递给链接器, option 中的每个逗号都要替代成一个空格,所以 -Wl,--wrap,malloc 就把 --wrap malloc 传递给链接器
  • 运行
 
7.13.3 运行时打桩
编译时打桩需要能够访问程序的源代码。
链接时打桩需要能够访问程序的可重定位对象文件。
 
运行时打桩,它只需要能够访问可执行目标文件。这个很厉害的机制基于动态链接器的LD_PRELOAD环境变量。
 
如果LD_PRELOAD环境变量被设置为一个共享库路径名的列表(以空格或分号分隔),那么当你加载和执行一个程序,需要解析未定义的引用时,动态链接器(LD-LINUX.so)会先搜索LD_PRELOAD库,然后才搜索任何其他的库。有了这个机制,当你加载和执行任意可执行文件时,可以对任何共享库中的任何函数打桩,包括libc.so。
  • 构建包含这些包装函数的共享库的方法
  • 编译主程序
  • 在bash shell中运行
 
 
7.14 处理目标文件的工具

在linux系统中有大量可用的工具可以帮助你理解和处理目标文件。特别地,GNU binutils包尤其有帮助,而且可以运行在每个Linux平台上。
  • AR:创建静态库,插入、删除、列出和提取成员。
  • STRINGS:列出一个目标文件中所有可打印的字符串。
  • STRIP:从日标文件中删除符号表信息。
  • NM:列出一个目标文件的符号表中定义的符号。
  • SIZE:列出日标文件中节的名字和大小。
  • READELF:显示一个目标文件的完整结构,包括ELF头中编码的所有信息。包含SIZE和NM的功能。
  • OBJDUMP:所有二进制工其之母。能够显示一个目标文件中所有的信息。它最大的作用是反汇编.text节中的二进制指令。
  • LDD:列出一个可执行文件在运行时所需要的共享库。
 
posted @ 2020-10-15 15:34  Yoke_cc  阅读(348)  评论(0)    收藏  举报