上一篇我們已經了解了 mruby 編譯出來的檔案大致上有怎樣的結構,要能夠執行 Ruby 只需要實際上存取機器碼的區段讀取出來之後依照行為執行就可以了,不過在 mruby-L1VM 開始執行前似乎又做了一些什麼處理,因此我們需要先來簡單的了解主區段 irep
到底是什麼。
實際上 irep
是什麼的縮寫我其實一直沒有找到,比較接近的單字應該是 Intermediate Representation
(中間語言)這個單詞,根據維基百科的條目他就是在討論編譯器編譯成機器碼的中介狀態,通常被稱作 IR
從這個角度來看,由 mruby 編譯器產生的機器碼確實也蠻接近這樣的概念。
我們接著看 irep_exec
方法起頭的部分,會發現一段充滿神秘數字的原始碼
int irep_exec(struct mrb_vm* vm, const uint8_t* irep, struct mrb_state* parent, int paramreg) {
const uint8_t* p = irep;
p += 4;
int nlocals = b2l2(p);
p += 2;
int nregs = b2l2(p);
intptr_t reg[nregs - 1]; // no need a reg for management
p += 2;
int nirep = b2l2(p);
p += 2;
x_printf("locals: %d, rergs: %d, ireps: %d\n", nlocals, nregs, nirep);
// ...
}
首先我們會先將 irep
指標的位置複製到 p
指標上,這樣在移動指標的時候就不會影響到外面傳進來的 irep
指標,在用做其他用途的時候就不會因為指標位置錯誤而造成不良的影響,但後續我們馬上看到連續幾個像是 Magic Number 的操作,經過幾次的偏移以及呼叫 b2l2
之類的處理,獲取了三個不同的變數數值:
這三個數值對接下來的實作上會有不少的幫助,像是 nregs
的資訊會告訴我們目前虛擬機需要有多少個「變數」來紀錄資訊,如果對組合語言有所接觸的話,應該會知道「寄存器」這樣的概念,通常在 CPU 中這個數量是有限制的,不過在虛擬中我們是透過模擬的方式實現也因此可以自由地去設計,而這個 nregs
就是用來告訴我們接下來需要多少寄存器才能夠讓後需要執行的機器碼正常運作。
那麼跳過的那些數字是對應哪些資訊呢?我們可以從 mruby 原始碼中的 src/load.c
這個檔案找到線索。
// src/load.c
/* skip record size */
src += sizeof(uint32_t);
/* number of local variable */
irep->nlocals = bin_to_uint16(src);
src += sizeof(uint16_t);
/* number of register variable */
irep->nregs = bin_to_uint16(src);
src += sizeof(uint16_t);
/* number of child irep */
irep->rlen = (size_t)bin_to_uint16(src);
src += sizeof(uint16_t);
最先跳過的 4 個 Byte 被稱作是 Record Size
,接下來就分別是 nlocals
、nregs
和 nirep
的資料,到這邊為止我們就完成了 irep
起始部分的讀取,接下來就是程式碼的本體 ISEQ
的階段。
ISEQ
在 Ruby 裡面就有一個明確的名稱,叫做InstructionSequence
在比較新版本的 Ruby (大約是 2.6 左右)可以透過標准函式庫RubyVM::InstructionSequence
獲取資訊,這個新的功能讓設計除錯工具或者一些比較特殊的應用更容易開發。
緊接著前面的程式碼,我們馬上又看到了一個有奇怪的區塊
{
int codelen = b2l4(p);
p += 4;
int align = (int)p & 3;
if (align) {
p += 4 - align;
}
}
在 mruby 的設計裡面,每個 IREP 中還會包含幾種不同的 Block(區塊)分別是儲存指令碼的 ISEQ 區、變數之類的資料所屬的 POOL 區以及符號(Symbol)的 SYMS 區塊,每個區塊都會有一個「長度」的資訊,像是 SYMS 區塊會用 4 Bytes 來儲存一共有多少 symbol
被儲存在裡面。
因為 ISEQ 的長度對後續的實作沒有太大的影響,因此這邊雖然讀取了 codelen
但實際上並沒有使用,後面緊接著的 align
相關處理是要將資料對齊。
繼續往下閱讀,就是 ISEQ 實際執行的部分,也會是這次我們需要不斷實作去支援不同的 OPCode 來讓我們的 VM 能夠執行對應的動作。
int32_t a = 0;
int32_t b = 0;
int32_t c = 0;
int opext = 0;
const uint8_t* porg = p;
reg[0] = paramreg ? parent->reg[paramreg] : MRB_OBJ_OBJECT; // self instance 0 means root object // vm->parentstate ? parentstate->caller : 0;
intptr_t* tclass = mrb_memory_find(vm, reg[0], "_cls");
if (tclass) {
x_printf("irep_exec self:%ld target_class:%ld (%d)\n", reg[0], tclass[2], paramreg);
} else {
x_printf("irep_exec self:%ld target_class:%d, (%d)\n", reg[0], 0, paramreg);
}
for (;;) {
// ...
}
關於 for
迴圈內的行為我們就留到後續再做討論,這邊需要注意的是 a
、b
、c
這三個變數,在組合語言中我們可能會看到像是 MOV exa, L1
這樣的語法,在 mruby 的虛擬機中也是類似的,像是 MOV
對應的是 OPCode 而後面的參數基本上就會是由 a
、b
、c
三個變數來負責,不過比較複雜的是他會在不同指令下表示寄存器的第幾個元素、長度、數量等等資訊,這就需要對照 mruby-L1VM 和 mruby 的原始碼,才能推測出當下應該如何處理。
下一篇我們會針對 mruby-L1VM
的幾個輔助方法來說明,像是 b2l2
和 b2l4
這類方法,我們在後續的實作中前期階段大多都是依靠這些方法來互相輔助來實現的。