编译器相关

516 阅读5分钟

编译器基本原理

1.是什么(编译器是什么) 2.为什么 (为什么需要编译器) 3.怎么做 (编译器如何工作)

编译器是什么

在编译器工作之前需要进行预处理,包括宏的替换,头文件的导入,以及类似#if的处理 编译器是一种把源程序语音翻译成目标程序语言的计算机程序。一般来说,源程序是高级语言比如Java,Objective-C等。 目标程序语言一般就是汇编语言或者二进制码。

编译器一般由前端和后端组成。

前端主要进行和源语言相关,和目标语言无关的工作,包括词法分析,语法分析,语义分析,中间码生成。 后端主要进行和源语言无关,但是和目标语言有关的工作,比如中间码优化,将中间码转化成目标码,对目标代码优化生成目标程序。

编译器阶段 生成产物 功能 用途
前端 词法分析 单词流 语法高亮 语法高亮
前端 语法分析 AST(抽象语法树) 语法高亮,代码格式化,语法检查 OCLint
前端 中间码生成 中间码

为什么需要编译器

自然语言最容易表述人们的要求,当用户用自然语言表述了需要的功能后,编译器将高级编程语言转换成汇编、由汇编到机器码,提高软件开发的效率

编译器如何工作

在命令行输入clang -ccc-print-phases main.m

1: preprocessor, {0}, objective-c-cpp-output
2: compiler, {1}, ir
3: backend, {2}, assembler
4: assembler, {3}, object
5: linker, {4}, image
6: bind-arch, "x86_64", {5}, imag

1.词法分析

将源文件的字符串,进行过滤,去除空格,注释等,然后将其分割成一个个的词(记号、token) 比如 int a = b + sum(1,2); 会拆分成12个token

int     类型标识符
a       标识符
=       赋值运算符
b       标识符
+       加号
sum     标识符
(       左括号
1       整数
,       逗号
2       整数
)       右括号
;       分号

2.语法分析

词法分析之后,字符流已经被转化为token流了 int<int> ID<a> '=' ID<value> '+' ID<sum> '(' Num<1> ',' Num<2> ')' ';' 上面的int表示一个标识符类型的token。内容为'int'

接下来,解析这个token流,首先这是一个语句,我们主要有用到4种语句,赋值语句,函数调用语句,if和while语句。很明显,这是一个赋值语句。

语法结构树.pic.jpg

赋值表达式用变量名、赋值符号= 和表达式构成。

将语法结构应用到token流上,把等号两边的内容放到对应的节点上,生成语法树如下:

14972724543701.jpg

接下来对expression<b+sum(1,2)>进行解析 表达式有很多种,变量表达式,数字表达式,加减法表达式,经过对比,只有加法表达式结构才能匹配,于是将加法表达式的语法结构应用到其中。如图

14972725257900.jpg

进一步解析语法树的<b><sum(1,2)>,发现只有变量表达式和函数调用表示式才能匹配成功,得到

14972730581069.jpg

接下来进行语法优化,有些节点是多余的,比如在复制表达式中 '='和';',对语法树进行浓缩得到最终的抽象语法树(AST)

14972734592756.jpg

由此看出,语法分析就是不断将语法规则用于源程序,将源程序解析成一颗抽象语法树。之后的语义分析,中间码生成和代码优化都是基于对这棵树进行遍历,检查,修改进行的。

3.语义分析

语义分析阶段,会对语法进行多次的遍历,进行语法检查,包括类型和声明的检查,OClint就是基于语义分析进行静态代码分析的。 还是以上面的赋值语句作为例子, 需要检查 1.b, sum是否已经声明过 2.sum函数的参数数量和类型是否和传入的参数数量和类型匹配。 3.加法运算的两个操作数的类型是否匹配,这里也就是检查函数的返回值类型了 4.赋值运算的两个操作数类型是否匹配

一般在遍历语法树过程中,遇到的变量声明和函数声明时,会将变量名-类型,函数名-返回类型-参数数量-参数类型保存到符号表里,当遇到使用变量和函数调用时,根据名称在符号表猴子那个查找,检查是否声明过,类型是否匹配。因此,对于java这种函数声明可以放在使用位置后面的语言,至少需要遍历两遍语法树。

语义检查时,也会对语法树进行优化,比如将常量的表达式先计算如: a = 1 + 2 * 3; 会被优化成 a = 7;

语义分析完成后,编译期错误被排除,所有使用过的变量名和函数名被绑定到声明的地址,就可以进行代码生成和优化了。

中间码生成

一般的编译器都不会直接生成目标代码,而是先生成中间码再生成目标代码。 中间码的作用: 计算机直接生成的代码比人手写的汇编要庞大、重复很多,计算机科学家们对一些具有固定格式的中间代码(最典型的是三地址中间码)的进行大量的研究工作,提出了很多广泛应用的、效率非常高的优化算法,可以对中间代码进行优化,比直接对目标代码进行优化的效果要好很多。 通过中间代码实现前后级分离,在多系统、多语言开发时,可大幅提高整体开发效率,减少开发成本缩短开发周期。 为了增加编译器的模块化和可移植化、可扩展性。中间代码既独立于任何高级语言,又独立于机器架构,因此可以通过编写m+n个编译模块二获得m *n种编译器。