1.词法分析
词法分析的过程中,源代码程序被输入到了一个叫做扫描器的东西中,扫描器的任务就是进行词法分析。他应用了一种叫做有限状态机的算法把源代码分割成一个一个的记号,举例比如array[index] = (index + 4) * (2 + 3)这行代码,经过扫描就会变成如下的一个个记号:
记号 | 类型 |
array | 标识符 |
[ | 左方括号 |
index | 标识符 |
] | 右方括号 |
= | 赋值 |
( | 左圆括号 |
index | 标识符 |
+ | 加号 |
4 | 数字 |
) | 左圆括号 |
* | 乘号 |
( | 左圆括号 |
2 | 数字 |
+ | 加号 |
3 | 数字 |
) | 右圆括号 |
以上的这些记号一般有以下几类:关键字、标识符、字面量(数字、字符串等)和特殊符号。
单词类型 | 种别 | 种别码 |
关键字 | if、else、for…… | 一词一码 |
标识符 | 变量名、数组名…… | 多词一码 |
常量 | 整型、浮点型、字符…… | 一型一码 |
运算符 | 算术(+ - * / %)、关系(> < =)、逻辑(& | ~) | 一词一码 |
界限符 | ; ( ) [ ] { } | 一词一码 |
在识别这些标志的同时,扫描器也同时把标识符存放到了符号表,将数字、字符串常量存放到文字表,以备后续步骤使用。对于C语言的预处理,他的宏替换和文件包含等工作不交给编译器范围而是交给独立的预处理器处理。
2.语法分析
语法分析则由分析器去扫描扫描器产生的那些记号去进行语法分析,产生语法树,整个过程采用了上下文无关语法的分析手段,由语法分析树生成的树是以表达式为节点的树,如下所示:
这个数的左分支是array[index]右分支是(index + 4) * (2 + 3)左分支和右分支又可以再拆开,形成语法树
通过语法树我们可以看到很多运算符号的优先级和含义也被确定了下来。对于有多重含义的符号,比如*可以做乘法,也可以作为指针,语法分析阶段,要去确定他们的含义来进行区分,出现了不合法的表示方式,则会抛出语法错误。
3.语义分析
语义分析由语义分析器来完成,语法分析器仅完成了语法的对错,他并不去关心代码实现的含义,在C语言中两个指针相乘是没有意义的,但是在语法层面确是合法的。编译器能分析的语义是静态语义,即编译期可以确定的语义,相反,动态语义则是在运行的时候才确定的语义。
静态语义包括声明类型和类型的匹配、转换。比如当一个浮点数赋值给整型的时候,隐藏了一个过程,就是浮点型到整型转换的过程。但是将浮点数赋值给指针的时候,在语法层面可以,但是语义阶段,会发现,类型不匹配则会报错。
经过语义分析后,整个语法数表达式都被标志了类型,如下:
该表达式中基本所有的类型都是整型,并不需要做类型转换,有些需要做转换的会在语法树上插入转换节点
4.z中间语言生成
目前的编译器都会有很多优化,在源代码中就会有一些优化过程,比如以上的(2+6)就会在编译的时候进行优化,优化后就直接变成了一个数字5
其实直接在语法树上边做优化比较困难,所以源代码优化器往往把整个语法树转换成中间代码,跟目标机器和运行环境无关,他不包含数据的尺寸、变量地址和寄存器的名字。常见的中间代码有三地址码,P-代码等。比如x = y op z,该三地址码表示将变量y和z进行op操作后赋值给x,比如x = y + z;一下是常用的三地址表示方式
指令类型 | 指令形式 |
赋值操作 | x = y op z 、 x = op y |
复制指令 | x = y |
条件跳转 | if x op y goto z |
非条件跳转 | goto z |
参数传递 | param z |
过程调用 | call p, n |
过程返回 | return x |
数组引用 | x = y[ i ] |
数组赋值 | y[ i ] = x |
地址以及指针操作 | x = &y 、x = *y 、*x = y |
我们把上述的例子的语法树翻译成三地址码如下
t1 = 2 + 3 t2 = index + 4 t3 = t1 * t2 array[index] = t3
在三地址码基础上进行优化,会把2+3的结果计算出来,得到t1 = 5然后把t1换成5。这样三地址码就变成了如下
t2 = index + 4 t2 = t2 * 8 array[index] = t2
中间代码把编译器分成了前端和后端(此前后端非彼前后端),前端负责生成与机器无挂的中间代码,后端则是将中间代码变成目标代码。这样对于一些跨平台的编译器而言,可以针对不同平台使用同一个前端,然后对应不同机器开发不同后端。
参考资料《编译原理》、《程序员的自我修养(链接、装载与库)》