编译器最基本的功能就是把高级语言编写的代码转化为机器指令(就是01
串),从这个角度来说它本质上是个转换过程。经典的编译过程主要包括:
- 词法分析(
Lexical Analysis
)
词法分析就是从输入代码中识别出各种记号(token
),例如对于C
语言我们就需要知道if
,else
等是语言的关键字,myvar
是个标识,而123myvar
不能被识别为一个标识。负责实现词法分析的模块有时也称为scanner
。
词法分析的关键当然是语言定义的规则了,比如哪些是关键词,哪些是合法的标识等,这些规则一般是通过正则表达式(RE
,Regular
)来给出,运行时从输入缓冲区中读入一部分,然后看和哪个
ExpressionRE
匹配就知道它到底是个什么token
。
下一个问题就是正则表达式的匹配过程如何实现,经典理论对此都会提到有限状态机(FSM
, Finite
)。关于
State MachineFSM
在可行性计算里一般都会有不少篇幅分析,在编译里谈到的FSM
和RE
主要是如何从输入的构成出对应的FSM
。构造的过程一般分为三个步骤,
- 根据
Thompson
构造法从RE
构造出对应的非确定性有限状态机(NFA
,Non-deterministic
);
Automata - 经过不断计算
epison
-闭包(也成为可到达集)构造出确定性有限状态机(DFA
,deterministic
);
Automata - 将
DFA
最小化,方法是增量式地合并不可区分(对于同一输入的下一个状态都一样)的两个状态。- 语法分析
语法分析的输入是一连串的token
(词法分析的输出),根据语言的语法规则不断解析最后得到一棵抽象语法树(AST
,Abstract
),负责语法分析模块通常也被叫做
Syntax TreeParser
。在词法分析中,我们经常使用正则表达式来表示语言所接受的token
的规则,类似的,在语法分析中,我们使用文法(Grammar
)来表示语言的语法规则,这也早期计算机语言设计中的研究热点(同样也是大学里学习编译时最容易让人头晕的东西)。
编译里常说的文法指的是一种上下文无关文法(Context-Free Grammar
),简单地说文法里包含终结符(terminal
,就是26个字符、数字等等)、非终结符(nonterminal
,实际是一种抽象)和产生式(production
)。上下文无关文法要求每个产生式的左边必须恰好是一个非终结符,而右边是0
个或多个终结符与非终结符的组合,最后整个文法还必须有一个起始符(某个终结符)。文法里还有些很重要的基本概念,例如推导(derivation
)、归约(reduction
)、二义性(ambiguity
)、最左推导等等。
文法中最重要的基本概念是FIRST
集和FOLLOW
集的构造。根据这两个集合就可以很容易构造出一个预测分析表,每个行的名字是一个非终结符,每个列的名字是一个终结符,如果每个表格内没有两个以上的项,那么说明是一个LL(1)
文法(Left-to-right
,
parseLeftmost-derivation
, 1-symbol
),简单地说就是向右边看一个符号就能确定下一步动作。当原文法不是
lookheadLL(1)
文法时,可以尝试通过消除左递归(Eliminate
)和提取左因子(
Left RecursionLeft Factoring
)对原文法进行变形得到等价的LL(1)
文法。
3、语义分析(Sematic Analysis
)
语义分析包括一些经典的问题。
(1)类型检查(Type Checking
),例如在语法树上a+b
看起来是没问题的,因为a
和b
都是合法的变量名,并且语法中支持变量间+
这种操作。但是可能a
是一个字符串,而b
是一个浮点数,这两者之间的+
操作就不符合语义规范了,这种问题在这个阶段都会被找出来。
(2)符号管理,最经典的问题就是如何管理变量(变量的名字,类型,变量的作用域(scope
)等),在分析代码时,符号管理肯定是被频繁的搜索,因此它通常会使用hash
来组织。
4、中间代码(IR, intermediate Representation
)生成
IR
是非常非常重要的,它被引入的初衷是提高编译器开发的效率。IR
是编译过程的一个汇聚点,在IR
之前我们通常都认为是编译的前端,而IR
之后是编译的后端,这样当编译器需要多支持一种高级语言时主要工作就是提供一个前端,而当需要移植到一种新的平台上时主要工作就是提供对应的后端。关于IR
的表示典型的有三地址码。IR
生成的过程就是将一棵抽象语法树(这是编译器前端对源代码的理解和抽象)变成一串IR
定义的代码(IR
指令种类简单,这便于优化)。这个转换过程都是比较固定的套路,例如if-else
,while/for
等基本结构如何转都是一个固定的套路。
5、编译优化
这一部分是现代编译器最核心所在,主要有两类,一类是通用的优化手段,比如死代码删除、循环不变量外提、强度削弱等,另一类就是体系结构相关的,说白了就是某种体系结构针对某类应用提供了特殊指令,例如intel
的MMX
,SSE2
等等。为支持优化工作的开展,我们首先需要能够比较方便的描述代码。最基本的当然是一条指令,但是这个太细微,于是往上抽象出基本块(Basic
),这个基本上是所有优化开展必备的工作,然后多个基本块还可以构成一个超级块(
Blockregion
)。此外,经典的方法还包括控制流分析和数据流分析,这里常用的包括d-u
链等。最后一个经典的topic
就是寄存器分配。
6、目标代码生成
这里直接和具体平台相关,这里的平台同时包括软件和硬件,例如哪种目标文件格式(ELF
, PE
),哪种平台(指令集)。不过现在编译器一般生成的是字符形式的汇编文件,所以前面一个问题基本不大,主要影响在后者。