龙书阅读笔记

星期四, 九月 21, 2006

1.3 THE PHASES OF A COMPILER

概念上,一个编译器操作于阶段phases,每一个阶段把源程序从一种表现形式转换为另一种。一个编译器的典型分解如图Fig 1.9所示。实践中,某些阶段可能组合在一起,如1.5所提到的,分组的阶段之间的中间表现形式不需要明确构造出来。



开始3个阶段,形成一个编译器的分析部分,在上一节中已经介绍。另外两个活动,符号表管理和错误处理,显示出和6个阶段之间的交互-词法分析、语法分析、语义分析、中间代码生成、代码优化和代码生成。口头上,我们把symbol-table manager和错误处理也叫做"阶段".

Symbol-Table Management

一个编译器的基本功能之一是记录源程序中使用的标识符,收集和每个标识符相关的各种属性信息。这些属性可能提供一个标识符的存储分配信息、它的类型、范围(在程序的哪些地方有效),以及,标识符是过程名字的情况下,象参数的数目和名字,每个参数传递的方法(例如通过引用)和返回的类型等等。

一个符号表symbol table 是记录每个标识符以及相应属性字段的数据结构。该数据结构允许我们快速寻找每一个标识符的记录,快速存储和获取该该记录中的数据。符号表在第2章和第7章讨论。

当一个源程序中的标识符被词法分析器检测到的时候,该标识符进入符号表。但是,标识符的属性通常无法在词法分析时决定。例如,一个Pascal 声明:

var position , initial , rate :real;

词法阶段看到position,initial和rate的时候,real类型还不知道。

剩下的阶段在符号表中加入标识符相关的信息,并且以各种方式使用这些信息。例如,在语义分析和中间代码生成阶段,我们需要知道标识符的类型,所以我们检查源程序有效使用他们的方式,从而我们能够生成相关的正确操作。代码生成器通常输入并使用标识符存储分配相关的详细信息。

Error Detection and Reporting

每一个阶段都可能碰到错误。
  • 但是,检测到一个错误以后,一个阶段必须处理错误,以便编译能够进行下去,允许检测到源程序中更多的错误。
    • 一碰到错误就停止的编译器没有能够提供它应该可以提供的帮助。


通常语法和语义分析阶段处理编译器能够检测到的大部分错误。词法阶段可以检测到错误其中输入中剩余的字符不能形成语言的任何token。那些token流违反语言的结构规则(语法)的错误由语法分析阶段判断。而在语义分析阶段,编译器试图检测具有正确的语法结构但是没有任何意义的操作,例如,如果我们试图把两个标识符相加,其中一个是数组的名字,而另外一个是过程的名字。

The Analysis Phases

随着翻译的进展,编译器对于源程序的内部表现形式也随之发生变化。我们通过考虑下面语句的翻译来演示这些表现形式:

position := initial + rate * 60 (1.1)

Figure 1.10 显示了该语句在每一阶段之后的表现形式:


词法阶段读取源程序中的字符,把它们分组为token流,每个token代表一个逻辑内聚的字符序列,例如一个标识符,一个关键字(if, while等等),一个标点字符,或者一个多字符操作符如:=。字符序列形成一个token成为token的lexeme.

某些token具有"词法值"。例如,发现rate这个标识符时,词法分析器不但需要生成一个toke,id,还需要把lexeme rate进入到符号表,如果还没有被加入的话。和这一次id出现相关的值指向符号表的rate条目。

这一节中,我们使用id1,id2,id3分别代表postion, initial和rate,重点突出一个标识符的内部表现形式和形成标识符的字符序列是不同的。因此,(1.1)词法分析以后的表现形式是:

id1 := id2 + id3 * 60 (1.2)


当然:=和60也应该有token,但目前先放一放

第2和第3阶段,语法和语义分析,已经介绍过。语法分析在token流之上构造一个层次结构,我们可以用Fig 1.11(a)描述。Fig 1.11(b)显示了实现这种树的一个常用的数据结构,
  • 其中一个内部节点是一个纪录,具有一个操作符相关的字段,以及两个指针字段指向左和右子节点。
  • 一个叶节点具有两个或多个字段,一个确定在叶子上的token,其它的记录该token的相关信息。
关于语言构造的附加信息可以在节点纪录中增加更多的字段来保存。




Intermediate Code Generation

语法和语义分析以后,某些编译器生成源程序的显式中间表现形式。我们可以把这种中间表达形式看作一个抽象机器的程序。这种中间表达形式应该具有两个重要属性:
  • 容易产生
  • 容易翻译为目标程序

可能具有各种形式,第8章有一种中间形式叫做"三地址代码" three-address code,类似于一种机器的汇编语言,其中每一个内存地址都可以当作一个寄存器。

三地址代码由一个指令序列组成,每一条最多可以有三个操作数。(1.1)源程序的三地址形式可能如下:

temp1 := inttoreal(60)
temp2 := id3 * temp1
temp3 := id2 + temp2
id1 := temp3 (1.3)

这种中间形式可以有几个属性。
  • 首先,每条三地址指令除了赋值外最多有一个操作符,因此,在生成这些指令的时候,编译器必须决定操作的顺序,源程序(1.1)中,乘法比加法优先
  • 其次,编译器必须生成临时的名字保存每一条指令计算得到的值。
  • 第三,某些“三地址”指令可能少于三个操作数,例如(1.3)的第一条和最后一条指令
Code Optimization

代码优化阶段试图增强中间代码,以便快速运行将被产生的机器代码。
某些优化很简单,例如一个自然的算法产生中间代码(1.3),语义分析后树表达形式中每一个操作符都需要使用一条指令,即使有更好的方法执行相同的计算。

使用两条指令

temp1 := id3 * 60.0
id1 = id2 + temp1 (1.4)

上面这种简单算法并没有问题,因为代码优化阶段能够修正。
  • 也就是说,编译器能够推断把60从整数转换为实数表现形式可以一次完全,并且可以在编译时刻完成,所以inttoreal操作可以消除。
  • 另外,temp3只使用一次,目的是把值传输给id1,因此用id1替换temp3是安全的,因此(1.3)最后一条语句是不需要的,结果产生1.4代码。

不同的编译器在执行代码优化的数量上有很大的差异。

  • 做得最多的那种,叫做“优化编译器”"optimizing compilers",编译的大部分时间都花在这个阶段。
  • 但是,某些简单的优化可以大大改善目标程序的运行时间而不会太降低编译的速度。

Code Generation

编译器的最后阶段是目标代码的生成,一般由可重定位的机器代码或汇编代码组成。程序使用的每一个变量都会选择好内存地址。然后,中间指令每一条被翻译成执行相同任务的一系列机器指令。一个关键的方面是把变量和寄存器之间的赋值。

例如,使用寄存器1和2,代码(1.4)的翻译可能如下:

MOVF id3, R2
MULF #60.0 R2
MOVF id2, R!
ADDF R2, R1
MOVF R1, id1 (1.5)

每一条指令的第一个和第二个操作数指令相应的源和目标。每条指令中的F告诉我们指令处理浮点数字。
  • 代码把地址id3的内容移到寄存器2,
  • 然后把它和实数常量60相乘。#表示60.0是一个常量。
  • 第3条指令把id2移到寄存器1,
  • 然后把它和前面计算后放在寄存器2中的值相加。
  • 最后,寄存器1的值移到id1的地址,因此代码实现Fig. 1.10中的赋值。

没有评论: