编译原理简明教程(第2版)第9章
合集下载
相关主题
- 1、下载文档前请自行甄别文档内容的完整性,平台不提供额外的编辑、内容补充、找答案等附加服务。
- 2、"仅部分预览"的文档,不可在线预览部分如存在完整性等问题,可反馈申请退款(可完整预览的文档不适用该条件!)。
- 3、如文档侵犯您的权益,请联系客服反馈,我们会尽快为您处理(人工客服工作时间:9:00-18:30)。
第九章 目标代码的生成
学习目标
了解和掌握目标代码生成程序中的有关问题
了解和掌握虚拟机
了解和掌握从中间代码生成目标代码
第九章 目标代码的生成
目标代码生成是编译程序的最后一个工作阶段。它取先行 阶段所产生的源程序的中间语言或优化后的中间语言表示 作为输入,产生等价的目标代码作为输出,如下图9.1所 示。
AC OP d=>AC
即累加器AC中的内容与d中的内容进行某种运算,结果送 到累加器AC中;其内存容量足够大;提供了21条符号汇 编指令。
9.2.2 虚拟机的汇编指令
虚拟机的汇编指令如表9.1所示。
9.2.2 虚拟机的汇编指令
1.填地址指令
设有一维数组a: array[m..n]of integer; 其元素有a[m], a[m+1], … , a[i], … , a[n],一共需要n-m+1个存储单元: <a[m]>,<a[m+1]>,…,<a[i]>,…,<a[n]> 一般有存储单元: <a[i]>=<a[m]>+i-m
9.2.2 虚拟机的汇编指令
2.按真转编写程序
9.2.2 虚拟机的汇编指令
3.按假转编写程序
9.3 从中间代码生成目标代码
9.3.1 从逆波兰表示生成目标代码
9.3.2 从四元式序列生成目标代码
9.3.1 从逆波兰表生成目标代码
从表达式的逆波兰表示生成相应的目标代码的算法可给出 如下。 首先设立一个运算分量栈(栈),用来存放暂时不能处理的 运算分量的名(即工作单元地址)以及中间运算结果的名 (即存放中间结果的工作单元地址或累加器AC)。 其具体算法步骤如下。 从左到右扫描所给定的逆波兰表达式中的每个符号: 若扫描到运算分量,则将其下推入运算分量栈。 若扫描到运算符,按该运算符是几目运算,把运算分 量栈中相应个数的栈顶元素取出,生成该运算相应的目 标代码,此后栈上退去相应个数的运算分量,运算结果 存放在“AC”中,把“AC”标志入运算分量栈。 如此继续,直到整个逆波兰表达式处理完毕为止。
9.1.1 目标代码生成程序的输入、输出
编译程序的最终输出是目标代码,这在编译程序的代码生成 阶段完成,也可在语义分析阶段生成。一般地,代码生成 部分称为代码生成程序。代码生成程序的功能是为源程序 生成与之等价的目标机器代码。也就是说,其输入是由先 行端产生的源程序的中间表示,输出是目标代码。
假定在代码生成前,编译的先行端已扫描、分析和翻译源程 序,成为足够详细的中间表示,这样,中间语言中名字的 值可以表示为目标机器能够直接操作的量(位、整数、实 数、指针等),还假定必要的类型检查与插入类型转换等 已完成,不仅语法上正确,语义也是正确的,这样代码生 成阶段可以认为它的输入是正确的。
《编Βιβλιοθήκη Baidu原理简明教程》
普通高等教育“十二五”规划计算机教材
---太原理工大学 ---计算机科学与技术学院 ---冯秀芳、崔冬华、段富等
目 录
•第一章 引言 •第二章 形式语言理论基础 •第三章 自动机理论基础 •第四章 词法分析 •第五章 语法分析—自顶向下分析方法 •第六章 语法分析—自底向上分析方法 •第七章 语义分析及中间代码的生成 •第八章 代码优化 •第九章 目标代码的生成 •第十章 符号表 •第十一章 目标程序运行时的存储组织与分配 •第十二章 出错处理 •第十三章 编译程序自动生成工具简介 •第十四章 面向对象语言的编译 •第十五章 并行编译技术
9.1.2 目标代码
代码生成程序的输出的形式一般有以下三种形式: ① 能够立即执行的机器语言代码,所有地址均已定位。即 具有绝对地址的机器语言代码。 ② 待装配的机器语言模块。当需要执行时,由连接装配程 这种形式(即已定位的机器语言代码作为输出)的好处是,它 序把它们和某些运行程序连接起来,装配成可以执行的机 可以放在内存固定的地方,可以立即执行,这样对于小的 器语言代码。即可浮动的机器语言代码。 程序可以迅速编译和执行。但由于不可重定位,其灵活性 比较差。在编译过程中,通常要把整个源程序一起编译。 ③这种形式是可浮动的机器语言代码,又称相对目标代码, 汇编语言形式的代码。需要经过汇编程序汇编转换成可 而不能独立地完成源程序各程序块的编译,即使是供源程 允许子程序分别编译,在具体执行前必须确定代码运行时 执行的机器语言代码。 序调用的子程序也必须同时进行编译。 在存储器中的位置,即给代码定位,从而形成可执行代码。 这种形式比前两种更具有灵活性。它的主要优点是可以产生 这种代码比较灵活,可以分别编译以及从目标模块中调用 符号指令和利用宏机制来帮助生成代码,目前不少编译程 先前已编译好的其他程序模块。常用的编译程序大多采用 序采用这种代码形式。 这种可浮动的代码形式。
图9.1 目标代码生成过程
第九章 目标代码的生成
9.1 目标代码生成程序中的有关问题 9.2 一个计算机模型——虚拟机 9.3 从中间代码生成目标代码
9.1 目标代码生成程序中的有关问题
对于一个好的代码生成程序,要求其使所生成的目标代码 尽可能地短,并能较充分地发挥目标计算机可用资源的效 率。如充分利用计算机的寄存器或变址器,以节省访问内 存的时间,尽可能使用执行速度较快的指令,存储管理合 熟悉目标机器和它的指令系统是设计一个好的代码生成程 理,等等。 序的先决条件,但在代码生成的一般性讨论中,不能对目 标机器细节描述到足够详细的程度,以便对一个完整的语 言产生好的代码。本章不以某一台具体的计算机做背景, 只是针对一个假想的计算机模型——虚拟机给出生成目标 代码的算法。
9.1.3 寄存器分配
对于目标机器,如果有多个寄存器可供目标程序运行时使用, 在编译时应采用合理的方法分配这些寄存器。把运算数据 根据访问内存数来定义每条指令的执行代价,则对于以下的 存放在寄存器中,将会减少访问内存的次数,从而可以提 一些操作有其相应的执行代价: 高执行速度。 操作码 操作数l 操作数2 分配中不是把寄存器平均分配给各个变量使用,而是从可用 执行代价 的寄存器中分出几个,固定分配给几个简单变量使用。按 OP 寄存器 寄存器 l 照什么标准来分配呢?为此引入一个术语:指令的执行代 价,并规定访问内存一次的代价为1。 寄存器 内存单元 2 寄存器 寄存器间接地址 2
9.1.2 目标代码
本章采用汇编语言代码作为目标代码,但不针对某种特定的 目标机指令系统或汇编语言来生成目标代码,而是假设有 一台计算机,其指令系统等均按某种需要而设定,为教学 目的往往采取这种虚拟机目标代码形式。 下面就以一种虚拟机指令系统来讨论目标代码的生成。
9.1.3 寄存器分配
通常,计算机存储之间不直接打交道,而是通过寄存嚣。寄 存器可以用来保存中间计算结果,而且运算对象在寄存器 寄存器的分配策略与目标机的资源密切相关。有些机器中的 中的指令一般比运算对象在内存的指令短些,执行速度也 寄存器分为变址器和数据寄存器,还有些机器的寄存器可 快些,因此,充分利用寄存器对生成高质量目标代码尤其 以通用。 重要。寄存器的分配也自然成为目标代码生成中的主要问 按用途不同,寄存器可分为作为变址器使用、专供操作系统 题。 使用、用于目标代码中存放引用次数最多的变量三类。对 于前两类寄存器的分配方法比较直观,则这里主要讨论第 三类寄存器的分配方法。
9.3.1 从逆波兰表生成目标代码
所生成的目标代码为 例如,对于逆波兰表达式: CLA a ab*cd+e/MPY b 具体生成目标代码处理过程如表9.2所示。 M STO CLA c ADD d DIV e SUB M
9.3.1 从逆波兰表生成目标代码
所生成的目标代码为 又如,对于逆波兰表达式: SGN a aQbc*-d/ STO M 具体生成目标代码的处理过程如表9.3所示。b CLA MPY c ISU M DIV d
如果把寄存器分配给某些变量,则该变量在定值前每引用一 次,将可少访问一次内存,从而可节省执行代价1;如果 某变量在基本块中被定值,且出基本块后还要被引用,则 把寄存器固定分配给该变量,可省去把它保存到内存单元 的操作,从而节省执行代价2。
9.1.4 运行时的存储管理
通常所见到的计算机都是冯· 诺依曼体系结构的,其特征是变 例如:假定一个字包含4字节,每个字符占1字节,每个整型 量——存储字,即用变量来仿效存储字,变量名实际上是 量占4字节,则对于字符型量的存储位置可以任意,然而, 存储字的名字。 对于整型变量的存储区域之开始地址必须是0,4,8, 对于编译程序来说,必须对源程序中出现的变量与常量分配 16,…,否则会引起错误。 运行时刻的存储空间,这一工作由先行端的分析和代码生 成程序共同完成。另从符号表的信息可以确定一个名字 (标识符)所代表数据对象在过程数据区中的相对地址。为 了存储分配的正确实现,除了必须考虑标识符的作用域问 题外,还必须考虑字边界对齐问题,即对于字节编址的计 算机,必须注意对于不同类型的量所分配存储区域的起始 地址都必须符合边界要求。
由于<a[0]>=<a[m]-m,所以有<a[m]>=<a[0]>+m。从而对于一维数组的存储单 元,有公式:
<a[i]>=<a[0]>+i 其中<a[0]>称为数组的假头,<a[m]>称为数组的真头。
9.2.2 虚拟机的汇编指令
例如,有赋值语句: X:=a[10] 设数组a为单块连续存放方式存放,<a[0]>为假头,则<a[10]>=<a[0]>+10。
9.3.1 从逆波兰表生成目标代码
上面首先把简单表达式改造成中间语言的逆波兰形式,然 ① 从左到右扫描简单表达式中的每个符号。若扫描到运算分量时就下推入栈, 后由逆波兰表达式生成目标代码。实际中也常把两步合 若扫描到运算符(包括括号和“#”)时就转到②,如此一直扫描下去,直到整 个表达式扫描结束为止。 为一步,根据运算符优先数的大小关系,直接对简单表
达式进行语法语义分析。 ② 检查栈有无元素。若没有,则当前运算符入栈,然后转回到步骤①检查下一 个符号;否则检查运算符的优先级。若当前的比栈顶的大,则当前运算符入 为了处理简单起见,规定被处理的简单表达式的前后都有 栈,然后转回步骤①,否则转到步骤③。
③ 检查当前符号和栈顶元素。若当前符号是“)”,而栈顶元素是“(”,则“(”上 退栈并转回第一步;若当前是“#”,栈项也是“#”,则处理完毕,算法结束。 #<简单表达式># 若不是上述两种情况则转到步骤④。
9.2 一个计算机模型——虚拟机
9.2.1 虚拟机
9.2.2 虚拟机的汇编指令
9.2.1 虚拟机
虚拟机不是一台实际的机器,而是便于讨论的一台假想和 抽象的计算机。假设这台虚拟机有如下特性:
该虚拟机是一台地址单累加器的计算机,用“AC”表示该 累加器;设OP为操作码,d为地址,则指令: 0P d 表示
假设指令编号从100开始,于是汇编指令编写的程序为 100 CLA <10> 101 ADD <<a[0]>> 102 STA l03 103 CLA 104 STO X
这里的第102条填地址指令“STA 103”填了第103条指令的地址部分,相当于@(Ac)=>103的地址部分, 因此第103条指令的地址部分为空。执行第103条指令时,隐含地址为<a[10]>,即CLA<a[10]>, 再执行第104条指令时,则有a[10] =>X,最终实现了X: = a[10]。 如果对X:=a[10]语句不采用填地址指令,其程序为 100 CLA <10> 101 ADD <<a[0]>> 102 STO M 103 ICA M 104 STO X 其中M为临时工作单元变量,用来存放<a[10]>,第103条指令使用间接取的方式,实现了X: = a[10]。
寄存器
内存间接地址
3
9.1.3 寄存器分配
基于以上原因,分配中尽可能把变量值保留在寄存器中,当 在一个基本块内,按照中间语言代码的顺序,逐个基本块产 一个寄存器的值不再需要时,该寄存器可以收回留作他用, 生目标代码。生成的目标代码应尽可能将运算的结果存放 但这只有对程序做了全面的数据流分析后才能确定,这种 在寄存器中,直到该寄存器必须用来存放别的变量或基本 分析要做大量的额外工作不太实际,在寄存器的分配中常 块结束为止。这样可以减少基本块访问内存的次数,从而 常以基本块为单位进行。 提高运行速度。