五、虛擬機(Virtual Machine)

為什麼我們需要虛擬機呢?因為我們要運行我們編譯好的程式。那為什麼我們不直接編譯到 native binary code 呢?這是因為真正的電腦資源十分有限,我們編寫起上來會比用虛擬機的做法難很多,那就嚴重超出了本教程的範圍了(其實是因為西傑還不太認識這個課題,要偷懶一下)。當然,虛擬機的做法和 native code 的也有幾分相似,這裡就給讀者們一個初步的概念,大家真的想再接觸多一點底層的東西就要自己摸索一下了……

接著下來,我們就稱我們的虛擬機為 Wemachine 吧。

遊戲開始之前,我們當然要先定義一下遊戲規則﹣ Instruction set architecture(ISA),Wemachine 會支持以下的 Instruction set:

Instruction Example Meaning Comments
Arithmetic
add add $1,$2,$3 $1 = $2 + $3
subtract sub $1,$2,$3 $1 = $2 – $3
mult mult $1,$2,$3 $1 = $2 * $3
div div $1,$2,$3 $1 = $2 / $3
modulo mod $1,$2,$3 $1 = $2 % $3
add immediate addi $1,$2,100 $1 = $2 + 100
subtract immediate subi $1,$2,100 $1 = $2 – 100
multiply immediate multi $1,$2,100 $1 = $2 * 100
divide immediate divi $1,$2,100 $1 = $2 / 100
modulo immediate modi $1,$2,100 $1 = $2 % 100
Logical
and and $1,$2,$3 $1 = $2 & $3 Logical AND
or or $1,$2,$3 $1 = $2 $3 Logical OR
Data transfer
move data from register to another register move $1,$2 $1 = $2
load data lwi $1,100 $1 = 100
load upper immediate lui $1,100 $1 = 100 * 2^16
Conditional branch
define LABEL label LABEL LABEL:
branch on equal beq $1,$2,LABEL if ($1 == $2) goto LABEL
branch on not equal bne $1,$2,LABEL if ($1 != $2) goto LABEL
branch on less than bl $1,$2,LABEL if ($1 < $2) goto LABEL
branch on greater than bg $1,$2,LABEL if ($1 > $2) goto LABEL
branch on less than or equal ble $1,$2,LABEL if ($1 <= $2) goto LABEL
branch on greater than or equal bge $1,$2,LABEL if ($1 >= $2) goto LABEL
Unconditional jump
jump j LABEL goto LABEL
Others
print print $1 print $1

而且西傑將會假設我們的虛擬機有無限個 register ,以便我們開發。

開始寫程式了,第一步我們要做的是讓 Wemachine 學會讀那堆指令,為了方便讀取,我們要限定指令的格式,格式如下:

opcode1 operand1, operand2, operand3;

opcode2 operand1, operand2;

每個指令由分號分隔開,在第一個空格號前的為指令的 opcode ,接下來是一至三個 operand ,以逗號分隔開,就是這麼簡單了。現在先開始寫一個 Parser 來分析以此格式寫的程式,西傑相信這一步應該很簡單,現在看看運行結果。

現在要開始實現功能了,首先要有 register 以記下數據,這個西傑會用一個 array 來做,$0 指向 array 第 0 個元素,$1 指向第 1 個元素,如此類推。然後我們要寫兩個 method 來 get set register 的數據,如果那個 register 未被使用過的話,它的數據就會是 0。於是我們就有了以下的程式:

Wemachine.prototype.resolveRegister = function (operand) {
    if (typeof operand == "string" && operand.length > 0) {
        if (operand[0] == "$") {
            return parseInt(operand.substr(1));
        }
    }
    Errors.push({
        type: Errors.RUNTIME_ERROR,
        msg: "Fail to resolve register",
        line: 0
    });
    return -1;
}
Wemachine.prototype.getRegisterContent = function (operand) {
    operand = this.resolveRegister(operand);
    if (operand != -1) {
        if (this.registers.length > operand) {
            return this.registers[operand];
        }
    }
    return 0;
}
Wemachine.prototype.setRegisterContent = function (operand, value) {
    operand = this.resolveRegister(operand);
    if (operand != -1) {
        this.registers[operand] = value;
    }
}

現在開始要編寫執行功能,執行功能很簡單,只是把每一句指令都執行一次就可以了。

Wemachine.prototype.run = function () {
    for (var i = 0, l = this.instructions.length; i < l; i++) {
        var instruction = this.instructions[i];
        this[instruction.opcode].apply(this, instruction.operands);
    }
}

最後就是要編寫指令的實際功能了,lwi 要做的就是直接把一個數值寫到 register 中,所以我們只需 call 一下 setRegisterContent 就可以了,而 print 要做的就是把一個 register 中的數值寫到 output 去,所以就有以下的代碼了:

Wemachine.prototype.lwi = function (operand1, operand2) {
    this.setRegisterContent(operand1, parseInt(operand2));
}
Wemachine.prototype.print = function (operand1) {
    var val = this.getRegisterContent(operand1);
    log(val);
}

放在一起,我們運行一下程式,看看結果。

很好,現在就把餘下的 Data transfer 功能都編寫下來吧。

Wemachine.prototype.move = function (operand1, operand2) {
    this.setRegisterContent(operand1, this.getRegisterContent(operand2));
}
Wemachine.prototype.lwi = function (operand1, operand2) {
    this.setRegisterContent(operand1, parseInt(operand2));
}
Wemachine.prototype.lui = function (operand1, operand2) {
    this.setRegisterContent(operand1, parseInt(operand2) << 16);
}

很直觀,沒什麼特別之處,這裡就不多解釋了。 Arithmetic 的處理其實都很直觀,唯一需要提醒讀者的地方是,做除數運算是有可能會出現 Division by zero 的情況,所以我們要發出 runtime error 。

Logical 跟 Arithmetic 的處理方法很相似,這裡就不著墨了。現在到最後要寫 branchjump 了,要實現這個功能,我們要改變一下程式執行的方法,記得我們的 run method 嗎?我們的 run method 是用 i 來做 loop counter 的,現在要改變一下了,要用 program counter 取代 i,這樣我們才可以在其他方法中改變運行次序。

label () 中,我們要把想定義的 label 和 program counter 的數值放到 map 裡,這樣我們才可以在後面的程式設定要跳到哪一個位置。現在看看我們的 bne :

Wemachine.prototype.bne = function (operand1, operand2, operand3) {
    var nextPC = this.labelMap[operand3];
    if (nextPC == null) {
        Errors.push({
            type: Errors.RUNTIME_ERROR,
            msg: "Label not found",
            line: 0
        });
    } else {
        var val1 = this.getRegisterContent(operand1);
        var val2 = this.getRegisterContent(operand2);
        if (val1 != val2) {
            this.pc = nextPC;
        }
    }
}

首先要在 map 中找找有沒有相關的 label ,沒有的話就要發出錯誤,有的話就要看看條件是否成立,是的話就要把 program counter 設定為要跳到的位置,這樣程式在下次 loop 的時候才可以跳到 label 那處。現在看看程式運行結果吧!

現在把剩下來的 branch 都寫下來。

大功告成,怎麼樣,如西傑之前所說,這部份不是很難吧,如果大家曾經學過 assembly language 的話應該更容易上手!現在有了這個簡單的虛擬機,下一步我們就可以把之前建立的 parse tree 變成可以在這個 Wemachine 運行的代碼了,下個星期再見吧。

results matching ""

    No results matching ""