延續上一篇的進度,我們將會開始撰寫 mrb_exec
的內容,我們先參考 mruby-L1VM 的方式直接讀取用於參考的 nlocals
、nregs
和 nirep
三個數值出來。
讀取 mruby 的數值我們將會需要能夠將 bin
轉為 uint32
或者 uint16
的輔助方法,參考 mruby-L1VM 的實作我們在 src/utils.h
裡面加入這兩個方法。
static inline uint32_t
bin_to_uint32(const uint8_t *bin)
{
return (uint32_t)bin[0] << 24 |
(uint32_t)bin[1] << 16 |
(uint32_t)bin[2] << 8 |
(uint32_t)bin[3];
}
static inline uint16_t
bin_to_uint16(const uint8_t *bin)
{
return (uint16_t)bin[0] << 8 |
(uint16_t)bin[1];
}
如此一來我們就可以利用 bin_to_uint32
和 bin_to_uint16
來獲取正確的數字。
後面的步驟我們會調整為 mruby 原始碼中所使用的
PEEK_B
的巨集,這是因為在開發的過程中因為使用次數頻繁而做出的改變。
// src/vm.c
#include <stdio.h>
#include "utils.h"
int mrb_exec(const uint8_t* data) {
const uint8_t* p = data;
p += 4;
uint16_t nlocals = bin_to_uint16(p);
p += 2;
uint16_t nregs = bin_to_uint16(p);
p += 2;
uint16_t nirep = bint_to_uint16(p);
p += 2;
printf("nlocals = %d, nregs = %d, nirep = %d", nlocals, nregs, nirep);
return 0;
} }
在設計這個區塊的時候,我參考了 mruby 的原始碼發現會將 irep
資訊封裝到 mrb_irep
的結構中,因此在這邊也做了相同的處理。
// src/irep.h
typedef struct mrb_irep {
uint16_t nlocals;
uint16_t nregs;
uint16_t rlen;
} mrb_irep;
mrb_irep* read_irep(const uint8_t* src, uint8_t* len);
我們定義了一個精簡版的 mrb_irep
結構,並且只儲存我們所需要的 nlocals
、nregs
和 nirep
資訊作為回傳。
// src/irep.c
mrb_irep* read_irep(const uint8_t* src, uint8_t* len) {
const uint8_t* bin = src;
// 使用 malloc 分配記憶體以免被自動釋放
mrb_irep* irep = (mrb_irep*)malloc(sizeof(mrb_irep));
// Skip record (+4)
bin += sizeof(uint32_t);
irep->nlocals = bin_to_uint16(bin);
bin += sizeof(uint16_t); // +2
irep->nregs = bin_to_uint16(bin);
bin += sizeof(uint16_t); // + 2
// Child ireps
irep->rlen = bin_to_uint16(bin);
bin += sizeof(uint16_t); // +2
*len = (uint8_t)(bin - src);
return irep;
}
如此一來我們就可以用 read_irep
一次性的將 IREP 資訊讀取完畢,裡面我們改為使用 sizeof(uint16_t)
的方式來移動指標,這相對於直接 += 2
更容易理解意圖,不過跟 mruby 實作的 PEEK_B
這樣的巨集比起來還是看起來非常髒亂。
除此之外因為我們使用了 malloc
分配記憶體,所以就需要負起回收記憶體的責任,我們需要安排一個適當的時機點將這些記憶體釋放掉。
因為在我們實作的 VM 中因為功能大多非常簡單,實際上並不需要像 mruby 一樣製作多個
mrb_irep
資料結構來儲存資訊,在微控制器有限的資源前提下就會變成是記憶體跟 CPU 之間的取捨,因為我們還是可以透過每次重新讀取來獲得資訊。
因為我們的首要目標是從 IREP 中取得 ISEQ 並且讀取到第一個 OPCode 來實現後續的虛擬機,但現階段我們只讀取到了 IREP 的資訊,因此在 read_irep
的方法中我又再加入了跳過 ISEQ 長度資訊以及對齊的處理,來讓我們可以在指標的下一個數值獲取到 OPCode 而不是需要再次跳過一部分資料。
// src/irep.c
static size_t skip_padding(const uint8_t* buf) {
const size_t align = sizeof(uint32_t);
return -(intptr_t)buf & (align - 1);
}
mrb_irep* read_irep(const uint8_t* src, uint8_t* len) {
// ...
// ISEQ Blocks
// TODO: Save ISEQ
bin += sizeof(uint32_t); // +4
bin += skip_padding(bin);
*len = (uint8_t)(bin - src);
return irep;
}
在前面的文章有提到 mruby 在 IREP 的資料區段有一部分是需要對齊處理的,因為目前只有被 src/irep.c
使用到,因此我們增加了一個 skip_padding
的方法來幫助我們用跟 mruby-L1VM 類似的處理資料的對齊。
如此一來我們在 mrb_exec
方法中再下一個讀取到的就會是 ISEQ 中第一個 OPCode 了!
在調整之前,我們先修改一下巨集讓 printf
用 LOG
巨集來替代,這是為了在微控制器無法使用 printf
時可以定義其他的行為作為替代,或者未來需要隱藏除錯訊息時可以利用編譯器來切換。
// src/utils.h
#define LOG(...) printf(__VA_ARGS__)
// ...
這樣我們就能夠用 LOG("nlocals: %d\n")
代替 printf
的使用,之後也更容易修改成其他輸出格式。
#include "irep.h"
#include "utils.h"
int mrb_exec(const uint8_t* data) {
const uint8_t* src = data;
uint8_t len;
mrb_irep* irep = read_irep(src, &len);
src += len;
LOG("DEBUG> locals: %d, regs: %d, ireps: %d\n", irep->nlocals, irep->nregs, irep->rlen);
}
最後稍微調整我們的 mrb_exec
方法即可,到了這個階段我們就已經有一個可以開發 Ruby VM 的雛形,下一篇我們會正式讀取 OPCode 並且加入一些基本的處理。