【龙书笔记】编译器内部实现流程初探

时间:2021-08-12 19:53:15
上篇龙书笔记对编译器在程序构建中的作用做了整体的黑盒介绍,本篇笔记的目的是拆开这个盒子,对编译器内部实现流程做说明。

1. Phases of a compiler
从整体来看,编译器内部流程可以分为2大类:分析(analysis)和综合(synthesis)。
在analysis阶段,编译器将源码分解为一系列片段并为它们构建语法结构(grammatical structure),然后利用这些语法结构来创建源码的中间表示(intermediate representation)。该阶段会检查源码的语法及语义正确性,此外还会为源码建立后续会用到的符号表(symbol table)。
而synthesis阶段则是用由aynlysis阶段生成的中间表示来构造目标程序。
在专业术语上,analysis阶段被称为编译器前端(front end),而synthesis阶段被称为编译器后端(back end)
编译器内部流程的每个阶段(phase)的任务都是把一种表示(输入)转换为另一种表示(输出),一个典型的编译器内部阶段示意图如下:
【龙书笔记】编译器内部实现流程初探
几点说明如下:
1) 某些编译器在前端和后端之间引入了一个与机器无关的代码优化阶段(该阶段通常被称为middle end),是否实现了middle end与具体的编译器有关
2) 上图中,从lexical analyzer到intermediate code generator均属于front end;而machine-independent code optimizer是可选的middle end阶段;从code generator到最后的阶段均属back end
3) 在编译器具体实现中,上图中的某些步骤可能会被合并
4) 某些编译器生成的中间表示码会被精心设计以使之具备这样的接口特性:为某个特定语言设计实现的编译器前端生成的中间码能作为“接口层”供为某类特定目标机器设计实现的编译器后端读取来生成那类目标机器上运行的目标程序。
这种
接口特性带来的好处是:通过组合多个为不同语言实现的编译器前端和一个为某目标机器实现的编译器后端,我们可以构建出能编译多种源码语言的编译器,它编译完的目标程序可以在某类特定目标机器上运行;同理,通过组合一个为某种语言实现的编译器前端和多个为不同特定目标机器实现的编译器后端,我们可以构建出能编译某个源码语言的编译器,它编译完的目标程序可以在不同的目标机器上运行(需要在编译选项中指定目标机器类型)。Linux下常用的GCC编译器工具链就具备这种能力。
5) 符号表存储了源码的关键信息(比如:对于变量来说,关键属性包括变量的存储位置、变量类型及其作用域;对函数来说,关键属性包括参数个数、参数类型、传参方式及返回值类型),所以它在编译器的各个阶段均会被用到下面开始对每个阶段的细节做展开说明。

2. Lexical Analysis
该阶段被称为词法分析(lexical analysis)或扫描(scanning)。词法分析器读取源码字符流并把它们分组成被称为lexemes的有效序列,对于每个lexeme,词法分析器以<token-name, attribute-value>格式输出一个token。其中,token-name是个抽象符号,语法分析阶段会被用到;而attribute-value指向该token在符号表中对应的实体,该实体在语义分析和目标机器码生成阶段会被用到。
举个例子,假设源码中有下面一行代码:
position = initial + rate * 60
则语法分析器处理后,会输出成如下格式:
<id,1> <=> <id,2> <+> <id,3> <*> <60>
其中,每个<>表示分组后的1个lexeme;变量名(即变量标识符)被映射成<id,n>格式的lexeme;而常量和操作符由于不需要attribute-value,故它们均被映射成只有token-name且token-name即是其字符本身的lexeme。

3. Syntax Analysis
该阶段被称为语法分析(syntax analysis)或解析(parsing)。语法分析器parser使用由词法分析器生成的tokens来创建中间表示树(tree-like intermediate representation)以描述这些token流的语法结构。这个“中间表示树”常被称为语法树(syntax tree),其根节点或子树的根节点通常代表一个操作(operation),这些节点的叶子节点表示该操作对应的操作数。此外,语法树需要反映出操作优先级。
备注:这些文字描述比较抽象,文末会给出一个实例示意图来帮助理解。

4. Semantic Analysis
该阶段被称为语义分析。语义分析器借助语法树和符号表来检查源码与语言规范的语义一致性,此外,它还收集类型信息并将其存入语法树或符号表以便后续使用。
语义分析最重要的任务就是做类型检查(type checking),此外,在语言规范允许的情况下,它会根据实际情况对某些变量做隐式类型转换。

5. Intermediate Code Generation
该阶段被称为中间码生成阶段,它为源码显式生成一个low-level或machine-like的中间表示,我们可以把这个“中间表示”理解为可以在抽象机器(abstract machine,即中间表示码与具体的物理机器无关)上运行的程序。
中间表示码需满足2个重要性质:1) 易于产生;2) 易于转换成目标机器码
一种典型的中间表示码是"three-address code",它是一种类似于汇编指令的表示方法,可以查看wikipedia这里的说明来了解。

6. Code Optimization
该阶段是与机器无关的代码优化阶段,它会尝试在上一阶段生成的中间表示码基础上,生成更高效的中间表示码。

7. Code Generation
该阶段以源码的中间表示码作为输入,然后将中间表示码映射成目标语言。若目标语言是机器码,则该阶段需要为每个变量选择寄存器或内存地址,以便中间表示码中的指令被转换成可以实现等价任务的机器指令。
该阶段的一个关键任务就是为变量分配合理的寄存器。

以上就是一个典型的编译器需要实现的处理流程。
下面是本文第2节提到的那个实例在编译器各个阶段的转换示意图,它可以帮助新手加深对编译器实现流程的理解。
【龙书笔记】编译器内部实现流程初探

【参考资料】
1. <Compilers Principles, Techniques, & Tools>即龙书第1.2节
2. Wikipedia - Symbol Table 
3. Wikipedia - Compiler  

======================= EOF =======================