程序的生命周期
在书本第一章第二节给出了下图所示的一个hello程序在系统上运行时的不同格式。
C 语言代码最终成为机器可执行的程序,会像流水线上的产品一样接受各项处理,在第七章第一节给出了下图所示的流程:
- c程序转化成. i 文件,这一步是由预处理命令cpp完成
- 将.i 文件的内容经过编译器处理转化为汇编代码
- 汇编程序经过汇编器变为对象程序,即可重定位目标文件
- 链接器通过将可重定位文件和静态库等处理变成可执行目标文件
- 最后可执行文件加载到内存运行。
在书的第三章讲解了汇编代码的部分语法,在书的第7章讲述了可重定位文件变成可执行目标文件的过程,在第8章提出了进程的有关概念,第9章详细阐述了内存分配。后续的内容会在7-9这三章内容里展开,也就是说在这三章的内容里,程序都是以机器语言的形式进行处理。
链接
几个重要的概念
符号: 对应一个函数、一个全局变量或一个静态变量
符号引用:程序中调用函数与变量
符号定义:函数与变量的定义
目标模块:一个字节序列
目标文件:以文件形式存放在磁盘的目标模块集合
静态链接
这一步主要完成两个工作:
- 符号解析:将符号引用和符号定义关联起来
- 重定位: 将符号定义与一个内存位置关联起来,从而在引用这些符号时可以使得引用指向该内存位置
个人觉得符号解析只是完成了符号定义的工作,真正的引用和定义想联系起来还是要靠内存位置。
包含函数与变量的二进制代码和数据存储在目标文件中,目标文件分为三类:
- 可重定位目标文件: 包含二进制代码和文件,可以在编译时与其它重定位目标文件合并起来,创建一个可执行目标文件
- 可执行目标文件: 包含二进制代码和文件,可直接复制到内存运行
- 共享目标文件: 一种特殊的可重定位目标文件 ,可以在加载或者运行时被动态加载进内存并链接。
可重定位目标文件格式详见书本7.4,这里不再赘述。
符号表
在可重定位目标模块中都有一个符号表,用来存放定义和引用的符号的信息。
- 有模块m定义并且能够被其他模块引用的全局符号,对应非静态c函数和全局变量
- 由其他模块定义并被模块m引用的全局符号
- 只能被模块m定义和引用的局部符号,对应于静态c函数和静态全局变量。
介绍了上述的定义后,一个可重定位目标文件ELF格式如下图。
下图引自小土豆博客https://wdxtub.com/csapp/thin-csapp-4/2016/04/16/
其中.bss与COMMON很像,.bss主要存放未初始化的静态变量,以及初始化为0的全局或静态变量
COMMON存放未初始化的全局变量。
符号表存放符号的位置,符号的大小,符号对应的类型(全局或者局部)。
符号解析
上面的内容只是定义了符号,并没有将符号定义与符号引用相结合起来。
在每个模块中,每个局部符号都只有一个定义,局部变量也会有本地链接器符号。对于全局符号的引用解析要麻烦的多。
麻烦:
- 当前的符号不是在当前模块定义的,编译器会生成一个链接器符号,交给链接器处理,如果链接器在输入里没有找到就会返回错误信息并终止。
- 有很多重复命名的全局变量。
这时编译器会产生一个强弱符号,函数和已经初始化的全局变量是强符号,未初始化的全局变量是弱符号。链接器根据一些规则来处理强弱符号:
- 不允许有多个同名的强符号
- 当强符号和弱符号同名时选择强符号
- 多个弱符号同名时选择弱符号
整个链接部分第一块到这里结束,我们假设链接器读取一组可重定位目标文件,并把它们链接起来,形成一个输出的可执行文件。
使用静态库链接
将所用通用与相关的目标模块打包称为一个单独的文件,随后在链接过程中只复制需要利用的那一部分模块。静态库以存档的形式存放在磁盘中,用.a后缀标识。
重定位
当链接器完成了符号解析,链接器就知道了它的输入目标模块中的代码节和数据节的确切大小,就可以开始重定位步骤了。重定位主要有以下两步
- 重定位节和符号定义 例如,data节被合并为输出可执行目标文件的.data节,内存地址赋予这个新的聚合节
- 重定位节中的符号引用 利用可重定位条目使得对每个符号的引用指向正确的内存位置。
重定位条目
代码的重定位条目放在.rel.text中,已初始化数据的重定位条目放在.rel.data中,这里再提一嘴,全局变量和静态变量靠内存位置,局部变量靠栈。
具体的格式如下图
symbol 告知要指向的符号名称
offset 引用的节偏移,比如在0xf处开始了调用sum函数了,0xf就是offset
type 如何修改新的引用,直接还是间接引用
addend 偏移调整
可执行目标文件
链接器的工作完成后,我们得到一个二进制文件,包含加载程序到内存并运行它所需的所有信息。可执行文件的格式如下所示:
可执行文件的连续的片被加载映射到连续的内存段,程序头部表表示了这种映射关系。
加载可执行目标文件
linux程序通过调用execve函数来调用加载器,加载器可以将可执行目标文件中的代码和数据从磁盘复制到内存中,然后通过入口点来运行该程序。这样的一个过程叫做加载。
在程序运行时都会有一个如下图所示的内存映像。内核是操作系统驻留在内存的部分。
动态链接
动态链接共享库
在上百个进程的运行过程中,利用静态库仍然存在内存资源的极大浪费。共享库是一个目标模块,在运行或者加载时可以加载到任意的内存地址,并和一个在内存中的程序链接起来,这个过程叫做动态链接。
整个动态链接的过程就是得到可执行文件以后,同时链接器复制一些重定位和符号表信息,这样在程序运行时链接器可以解析对动态库的引用。随后在加载时注意到了动态库的信息,动态链接器会重定位动态库中的文本数据到某个内存段,这样动态库的位置就确定了