跳转到内容

编译器构造/代码生成

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

代码生成

[编辑 | 编辑源代码]

编译器通常被设计为输出一个可执行程序,该程序将允许用户运行你的程序,并被处理器直接运行,而无需像解释过程那样使用中间解释器。然而,为了使你的程序能够被处理器运行,你需要将你特定编程语言中的指令转换为汇编代码,然后将其发送到汇编工具以创建目标代码,然后将目标代码与特定库链接起来以创建你的可执行代码。目前,我们只需要关心将指令转换为汇编代码。这个过程是我们将在本节中讨论的内容。你需要熟悉你想要输出的汇编语言。如果你希望你的程序在 x86 架构上运行,你需要熟悉 x86 汇编代码,等等。

代码生成发生在语义分析之后,语义分析为我们提供了足够的信息来生成更原始的具体代码。代码生成背后的基本思想是将语法树的树结构分解成一系列指令,无论指令集是什么。在这个阶段,由于我们已经完成了语义程序,我们对程序的语法和语义结构不感兴趣,而是对指令执行的顺序感兴趣。

在生成实际机器代码之前,通常会生成某种中间代码。这样做的好处是

  1. 更容易生成更抽象的代码,不必过于关注寄存器分配等问题,
  2. 可以进行独立于机器架构的优化,以及
  3. 更容易发现编译器错误。

然而,对于你的程序来说,直接输出汇编代码可能更简单,但你会失去上述优点。有关此方面的更多技术,请参见下一节。

在本章中,我们将使用三地址格式来表示中间代码。这种格式很有用,因为它类似于某些架构中的实际机器指令,更重要的是,它允许我们轻松地更改指令的执行顺序,这比像 Java 字节码这样的基于堆栈的中间代码具有巨大优势。

虽然在名称已经使用过之后重复使用它们并不是一个复杂的问题,但实际上每次需要时都分配一个新名称是有益的,因为它允许我们形成调用图并轻松优化,正如我们将在后面看到的那样。出于这个原因,我们只简要介绍了重复使用名称的方法。你可以在优化章节中找到更多关于名称分配优化的内容。

顾名思义,三地址代码由三个地址和操作码组成,操作码表示要执行的操作类型。例如,表达式 (a + b) * 3 可以转换为

temp1 := a + b; temp2 := temp1 * 3

在第一行中,temp1、a 和 b 是地址,+ 是操作码,第二行类似于第一行。与加载-存储机器不同,不需要将变量加载到寄存器中并存储回寄存器。你明白为什么三地址代码易于处理了。

选择可移植、灵活和表达能力强的指令至关重要;指令不足会使生成的代码变得复杂,需要组合多个指令才能完成一项操作,而指令过多显然会使维护变得更加艰巨。也许最好的方法是检查现有的机器代码。将接近底层机器代码的代码转换为抽象代码比将抽象代码转换为底层机器代码更直接。

表达式

[编辑 | 编辑源代码]

代数表达式可以非常直接地转换为三地址代码。这可以通过递归的方式完成,如下所示:假设两个表达式 left 和 right 带有一个操作 op-code,则结果应该是

   code for left
   code for right
   temp = place for left + place for right

控制结构

[编辑 | 编辑源代码]

生成控制结构代码的基本思想与用汇编编程相同。也就是说,例如,if 语句将被转换为使用条件跳转和无条件跳转的代码块。

汇编语言技术

[编辑 | 编辑源代码]

如果你的编译器的输出是汇编语言代码,那么理解汇编语言编程的基本技术是必要的。大多数编程语言并不容易映射到大多数汇编语言,因此在尝试编写将输出汇编代码的代码之前,可能需要理解一些技术或技能。这些技术并不打算创建高度优化的代码——你将在后面学习优化技术——而是旨在确保你充分理解编译器构造过程中数据和指令的管理方式。

管理变量

[编辑 | 编辑源代码]

许多程序使用数百个不同的变量(不包括数组)。

大多数 计算机架构 提供的寄存器少于 32 个(MIPS 架构ARM 提供将近 32 个指针寄存器;i386 仅提供约 4 个指针寄存器;PIC 微控制器 只有一个指针寄存器)。

由于我们无法将 100 个不同的变量塞入 32 个处理器寄存器中,因此必须使用内存来存储大多数变量。

我们将从将几乎所有变量存储在内存中开始。稍后我们将介绍尝试将尽可能多的变量保留在处理器寄存器中的优化技术。

你正在使用的汇编工具可能会保留助记符的名称。例如,你的汇编器可能不允许使用名为add的变量,因为它已保留用于加法指令。

在这种情况下,可能需要为你的变量标签使用前缀。一些编译器使用单个下划线,但你可以选择你喜欢的任何一个。

华夏公益教科书