跳转到内容

编译器构造/简介

来自维基教科书,开放的书籍,开放的世界

介绍编译器和解释器

[编辑 | 编辑源代码]

一个 **编译器** 是一种计算机程序,它实现一种编程语言规范来“翻译”程序,通常是一组构成以 *源语言* 编写的 *源代码* 的文件,将其转换为等效的机器可读指令 **(目标语言,通常具有称为目标代码的二进制形式)**。这种翻译过程称为 *编译*。我们 *编译* 源程序以创建 *编译后的程序*。编译后的程序随后可以运行(或执行)以执行原始源程序中指定的操作。

**源语言** 与机器代码相比,始终是一种更高级别的语言,使用英语单词和数学符号的混合编写,汇编语言是最低级的可编译语言(*汇编器* 是编译器的一种特殊情况,将 *汇编语言* 翻译成机器代码)。更高级别的语言在编译器/解释器中是最复杂的,不仅因为它们增加了源代码与生成的机器代码之间的抽象级别,而且因为需要更高的复杂性来形式化这些抽象结构。

**目标语言** 通常是一种低级语言,例如汇编语言,使用机器指令的加密缩写编写,在这种情况下,它还会运行汇编器来生成最终的机器代码。但有些编译器可以直接为某些实际或虚拟计算机生成机器代码,例如为 Java 虚拟机生成的字节码。

另一种常见的编译结果方法是针对 *虚拟机*。它将进行即时编译和字节码解释,并将传统的编译器和解释器分类模糊化。

例如,C 和 C++ 通常会为目标 `架构` 编译。缺点是由于存在多种类型的处理器,因此需要进行许多不同的编译。相比之下,Java 将针对 Java 虚拟机,Java 虚拟机是 `架构` 上方的一个独立层。区别在于生成的字节码,而不是真正的机器代码,带来了可移植性的可能性,但需要为每个平台提供一个 Java 虚拟机(字节码解释器)。这个字节码解释器的额外开销意味着执行速度更慢。

一个 **解释器** 是一种计算机程序,它在运行时执行源程序的翻译。它不会生成独立的可执行程序,也不会生成可包含在其他程序中的目标库。

进行大量计算或内部数据操作的程序,通常在编译形式下运行速度比解释时快。但进行大量输入/输出和很少计算或数据操作的程序,在这两种情况下可能以大致相同的速度运行。

编译器和解释器本身都是计算机程序,必须用某种 *实现语言* 编写。直到 20 世纪 70 年代初,大多数编译器都是用某种特定类型计算机的汇编语言编写的。C 和 Pascal 编译器的出现,每个编译器都用自己的源语言编写,导致更普遍地使用高级语言来编写编译器。如今,操作系统至少会为用户提供一个免费的 C 编译器,有些操作系统甚至将它包含在操作系统分发中。

编译器构造通常被认为是一项高级编程任务,而不是新手任务,主要是因为需要大量的代码(以及理解这些代码的难度),而不是任何特定编码结构的难度。对此,大多数关于编译器的书籍都有一些责备。生产编译器和教育练习之间的巨大差距加剧了这种悲观态度。

对于用其自己的源语言编写的第一个版本的编译器,你有一个 引导 问题。一旦你让一个简单的版本工作,你就可以用它来改进它本身。

注意
编译器是一个非平凡的计算机程序;当完全手动编写时,针对简单源语言的非优化编译器可能长达 3000 行以上。一些编译器编写工具可以减少这种大小,但会添加相应的依赖关系。

编译过程

在最高级别,编译被分成多个部分

  1. 词法分析(标记化)
  2. 语法分析(解析)
  3. 类型检查
  4. 代码生成

注意
你应该阅读本章的大部分内容,因为本书的其余部分将假定它作为背景信息。

任何编译器都有一些基本需求,这些需求可能比大多数程序更严格

  • 任何有效的程序都必须被正确地翻译,即不允许错误翻译。
  • 任何无效的程序都必须被拒绝,不能翻译。

不可避免地,有一些有效的程序由于其大小或复杂性(相对于可用硬件)而无法被翻译,例如由于内存大小而产生的问题。编译器还可能有一些固定大小的表,这些表限制了可以编译的内容(一些语言定义对某些表的尺寸设置了明确的下限,以确保可以编译合理大小/复杂性的程序)。

还有一些理想的需求,其中一些可能是相互排斥的

  • 错误应该用源语言或程序来报告。
  • 应该指出检测到错误的位置;如果实际错误可能发生在更早的时间,那么也应该提供一些关于可能原因的指示。
  • 编译应该很快。
  • 翻译后的程序应该很快。
  • 翻译后的程序应该很小。
  • 如果源语言有一些国家或国际标准
    • 理想情况下,应该实现整个标准。
    • 任何限制都应该记录清楚。
    • 如果已经实现了对标准的扩展
      • 这些扩展应该记录为扩展。
      • 应该有一种方法可以关闭这些扩展。

还有一些可能引起争议的需求需要考虑(参见关于 处理错误 的章节)

  • 在翻译后的程序运行时检测到的错误仍然应该相对于原始源程序进行报告,例如行号。
  • 在翻译后的程序运行时检测到的错误应该包括除以 0、内存不足、使用过大或过小的数组下标/索引、尝试使用未定义的变量、指针使用不当等。

整体结构

[编辑 | 编辑源代码]

为了便于解释,我们将编译器分为前端和后端。它们甚至不需要用相同的实现语言编写,只要它们可以通过某种中间表示有效地通信即可。

以下列表详细说明了前端和后端执行的任务。请注意,这些任务不是按照下面概述的顺序执行的,并在后面的章节中进行了更详细的讨论。

  • 前端
    • 词法分析 - 将字符转换为标记
    • 语法分析 - 检查标记的有效序列
    • 语义分析 - 检查含义
    • 一些全局/高级优化
  • 后端
    • 一些本地优化
    • 寄存器分配
    • 窥孔优化
    • 代码生成
    • 指令调度

几乎所有源语言方面的处理都由前端完成。可以为不同的高级语言使用不同的前端,以及一个共同的后端,它执行大部分优化。

几乎所有与机器相关的方面都由后端处理。可以为不同的计算机使用不同的后端,以便编译器可以为不同的计算机生成代码。

前端通常由语法分析处理控制。在必要时,语法分析代码将调用一个执行词法分析并返回下一个标记的例程。在语法分析过程中的选定点,将调用适当的语义例程,这些例程执行任何相关的语义检查和/或将信息添加到内部表示中。

下一步 - 描述一种编程语言

华夏公益教科书