编译器实现之旅——第一章 编译器概观
编译器,近在咫尺却又远在天边。当我们写下任何非机器语言代码后,我们都需要借助编译器将这些代码变为通过计算机可运行的状态。但是,就是这样一个使用率极高的程序,我们对其却知之甚少。什么是编译器?编译器对我们的代码做了什么?又是怎么做的呢?如果你也怀有这些疑问,想要深入编译器内部一探究竟的话,那就随我一起踏上这趟编译器实现的旅程吧。
1. 什么是编译器
广义上,编译器是这样一个程序:其读入A语言代码,并输出B语言代码。如下图所示:
+-------+
A语言代码 -> | 编译器 | -> B语言代码
+-------+
仅从定义上看,A、B可以是同一种语言。也就是说,如果我们写了一个只是具有“复制粘贴”功能的程序,其也可以被称为是一个编译器。但显然,这样的编译器是无意义的。在实际中,编译器的输入一般是高级语言代码,如C语言、Python语言等,而编译器的输出一般是低级语言代码,如汇编语言、各种字节码等。汇编语言代码经由汇编语言编译器继续编译,最终产生机器语言,以供计算机执行;而字节码可由能够执行此字节码的虚拟机执行。这样,就完成了一个程序从编写到执行的过程。
2. 编译器的结构组成
编译器的内部并不是一个整体,而是由多个组件分工合作,共同完成编译功能。这些组件总体上可被分为两个部分:编译器前端和编译器后端。如下图所示:
+----------+ +----------+
A语言代码 -> | 编译器前端 | -> 中间代码 -> | 编译器后端 | -> B语言代码
+----------+ +----------+
由于我们写下的高级语言代码并不是编译器比较喜欢的形式,故编译器通过编译器前端读取、检查并重新组织源代码,使之等价变换为编译器喜欢的形式,即中间代码;一般来说,语法错误也由编译器前端负责检查。接下来,编译器后端就拿着中间代码进行进一步的检查、优化,最终生成目标代码。
事实上,编译器前后端又分别可以进一步细分为多个组件,这些组件将在我们接下来的旅程中逐一讲述。
3. 我们将要实现什么
在这次旅程的终点,我们将实现一个名为CMM(即C Minus Minus)语言的编译器,这个编译器的输出将是由我们自己设计的一套指令集中的指令所构成的指令文件。所以,我们还将实现一套虚拟机程序,以运行编译器输出的指令文件。
CMM语言是一门将C语言的语法进行缩减后得到的语言。其主要特点如下:
- 只有一种类型:int
- 支持赋值、四则运算与比较运算
- 支持if、while语句
- 支持函数
- 支持数组
- 区分全局作用域与局部作用域
接下来,就让我们深入编译器前端一探究竟吧。请看下一章:《编译器前端概观》。