GCC编译程序的过程可以分为以下四个步骤:
实例代码:
#include <stdio.h>
int main()
{
printf("hello\n");
return 0;
}
预编译过程主要处理那些源代码文件中以#开始的预编译指令。
gcc -E hello.c -o hello.i
编译过程就是把预处理完的文件进行一系列词法分析、语法分析、语义分析及优化后生产相应的汇编代码文件,这个过程往往是我们所说的整个程序构建的核心部分,也是最复杂的地方之一。
gcc -S hello.i -o hello.s
编译过程一般分为6步:扫描、语法分析、语义分析、源代码优化、代码生成和目标代码优化
首先源代码程序被输入到扫描器(Scanner),扫描器只是简单的进行词法分析,运用一种类似于有限状态机(Finite State Machine)的算法可以很轻松的将源代码的字符序列分割成一系列的记号(Token)。
有一个叫做lex的程序可以实现词法扫描,他会按照用户之前描述好的词法规则将输入的字符串分割成一个个记号。
语法分析器(Grammar Parser)将对由扫描器产生的记号进行语法分析,从而产生语法树(Syntax Tree)。整个分析过程采用了上下文无关语法(Context-free Grammar)的分析手段。简单地说,由语法分析器生成的语法树就是以表达式(Expression)为节点的树。
语法分析器也有一个工具叫做yacc(Yet Another Compiler Compiler)。他也像lex一样,可以根据用户给定的语法规则对输入的记号序列进行解析,从而构建出一颗语法树。对于不同的编程语言,编译器的开发者只须改变语法规则,而无需为每个编译器编写一个语法分析器,所有他又被称为“编译器编译器”。
语义分析器(Semantic Analyzer)负责语义分析,语法分析仅仅是完成了对表达式的语法层面的分析,但是他并不了解这个语句是否真正有意义。编译器所能分析的语义是静态语义(Static Semantic),所谓静态语义是指在编译器可以确定的语义,与之对应的动态语义(Dynamic Semantic)就是只有在运行期才能确定的语义。
经过语义分析阶段之后,整个语法树的表达式都被标识了类型,如果有些类型需要做隐式转换,语义分析程序会在语法树中插入相应的转换节点。
现代的编译器有着很多层次的优化,往往在源代码级别会有一个优化过程,源码级优化器(Source Code Optimizer)会在源代码级别进行优化。
直接在语法树上做优化比较困难,所有源代码优化器往往将整个语法树转换成中间代码(Intermediate Code),他是语法树的顺序表示,其实它已经非常接近目标代码了,但是它一般跟目标机器和运行时环境是无关的。
中间代码使得编译器可以被分为前端和后端,编译器前端负责产生机器无关的中间代码,编译器后端将中间代码转换成目标机器代码。这样对于一些可以跨平台的编译器而言,他们可以针对不同的平台使用同一个前端和针对不同机器平台的数个后端。
源代码优化器产生的中间代码标志这下面的过程都属于编译器后端。编译器后端主要包括代码生成器(Code Generator)和目标代码优化器(Target Code Optimizer)。
汇编器是将汇编代码转变成机器可执行的指令,每一个汇编语句几乎都对应一条机器指令。所以汇编器的汇编过程相对于编译器来说比较简单,他没有复杂的语法,也没有语义,也不需要做指令优化,只是根据汇编指令和机器指令的对照表一一翻译即可,“汇编”这个名字也来源于此。
as hello.s -o hello.o
# gcc -c hello.s -o hello.o
# gcc -c hello.c -o hello.o
链接(Linking)的主要内容就是把各个模块之间相互引用的部分都处理好,使得各个模块之间能够正确衔接。
链接过程主要包括了地址和空间分配(Address and Storage Allocation)、符号决议(Symbol Resolution)和重定位(热咯擦提欧尼)
等这些步骤。
符号决议有时候也被称为符号绑定(Symbol Binding)、名称绑定(Name Binding)、名称决议(Name Resolution)。