程序: 从源代码到可执行的二进制
1.编译阶段
预处理阶段
- 将#include关键字包含的定义文件的代码包含到源文件代码
- 将#define指定的值转换为常量,将代码中的宏替换为实际代码
- 根据#if、#elif和#endif的位置,删除特定代码
语言分析阶段
- 词法分析:将源代码分割成不可分割的单词(删除注释以及不必要的空格,从文本中提取符号)。
- 语法分析:将提取出来的代词链接成代词序列,并根据编程语言规则验证其顺序是否合理
- 语义分析:发现符合语法规则的语句是否具有实际意义。比如将两个整数相加并赋给一个对象(满足词法和语法),要检查这个对象是否重载了赋值操作符(是否满足语义)。
汇编阶段
将标准的语言集合转换成特定CPU指令的集合,不同的CPU包含不同的功能性需求,通常会包含不同的指令集、寄存器和中断,不同的CPU可能要求不同的编译器对其支持;
gcc 编译器的汇编阶段:
将输入源代码文件转换为对应的ASCII编码的文本文件,该文本文件其中包含了特定芯片或者操作系统对应汇编指令集的代码,x86处理器,编译器支持AT&T格式和Intel格式:
gcc -S -masm=att in.c -o in.s -----这是AT&T汇编格式
gcc -S -masm=intel in.c -o in.s -----这是Intel汇编格式
优化阶段
将程序寄存器使用率最小化,分析预测实际上不需要执行的部分代码,并将其删除。
代码生产阶段
生产目标文件的阶段,每一个编译单元(源文件,如.c文件)对应生成一个目标文件(.o文件)。由上一阶段生成的汇编指令(可读的ASCII码)会在此阶段转换成对应机器指令的二进制值,并写入到目标文件的特定位置。
目标文件包含各种各样的节(section):代码、初始化数据、非初始化数据、调试信息等。
至此,编译阶段结束。
编译阶段是分别对每一个编译单元进行转换,如果一个编译单元引用了定义在其他编译单元的函数或者全局变量,这些函数或者变量的实际地址无法确定,对于此编译单元,这些函数或者全局变量称为未解析引用。
2.链接阶段
链接器的任务是将独立的节组合成最终的程序内存映射节,与此同时,解析所有的引用。由于虚拟内存的概念,允许链接器将程序内存映射假想成从0地址开始的地址范围,每个程序的地址范围都相同,而不需要考虑操作系统运行时为进程分配了多少地址范围。
链接阶段包括:重定位和解引用
重定位
这个阶段仅仅进行拼接,将分散在单独目标文件中不同类型的节拼接到程序内存映射对应的节中。对于每一个目标文件,它的节是从0地址开始,拼接到程序内存映射中相应的位置。虚拟内存机制使得每个程序都拥有相同的、一致的且简单的程序地址空间视图,地址范围从0到2^N,但是程序执行时的实际物理地址是由操作系统在运行时决定的,这对程序和程序员来说是透明的。
重定位以后,绝大部分程序的内存映射已经完成。
解析引用
如果只有一个目标文件,程序内存映射中不存在未解析的引用。但实际上多个编译单元之间存在相互引用,经过重定位之后,这些引用仍然是未解析的。链接器的解析引用主要完成:
- 检查拼接到程序内存映射中的节
- 找出那部分代码产生了外部调用
- 计算该引用在内存映射中的精确地址
- 将机器指令中的伪地址替换为内存映射中实际地址