虚拟机——TEST编译器(4)


词法分析: 词法分析——TEST编译器(1)
语法分析: 语法分析——TEST编译器(2)
语义分析: 语义分析——TEST编译器(3)

1 虚拟机

1.1 功能

读取中间代码并执行,此处的虚拟机相当于模拟了一个在机器上运行编译好的TEST代码的过程

1.2 特点

  • 读取时采用结构体数组code储存指令及其操作数
  • 执行时采用操作数栈stack进行操作
  • 栈顶寄存器top为栈顶的下一个单元的下标,基址寄存器base为当前函数的数据区在stack中的起始地址
  • 到此处一般就没有什么错误了,因为错误都在前面几个过程报错了,能到达虚拟机的都是没有问题的了

1.3 设计思路

  • 由于虚拟机的执行过程就是不断读取中间代码的指令,所以关键就是把每一个指令对应的功能实现,并且确定下一个指令是哪一条。所以只需要一条条读取中间代码,每个指令作为一个分支,根据指令名字执行对应的功能即可

  • 如何确定下一条指令是哪一个:如果学过计算机组成原理的话,就知道在运行程序的时候会有一个程序计数器ip,用来确定下一条指令是第几个,此处虚拟机也要使用这个,正常情况都是按顺序读取中间代码的指令,每读取一条ip就加一,但如果遇到BR、BRF等需要跳转位置的指令的话,ip就会直接赋值成对应的操作数,然后再继续按顺序执行。

  • 操作数栈原理:操作数栈就是记录在程序运行过程中每个变量的值怎么变化,判断条件是什么等等。既然说是栈,那么就说明所有的操作都是在栈顶完成的,例如STO就是把栈顶元素的值赋值给操作数对应的变量。操作数栈还会涉及到函数开辟空间的问题,每进入一个函数就要开辟一个空间,用来存放返回的位置,基地址和该函数的局部变量,有了这些之后就知道该函数调用完后要回到哪里,该函数从栈的第几个位置开始。这些东西都是在语义分析的时候就确定了具体要开辟多少空间。
    在这里插入图片描述

  • 由于指令有很多种,在判断每一个指令时可以选择使用许多的if或者switch进行判断,但是选哪一个更好呢,我之前看过一篇关于if和switch进行比较的博客,里面提到switch的平均执行完成时间比 if 的平均执行完成时间快了约 2.33 倍。原因是在 switch 中只取出了一次变量和条件进行比较,而 if 中每次都会取出变量和条件进行比较,因此 if 的效率就会比 switch 慢很多。而且分支越多switch性能高的特性体现就越明显,所以在我们的虚拟机程序中选择各种指令时,使用了switch进行分支选择。同时,为了使代码更容易理解、阅读,可以用枚举类型表示每一个case。

  • 使用switch进行分支选择,但是如何表示每一个case呢?如果是用数字1、2、3等等代表每一个执行指令则会非常不方便,使代码的阅读、理解难度增大,所以是不适合的,如果换成每个指令的字符串形式,也有点麻烦但是阅读起来就很容易理解很多了。所以我们选择结合这两种类型,使用枚举类型表示每一个指令,这样一来,判断相等、阅读代码等很多方面就会便捷很多。在我理解中,枚举就有点像#define指令,给一个东西一个重命名,可以方便我们读代码的时候好理解含义。思考之后认为使用枚举的好处是:如果有大量需要重命名的内容,如果全部用#define指令会把代码弄的很长、很麻烦。而且在判断枚举类型数据时不需要像字符串一样使用strcmp,直接使用==就可以判断,就很方便。

  • 最开始打虚拟机代码的时候,就一直在纠结虚拟机部分的代码需不需语义分析产生的符号表,以为没有符号表就找不到具体的标识符了。但是在重复学习了分配内存的原理之后,明白了实际上是不需要再传入符号表的。在中间代码中也只有需要操作数的时候才会查符号表,那么在语义分析的时候比如STO的操作数就已经得到变量的相对地址了,那么对我有用的就是通过这个相对地址区分变量就好了,从而也没必要再次知道这个变量长什么样子。所以在虚拟机部分只需要传入中间代码即可,不再需要符号表了。

2 完整代码

#include<stdio.h>
#include<ctype.h>
#include<stdlib.h>
#include<string.h>
#include<string>
#include<map>
using namespace std ;

struct Code{        //中间代码
	char opt[10];
	int operand;
};

Code code[1000];//用于保存中间代码
map<string, int> choseOpt ;     //用map集合方便switch中选择不同case,判断字符串方便
enum opts {LOAD, LOADI, STO, ADD, SUB, MULT, DIV, BR, BRF, EQ, NOTEQ, GT, LES, GE, LE, AND,
            OR, NOT, IN, OUT, CAL, ENTER, RETURN};  //用具体的名称代替1234使代码更易阅读

//初始化map,方便之后switch中选择case
void mapInit() {
    choseOpt["LOAD"] = LOAD ;   choseOpt["LOADI"] = LOADI ;     choseOpt["STO"] = STO ;
    choseOpt["ADD"] = ADD ;     choseOpt["SUB"] = SUB ;     choseOpt["MULT"] = MULT ;
    choseOpt["DIV"] = DIV ;     choseOpt["BR"] = BR ;       choseOpt["BRF"] = BRF ;
    choseOpt["EQ"] = EQ ;       choseOpt["NOTEQ"] = NOTEQ ;     choseOpt["GT"] = GT ;
    choseOpt["LES"] = LES ;     choseOpt["GE"] = GE ;       choseOpt["LE"] = LE ;
    choseOpt["AND"] = AND ;     choseOpt["OR"] = OR ;       choseOpt["NOT"] = NOT ;
    choseOpt["IN"] = IN ;       choseOpt["OUT"] = OUT ;     choseOpt["CAL"] = CAL ;
    choseOpt["ENTER"] = ENTER ;     choseOpt["RETURN"] = RETURN ;
}
//虚拟机
void TESTmachine(){
	FILE *in;
	char codein[100];       //输入文件名
	int codenum=0;          //指令条数
	int top=0, base=0 ;     //栈顶和栈底
	int ip=0;               //当前指令位置
	int stack[1000];        //操作数栈
	printf("请输入目标文件名(包括路径):");
	scanf("%s",codein);
	if((in=fopen(codein, "r"))==NULL){//打开输入文件
		printf("\n打开%s错误!\n",codein);
		exit(-1) ;      //出错就运行结束
	}
	while(!feof(in)){   //读取中间代码
		fscanf(in,"%s %d",&code[codenum].opt,&code[codenum].operand);
		codenum++;
	}
	codenum-- ;     //最后一次读取会多加1
	fclose(in);
//	for(int i=0;i<codenum;i++)
//	    printf("%s          %d\n",code[i].opt,code[i].operand);
	stack[0]=0;
	stack[1]=0;
    mapInit() ;     //将map初始化
    memset(stack, 0, sizeof(stack)) ;
	while(ip < codenum){        //执行指令直到执行到最后一条指令为止,最后一条指令代表主函数结束
        Code temp = code[ip] ;  //用一个临时变量来执行操作
        ip++ ;                  //每执行一个指令地址就往后移动一位
	    switch(choseOpt[temp.opt]) {    //根据操作码进行选择要执行的指令
            case LOAD : {       //LOAD D 将D中的内容加载到操作数栈
                stack[top] = stack[temp.operand+base] ;     //找到栈中存放变量的位置
                top++;
                break ;
            }
            case LOADI : {      //LOADI a 将常量a压入操作数栈
                stack[top]=temp.operand;
                top++;
                break ;
            }
            case STO : {        //STO D将操作数栈顶单元内容存入D
                top--;      //先将栈顶元素减一
                stack[temp.operand+base] = stack[top] ;
                break ;
            }
            case ADD : {        //ADD 将栈顶单元与次栈顶单元出栈并相加,和置于栈顶
                stack[top-2] += stack[top-1] ;
                top--;
                break ;
            }
            case SUB : {   		//将次栈顶单元减去栈顶单元并出栈,差置于栈顶。
                stack[top-2] = stack[top-2]-stack[top-1];
                top--;
                break ;
            }
            case MULT : {      	//将次栈顶与栈顶单元出栈并相乘,积置于栈顶。
                stack[top-2] = stack[top-1]*stack[top-2];
                top--;
                break ;
            }
            case DIV : {		//将次栈顶与栈顶单元出栈并相除,商置于栈顶
                stack[top-2] = stack[top-2]/stack[top-1];
                top--;
                break ;
            }
            case BR : {         //BR lab 无条件转移到lab
                ip = temp.operand;      //操作数记录的就是要跳转的位置
                break ;
            }
            case BRF : {		//BRF 若栈顶单元逻辑值,假(0)则转移到lab
                if(stack[top-1]==0)
                    ip = temp.operand;  //操作数记录的就是要跳转的位置
                top-- ;
                break ;
            }
            case EQ : {		    //将栈顶两单元做相等比较,并将结果真或假(1或0)置于栈顶
                stack[top-2] = (stack[top-2] == stack[top-1]) ;
                top--;
                break ;
            }
            case NOTEQ : {		//栈顶两单元做不等于比较,并将结果 (1或0)置于栈顶
                stack[top-2] = (stack[top-2] != stack[top-1]) ;
                top--;
                break ;
            }
            case GT : {		    //次栈顶大于栈顶操作数,则栈顶置1,否则置0
                stack[top-2] = (stack[top-2] > stack[top-1]) ;
                top--;
                break ;
            }
            case LES : {		//次栈顶小于栈顶操作数,则栈顶置1,否则置0
                stack[top-2] = (stack[top-2] < stack[top-1]) ;
                top--;
                break ;
            }
            case GE : {		    //次栈顶大于等于栈顶操作数,则栈顶置1,否则置0
    			stack[top-2] = (stack[top-2] >= stack[top-1]) ;
                top--;
                break ;
            }
            case LE : {		    //次栈顶小于等于栈顶操作数,则栈顶置1,否则置0
                stack[top-2] = (stack[top-2] <= stack[top-1]) ;
                top--;
                break ;
            }
            case AND : {		//将栈顶两单元做逻辑与运算,并将结果 (1或0)置于栈顶
                stack[top-2] = (stack[top-2] && stack[top-1]) ;
                top--;
                break ;
            }
            case OR : {		    //将栈顶两单元做逻辑或运算,并将结果 (1或0)置于栈顶
                stack[top-2] = (stack[top-2] || stack[top-1]) ;
                top--;
                break ;
            }
            case NOT : {		//将栈顶的逻辑值取反
                stack[top-1] = !stack[top-1];
                break ;
            }
            case IN : {        	//从标准输入设备(键盘)读入一个整型数据,并入操作数栈
                printf("输入数据:\n");
                scanf("%d", &stack[top]) ;
                top++;
                break ;
            }
            case OUT : {		//将栈顶单元内容出栈,并输出到标准输出设备上(显示器)
                printf("输出:%d\n",stack[top-1]);
                top--;
                break ;
            }
            case CAL : {        //调用函数
                stack[top] = base ;     //记录主函数的基地址
                stack[top+1] = ip ;     //记录函数执行完要返回主函数的位置
                ip = temp.operand ;     //执行指令位置跳转到函数开始的位置
                base = top ;            //进入函数后对于当前函数的基地址
                break ;
            }
            case ENTER : {      //进入函数体
                top += temp.operand ;   //为函数开辟空间
                break ;
            }
            case RETURN : {             //函数返回
                top = base ;            //释放函数开辟的空间
                ip = stack[top+1] ;     //第二个位置存放的是返回到主函数的位置
                base = stack[top] ;     //回到栈底就是主函数的基地址,重新赋值
                break ;
            }
	    }
    }
}
int main() {
	TESTmachine();
	return 0 ;
}


3 总结

  • 这就是TEST编译器实验的全部内容了,说实话上理论课的时候,讲的都是啥根本听不懂,只有自己打过这个代码之后才恍然大悟。还有学过计算机组成原理之后再来看编译原理就感觉轻松很多。
  • 过段时间还要参加一个编译器的比赛,希望到时候能做出比这个好很多倍的代码,说实话做这个代码的时候老师太恶心人,实在没有心情好好做,就随便做了一下应付,真正的还要做好多优化,并且我这个代码还存在许多不合理但是能混过检查的地方。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章