编译原理大设计——基于Cminus虚拟机(TINY虚拟机改进版)的目标代码可运行的Cminus编译器

实验介绍了基于TINY虚拟机的C-编译器设计,涵盖代码生成器的实现,涉及TM虚拟机的指令结构、寻址方式、虚拟机流程。通过学习经典代码生成器,理解TM虚拟机指令集,实现C-语言的代码生成器,包括函数调用、栈帧管理,并通过TM虚拟机测试案例验证编译器的正确性。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

HNU编译原理

基于TINY虚拟机的目标代码可运行的C-编译器

这个实验挺难的,按照HNU实验文档的意思应该是将TINY的虚拟机进行改造,做成可以运行自己的C-编译器编译成的伪指令

一、 实验目的

  1. 学习已有编译器的经典代码生成源程序。
  2. 通过本次实验,加深对代码生成的理解,学会编制代码生成器。

二、 实验任务

  1. 阅读已有编译器的经典代码生成源程序,并测试代码生成器的输出。
  2. 用C或JAVA语言编写一门语言的代码生成器。

三、 实验内容

(一) 学习经典的代码生成器

1. 选择一个编译器

选择TINY编译器来进行代码生成器部分的学习。

2. 阅读TM虚拟机的有关文档,了解TM机的指令结构与寻址方式等相关内容

TM ( Tiny Machine)虚拟机是易于模拟运行代码产生器生成的目标代码的简单机器。TM指令本身总是以汇编代码形式给出 (模拟器总是只读入汇编代码)。

2.1. TM基本结构

T M由只读指令存储区、数据区和 8个通用寄存器构成。它们都使用非负整数地址且以 0开始。寄存器7为程序记数器,它也是唯一的专用寄存器。
在这里插入图片描述

在开始点, Tiny Machine将所有寄存器和数据区设为 0,然后将最高正规地址的值 (名为DADDR_SIZE -1,即1023)装入到dMem[ 0 ]中。
在这里插入图片描述
机器在执行到HALT指令时停止。可能的错误条件包括IMEM_ERR (它发生在取指步骤中若reg [PC_REG]< 0或reg [ PC_REG ]≥I ADDR_SIZE时),以及两个条件DMEM_ERR和ZERO-DIV,它们发生在指令执行过程中;

2.2. TM指令集

寄存器与寄存器间的操作,包括输入输出、运算、HALT结束指令、opRRLim代表RR类型指令的结尾;
在这里插入图片描述
寄存器与内存之间的操作指令,LD表示从内存到寄存器、ST表示从寄存器到内存,opRMLim表示RM指令的结尾;
在这里插入图片描述
R A指令包括对应于2种地址模式的2种不同的装入指令:“装入常数”(L D C),“装入地址” (L D A)。另外,还有6条转移指令。
在这里插入图片描述

在指导书中RA、RM统称为RM,基本指令格式有两种:寄存器,即R O指令。寄存器-存储器,即R M指令。
寄存器指令有如下格式:

opcode  r , s, t

这里操作数r、s、t为正规寄存器(在装入时检测)。这样这种指令有3个地址,且所有地址
都必须为寄存器。所用算术指令被限制到这种格式,以及两个基本输入/输出指令。
一条寄存器-存储器指令有如下格式:

opcode  r , d( s )

在代码中r和s必须为正规的寄存器(装入时检测),而d为代表偏移的正、负整数。这种指令为两地址指令,第1个地址总是一个寄存器,而第2个地址是存储器地址a,用a = d + r e g [ s ]给出。
在R D和R M中,即使其中一些可能被忽略,所有的 3个操作数也都必须表示出来。这是由于简化了装载器,它仅区分两类指令( R O和R M )而不允许在一类中有不同的指令格式。
值得注意的指令集约定:

  • 算术运算中目标寄存器、I N以及装入操作先出现,然后才是源寄存器。
  • 所有的算术操作都限制在寄存器之上。
  • 在指令中没有限制使用p c。实际上由于没有无条件转移指令,因此必须由将pc作为L D A指令的目标寄存器来模拟、条件转移指令(J L T等)可以与程序中当前位置相关,只要把p c作为第2个寄存器,
2.3. TM虚拟机实现
  • 模拟器接受包含上面所述的T M指令的文本文件, 并有以下约定:
  1. 忽略空行。
  2. 以星号打头的行被认为是注释而忽略。
  3. 任何其他行必须包含整数指示位置后跟冒号再接正规指令。任何指令后的文字都被认为
    是注释而被忽略掉。
  • 在TINY源码中TM虚拟机的实现全部在tm.c文件中,接下来详细的分析它的实现:
    从main函数出发,首先是打开以tm结尾的TINY语言生成的指令代码文件,实现类似于TINY的源程序文件读取;
    接下来通过readInstructions()函数将文件中的指令全部读取并且存储在指令区iMem中:
    在这里插入图片描述
    在这里插入图片描述
    这里不得不观察指令区的数据结构INSTRUCTION:
    在这里插入图片描述
    这是非常简单的结构体,很好理解,第一个iop表示指令的种类,后面三个iarg表示指令的操作数,比如对于运算指令MUL等来说,iarg1、2、3分别存储的是r、s、t,该指令的含义为reg[r] =reg[s]/reg[t];

(1) readInstructions()函数
tm.c虚拟机实现最重要的函数之一,接下来详细理解:
首先,进入函数后,第一部操作是进行初始化的操作,将数据区(数组实现)、指令区、寄存器都进行初始化;
接下来,通过while循环进行一行行的指令读入;
对指令的读入分为以下步骤:

  • 将一整行指令读取的字符串中;
    在这里插入图片描述
  • 通过getNum读取指令的标号;
    在这里插入图片描述
  • 获得指令码op(字符串转化为数字);
    在这里插入图片描述
  • 根据指令码的种类进行行操作数的读取;
    在这里插入图片描述
  • 将获得的整个指令相关的信息存储到iMem中;
    在这里插入图片描述

其中最重要的就是操作数的读取,其中RR不同于RM、RA,因为它有三个操作数需要读取,并且三个操作数之间的分隔符都是’,’,形式为op r,s,t ,而后者不同,它们的形式为op r , d(s)
因此需要不同的方式进行读取。读取过程中检查方式也有区别,d仅仅表示偏移,因此只需要保证是数字即可、而r、s、t都是寄存器,所以还要检查它们是否是寄存器(小于7);
详细的代码实现见源码;
readInstructions函数执行完成之后文件中所有的指令都已经读取到了Imem指令区,接下来就可以进行执行了;

(2) doCommand函数
包括命令与具体指令执行都是依靠的doCommand的循环实现的:
在这里插入图片描述
接下来详细的分析该函数;
doCommend函数首先进行的是命令的读取,读取到命令之后,再通过命令的类型进行详细的处理,虚拟机各种命令:
TM虚拟机在输入执行文件进入输入命令提示之后,我们可以通过简单的输入命令来达到各种效果。

g :开始运行直到结束;
s n :运行n条指令;
p :对应icluntflag标志,是否打印执行的指令数目;
t :打开或者关闭追踪,即执行的指令打印;
h :打印帮助说明;
r :打印所有8个寄存器的值;
i n :打印出当前指令向后的n条指令;
d n:打印n个数据寄存器的值;
c :将TM虚拟机的状态重置;
q :退出;

这里的所有命令都是在通过doComment函数的switch语句实现的;
里面以s和g为例进行分析:
在这里插入图片描述
明显,对命令的处理直接在case内进行完成,这里将s的参数n通过getNum得到赋值给stepcnt,直接break;而g命令则是将stepcnt赋值1后break;

退出命令的switch之后需要判断是否需要运行指令(g、s),不需要则doCommend结束,循环再次进入该函数等待下一个指令,需要运行则:
在这里插入图片描述
通过stepcnt判断是否需要运行(只有g、s使其非零),如果是g,那么就是直接所有指令全部运行,即通过while循环依次通过stepTM执行指令,直到出错或者执行完得到srHALT状态,这里的stepTM是执行指令的关键函数,一次执行一条指令;
而对于s命令,它执行的是stepcnt行指令,因此while的条件需要多加判断;

(3) stepTM函数
接下来就分析最后的指令执行函数stepTM,它实现了指令的执行,并将执行结果装在寄存器、数据区。该函数的原型为:
STEPRESULT stepTM (void)
它返回一个运行状态,即stepresult,如果未出错并且未结束则返回srOKAY,如果所有指令结束返回srHALT,否则表示出错,根据出错信息进行返回。

stepTM函数步骤如下:

  • 根据当前的RC_REG寄存器读出pc,即当前进行到的指令,然后读出在iMem代码区中的这条指令,并将PC_REG指向下条指令便于后续操作;
    在这里插入图片描述

  • 根据指令的种类iop(switch)选择操作符的读取方式,RR直接读取到r、s、t中,而RM需要读取到r、s、m中,m表示地址(因此需要判断越界)、RA中的m表示的是s寄存器中值加上偏移得到的值(不需判断);以RM为例:
    在这里插入图片描述

  • 加载完毕后通过iop的类型(switch)进行运算,运算结果存在寄存器或数据区中,并且返回运行状态值,比如遇到HALT指令则返回srHALT表示所有指令运行完了;

以DIV指令为例,需要执行除法操作,reg[r] = reg[s] / reg[t],执行前需要判断 是否出现除零,是则返回除零错误。其余计算指令类似。
在这里插入图片描述
以JLT为例,如果reg[r]满足小于零,需要将当前的PC寄存器的值reg[PC_REG]改为 设定的地址m,其余的条件转移指令类似。
在这里插入图片描述
R M指令包括对应于3种地址模式的3种不同的装入指令:“装入常数”(L D C),“装入 地址” (L D A)和“装入内存”(L D),实现很简单。
在这里插入图片描述
比较特别的是输入指令,它通过gets函数得到输入的数字字符串,通过getNum函数转 变为数字,如果不是合法数字则需要重新输入(通过while)。
在这里插入图片描述
stepTM函数覆盖了所有的TM指令的执行(通过C语言实现),这也是为什么TM是虚拟机,因为它并没有真正的实现底层的机器码,而是通过C语言处理所有生成的代码指令,用int等数据类型模仿内存等等。

至此,TM虚拟机的实现就完全掌握了,接下来只需要将源码通过代码生成器转变为指令就可以在虚拟机上运行出结果了。

2.4. TM虚拟机流程图

在这里插入图片描述

3. 阅读语义分析源程序并理解

对相关函数与重要变量的作用与功能进行稍微详细的描述,重点理解适合于TM虚拟机运行的指令码是如何生成的。

3.1. 相关文件

在这里插入图片描述
如图所示,代码生成部分相对于前面已经学习过的语法分析器多了四个文件,分别是code.h、code.c、cgen.h、cgen.c;
code.h、code.c文件是生成指令的相关代码,里面包含了生成R0、RM指令的各种emit相关函数,被cgen中的代码生成函数所调用;
cgen.h、cgen…c文件是根据抽象语法树与符号表遍历语法树生成指令代码,具体作用就是根据树结点的类型调用code中的各个emit函数生成指令。
接下来根据四个文件中的函数与变量进行详细的分析。

3.2. 重要变量/结构

在这里插入图片描述
上面已经提到了,TM虚拟机一共有8个寄存器标号0~7,将PC_REG设为7号寄存器,也就是说这里将pc表示为7是为了与之对应;
而寄存器0、1专门用作运算寄存器,计算结果存放在ac中;
在这里插入图片描述
寄存器6、5分别用来存储内存顶部的指针mp(内存顶部用于存储临时变量)、存储内存底部的指针gp(内存底部也就是0开始的位置用于存储全局变量),如左图所示:
t 1的地址为0 ( m p ),t 2为- 1 ( m p ),
x的地址为0 ( g p ),而y为1 ( g p );

3.3. 相关TM代码生成函数

① emitCommend
在这里插入图片描述
该函数用于生成注释存入指令文件中;

② emitR0
在这里插入图片描述
函数作用很明确,就是生成在TM中所述的R0指令,包括MUL运算等,按照指令格式分别对寄存器r、s、t赋值得到指令写入代码文件;

③ emitRM
在这里插入图片描述
该函数是生成RM指令的,因此这里的r、s为寄存器,而d为偏移量,c则与R0中的相同用于添加注释;

④ emitSkip、emitBackup、emitRestore、emitRM_Abs
在这里插入图片描述
3个函数用于产生和反填转移。 e m i t S k i p函数用于跳过将来要反填的一些位置并返回当前指令位置且保存在 c o d e . c内部。典型的应用是调用 e m i t S k i p ( 1 ),它跳过一个位置,这个位置后来会填上转移指令,而 e m i t S k i p ( 0 )不跳过位置,调用它只是为了得到当前位置以备后来的转移引用。函数 e m i t B a c k u p用于设置当前指令位置到先前位置来反填,e m i t R e s t o r e用于返回当前指令位置给先前调用 e m i t B a c k u p的值。
最后代码发行函数(e m i t R M _ A b s)用来产生诸如反填转移或任何由调用 e m i t S k i p返回的代码位置的转移的代码。它将绝对代码地址转变成 p c相关地址,这由当前指令位置加 1 (这是p c继续执行的地方)减去传进的位置参数,并且使用 p c做源寄存器。通常地,这个函数仅用于条件转移。

用if then else来具体解释它们的作用:
当我们代码生成执行到if语句时,此时应该判断if是否成立,成立则执行then部分不成立执行else部分,按照普通格式来看then部分的代码指令应该紧挨着if判断指令,那么如果不成立我们需要跳过then部分的指令去执行else部分的指令,因此这里需要一个跳转指令,但是我们却并不知道else此时的位置(因为还要先生成then部分的指令而不知道它会有几条),因此只能先空出一行当生成then指令后再回到这一条指令去填充它要跳转的位置;
在这里插入图片描述
用于TINY代码的实用程序也就是与TM指令的生成接口就描述完毕了,下面看看代码生成程序本身;

4. TINY代码生成器

TINY代码生成器在文件cgen.c中,其中提供给TINY编译器的唯一接口是CodeGen在接口文件cgen.h中给出了唯一的定义,其原型为:
void CodeGen (void);
在这里插入图片描述

  • 函数CodeGen本身产生一些注释和指令 (称为标准序言(standard prelude))、设置启动时的运行时环境,然后在语法树上调用 cGen,最后产生HALT指令终止程序。
    因此关键的生成函数为cGen,负责完成遍历并以修改过的顺序产生代码的语法树;

  • 函数cGen仅检测节点是句子或表达式节点 (或空),调用相应的函数genStmt或genExp,然后在同属上递归调用自身 (这样同属列表将以从左到右格式产生代码)。

  • 抽象语法树有两种树节点:句子节点和表达式节点。如果节点为句子节点,那么它代表 5种不同TINY语句( if、repeat、赋值、read或write )中的一种,如果节点为表达式节点,则代表 3种表达式(标识符、整形常数或操作符 )中的一种。

  • 函数genStmt包含大量switch语句来区分5种句子,它调用emit函数产生代码并在每种情况递归调用cGen,genExp函数也与之类似。在任意情况下,子表达式的代码都假设把值存到ac中而可以被后面的代码访问。当需要访问变量时 (赋值和read语句以及标识符表达式),通过下面操作访问符号表: loc = lookup(tree->attr.name); loc的值为问题中的变量地址并以gp寄存器基准的偏移装入或存储值;

这里以if语句的生成为例,描述指令码的生成过程:
① 代码生成器为i f语句所做的第1个动作是为测试表达式产生代码。如前所述测试代码,在假时将0存入ac,真时将1存入。
② 生成代码接下来要产生一条JEQ到if语句的else部分。然而这些代码的位置当前是未知的,这是因为 t h e n部分的代码还要生成。因此,代码生成器用emitSkip来跳过后面语句并保存位置用于反填:

savedLoc1 = emitSkip(1) ; 

③ 代码生成继续处理if算语句的then部分。之后必须无条件转移跳过else部分。同样转移位置未知,于是这个转移的位置也要跳过并保存位置:

savedLoc2 = emitSkip(1) ; 

④ 现在,下一步是产生else部分的代码,于是当前代码位置是正确的假转移的目标,要反填到位置savedLoc1。下面的代码处理:

*currentLoc = emitSkip (0) ; 
emitBack up(savedLoc1) ; 
emitRM_Abs("JEQ",ac,currentLoc,"if: jmp to else") ; 
emitRestors() ;*

这就和上面的if语句生成的图示是相同的步骤。
TINY代码生器可以合谐地与 TM模拟器一起工作的原因就是TM模拟器定义了一套可以去分析指令的伪指令,而TINY代码生成器专门生成这一套指令。

5. 测试代码生成器

对TINY语言要求输出测试程序的代码文件与TM虚拟机测试结果。

5.1. 修改主函数,使得TINY编译器进行代码生成;

在这里插入图片描述

5.2. 测试用例一:sample.tny。

样例与测试结果:
在这里插入图片描述
样例文件sample.tny以及得到的代码文件sample.tm
在这里插入图片描述
在这里插入图片描述
此样例是多次使用的指导书上的样例,代码生成输出结果正确,详细注释每一行都有分析,且与样例一一对应,不再分析;
TM虚拟机执行测试:
在这里插入图片描述
可以看到执行g命令运行sample.tm,得到的结果正确,且r命令分析寄存器reg0存储结果为24、reg7存储PC,执行结束为42,reg5、reg6栈顶栈底为0、1023;

5.3. 测试用例二

用TINY语言自编一个程序计算任意两个正整数的最大公约数与最大公倍数。
自己编写的最大公约数与最大公倍数的程序:
因为没有%求余符号,只能通过下面的乘法与除法结合的方式实现求余。

此外,不知道是不是因为提供的TINY的bug的原因,end、until的前面的语句不能有分号,且程序最后一行也不能有分号!
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
中间稍微省略了一部分实现方法重复的计算指令,上面已经有详细注释,不再分析;

下面在TM虚拟机上运行测试正确性:
在这里插入图片描述
可以看到,我输入的数字x、y分别为4、6,求得的结果对应最大公约数与最小公倍数为2、12,答案正确!

至此,TINY语言的代码生成部分就全部掌握了,有了这些基础,完成C-语言的代码生成部分只需要增量编程即可(tm虚拟机可以直接用于C-生成代码的运行)。

(二) 实现一门语言的代码生成器

1. 语言确定:

C-语言,其定义在《编译原理及实践》附录A中。

2. C-语言的代码生成器具体设计见下面的系统设计;

3. 具体代码见源程序。

四、 系统设计(C-语言的代码生成器)

1. C-语言代码生成器设计

1.1. C-语言所需要的指令结构

实际上,TM虚拟机中的指令集R0、RM已经足以支撑起大多数的简单语言了,因此这里直接选择TM的指令集作为我们的目标代码指令,这样的话代码生成后可以直接使用TM虚拟机来运行我们C-编译出的指令码。
直接使用TM指令集的另一个好处是可以直接使用TM虚拟机中的已经存在的生成指令的使用函数,比如emitskip、emitR0、emitRM等等函数,此外在代码生成的过程中需要遵循TM指令集格式。

1.2. C-语言需要扩展的数据结构

C-比TINY语言要复杂的多,最主要的区别就在函数上。C-支持函数定义与调用,因此不可避免地就需要使用函数栈来表示函数的调用关系。
回顾函数栈的调用,可以得到下图过程:
在这里插入图片描述
(这幅图详细的表示了函数调用的关系与过程,是实现函数调用的关键)
即我们需要新的寄存器ebp、esp表示栈底和栈顶,同时需要eax寄存器存储函数调用的返回结果。
调用过程如下:

  • 首先,调用call的时候需要将参数入栈,此时esp-n,n为参数个数,并且n个参数需要写入到esp~esp+n-1,接着需要将当前pc的下一条指令(返回地址)入栈。
  • 接着进入到被调用函数的代码中执行,此时需要将旧ebp入栈,然后将esp赋值给ebp作为新的栈底,而esp则根据函数中变量的数目分配相应的空间(esp-m,m为需要分配的局部变量数目);

那么对于参数以及本地变量的访问只需要通过ebp的向上和向下的偏移即可简单实现。

此时,本地变量可以在函数调用的过程中存储在栈帧中,那么全局变量呢?很简单,参照TINY的实现方式,使用一个寄存器gp存储全局变量所在的位置,TINY中就是数据区的最高位置处,gp保持不变,访问全局变量时,通过gp的偏移进行即可。在TINY还会使用一个寄存器mp来保存临时变量,而在C-中我们将本地变量与全局变量完全区分开,因此并不需要mp寄存器,这里将0号寄存器标记为zero表示该寄存器仅存储0(这样对于加载地址很有好处,可以直接r = addr(0)这样的指令简单的加载地址addr)。

1.3. 寄存器的分配

TM虚拟机一共支持8个寄存器,在TINY中寄存器0、1用于操作数运算的寄存,5、6号寄存器用于gp、mp,7号寄存器用于PC,而2、3、4号寄存器则是没有特定的作用,只在变量加载的时候作为了缓存使用。
C-则无法松松散散的使用寄存器了,8个寄存器甚至不怎么够用,我对寄存器的定义如下:
在这里插入图片描述
可以看到此时只剩下一个bx寄存器用于缓存了,其余都是有特定用途的寄存器,值无法随意改变。

1.4. 扩展生成代码函数

通过对于函数栈的分析就可以很清楚的看出来,原来的TINY的TM接口函数(而就是用于生成指令的函数emit_)虽然可以支撑C-语言构造指令,但是因为有函数调用过程,因此很麻烦,并且C-语言的输入输出为input、output(因为在C-中添加函数概念,因此调用这两个函数也需要栈帧变化),这一点需要与TINY区别开。
所以我添加了几个和函数调用相关的接口函数用于生成函数调用的指令代码;

① Input函数的指令码生成
在这里插入图片描述
注释已经很详细了,这里再详细的讲一下为什么在返回调用者的函数栈帧时不需要pop ebp,这是因为我们的ebp主要用于得到参数和变量,input函数没有变量也没有参数,因此整个函数的执行码用不到input栈帧的ebp,那么就没有必要将旧的ebp存储起来后再将ebp=esp,也因此这里执行input的时候的ebp还是调用者的栈底,那么没有push自然就不用pop了;
而pc必须要保存,这是因为我们不知道call的具体pc位置(哪里都可以进行调用),因此需要在call前保存返回pc;
output函数的实现类似,但是因为output函数需要参数,因此实现稍有不同。

② output函数的指令码生成
在这里插入图片描述
这里一样没有存储旧ebp,也就是说整个output的代码过程中ebp都是调用者的栈底(与input一样),那么ebp不能用来获得output的参数,这里因为没有本地变量,因此output的栈顶和栈底一致,直接把esp当作ebp来获得参数即可。

省略旧ebp的push代表着可以省略很多的指令码,但是也意味着对于input、output函数来说他们的栈帧变化不怎么全面,但是功能是完整的。

③ 函数调用部分的指令生成
在这里插入图片描述
这里我们上面已经提到过了,一个函数的执行部分代码仅有一份,比如从10~15的几行指令表示某一函数的内容,那么10就是这个函数的入口指令,我们将该指令位置存储在了函数的offset中,便于直接获得入口;

④ 得到变量地址的指令生成
在这里插入图片描述
在这里插入图片描述

这里我将本地变量、参数位置存储的都是它们的地址,因此要获得值需要先获得地址;
为什么存储的是地址而不是直接存储变量值呢?因为这里参数有可能会是数组,因此需要对数组本身进行改变(传入的是指针),所以需要存入指针。

⑤ pushParam、popParam传入参数需要用要的函数
上面我们也提到了,需要在call的时候进行参数的传入,这一部分就需要调用这两个函数来进行了;
为了保证参数在ebp向上是从左向右的顺序依次排序,因此这里用一个栈的方式,先入后出将我们的参数进行从右向左依次入栈。
在这里插入图片描述在这里插入图片描述
这里的paramStack的作用就是栈,使得参数地址按照正确的顺序入函数栈帧。

2. C-语言的代码生成器实现

2.1. 文件结构

在这里插入图片描述
基于前面的语义分析实验,扩展语代码生成部分的功能,将代码生成需要的TM接口函数(emit)与遍历函数的cgen函数均写在了codegen.c中而codegen.h头文件中含代码生成函数的声明void cGen()以及对寄存器的分配和对接口函数的生成;
我是通过unbuntu实现的代码生成器,并且通过编写makefile文件来进行工程的编译链接(这里主要直接用了flex和bison生成的词法和语法分析程序)。

2.2. cGen函数的大致处理

这里我只挑选和函数调用相关的结点来进行说明;详细见代码。
① 函数定义
在这里插入图片描述
可以看到的是我们的函数定义是只需要进行一次代码生成的,生成之后对它的调用其实就是将PC调整为该函数的入口地址。
根据函数调用处理顺序,我们进入函数时需要调整栈帧,然后因为此时函数定义仅知道形参的名字,因此将形参表加入到符号表中,这样就可以通过形参的名字name得到我们实参的地址!接下来才可以进行函数体中代码的生成。
然后如果该函数是返回VOID的,那么说明没有显示的return语句,需要在这里直接构造函数返回时的栈帧变化。

② 函数调用
在这里插入图片描述
在函数调用之前需要将参数的值放入合适位置,如果是数组,那么放入的就是它的地址;
然后调用emitCall函数进行栈帧处理(包括PC入栈、栈帧变化);

③ 返回语句的代码生成
在这里插入图片描述
这里对于返回语句,返回VOID的函数是在函数定义部分就已经处理了函数栈帧的返回了,那么这里对于VOID的返回语句return的代码生成没有意义,因为最终都不会有值返回去,返回的计算结果或者值的结果在cGen return语句时已经保存在了ax中,因为我是将ax作为所有的计算结果的寄存器的,因此下面的4条代码指令所需的就是将栈帧变为调用者栈帧并且将PC设为返回PC。

至此关于函数调用的树结点的代码生成就结束了,至于其余的一些结点包括运算结点、赋值结点、循环节点与TINY语言的实现类似,不再赘述。

注:为了将C-简单化,这里将scope域局限在global、参数、本地变量三种,也就是说不支持在while循环中定义一个本地变量同名的量。

④ cGen函数的入口函数
在这里插入图片描述
如图所示,在调用cGen函数生成语法树的代码前,需要进行Input、Output函数的代码生成,并且,非常重要的,main函数是C-语言的入口函数,没有函数会调用它,那么我们就需要手工构造调用它的函数,并且main函数的执行结束代表代码执行结束需要halt指令。

2.3. 代码生成的大致流程图示

在这里插入图片描述

3. 准备2~3个测试用例,测试并解释程序的运行结果。

3.1. test1.cm

这里的测试样例使用C-指导书中的样例(即最大公约数);
在这里插入图片描述
./C- test1.cm输出指令码文件test1.tm如下:
在这里插入图片描述
在这里插入图片描述

接下来很多关于u-u/v*v的计算过程就不放出来了,直接进入gcd的调用:
在这里插入图片描述
到这里gcd的定义就结束了,接下来是main函数的定义:
在这里插入图片描述
还有一条input和output执行的指令就不再复述了,最后就是要填上我们开始时留下的mian函数调用指令;
在这里插入图片描述

下面用TM虚拟机来执行我们生成的C-语言的代码文件(上面也说过使用的是TM虚拟机的指令集,因此可以直接使用TM虚拟机):
在这里插入图片描述
可见,我们运行test1.tm时,能够正确的执行该最小公约数的运算,其中执行两次之间必须要有一个c命令(初始化),否则出现错误(因为第一个g会占用数据区清空数据区0号位):
在这里插入图片描述

3.2. test2.cm

这个例子不进行详细的分析,只是为了确认数组和循环等等能够正常运行:
在这里插入图片描述在这里插入图片描述
sort函数通过调用minloc函数得到最小值的下标进行排序的工作,其实就是一种简单的排序(每次选最小的放前面)。
main函数使用sort函数进行a数组的排序;
在这里插入图片描述
最后通过TM虚拟机运行得到:
在这里插入图片描述
可以看到,输入乱序的2、4、3、1、5,输出的是1、2、3、4、5;
说明C-的代码生成器基本上正确了!!!

五、 实验总结

实验到此就结束了,这次实验非常的有难度,但也带来了很大的收获。
从元旦前夕开始一直到现在,一共用了十几天的时间才勉强完成了C-语言的编译器,并且可以在TM虚拟机上运行真的很不容易,这次实验将前面五次实验都融汇了,因为想要做到虚拟机运行代码的话非常的困难,前面的几次实验的内容都需要更改,以使得他们可以协作运行,改了很多东西最后还是选择使用FLEX、BISON生成词法、语法分析其的方法,同时将实验5的符号分析表进行了很大的改动,为了使得代码简单化,我把scope作用域给简化到了只有三层(参数、全局、本地),也就是说不再支持同一个函数内有同名变量定义。整个实现的过程非常漫长,bug改了又改,可能现在依然存在一些隐藏bug没有找到,能做到现在test2的成功运行真的很感动。
网上的资料非常少,即使有也不知其所云,完全没有用到TINY这样的指令格式,不得不借鉴TINY的实现方法同时把函数调用的栈帧变化考虑进来然后实现C-的简化版。到此该实验就结束了,学到了非常多的东西,能够完成这个实验也让人十分感动,算是给编译原理实验画上了圆满的句号。

最后附上代码,仅供参考;
C-源码.

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值