编译原理:引论(一):语言处理器、编译器的结构

本文为《编译原理》(龙书) 的读书笔记 (1.1~1.3)

语言处理器

编译器、解释器

编译器

  • 简单地说,一个编译器就是一个程序,它可以把源语言编写的程序翻译为一个等价的、用目标语言编写的程序。如果目标程序是一个可执行的机器语言程序,那么它就可以被用户调用,处理输入并产生输出
    • 如果要翻译的源语言是汇编语言,目标语言是机器语言,则翻译程序称为“汇编程序
    • 如果要翻译的源语言是高级语言,目标语言是机器/汇编语言,则翻译程序称为“编译程序
  • 源程序是在编译阶段处理,而数据则是在程序的运行阶段处理
    在这里插入图片描述

解释器(interpreter)

  • 解释器是另一种常见的语言处理器。它并不通过翻译的方式生成目标程序。解释器直接利用用户提供的输入执行源程序中指定的操作。它按源程序中语句的动态顺序,逐句的进行分析解释,并立刻予以执行(编译+运行)
    • 解释器和编译器的根本区别是 解释器最终不生成目标程序

在这里插入图片描述


编译器 VS 解释器

  • 在把用户输入映射成为输出的过程中,由一个编译器产生的机器语言目标程序通常比一个解释器快很多
  • 然而,解释器的错误诊断效果通常比编译器更好,因为它逐个语句地执行源程序,而且解释程序容易实现与用户的交互会话

混合编译器

例1.1

  • Java语言处理器结合了编译和解释过程,如图1-4所示
    • 一个Java源程序首先被编译成一个称为字节码(bytecode)的中间表示形式。然后由一个虚拟机对得到的字节码加以解释执行。这样安排的好处之一是在一台机器上编译得到的字节码可以在另一源程序台机器上解释执行。通过网络就可以完成机器之间的迁移
    • 为了更快地完成输入到输出的处理,有些被称为即时(just in time)编译器的Java编译器在运行中间程序处理输入的前一刻首先把字节码翻译成为机器语言,然后再执行程序

在这里插入图片描述

语言处理系统

  • 如图1-5所示,除了编译器之外,创建一个可执行的目标程序还需要一些其他程序:
    在这里插入图片描述
  1. 一个源程序可能被分割成为多个模块,并存放于独立的文件中。把源程序聚合在一起的任务有时会由一个被称为 预处理器 (preprocessor) 的程序独立完成。预处理器程序还负责把那些称为宏的缩写形式转换为源语言的语句
  2. 然后,将经过预处理的源程序作为输入传递给一个编译器。编译器可能产生一个汇编语言程序作为其输出,因为汇编语言比较容易输出和调试
  3. 接着, 这个汇编语言程序由称为 汇编器 (assembler) 的程序进行处理,井生成可重定位的机器代码
  4. 大型程序经常被分成多个部分进行编译,因此,可重定位的机器代码有必要和其他可重定位的目标文件以及库文件连接到一起,形成真正在机器上运行的代码。一个文件中的代码可能指向另一个文件中的位置,而 链接器 (linker) 能够解决外部内存地址的问题。最后,加载器 (loader) 把所有的可执行目标文件放到内存中执行

编译器程序的组成

编译分为前后两个部分:分析部分和综合部分

  • 分析 (analysis) 部分
    • 词法分析语法分析语义分析
      • 分析部分把源程序分解成为多个组成要素,并在这些要素之上加上语法结构。然后,它使用这个结构来创建该源程序的一个中间表示
    • 错误处理
      • 如果分析部分检查出源程序没有按照正确的语法构成,或者语义上不一致,它就必须提供有用的信息,使得用户可以按此进行改正
    • 符号表 (symbol table)
      • 分析部分还会收集有关源程序的信息,并把信息存放在符号表中。符号表将和中间表示形式一起传送给综合部分
  • 综合 (synthesis) 部分根据 中间表示符号表 中的信息来构造用户期待的目标程序
    • 综合部分包括 中间代码生成代码优化目标代码生成

在这里插入图片描述


  • 如果我们更加详细地研究编译过程,会发现它顺序执行了一组步骤(phase)。每个步骤把源程序的一种表示方式转换成另一种表示方式。一个典型的把编译程序分解成为多个步骤的方式如下图所示:
    • 在实践中,多个步骤可能被组合在一起,而这些组合在一起的步骤之间的中间表示不需要被明确地构造出来。存放整个源程序的信息的符号表可由编译器的各个步骤使用
    • 有些编译器在前端和后端之间有一个与机器无关的优化步骤。这个优化步骤的目的是在中间表示之上进行转换,以便后端程序能够生成更好的目标程序。因为优化是可选的,所以下图所示的 中间代码生成、代码优化环节 可以省略

在这里插入图片描述

词法分析

  • 编译器的第一个步骤称为 词法分析 (lexical analysis) 或 扫描 (scanning)
  • 词法分析器读入组成源程序的字符流.并且将它们组织成为有意义的 词素 (lexeme) 的序列。对于每个词素,词法分析器产生如下形式的 词法单元 (token) 作为输出:
    < t o k e n − n a m e , a t t r i b u t e − v a l u e > <token-name, attribute-value> <tokenname,attributevalue>
    • t o k e n − n a m e token-name tokenname 是一个由语法分析步骤使用的抽象符号
      • 常常把 t o k e n − n a m e token-name tokenname 称为 终 结 符 号 终结符号 ,因为它们在描述程序设计语言的文法中是以终结符号的形式出现的;例如,标识符、分界符、数、if、while …
    • a t t r i b u t e − v a l u e attribute-value attributevalue 指向符号表中关于这个词法单元的条目。符号表条目的信息会被语义分析和代码生成步骤使用

比如,假设一个源程序包含如下的赋值语句

position = initial + rate * 60 

这个赋值语句中的字符可以组合成如下词素,并映射成为如下词法单元:

  • position是一个词素,被映射成词法单元 < i d , 1 > <id, 1> <id,1>,其中 i d id id 是表示标识符 (identifier) 的抽象符号,而 1 1 1 指向符号表中 position 对应的条目。一个标识符对应的符号表条目存放该标识符有关的信息,比如它的名字和类型
  • 赋值符号=是一个词素,被映射成词法单元 < = > <=> <=>。因为这个词法单元不需要属性值,所以我们省略了第二个分量。也可以使用 a s s i g n assign assign 这样的抽象符号作为词法单元的名字,但是为了标记上的方便,我们选择使用词素本身作为抽象符号的名字
  • initial是一个词素,被映射成词法单元 < i d , 2 > <id, 2> <id,2>,其中 2 2 2 指向initial对应的符号表条目
  • +是一个词素,被映射成词法单元 < + > <+> <+>
  • rate是一个词素,被映射成词法单元 < i d , 3 > <id, 3> <id,3>
  • *是一个词素,被映射成词法单元 < ∗ > <*> <>
  • 60是一个词素,被映射成词法单元 < 60 > <60> <60>
    • 从技术上讲,我们应该为语法单元60 建立一个形如 < n u m b e r , 4 > <number,4> <number,4>的词法单元,其中 4 4 4 指向符号表中对应于整数 60 的条目
  • 分隔词素的空格会被词法分析器忽略掉

经过词法分析之后,赋值语句被表示成如下的词法单元序列:
< i d , 1 > < = > < i d , 2 > < + > < i d , 3 > < ∗ > < 60 > <id,1><=><id,2><+><id,3><*><60> <id,1><=><id,2><+><id,3><><60>


  • 再例如,对下面的 for 语句进行词法分析,识别出此法单元:
    在这里插入图片描述

语法分析

  • 编译器的第2个步骤称为 语法分析 (syntax analysis) 或 解析 (parsing)
  • 语法分析在词法分析的基础上,接受一个终结符号串作为输入 (这些终结符号串为词法单元的第一个分量),找出从文法的开始符号推导出这个串的方法。如果不能从文法的开始符号推导得到该终结符号串,则报告该终结符号串中包含的语法错误
    • 单词符号 转化为 语法单元,并确定整个输入串是否同构成语法上正确的程序。例如:算术表达式、赋值表达式
    • 赋值语句的语法结构为: 赋 值 语 句 → 标 识 符 : = 表 达 式 赋值语句 \rightarrow 标识符 := 表达式 :=
    • 表达式的语法结构为: 表 达 式 → 表 达 式 + 表 达 式 ∣ 表 达 式 − 表 达 式 ∣ 标 识 符 ∣ 常 数 \begin{aligned}表达式 \rightarrow& 表达式 + 表达式\\&|表达式-表达式\\&|标识符\\&|常数\end{aligned} +
      在这里插入图片描述
  • 语法分析器使用由词法分析器生成的各个词法单元的第一个分址(词性)来创建树形的中间表示。该中间表示给出了词法分析产生的词法单元流的语法结构。一个常用的表示方法是语法树(syntax tree) , 树中的每个内部结点表示一个运算,而该结点的子结点表示该运算的分量
    • 之前例子中的词法单元序列在经过语法分析之后被识别为赋值语句,识别过程相当于建立一棵语法树
      在这里插入图片描述
      这棵树显示了赋值语句中各个运算的执行顺序。这棵树的根结点的标号为=,它表明我们必须把相加的结果存储到标识符position 对应的位置上去

单词的词性在语法分析时使用,值在语义分析中使用

语义分析

  • 语义分析器 (semantic analyzer) 使用 语法树符号表 中的信息来检查源程序是否和语言定义的语义一致
    • 语义分析主要能识别的语义错误有:变量未声明或重复声明、运算对象类型不匹配…
  • 它同时也收集类型信息,并把这些信息存放在语法树或符号表中,以便在随后的中间代码生成过程中使用
    • 语义分析的一个重要部分是类型检查(type checking)。编译器检查每个运算符是否具有匹配的运算分量。比如,用一个浮点数作为数组下标,编译器就必须报告错误
    • 必要时还会进行 自动类型转换 (coercion)

在这里插入图片描述

  • 上图中显示了一个这样的自动类型转换。假设positioninitialrate已被声明为浮点数类型,而词素60本身形成一个整数.。在这种情况下,这个整数可以被转换成为一个浮点数

请注意,上图中,语义分析器输出中有一个关于运算符inttofloat的额外结点。inttofloat明确地把它的整数参数转换为一个浮点数

中间代码生成

语法树是一种中间表示形式,它们通常在语法分析和语义分析中使用。在源程序的语法分析和语义分析完成之后,很多编译器将语法分析所识别出的各类语法范畴初步翻译为一个明确的低级的或类机器语言的中间表示(独立于具体机器的代码)

该中间表示应该具有两个重要的性质:

  • 易于生成
  • 能够被轻松地翻译为目标机器上的语言

我们将考虑一种称为三地址代码(three-address code)的中间表示形式。这种中间表示由一组类似于汇编语言的指令组成,每个指令具有三个运算分量。每个运算分量都像一个寄存器

在这里插入图片描述
很多编译程序都采用了三地址代码的“四元式”中间代码,这种四元式的形式为:
( 运 算 符 , 运 算 对 象 1 , 运 算 对 象 2 , 结 果 ) (运算符,运算对象1,运算对象2,结果) (,1,2,)

例如 t 1 = a + b t_1=a+b t1=a+b 可以表示为 ( + , a , b , t 1 ) (+,a,b,t_1) (+,a,b,t1) ( t 1 t_1 t1 为编译程序生成的临时名字)
在这里插入图片描述

关于三地址指令,有几点是值得专门指出的:

  • 每个三地址赋值指令的右部最多只有一个运算符
  • 编译器应该生成一个临时名字以存放一个三地址指令计算得到的值
  • 有些三地址指令的运算分量的少于三个

代码优化

主要理论基础:数据流方程

机器无关的代码优化步骤试图改进中间代码,以便生成更好的目标代码。在局部范围内可能做的优化有常数表达式的计算或根据操作符的某些性质如结合律、交换律、分配律以及检测公共子表达式进行优化

”更好“ 通常意味着更快,但是也可能会有其他目标,如更短的或能耗更低的目标代码

在这里插入图片描述

优化器可以得出结论:把60从整数转换为浮点数的运算可以在编译时刻一劳永逸地完成。因此,用浮点数60.0来替代整数60就可以消除相应的inttoftoat运算。而且, t3 仅被使用一次,用来把它的值传递给id1。因此,优化器可以把中间代码转换为更短的指令序列

不同的编译器所做的代码优化工作量相差很大。有些简单的优化方法可以极大地提高目标程序的运行效率而不会过多降低编译的速度

代码生成

代码生成器以源程序的中间表示形式作为输入,并把它映射到目标语言

如果目标语言是机器代码,那么就必须为程序使用的每个变量选择寄存器或内存位置。然后,中间指令被翻译成为能够完成相同任务的机器指令序列。代码生成的一个至关重要的方面是合理分配寄存器以存放变量的值

在这里插入图片描述

  • 每个指令的第一个运算分量指定了一个目标地址
  • 各个指令中的F告诉我们它处理的是浮点数
  • #表示60.0应该作为一个立即数处理

上面对代码生成的讨论忽略了对源程序中的标识符进行存储分配的重要问题。我们将在第7章中看到,运行时刻的存储组织方法依赖于被编译的语言。编译器在中间代码生成或代码生成阶段做出有关存储分配的决定

符号表管理

  • 编译器的重要功能之一是记录源程序中使用的变量的名字(标识符),并收集和每个名字的各种属性有关的信息。这些属性可以提供一个名字的存储分配、它的类型、作用域等信息
    • 对于过程名字,这些信息还包括:它的参数数量和类型、每个参数的传递方法(比如传值或传引用)以及返回类型
  • 标识符的各种属性是在编译的不同阶段填入符号表的
    • 词法分析只能分析出标识符名
    • 语法分析只能判断标识符在语句中出现是否合法
    • 语义分析将标识符的各种属性填入符号表,以供中间代码生成时使用
  • 符号表数据结构为每个变量名字创建了一个记录条目。记录的字段就是名字的各个属性。这个数据结构应该允许编译器迅速查找到每个名字的记录令并向记录中快速存放和获取记录中的数据

错误处理

编译的各个阶段都可能发现源程序中的错误,任何时刻发现错误都应报告错误信息。包括错误出现的位置及错误性质:

  • 词法分析可以检测出源程序中的非法符号
  • 语法分析能发现程序语句中的语法错误,如括号不匹配…
  • 语义分析能判断运算对象的类型是否匹配、变量是否重复声明或者未声明就使用、作用域错误…

将多个步骤组合成趟

前面关于步骤的讨论讲的是一个编译器的逻辑组织方式。在一个特定的实现中,多个步骤的活动可以被组合成一趟/遍(pass)。一趟就是指编译程序将源代码或中间形式从头到尾扫描一遍,并做相关处理,生成新的中间形式或目标代码

一趟扫描中,可完成编译步骤中的一个或几个。采用不同的分趟方式,编译程序的结构也有所不同。比如,前端步骤中的词法分析、语法分析、语义分析,以及中间代码生成可以被组合在一起成为一趟。代码优化可以作为一个可选的趟。然后可以有一个为特定目标机生成代码的后端趟

单遍编译程序

单遍编译程序只对源程序进行一遍扫描,就完成编译的各项任务,产生目标代码。在单遍编译程序中,往往以语法分析程序为中心,词法分析和语义分析作为语法分析的子程序。其工作过程如下:

  1. 当语法分析需要读进一个新单词时,就调用词法分析子程序。词法分析子程序则从源程序中依次读入字符,组合成单词符号,并将单词符号返回给语法分析程序
  2. 当语法分析程序识别出一个语法成分时,就调用语义分析子程序进行语义分析,并生成目标程序
  3. 当源程序处理完后,进行善后处理,优化目标程序

在这里插入图片描述

多遍编译程序

多遍编译程序的工作过程如下:

  1. 调用词法分析程序将高级语言源程序转换成用单词符号表示的程序,即将字符串程序转换成单词符号串源程序
  2. 调用语法分析程序对单词串源程序进行语法归类检查
  3. 调语义分析程序进行语义检查,并生成中间的代码程序
  4. 调用代码优化程序对中间代码程序进行优化
  5. 调用目标生成程序将优化后的中间代码程序转换成目标代码程序

编译程序分遍的优缺点

编译程序是否分遍、如何分遍,要根据计算机内存大小、源程序语言的复杂性和目标程序的质量要求而定

编译程序分为多遍,其优点是:

  • 可以减少内存容量的需求
  • 可使各遍的编译程序相互独立,结构清晰
  • 能够进行充分的优化,产生高质量的目标程序
  • 可将编译程序分为“前端”和“后端”,有利于编译程序的移植

编译程序分为多遍,其缺点是:

  • 每遍都要读符号、送符号,增加了许多重复性工作,降低编译效率,比单遍编译程序更慢

“端”的概念

  • 前端主要与源语言有关,包括词法分析、语法分析、语义分析和中间代码生成、符号表的建立以及相应的错误处理和符号表操作
  • 后端主要与目标机器有关,包括代码优化、目标代码生成以及相应的错误处理和符号表操作

把编译程序分为前端和后端的优点是便于移植

  • 我们可以把不同的前端和某个目标机的后端结合起来,为不同的源语言建立该目标机上的编译器
    在这里插入图片描述

  • 类似地,我们可以把一个前端和不同的目标机后端结合,建立针对不同目标机的编译器
    在这里插入图片描述

从理论上讲,可以设计一个通用的抽象机,各种语言的源程序都可以翻译成这个抽象机的指令序列,而这个抽象机可以在各种特定的计算机上实现。

若需在 n n n 种机器上实现 m m m 种语言的编译程序,不是非得写 m ∗ n m*n mn 个编译程序不可,而只需写 m m m 个前端(将 m m m 种语言的源程序映射成抽象机代码)和 n n n 个后端(将抽象机代码分别映射到不同的机器的目标指令),如图所示:

在这里插入图片描述

编译器构造工具

一些常用的编译器构造工具包括:

  • 语法分析器的生成器:可以根据一个程序设计语言的语法描述自动生成语法分析器
  • 扫描器的生成器:可以根据一个语言的语法单元的正则表达式描述生成词法分析器
  • 语法制导的翻译引擎:可以生成一组用千遍历分析树井生成中间代码的例程
  • 代码生成器的生成器:依据一组关于如何把中间语言的每个运算翻译成为目标机上的机器语言的规则,生成一个代码生成器
  • 数据流分析引擎:可以帮助收集数据流信息,即程序中的值如何从程序的一个部分传递到另一部分。数据流分析是代码优化的一个重要部分
  • 编译器构造工具集:提供了可用于构造编译器的不同阶段的例程的完整集合
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值