C/C++编译技术——程序生命周期
-
编写源代码
-
编译
编译就是讲某种编程语言编写的源代码转换成另一种编程语言描述的源代码。
基本概念:
- 编译器负责编译程序。
- 编译器的输入是一个编译单元。编译单元通常是一个包含源代码的文本文件。
- 一个程序通常会包含多个编译单元。源代码包括多个文本文件。
- 编译过程的输出是一系列二进制目标文件的集合,其中每一个目标文件对应一个作为输入的编译单元。
- 若想让程序能够执行,这些目标文件还需要经过链接。
编译的各个阶段:
编译过程可以粗略地划分为:预处理阶段、语言分析阶段、汇编阶段、优化阶段和代码生成阶段。
-
预处理阶段
- 将#include关键字标示的含有定义的文件包含到源代码文件中。
- 将#define语句定义的值转换为常亮。
- 在代码中调用宏的位置将宏定义转换成代码。
- 根据#if、#elif和#endif指令的位置包含或排除特定部分的代码。
-
语言分析阶段
编译器会将C/C++代码删去注释和不必要的空格,以及从文本中提取符号等。
- 词法分析:将源代码分割成不可分割的单词。目的在于检查程序是否满足编程语言的语法规则,编译器会在检查到不满足语法规则错误的时候报告错误或发出警告,编译错误会导致编译过程中断,警告则不一定,取决于用户的设置。
- 语法分析:将提取出来的单词连接成单词序列,并根据编程语言规则验证其顺序是否合理。
- 语义分析:目的是发现符合语法规则的语句是否具有实际意义。
-
汇编阶段
当源代码经过校验,其中不包括任何语法错误后,编译器才会执行汇编阶段。在这个阶段中,编译器会将标准的语言集合转换成特定CPU指令集的语言集合。不同的CPU包含不同的功能性需求,通常包含不同的指令集、寄存器和中断。
-
优化阶段
当由源代码生成最初版本的汇编代码后,优化阶段就开始了,这可以将程序的寄存器使用率最小化。此外,通过分析能够预测出实际上不需要执行的部分代码,并将其删去。
-
代码生成阶段
最后到生成编译输出目标文件的阶段,其中每一个目标文件对应一个编译单元。汇编指令(ASCII)会在此阶段转换成对应机器指令(操作码)的二进制值,并写入目标文件的特定位置。
使用objdump工具输出AT&T(默认)和Intel两种风格汇编代码。
objdump -D <input file>.o objdump -D -M intel <input file>.o
目标文件属性
- 目标文件是通过对其对应的源代码翻译得到的。由于一个项目中有许多的源代码文件,所以编译的结果是一组目标文件。当编译完成后,后续的构建过程将基于目标文件进行。
- 符号和节是目标文件的基本组成部分,其中符号表示的是程序中的内存地址或数据内存。绝大多数的目标文件中都包含代码节(.text)、初始化数据节(.data)、未初始化数据节(.bss)以及一些特殊节。
- 构建程序的目的在于:将编译的每个独立的源代码文件生成的节拼到一个二进制可执行文件中。
- 目标中独立的节都有可能包含在最终的程序内存映射中,因此目标文件中每个节的起始地址都会被临时设置为0,等待链接时调整。
- 在将目标文件的节拼接到程序内存映射的过程中,其中唯一重要的参数是节的长度,准确地说是节的地址范围。
- 目标文件中不包含专门的节会影响堆和栈中的数据。
- 目标文件中只包含了程序.bss节的基本信息,而.bss节本身也仅仅只有字节长度信息。
编译过程的局限性
为什么编译过程不能完成目标文件的拼接任务呢?
- 拼接节的操作不是一件轻松的事情
- 程序构建过程中需要支持代码复用
-
链接
链接过程的输入是目标文件的集合,链接器的最终任务是将独立的节组合成最终的程序内存映射节,与此同时解析所有的引用。
-
重定位
链接过程的第一个阶段仅仅进行拼接,将分散在单独目标文件中不同类型的节拼接到程序内存映射节中。为了完成任务,需要将之前预留的空间,也就是节中从0开始的地址范围转换成最终程序内存映射中更具体的地址范围。
当重定位阶段完成后,绝大部分的程序内存映射就已经创建完成了。
-
解析引用
将节的地址范围线性地转换成程序内存映射地址范围。
代码片段在不同的编译单元中,他们之间尝试相互引用,但在将目标文件拼接成程序内存映射之前,无从知晓引用对象的内存地址。
链接器对应用进行解析:
- 检查拼接到程序内部映射中的节。
- 找出那些部分代码产生了外部调用。
- 计算该引用的精确地址(在内存中映射的地址)。
- 将机器指令中的伪地址替换成程序内存映射的实际地址。
-