mrubyのバイトコードフォーマット解説その2

2014/05/14、mrubyでローカル変数情報を扱うようになりlocal_variablesメソッドも使用可能に。
それに伴いバイトコードのフォーマットも変更となり、新たにLVARセクションが追加されました。

そこで今回は追加されたLVARセクションについて、簡単にフォーマット紹介をしようと思います。

過去の記事の続きなので、まだの人はそちらからどうぞ。

いつものようにサンプルコード
http://wordpress.click3.org/garakuta/parse_mruby3.zip
内容は過去の記事の物をベースにローカル変数情報を追加してあります。
動作としては、parse_mruby.exeに.mrbファイルをD&Dするとローカル変数情報とirep情報を表示する、というものです。

この記事で対象としているmrubyは2014/05/17当時(git hash: 13db4da204d2bedec4c0c5de939e662a44d477a6 )の物です。
それ以降のmrubyを使用する場合は以下の解説の通りではない可能性があります。

この記事の目次

セクション

ローカル変数は新設されたセクションに含まれています。
過去の記事でも書いていますがセクションには共通のセクションヘッダーと呼ぶべきものを含んでおり、それは以下の通りになります。

ubig8_t signature[4];
ubig32_t size;

LVARセクション

ローカル変数名を保持するセクションで、signatureは0x4C 0x56 0x41 0x52(ASCIIでLVAR)になります。
LVARセクション専用の追加ヘッダーは特に存在しません。

LVARセクションは一つのmrbファイル一つにつき一つのみ存在し、複数のメソッドが含まれている場合はそれらすべてのローカル変数情報を持ちます。
LVARセクションはセクションヘッダーを除くと二つの領域からなりローカル変数名を表すシンボルテーブルと、各レジスタとシンボルを紐づけるレコードテーブルに分けられます。

シンボルテーブル

ローカル変数名のシンボルを格納した領域。
含まれるirepすべてのローカル変数名情報のリストであり、重複も排除されている。
構造は以下の構造の通りになる。

ubig32_t count;
struct symbol {
  ubig16_t len;
  ubig8_t body[len];
} symbolList[count];

サンプル内で実際に読み込んでいるコードは以下の通り。

  static boost::shared_ptr<const LocalVariableSymbolList> Read(const uint8_t *ptr, unsigned int size) {
    boost::shared_ptr<const LocalVariableSymbolList> result;
    if (size < 4) {
      return result;
    }
    const unsigned int count = *reinterpret_cast<const endian::ubig32_t *>(ptr);
    ptr += 4;
    size -= 4;
    if (count * 1 > size) {
      return result;
    }
    std::vector<std::string> list(count);
    BOOST_FOREACH(std::string &str, list) {
      if (size < 2) {
        return result;
      }
      const unsigned int len = *reinterpret_cast<const endian::ubig16_t *>(ptr);
      ptr += 2;
      size -= 2;
      if (len > size) {
        return result;
      }
      str.assign(reinterpret_cast<const char *>(ptr), len);
      ptr += len;
      size -= len;
    }
    result.reset(new LocalVariableSymbolList(list));
    return result;
  }

最初にシンボルの個数を取得し、その個数分だけループをまわして文字列長を取得した後その長さだけ文字列として読み込み、最後にそれを保持するインスタンスを作成して返しているだけです。
irepのシンボルテーブルと違い終端文字列は含まない点に注意。
また現在の版だとブロック引数に(たとえ使用していなくても)常に空文字列がローカル変数名として割り当てられており、それがこのシンボルテーブルにも含まれていることがあります。

レコードテーブル

ローカル変数シンボルと実際にローカル変数が保持されているレジスタを紐づけるテーブル。
irepデータの数だけ存在し、深さ優先探索的にならんでいる。
構造は以下の通り

ubig16_t symbolIndex;
ubig16_t registerIndex;

symbolIndexは前述のシンボルテーブル内のインデックス、registerIndexはirep内の該当するローカル変数に紐づくレジスタのインデックスです。
サンプルで実際に読み上げている処理は以下の通り。

  static boost::shared_ptr<const LocalVariableData> Read(const uint8_t *ptr, unsigned int size, const boost::shared_ptr<const IrepRecord> irep, const boost::shared_ptr<const LocalVariableSymbolList> symbolList) {
    boost::shared_ptr<const LocalVariableData> result;
    if (static_cast<unsigned int>(2 + 2) * (irep->header->localCount - 1) > size) {
      return result;
    }
    std::vector<Record> records(irep->header->localCount - 1);
    BOOST_FOREACH(Record &record, records) {
      const unsigned int nameIndex = *reinterpret_cast<const endian::ubig16_t *>(ptr);
      if (nameIndex > symbolList->list.size()) {
        return result;
      }
      ptr += 2;
      size -= 2;
      record.name = &symbolList->list[nameIndex];
      record.registryIndex = *reinterpret_cast<const endian::ubig16_t *>(ptr);
      ptr += 2;
      size -= 2;
    }
    std::vector<boost::shared_ptr<const LocalVariableData> > childs(irep->child.size());
    for (unsigned int i = 0; i < childs.size(); i++) {
      const boost::shared_ptr<const LocalVariableData> child = Read(ptr, size, irep->child[i], symbolList);
      if (!child) {
        return result;
      }
      childs[i] = child;
      ptr += child->GetSize();
      size -= child->GetSize();
    }
    result.reset(new LocalVariableData(records, childs));
    return result;
  }

最初に該当するirepのローカル変数の数を取得している。
-1しているのは自信を指すselfの分は含まれないためだ。

次はローカル変数の数だけレコードを読み込み、シンボルとの関連付けを解決している。

その後子のirepに対して再帰実行して読み込んだのち、データを保持するインスタンスを作成して返している。

irepとの紐づけ

irepとの紐づけといってもレコードテーブルの時点で終わっているも同然です。
すでにirepごとに変数の紐付けは終わっており、あとはirepでの処理の際に該当するレコードからシンボルを引っ張ってくるだけです。

一応、サンプルにおけるローカル変数情報の表示部分を紹介します。

bool PrintLocalVariable(const boost::shared_ptr<const LocalVariableData> data, std::vector<unsigned int> &index) {
  std::cout << boost::format("lvar%s\n") % std::accumulate(index.begin(), index.end(), std::string(), IndexListJoin());
  BOOST_FOREACH(const LocalVariableData::Record &record, data->records) {
    std::cout << boost::format("reg[%d] : %s") % record.registryIndex % *record.name << std::endl;
  }
  std::wcout << L"\n";
  unsigned int i = 0;
  BOOST_FOREACH(const boost::shared_ptr<const LocalVariableData> &child, data->childs) {
    index.push_back(i);
    if (!PrintLocalVariable(child, index)) {
      return false;
    }
    index.pop_back();
    i++;
  }
  return true;
}

indexは階層表示用の配列でlvar->child[3]->child[2]->child[1]のirepのローカル変数情報を表示する際には[3, 2, 1]と保持しており、それを文字列に変換して使用しています。
あとは単にforeachで配列を展開して表示し、childがいれば再帰して呼び出しているだけです。

終わりに

いかがだったでしょうか。
新しくセクションが増えるというそれなりに大きな変更ですが、実はバージョン番号などには変化がありません。
Matzさん曰く「sectionが追加されただけで、かつ知らないsectionは読み飛ばす(はず)ですから、変更しなくても大丈夫だと思いました。」とのことで、過去の版で動作する限りは特にバージョンは上がらないようです。

今回の更新でローカル変数をevalで取得するようなコードも動くようになったので、また一歩rubyとの互換性が上がったと言えます。
この記事を読んで少しでもmrubyの動作について理解が深まれば幸いです。
では、また。

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です