目录
1 预编译
2 编译
2.1 扫描(词法分析)
2.2 语法分析
2.3 语义分析
2.4 中间语言生成
2.5 目标代码生成与优化
3 汇编
4 链接
一个程序从编辑完成到执行一共包括四个部分:预编译、编译、汇编、链接,那么每一个部分具体做了哪些工作呢?接下来将对这四个部分进行详细的介绍。
预编译是程序声明周期的第一个环节,在预编译阶段,源代码文件(.c)和相关的头文件会被预编译器预编译成一个.i文件(.cpp文件会被预编译成.ii文件),Linux下的预编译命令为:
gcc -E hello.c -o hello.i
预编译过程主要是处理以“#”开头的预编译指令,如“# include”和"# define"等:具体工作如下所示:
当生成.i文件之后,可以直接查看.i文件,会发现.i文件里面除了当初我们自己些的程序,还有许多外部的文件被包含进去了,最简单的hello.c文件,通过查看点.i文件,会发现,main函数里面的内容,出现在了.i文件的最末端,前面的都是stdio.h文件以及stdio.h所包含文件的内容。
编译过程就是把预编译完的文件进行一系列的词法分析、语法分析、语义分析以及优化后产生相应的汇编代码文件,编译过程是程序的整个声明周期的核心部分。可以使用如下Linux指令完成程序的编译过程。
gcc -S hello.i -o hello.s
虽然编译的指令很简单,但是编译过程是一个很复杂的过程。编译过程一般可以分为6部:扫描、语法分析、语义分析、源代码优化、目标代码代码生成和优化。
首先,源代码会被输入到扫描器,进行简单的词法分析,运用一种类似于有限状态机(学过《形式语言与自动机》应该很了解)的算法可以将源码分解成一系列的记号。产生的记号一般可以分为如下几类:关键字、标识符、字面值和特殊符号(如加号、减号)。
例如:如下代码的扫描
str[index] = str[index-1]+4
扫描结果:
在识别记号的同时,扫描器也完成了其他工作。比如将标识符放到符号表、将数字、字符串常量存放到文字表等,以备后面的步骤使用。
接下来语法分析器将扫描器产生的记号进行语法分析,从而产生语法树。语法树是通过按照上下文无关语法进行推导所形成的树。下面看一个语法树的例子。
array[index]=(index+4)*(2+6)
上述表达式产生的记号表后,进行语法分析产生的语法树如下图所示:
从图中可以看出,整个语句被看成是一个赋值表达式;赋值表达式左边是一个数组表达式,右边是一个乘法表达式;数组表达式又由连个符号的表达式来组成。在语法分析的同时,很多运算符号的优先级也被确定下来了。另外有些符号具有多重含义,比如星号*在c语言中可以表示乘法,也可以表示对指针取内容,所以语法分析阶段必须对这些内容进行区分。如果出现了表达式不合法,比如各种括号不匹配,表达式缺少操作符等等,编译器就会报告语法分析阶段的错误。
语义分析由语义分析器来完成。语法分析仅仅是对表达式的语法层面的分析,但是它并不了解这个语句是否真正有意义。比如在C语言里面两个指针做乘法运算是没有意义的,但是这个语句在语法上是合法的。编译器所能分析的语义是静态语义,即在编译器可以确定地语义,与之对应地是动态语义,即在运行期才能确定的语义。
静态语义通常包括声明和类型的匹配,类型的转换。比如当一个浮点型的表达式赋值给一个整型的表达式时,其中隐含了一个浮点型到整型的转换过程,语义分析需要完成这个步骤。比如当一个浮点型赋值给一个指针的时候,语义分析器会发现这个类型不匹配,编译器就会报错。
动态语义一般指在运行期出现的语义的相关问题,比如将0作为除数是一个运行期的语义错误。
经过语义分析阶段以后,所有的表达式都被标识了类型,如果有些类型需要做隐式转换,语义分析程序就会在语法树中插入相应的转换节点。上面的语法树经过语义分析后成为如下图所示的形式:
同时,语义分析器还对符号表里面的符号类型做了相应的更新。
在这一部分,源码级优化器会在源代码级别进行优化,在上述的语法树中,(2+6)这个表达式是可以被优化掉的,因为它的值在编译期就可以确定下来。类似的还有很多复杂的优化过程。由于直接在语法树上做优化比较困难,所以源码级优化器往往会将整个语法树转化为中间代码,它是语法树的顺序表示,已经很接近目标代码了,但它一般跟目标机器和运行时的环境是无关的,比如它不包含数据的尺寸。变量地址和寄存器的名字等。
中间代码有很多类型,比较常见的有三地址码和P-代码。拿三地址码来说,它是这样子的:
x = y op z
在上述例子中,语法树可以被翻译成如下所示的三地址码:
t1 = 2 + 6
t2 = index + 4
t3 = t1 * t2
arry[index] = t3
这里有三个临时变量:t1、t2和t3。在对三地址码进行优化时,优化器程序会将2+6的结果计算出来,得到t1=8,然后将后面表达式中所有的t1都替换成8,还可以省掉临时变量t3,因为t2可以反复利用,经过优化后的三地址码如下所示:
t2=index+4
t2=t2*8
array[index] = t2*8
中间代码的生成使得编译器分为了前端和后端,前端复杂产生机器无关的中间代码,后端则将中间代码转化为机器代码(汇编代码)。
代码生成器会将中间代码转化为机器代码,这个过程非常依赖机器,因为不同的机器有着不同的字长、寄存器、整数数据类型和浮点数据类型等。对于上述产生的中间代码生成的机器代码可能如下所示:
最后目标代码优化器会对上述的机器代码进行优化,比如选择何时的寻址方式、使用位移来代替乘法运算、删除多余的指令等。
经过这些扫描、语法分析、语义分析、中间代码生成、代码生成和目标代码优化,源码会被编译成目标代码(机器代码)。
汇编器是将汇编代码(机器代码)转变成机器可以执行的指令,每一个汇编语句几乎都对应一条机器指令。所以汇编器的汇编过程相对于编译器的编译过程来说时非常简单的,只需要根据汇编指令和机器指令的对照表一一翻译就可以了。
汇编可以使用如下操作来完成:
gcc -c hello.s -o hello.o
//或者
gcc -c hello.c-o hello.o
链接可以通过如下linux命令来完成:
gcc -o hello hello.o
链接包含的内容比较杂,包含文档结构、可重定位文件等。可以参照如下链接的内容进行了解:
链接详细介绍