第二部分
第 7 章 链接
链接(linking)是将各种代码和数据片段收集并组合成为一个单一文件的过程,这个文件可以被加载到内存并执行。
链接可以执行于编译时、加载时甚至运行时。
7.1 编译器驱动程序
shell 调用操作系统中一个叫做加载器的函数,它将可执行文件中的代码和数据复制到内存,然后将控制权移动到这个程序的开头。
7.2 静态链接
静态连接器(static linker) 以一组可重定位目标文件和命令行参数作为输入,生成一个完全连接的、可以加载和运行的可执行目标文件作为输出。连接器的主要任务:
-
符号解析 解析目标文件中定义和引用符号,每个符号对应于一个函数、一个全局变量或一个静态变量。
-
重定位 把每个符号定义与一个内存地址关联起来,修改所有对这些符号的引用,使他们指定这个内存地址。
7.3 目标文件
目标文件有三种形式:
- 可重定位目标文件。包含二进制代码和数据。
- 可执行目标文件,就是可执行程序。
- 共享目标文件,就是动态库,可以在运行时动态地加载到内存并链接。
Linux 和 Unix 系统使用可执行可连接格式(Executable and Linkable Format,ELF)作为可执行文件格式。
7.4 可重定位目标文件
文件头部信息以一个16字节的序列开始。包含的节有:
- .text :已编译程序的机器代码。
- .rodata : 只读数据,比如常量字符串。
- .data : 已初始化的全局和静态C变量。不包括临时变量。
- .bss : 未初始化以及被初始化为0的全局和静态C变量,仅仅是一个占位符不占磁盘空间,运行时,在内存中分配这些变量初始化为0。
- .symtab : 符号表,存放全局信息,函数和变量。
- .debug : 一个调试符号表。包含局部变量和类型定义,原始C源文件。只有gcc -g 选项才可以生成这张表。
- .line : 原始 C 源程序中的行号和 .text 节中机器指令之间的映射。只有gcc -g 选项才可以生成这张表。
- .strtab : 一个字符串表,包含 .symtab 和 .debug 节中的符号表,以及节头部中的节名字。字符串表就是以null结尾的字符串的序列。
7.5 符号和符号表
每个可重定位模块都有一个符号表,包含它定义和引用的符号信息。在连接器的上下文中有三种不同的符号:
- 当前模块中定义并能被其他模块引用的全局符号。对应于非静态函数和全局变量。
- 由其他模块定义并被本模块引用的全局符号。称为外部符号。对应于其他模块中的非静态函数和全局变量。
- 只被当前模块定义和引用的局部符号。使用 static 修饰的函数和全局变量,对其它不可见。
连接器符号不包括临时变量,临时变在运行时栈中被管理。
在C语言中,源文件扮演模块的角色,任何只在本模块内使用的函数或者全局变量应该声明为 static ,这样起到保护作用,就像 C++ 中的 private 一样。
7.6.1 连接器如何解析多重定义的全局符号
连接器的输入是一组可重定位目标模块。每个模块定义一组符号,有些是局部的只对当前模块可见,有些是全局的对所有模块可见。如果有多个模块定义同名全局符号,会发生什么?
符号分为强符号和弱符号。函数和已初始化的全局变量是强符号,未初始化的全局变量是弱符号。链接处理多重符号的规则:
- 不允许有多个同名强符号。
- 如果一个强符号和多个弱同名,选择强符号。
- 如果有多个同名弱符号,则任选一个弱符号。
7.6.2 与静态库链接
- 和与目标文件链接不同,在连接时,连接器只复制被程序引用的模块。
- 在 Linux 系统中,静态库以一种称为存档的特殊文件格式存放在磁盘中。存档文件是一组连接起来的可重定位目标文件的集合。有一个头部用来描述每个成员目标文件的大小和位置。
7.7 重定位
完成符号解析后,连接器知道它的输入目标模块中代码节和数据节的大小。就可以进行重定位步骤,合并输入模块为每个符号分配运行时地址。
- 重定位节和符号定义。将所有模块中同名的节合并。
- 重定位节中的符号引用。修改代码节和数据节中对每个符号的引用,使他们指向正确的运行时地址。
7.7 重定位条目
目标文件并不知道数据和代码最终放在内存什么位置。也不知道引用外部符号的位置。代码生成一个重定位条目放在 .rel.text 中。数据生成重定位条目放在 .rel.data 中。
- 汇编器生成一个目标模块时从地址0开始生成代码和数据节 ,它并不知道数据和代码最终将放在内存的什么位置,也不知道这个模块引用的任何外部定义的函数或者全局变量的位置。
- 链接器在重定位步骤中,合并输入模块并将运行时地址赋给输入模块定义的每个节、符号。当这一步完成时,程序中的每条指令和全局变量才拥有唯一的运行时内存地址。
7.8 可执行目标文件
可执行目标文件包括程序的入口点,包含 .init 节,这个节中定义了一个小函数 _init。程序的初始化代码会调用它。可执行文件已完全链接,即已重定位,所以不需要 rel 节。
7.9 加载可执行目标文件
Linux 程序都可以通过调用 execve 函数来调用加载器。
7.10 动态链接共享库
共享库(shared library)在运行时可以加载到任意地址,并和内存中的程序链接,称为动态链接。
// 通过 -fpic 指示编译器生成位置无关代码
// -shared 指示编译生成共享目标文件
gcc -shared -fpic -o lib.so a.c b.c
// 使用共享库,这样生成的可执行文件含所有 .interp 节。
// 这一节包含动态库的路径名
gcc -o a.out main.c ./lib.so
动态连接器本身就是一个共享目标,
Java 本地接口(Java Native Interface, JNI )通过 dlopen 接口加载C/C++ 生成的动态库。
7.12 位置无关代码
可以加载而无需重定位的代码称为位置无关代码(Position Independent Code)。
PIC数据引用
在内存中任意位置加载一个目标模块,数据段和代码段的距离总是保持不变。
在数据段开始地方创建一个表,叫做全局偏移量表(GLobal Offset Table,GOT)。每个被这个模块引用的全局数据都有一个8字节条目。
PIC函数调用
延迟绑定 (lazy binding)将过程地址绑定推迟到第一次调用该过程时。
7.13 库打桩机制
使用打桩机制,可以追踪某个库函数的调用次数,验证和追踪输入输出值,甚至可以替换成一个不同的实现。
7.14 处理目标文件的工具
- ar :创建静态库,插入,删除,列出和提取成员。
- strings : 列出一个目标文件中所有可打印的字符串。
- strip : 从目标文件中删除符号表信息。
- nm : 列出一个目标文件的符号表中定义的符号。
- size : 列出目标文件中节的名字和大小。
- readelf : 显示一个目标文件的完整结构。包含 size 和 nm 的功能。
- objdump : 所有二进制工具之母。能够显示一个目标文件中所有的信息。可以反汇编 .text 节中的指令。
- ldd : 列出一个可执行文件在运行时所需要的共享库。
7.15 小结
链接可以在编译时由静态连接器来完成,也可以在加载时和运行时由动态连接器来完成。
第 8 章 异常控制流
异常发生在计算机系统的各个层次,在硬件层,硬件检测到事件会触发控制突然转移到异常处理程序。在操作系统层,内核通过上下文切换将控制从一个用户进程转移到另一个用户进程。
8.1 异常
异常是异常控制流的一种形式,它一部分由硬件实现,一部分由操作系统实现。
异常就是控制流中的突变,用来响应处理器状态中的某些变化。
8.1.1 异常处理
系统中所有可能的每种类型的异常都分配了一个唯一的非负整数的异常号。当系统启动时,操作系统分配和初始化一张称为异常表的跳转表。使得表目 k 包含 k 的处理程序地址。
- 一些是由处理器的设计者分配的,列如: 除0异常、缺页、内存访问异常、断点、算数运算溢出。
- 其他号码是由操作系统内核的设计者分配的。列如:系统调用、来自外部 I/O 设备的信号。
8.5 信号
一个信号就是一条消息,通知进程系统中发生了一个某种类型的事件。
第 9 章 虚拟内存
虚拟内存是硬件异常、硬件地址翻译、主存、磁盘文件和内核软件的完美交互,它为每个进程提供了一个大的、一致的私有地址空间。
虚拟内存提供了三个重要能力:
- 把内存看成是磁盘的高速缓存,在内存中保存活动区域,并根据需要在内存和磁盘之间来回传送数据。
- 为每个进程提供一致的地址空间,从而简化了内存管理。
- 保护了每个进程的地址空间不被其他进程破坏。