作者:张裕鹏 日期:2021-3-16
编译和链接合并到一起的过程称为构建(Build)
构建过程:预处理 -> 编译 -> 汇编 -> 链接
预处理
预编译过程主要处理那些源代码文件中的以“#”开始的预编译指令。比如“#include”、“#define”等,主要处理规则如下:
- 将所有的“#define”删除,并且展开所有的宏定义。
- 处理所有条件预编译指令,比如“#if”、“#ifdef”、“#elif”、“#else”、“#endif”。
- 处理“#include”预编译指令,将被包含的文件插入到该预编译指令的位置。注意,这个过程是递归进行的,也就是说被包含的文件可能还包含其他文件。
- 删除所有的注释“//”和“/ /”。
- 添加行号和文件名标识,以便于编译时编译器产生调试用的行号信息及用于编译时产生编译错误或警告时能够显示行号。
- 保留所有的#pragma编译器指令,因为编译器须要使用它们。
命令
gcc –E hello.c –o hello.
编译
编译过程就是把预处理完的文件进行一系列词法分析、语法分析、语义分析及优化后生产相应的汇编代码文件
编译过程一般可以分为6步:
- 扫描
- 语法分析
- 语义分析
- 源代码优化
- 代码生成
- 目标代码优化
如下图所示:
命令
gcc –S hello.c –o hello.s
//或者
gcc –S hello.i –o hello.s
汇编
汇编器是将汇编代码转变成机器可以执行的指令,每一个汇编语句几乎都对应一条机器指令。所以汇编器的汇编过程相对于编译器来讲比较简单,它没有复杂的语法,也没有语义,也不需要做指令优化,只是根据汇编指令和机器指令的对照表一一翻译就可以了
命令
gcc –c hello.c –o hello.o
//或者
as hello.s –o hello.
链接
程序设计的模块化是人们一直在追求的目标,因为当一个系统十分复杂的时候,我们不得不将一个复杂的系统逐步分割成小的系统以达到各个突破的目的。一个复杂的软件也如此,人们把每个源代码模块独立地编译,然后按照需要将它们“组装”起来,这个组装模块的过程就是链接(Linking)。链接的主要内容就是把各个模块之间相互引用的部分都处理好,使得各个模块之间能够正确地衔接。链接过程主要包括了:地址和空间分配(Address and Storage Allocation)、符号决议(Symbol Resolution)和重定位(Relocation)等这些步骤
重定位
程序并不是一写好就永远不变化的,它可能会经常被修改。比如我们在第1条指令之后、第5条指令之前插入了一条或多条指令,那么第5条指令及后面的指令的位置将会相应地往后移动,原先第一条指令的数字将需要相应地调整。在这个过程中,程序员需要人工重新计算每个子程序或跳转的目标地址。当程序修改的时候,这些位置都要重新计算,十分繁琐又耗时,并且很容易出错。这种重新计算各个目标的地址过程被叫做重定位(Relocation)。
**
符号
后来人们发现如果每次修改一段程序都需要重新计算之前已经计算好的地址是不可接收的,因此急需解决这个问题,这个时候“符号”就诞生了。
符号(Symbol)这个概念随着汇编语言的普及迅速被使用,它用来表示一个地址,这个地址可能是一段子程序(后来发展成函数)的起始地址,也可以是一个变量的起始地址。
比如前面程序中,我们把刚开始第5条指令开始的子程序命名为“foo”,那么第一条指令的汇编就是:
jmp foo
当然人们可以使用这种符号命名子程序或跳转目标以后,不管这个“foo”之前插入或减少了多少条指令导致“foo”目标地址发生了什么变化,汇编器在每次汇编程序的时候会重新计算“foo”这个符号的地址,然后把所有引用到“foo”的指令修正到这个正确的地址。
常用工具
预编译编译程序cc1、汇编器as、链接器ld。