iT邦幫忙

第 12 屆 iT 邦幫忙鐵人賽

DAY 8
1
IoT

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

Day 8 - 加法 OPCode 處理

我們已經在 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/ironman2021feature/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 裡面如果讀取的數字是 -17 的話會有獨立的 OPCode 處理,除此之外的數值則會由 OP_LOADI 讀取正整數跟用 OP_LOADINEG 處理負數。

接下來我們對 src/vm.c 增加對應的處理,這邊 mruby-L1VM 利用了 OPCode 是連續數字的特性將 OPLOADI__1OP_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 語法支援。


上一篇
Day 7 - 讀取 IREP 資料(二)
下一篇
Day 9 - 顯示運算結果
系列文
拿到錘子的我想在微控制器上面執行 Ruby30

尚未有邦友留言

立即登入留言