龙书阅读笔记

星期五, 九月 22, 2006

1.5 THE GROUPING OF PHASES

1.3的讨论是丛一个编译器的逻辑方面来得。实现的时候,几个阶段的活动通常分组在一起。

Front and Back Ends

通常,阶段分为一个前端front end和一个后端back end

  • 前端包括哪些阶段或者阶段的部分,主要依赖于源程序,而和目标机器尽量无关。通常包括词法和语法分析,符号表的创建,语义分析以及中间代码的生成。一定数量的代码优化也可以在前端完成。前端当然也包括这些阶段相应的错误处理。
  • 后端包括编译器中那些依赖于目标机器的部分,一般来说,这些部分不依赖于源程序,而只依赖于中间语言。包括代码优化阶段,代码生成阶段以及相应的错误处理和符号表操作。如果后端被小心设计,那么可能不必要对后端做太多的重新设计。

分开的好处
  • 可以直接取一个编译器的前端,然后重做相关的后端,那么编译器就可以把同一种源语言编译到不同的机器。
  • 也有可能编译不同的语言到相同的中间语言,为不同的前端使用一个通用的后端,从而在同一台机器上获得不同的编译器。

Passes

编译的不同阶段通常实现为一个pass遍,一遍从输入文件读取代码并且写出到输出文件中。
而实践中,编译器的各个阶段如何被组织成遍有很大的差异,因此我们从阶段而不是遍的角度来讨论更加好一点。

12章会讨论一些代表性的编译器,以及它们如何把阶段组织成遍。

通常把几个阶段组织成一遍,而这些阶段的活动在遍中相互交叉。例如,词法分析、语法分析、语义分析和中间代码生成可能组织成一遍。如果这样做得话,词法分析后的token流可能被直接翻译成中间代码:
  • 更细一点,我们可以把语法分析器考虑为正被"in charge"。
    • 它试图发现所看到的token的文法结构,在需要的时候获取token,
    • 通过调用词法分析器来寻找下一个token.
    • 当它发现文法结构的时候,解析器调用中间代码生成器执行语义分析、并生成这部分代码。
Reducing the Number of Passes

遍数的两难:
  • 我们希望遍数相对较少,因为读取和写出中间文件需要时间。
  • 另一方面,我们把几个阶段组织成一遍,我们可能被强制要求在内存中保留整个程序,因为一个阶段需要的信息和前一个阶段直接产生的信息可能具有不同顺序。
    • 程序的中间形式可能会比整个源程序和目标程序大得多,因此空间不是一件简单的事情。
不同的情况:
  • 对某些阶段而言,把它们组织成一遍不会有太多问题。例如,词法分析器和语法分析器之间的接口通常只限于一个token。
  • 另一方面,如果中间表现形式还没有完全生成,那么就很难进行代码生成。
    • 例如,某些语言如PL/1和Algol 68允许在变量声明前使用它们。我们不能生成该构造的目标代码,如果我们不知道产生这个构造的相关变量类型。
    • 类似,绝大多数语言允许用goto在代码中向前跳跃。我们不能决定一个跳跃的目标地址,如果还没有看到介于中间的源代码和为它生成的中间代码
某些情况下有可能先为缺失的信息保留好空位,然后在信息可用的时候再去填充这些空位。
特别是,中间代码和目标代码生成通常能够被合并成一遍,使用一种叫做"backpatching"的技术。

虽然在第8章中间代码生成之前,我们不能解释所有的细节,但是我们还是能够用汇编器演示backpathcing的过程。

回忆一下,我们将过两遍汇编器,第一遍发现所有代表内存位置的标识符并且推断它们的地址。第2遍用地址替换标识符。

我们可以组合各遍的动作如下:在碰到一个是向前引用的汇编语句时:

GOTO target

我们生成一个骨架指令,GOTO的机器操作码,但是地址为空白。所有地址为空白target的指令都保存在一个列表中,和符号表中的target的条目关联。当我们最后遇到一个下面这样的指令:

target: MOV foobar, R1

的时候,可以确定target的值:就是当前指令的地址,并且可以把空白填上了。
于是我们进行"backpatch",通过遍历target对应的列表,所有的指令都需要它的地址,把地址中空白的位替换为当前的地址。这种方法比较容易实现,如果指令保存在内存中直至所有的目标地址都计算完毕。

这种方法对那些能够把所有的输出保存在内存中的汇编器还是不错的。因为对汇编器而言,代码的中间和最终表现形式大致相同,因此具有近似的长度,baclpatching 整个汇编程序的长度并不是不可行的。

但是,在一个编译器中,中间代码需要消耗大量的空间,我们可能需要小心backpatching发生的距离。

没有评论: