1. 翻译程序

机器不能理解我们用高级语言编写的代码,所以要在程序执行前将高级语言“翻译”为机器语言。这是一个将源语言程序转化为目标语言程序的过程,它依靠翻译程序来完成。

翻译程序包括:

  • 编译器:将编译型语言(C++,Go)翻译为机器语言。
  • 解释器:将解释型语言(JavaScript、Python)翻译为机器语言。

编译与解释的不同:

编译和解释都可以将高级语言翻译为机器语言,不同之处在于:

  • 编译是将源代码经过分析后生成语法树,再优化生成中间代码,最后生成机器码。编译的结果是生成一个可执行的二进制文件
  • 而解释也是将源代码经过分析后生成语法树,只不过此后它是基于语法树生成字节码,再根据字节码去执行程序。它并不会生成目标文件,更多的是一个结果

PS:JavaScript 本身是解释型语言,但是在“翻译”过程中同时有解释器和编译器(JIT)的参与。在其它文章会学习这个知识,此处不做进一步讨论。Anyway,这个系列的笔记会将重点聚焦在编译型语言上。

2. 编译器的演进

二阶段编译器(单盒模型)

早期的二阶段编译器,任务主要有两个,一是理解输入的源程序,二是将其功能映射到目标机上,据此将编译器内部划分为前端后端两个模块 —— 前端负责理解,后端负责映射。前端对源代码的理解反映在 IR (intermediate representation,中间表示)这一结构中,IR 再传递给后端进行处理。

三阶段编译器

后面出现了三阶段编译器,也就是在前后端之间增加了一个用以改进 IR 的优化器。注意:这个优化器本身是一个源到源的编译器。自此,三者的分工变为:

  • 前端:理解源程序,并将理解的结果映射到 IR 中
  • 优化器:改进 IR 的形式
  • 后端:将改进后的 IR 映射到目标机的有限资源上

3. 编译步骤一览

3.1 前端

  • 词法分析:词法分析器(扫描器)以字符流为输入,对其进行扫描和分解,产生多个 token(单词、符号),这个过程叫做 tokenize(分词化)。接着,这些 token 被归入对应的词类,最后再输出由已归类单词构成的流(形如(typeA,"str1"),(typeB,"str2"),(typeA,"str3"),(typeC,"str4")......

  • 语法分析:语法分析器(分析器)在词法分析的基础上,根据事先约定的语法规则,对已归类单词构成的流进行匹配,以语法单位的形式进行重组,构造并输出一棵语法树(syntax tree | | parsing tree | | derivation tree)。

  • 语义分析与中间代码生成:语义分析与中间代码生成器基于语义规则,对语法树进行语义分析(变量是否定义,类型是否正确)和中间代码生成(三元式、四元式等)。

3.2 优化器

  • 中间代码优化:包括分析转换(数据流分析、相关性分析)两个过程

3.3 后端

  • 指令选择:将每个 IR 操作映射为一个或多个实际的目标机操作
  • 寄存器分配:将指令选择阶段使用的虚拟寄存器映射到实际的目标机寄存器,最小化寄存器的使用
  • 指令调度:重排代码中的各个操作,最小化等待操作数所浪费的周期数