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

この投稿はmruby Advent Calendar 2013の3日目の記事です。
2日目はsuzukazeさんのmruby-redisでランキングを実装、4日目はdycoonさんのGUIアプリケーションなどが保持するmrubyのオブジェクトのGC対策です。

はじめに

mrubyのエンジンはソースコードを逐次解釈して実行しているわけではなく、専用のバイトコードに変換して保持し、それをVirtualMachineの上で解釈して実行しています。
であれば当然コンパイル済みバイナリファイルというのも存在するわけで、ならば当然どんな仕組みになっているか気になるわけです。

というわけで、大まかなファイルフォーマットについて調べたので今回説明しようと思います。

2013/11/25当時(git hash: f5bd87b9e6d0d8a84cf866b4847c1416e4f5c622 )の物です。
それ以降のmrubyを使用する場合は以下の解説の通りではない可能性があります。

全体の構造

ヘッダーとセッションの配列からなります。
ヘッダーは必ず一つ、セッションは複数個ですが必ず終端セクションで終わります。

ヘッダー

C風に書くとこんな感じ

ubig8_t signature[4];
ubig8_t version[4];
ubig16_t crc;
ubig32_t size;
ubig8_t compiler_name[4];
ubig8_t compiler_version[4];

※ubig%d_t: 符号なし %d bitビッグエンディアンの整数です、ただしubig8_t配列は文字列の場合もあります

signature: 0x52 0x49 0x54 0x45固定(ASCIIでRITE)
version: 0x30 0x30 0x30 0x32固定(ASCIIで0002)
crc:

uint16_t CalcCrc(const uint8_t * const ptr, const unsigned int size) {
  unsigned int a = 0;
  for (unsigned int i = 0; i < size; i++) {
    a |= ptr[i];
    for (unsigned int l = 0; l < 8; l++) {
      a = a << 1;
      if (a & 0x01000000) {
        a ^= 0x01102100;
      }
    }
  }
  return a >> 8;
}

※ptrはファイル全体のうちヘッダーのはじめからcrcまでを除いた部分、sizeはそのバイト数

こんな感じらしいのだが実際動かしてみるとずれるのでたぶんエンバグしている、が、めんどいので調べていない。
mrubyの/src/crc.cに実装があるので、知りたい人はそちらを見るとよい。

size: ファイル全体の長さ、ヘッダーも含む
compiler_name: コンパイルに使われたコンパイラーの識別子、特に使用されていない
compiler_version: コンパイルに使われたコンパイラーのバージョン、特に使用されていない

セクション

セクションは全部で3種類存在します。

  • 実行コード本体
  • 元となったソースコードの行番号情報
  • 終端セクション

詳しい説明は後で書くとして、共通のセクションヘッダーは以下の通りです。

ubig8_t signature[4];
ubig32_t size;

signature: セクションの識別子です、ここでセクションが何の情報を持つかが分岐します、0x49 0x52 0x45 0x50(ASCIIでIREP)か0x4C 0x49 0x4E 0x45(ASCIIでLINE)か0x45 0x4E 0x44 0x00(ASCIIでEND\0)のどれかになります。
size: セクションのサイズです、共通のセクションヘッダーのサイズも含みます。

linenoセクション

元となったソースコードのライン番号情報です、signatureは0x4C 0x49 0x4E 0x45(ASCIIでLINE)になります。
linenoの追加ヘッダー以下の通りです。

ubig16_t count;
ubig16_t start;

count: ライン情報の個数です。
start: よくわかりません。

ここにはlinenoの詳細を書くべきなのでしょうが、ぶっちゃけ興味なかったので調べておらずわからないのですっ飛ばします。

endセクション

終端セクションです、signatureは0x45 0x4E 0x44 0x00(ASCIIでEND\0)になります。
追加ヘッダーは特にありません。

このセクションが存在した時点でセクションの読み取りは終了となります。
一応ファイルサイズなどの情報もあるのでこの情報はなくても実行できますが、おそらくは利便性か速度上の都合なのでしょう。

irepセクション

実行コード本体です、signatureは0x49 0x52 0x45 0x50(ASCIIでIREP)になります。
irepの追加ヘッダー以下の通りです。

ubig8_t version[4];

version: irepの命令セットのバージョンです、現在は0x30 0x30 0x30 0x30(ASCIIで0000)固定です。

また、irepデータ本体を1つ以上持ちます。
いくつになるかは実際にirepデータを読んでみるまでわかりません。

irepデータ

関数1つやクラスの定義など1スコープの処理をまとめたものです。
定義は以下の通り

ubig32_t size;
ubig16_t localCount;
ubig16_t registerCount;
ubig16_t childCount;
ubig32_t code_count;
Code code[code_count];
ubig32_t pool_count;
Pool pool[pool_count];
ubig32_t symbol_count;
Symbol symbol[symbol_count];

size: irepデータのサイズです、sizeそのものも含みます。
localCount: ローカル変数の個数らしいですが何に使うのかわかりませんでした。
registerCount: このirepデータを実行するのに必要なレジスタの数です。
childCount: このirepに紐づく子のirepデータの数です。
code_count: irepバイトコードの個数です。
code: irepバイトコードです。
pool_count: リテラルプールの数です。
pool: リテラルプールです。
symbol_count: シンボルリストの数です。
symbol: シンボルリストです。

さらにchildCountの分だけ、irepデータが後に続きます。
それらのirepデータにも当然childCountがついているので、childCountが1の場合でも何十個とirepデータが続いている可能性があります。

リテラルプール

コード中で使用する文字列/整数/実数のリテラルを保持しています。
各リテラルはフォーマット上は文字列として保管され、パース時点でmrb_valueへと変換されます。
定義は以下の通り

ubig8_t type;
ubig16_t len;
ubig8_t body[len];

type: mrb_vtypeの値がそのまま入っており、0:文字列、1:整数、2実数となります。
len: bodyの長さです。
body: 文字列で表現されたリテラルで、終端記号(\0)は含まれていません。

シンボルリスト

コード中で使用するシンボル(メソッド名やクラス名など)を保持しています。
各シンボルはフォーマット上は文字列として保管され、パース時点でmrb_symへと変換されます。
定義は以下の通り

ubig16_t len;
ubig8_t body[len];
ubig8_t terminate;

len: bodyの長さです。
body: 文字列で表現されたシンボルです、リテラルと違いこちらは続くterminateが必ず0x00固定なのでC言語文字列として扱うことができます。
terminate: 必ず0x00固定です、bodyをC言語文字列として扱うためで、実際load.cを覗いてみるとchar *にキャストして使用されています。

irepバイトコード

VM上で実行できるバイトコードです。
すべての命令がオペコードと引数を合わせて4byteとなっています。
書くまでもないですが、定義は以下の通り

ubig32_t body;

body & 0x0000007F: オペコードです、2013/11/25時点では0~75まで定義されています。
body & 0xFFFFFF80: 引数ですが、オペコードにより引数の数とサイズが異なります。

各オペコードの説明まで始めるとそれだけで本一冊書けてしまうのと、ファイルフォーマットとは関係が薄いためここでは解説しません。

終わりに

いかがだったでしょうか。
実はオペコード関連も多少は読んでいて、ローカル変数はインデックス管理で名前が消滅しているとか面白い話もあったのですが、文中でも書いたように一冊本が書ける勢いなので省略しました。
mrubyを使う上ではあまり役に立たないかもしれませんが、個人的には面白かったです。

おまけとして、上記通りにパースして、ちょっとだけ命令の翻訳も行うようなアプリを書いてみたのでおいておきます。

parse_mruby2.zip

ではまた。

※2014/05/14付けでLVARセクションが追加されました、それらについての追加情報は以下を参照のこと。
rubyのバイトコードフォーマット解説その2