虛擬機——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編譯器實驗的全部內容了,說實話上理論課的時候,講的都是啥根本聽不懂,只有自己打過這個代碼之後才恍然大悟。還有學過計算機組成原理之後再來看編譯原理就感覺輕鬆很多。
  • 過段時間還要參加一個編譯器的比賽,希望到時候能做出比這個好很多倍的代碼,說實話做這個代碼的時候老師太噁心人,實在沒有心情好好做,就隨便做了一下應付,真正的還要做好多優化,並且我這個代碼還存在許多不合理但是能混過檢查的地方。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章