iT邦幫忙

第 12 屆 iT 邦幫忙鐵人賽

DAY 7
0
IoT

拿到錘子的我想在微控制器上面執行 Ruby系列 第 7

Day 7 - 讀取 IREP 資料(二)

延續上一篇的進度,我們將會開始撰寫 mrb_exec 的內容,我們先參考 mruby-L1VM 的方式直接讀取用於參考的 nlocalsnregsnirep 三個數值出來。

bin_to_uint32 和 bin_to_uint16

讀取 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_uint32bin_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 結構,並且只儲存我們所需要的 nlocalsnregsnirep 資訊作為回傳。

// 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 之間的取捨,因為我們還是可以透過每次重新讀取來獲得資訊。

處理 ISEQ 和對齊

因為我們的首要目標是從 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 了!

調整 mrb_exec

在調整之前,我們先修改一下巨集讓 printfLOG 巨集來替代,這是為了在微控制器無法使用 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 並且加入一些基本的處理。


上一篇
Day 6 - 讀取 IREP 資料(ㄧ)
下一篇
Day 8 - 加法 OPCode 處理
系列文
拿到錘子的我想在微控制器上面執行 Ruby30

尚未有邦友留言

立即登入留言