我們已經在 mrb_exec
中將 IREP 的資訊讀取完畢,現在我們就可以開始針對 OPCode 的方式來讀取。
基本上在處理的時候會是一個無限迴圈,我們會不斷的讀取指標上的資訊直到遇到錯誤或者收到「結束」的指令才會停下來,在結合一些像是 JMP
之類指令,就能實現像是迴圈之類的效果,在使用上來說是很類似組合語言的應用技巧,這邊推薦大家可以嘗試看看 Steam 上面的 SHENZHEN I/O 這款遊戲,可以用相對輕鬆的方式來體驗類似的經驗。
不過我自己有玩的朋友幾乎每關都滿分,玩起來壓力好像也是不小呢⋯⋯
因為目前我們還不會處理任何 OPCode 因此製作一個無限迴圈,並在遇到「錯誤」的狀況下終止並且回報「未知」的 OPCode 讓我們可以根據希望實作的功能依序實現。
// src/vm.c
int mrb_exec(const uint8_t* data) {
// ...
int error = 0;
while(!error) {
uint8_t op = *p++;
switch(op) {
default:
LOG("DEBUG> Unsupport OP Code: %d\n", op);
error = 1;
}
}
}
我們先在 app.rb
裡面加入簡單的加法行為,讓我們可以產出一些有實際作用的機器碼。
# app.rb
10 + 10
接下來執行 make
指令,就會看到類似下面的訊息:
DEBUG> locals: 1, regs: 5, ireps: 0
DEBUG> Unsupport OP Code: 16
因為 OPCode 基本上都是整數呈現的,我們還需要找到原本的 OPCode 應該是什麼指令,才能執行對應的處理。
因此需要參考 mruby 原始碼中的 mruby/ops.h
以及文件 opcode.md 來獲取對應處理的資訊。
我們在文件上看到的 OPCode 會以 enum
的方式記錄,因此從第一個開始依序會從 0 開始自動遞增,對應到 ops.h
的行號,剛好就是 OPCode 的整數值 + 14
就會是對應的行號因此我們可以透過這個反推出 16
就是 OP_LOADSELF
這個指令。
先建立 src/opcode.h
來加入對應的指令
#ifndef _IRON_OPCODE_H_
#define _IRON_OPCODE_H_
/**
* Implement mruby 2.1.2
*
* Reference: https://github.com/mruby/mruby/blob/2.1.2/include/mruby/ops.h
*
* CODE = (Line Number - 14)
*
* Example:
* OP_LOADI__1
* Line Number = 19
* OP_CODE = (19 - 14) = 5
*/
enum {
OP_LOADSELF = 16,
};
#endif
參考 mruby-L1VM 的實作,我們加入我們第一個 OPCode 的處理
// src/vm.c
// ...
switch(op) {
case OP_LOADSELF:
a = *p++;
reg[a] = reg[0];
LOG("DEBUG> r[%d] = self %ld\n", a, reg[a]);
default:
LOG("DEBUG> Unsupport OP Code: %d\n", op);
error = 1;
}
// ...
裡面的 reg[0]
可以參考我的 Repo elct9620/ironman2021
的 feature/4-opcode
步驟,這邊的 OP_LOADSELF
主要是用於設定執行這段程式碼的物件是哪一個,目前我們直接設定成 0
就好了,因為暫時用不到。
假設 Ruby 呼叫這個方法是
Example
物件,他就會把self
設定為#<Example:0x1234567>
來作為參考
接下來我們會發現遇到 OPCode = 3 找不到的問題,對照 mruby/ops.h
確認是 OP_LOADI
這個指令,不過跟這指令相關的大概還有七個左右,都是用來將「整數」讀取進來的處理,因此我們一起增加對應的 OPCode 到 src/opcode.h
裡面。
// src/opcode.h
enum {
OP_LOADI = 3,
OP_LOADINEG,
OP_LOADI__1,
OP_LOADI_0,
OP_LOADI_1,
OP_LOADI_2,
OP_LOADI_3,
OP_LOADI_4,
OP_LOADI_5,
OP_LOADI_6,
OP_LOADI_7,
OP_LOADSELF = 16,
};
在 mruby 裡面如果讀取的數字是 -1
到 7
的話會有獨立的 OPCode 處理,除此之外的數值則會由 OP_LOADI
讀取正整數跟用 OP_LOADINEG
處理負數。
接下來我們對 src/vm.c
增加對應的處理,這邊 mruby-L1VM 利用了 OPCode 是連續數字的特性將 OPLOADI__1
到 OP_LOADI_7
一起處理(在 mruby 裡面也是)
// src/vm.c
// ...
switch(op) {
case OP_LOADI:
case OP_LOADINEG:
a = *p++; b = *p++;
if (op == OP_LOADINEG) {
b = -b;
}
reg[a] = b;
LOG("DEBUG> r[%d] = %d\n", a, b);
break;
case OP_LOADI__1:
case OP_LOADI_0:
case OP_LOADI_1:
case OP_LOADI_2:
case OP_LOADI_3:
case OP_LOADI_4:
case OP_LOADI_5:
case OP_LOADI_6:
case OP_LOADI_7:
a = *p++;
reg[a] = op - OP_LOADI_0;
LOG("DEBUG> Load INT: %ld\n", reg[a]);
break;
// ...
default:
LOG("DEBUG> Unsupport OP Code: %d\n", op);
error = 1;
}
// ...
如此一來我們的 Ruby VM 就能夠將原始碼中的整數載入到記憶體裡面等待我們處理,接著繼續用 make
執行修改後的程式取得下一個需要實作的 OPCode 出來。
當我們可以載入整數後,Ruby 接下來要對他進行相加的動作,因此下一個需要實作的就是 OP_ADDI
(60
) 這個指令,到目前為止大家應該已經可很熟悉的在 opcode.h
增加指定的指令了吧?
// src/opcode.h
enum {
// ...
OP_LOADSELF = 16,
// ...
OP_ADDI = 60,
};
增加指令之後我們就可以繼續更新 src/vm.c
這個檔案來把「相加」的行為實作出來。
// src/vm.c
// ...
switch(op) {
// ...
case OP_ADDI:
a = *p++; b = *p++;
reg[a] += b;
LOG("DEBUG> r[%d] = r[%d] + %d\n", a, a, b);
break;
// ...
default:
LOG("DEBUG> Unsupport OP Code: %d\n", op);
error = 1;
}
// ...
在 Ruby 裡面,如果程式碼沒有寫任何 return
的話,就會自動將最後一行當作 return
的數值,也因此我們在執行完 Ruby 的 10 + 10
這段程式碼之後,會進入到 OP_RETURN
(55
) 的處理。
增加 OPCode 到
src/opcode.h
的部分就不另外舉例
// ...
switch(op) {
// ...
case OP_RETURN:
a = *p++;
LOG("DEBUG> %s r[%d]\n", op == OP_RETURN ? "return" : "break", a);
LOG("RES> %d\n", (int)reg[a]);
return reg[a];
// ...
default:
LOG("DEBUG> Unsupport OP Code: %d\n", op);
error = 1;
}
// ...
因為我們目前並沒有實作從 Block 回傳或者 Break 的處理,因此先實作 OP_RETURN
來測試即可,同時這個動作也會直接將我們的 VM 停止並且結束程式的運行。
這樣我們就實作出了能夠處理整數資料以及加法的 Ruby VM 雖然支援的語法有限,但是利用這樣的方式就可以逐步的將不同的 Ruby 語法支援。