ローファイ日記

出てくるコード片、ぼくが書いたものは断りがない場合 MIT License としています http://udzura.mit-license.org/

mruby 3.2.0 のバイナリフォーマット

なんとなくバイナリを解析してえ〜と思ったので、mruby 3.2.0 (最新stable?)の .mrb ファイルのフォーマットを眺めることにした。

Rustでパースしました!だとかっこいい、ナウだなと思ったけれど、動的型育ちな自分をどうしても甘やかしてしまい、 Rubyunpack を軸に解析した。いやほんと、今やRubyで一番使うメソッドでは。

こういう感じのRubyスクリプトがある。

puts "Hello"

mrbファイルにコンパイルするとこうなるらしい。この際、デバッグセクションとLVセクションは無かったことにする。今度ね。

$ ./bin/mrbc --remove-lv tmp/hello.rb
$ xxd tmp/hello.mrb                  
00000000: 5249 5445 3033 3030 0000 0056 4d41 545a  RITE0300...VMATZ
00000010: 3030 3030 4952 4550 0000 003a 3033 3030  0000IREP...:0300
00000020: 0000 002e 0001 0004 0000 0000 0000 000a  ................
00000030: 5102 002d 0100 0138 0169 0001 0000 0548  Q..-...8.i.....H
00000040: 656c 6c6f 0000 0100 0470 7574 7300 454e  ello.....puts.EN
00000050: 4400 0000 0008                           D.....

このバイナリはざっくりとこういう感じで分割される。

  • 最初のオレンジがバイナリヘッダ
  • セクションが二つある
  • 濃い赤がirepセクションヘッダ
  • 薄い赤がセクションボディ
  • 青はendセクション(ヘッダのみ)

バイナリヘッダ

/* binary header */
struct rite_binary_header {
  uint8_t binary_ident[4];    /* Binary Identifier */
  uint8_t major_version[2];   /* Binary Format Major Version */
  uint8_t minor_version[2];   /* Binary Format Minor Version */
  uint8_t binary_size[4];     /* Binary Size */
  uint8_t compiler_name[4];   /* Compiler name */
  uint8_t compiler_version[4];
};

irbなどを使って簡単に取り出せる。なお、数値であっても uint8_t[4] や uint8_t[2] で扱われ、ビッグエンディアンで解釈される。

data = File.read "tmp/hello.mrb"
data[0,20]
=> "RITE0300\u0000\u0000\u0000VMATZ0000"
data[0,20].unpack("Z4 Z2 Z2 I> Z4 Z4")
=> ["RITE", "03", "00", 86, "MATZ", "0000"]

注目ポイントは compiler_name で、 MATZ 実装以外のコンパイラも将来は...???

irepセクション

mrubyの load.c の実装を見る限り、残りバイト数が sizeof(struct rite_section_header) 未満になるまでバイナリをパースし続ける感じ(なので後ろに短いゴミデータがあっても実は無視される)。

/* section header */
#define RITE_SECTION_HEADER \
  uint8_t section_ident[4]; \
  uint8_t section_size[4]

struct rite_section_header {
  RITE_SECTION_HEADER;
};

struct rite_section_irep_header {
  RITE_SECTION_HEADER;

  uint8_t rite_version[4];    /* Rite Instruction Specification Version */
};

// 他のセクションヘッダは実は struct rite_section_header と同様っぽい

セクションの最初4バイトが IREP なら irepヘッダとして処理される。

cur = 20 # sizeof struct rite_binary_header
data[cur, 12]
=> "IREP\u0000\u0000\u0000:0300"
data[cur, 12].unpack("Z4 I> Z4")
=> ["IREP", 58, "0300"]

58 というのはヘッダ先頭からのセクション全体のサイズである。

data[cur, 58]
=> "IREP\u0000\u0000\u0000:0300\u0000\u0000\u0000.\u0000\u0001\u0000\u0004\u0000\u0000\u0000\u0000\u0000\u0000\u0000\nQ\u0002\u0000-\u0001\u0000\u00018\u0001i\u0000\u0001\u0000\u0000\u0005Hello\u0000\u0000\u0001\u0000\u0004puts\u0000"

この内容を read_irep_record_1() という関数で前から順番に解釈している。のだが unpack で表現できるので:

# record size, nlocals, nregs, rlen, clen, ilen, 
data[cur+12..].unpack("I> S> S> S> S> I>")
=> [46, 1, 4, 0, 0, 10]

ilen が10と分かったので:

data[cur+12+16..].unpack("c10")
=> [81, 2, 0, 45, 1, 0, 1, 56, 1, 105]

mrbc --verbose の結果と比較すると

    1 000 STRING        R2      L(0)    ; Hello
    1 003 SSEND         R1      :puts   n=1
    1 007 RETURN        R1
    1 009 STOP

mruby/ops.h で順番を数えたけど合ってそう(STRING=81 SSEND=45 RETURN=56 STOP=105)。

残りは pool, syms なんだけど:

data[20+12+16+10..]
=> "\u0000\u0001\u0000\u0000\u0005Hello\u0000\u0000\u0001\u0000\u0004puts\u0000END\u0000\u0000\u0000\u0000\b"

pool はこういう感じなので:

  • \u0000\u0001 = poolの要素数1
  • \u0000 = pool内の要素の型(ここでは IREP_TT_STR
  • \u0000\u0005 = pool にある文字列の長さ(5、終端文字含まず)
  • Hello\u0000 = pool内の文字列

参考:

// in irep.h
enum irep_pool_type {
  IREP_TT_STR   = 0,          /* string (need free) */
  IREP_TT_SSTR  = 2,          /* string (static) */
  IREP_TT_INT32 = 1,          /* 32bit integer */
  IREP_TT_INT64 = 3,          /* 64bit integer */
  IREP_TT_BIGINT = 7,         /* big integer (not yet supported) */
  IREP_TT_FLOAT = 5,          /* float (double/float) */
};

残った \u0000\u0001\u0000\u0004puts\u0000 が symsで:

  • \u0000\u0001 = sym数(1
  • \u0000\u0004 = シンボル名の長さ(4、終端文字含まず)
  • puts\u0000 = シンボルの文字列表現

とのこと(Cコードを読む限りそう)。

2つ目、かつ最後のセクションは END\u0000\u0000\u0000\u0000\u0008 *1 だけど:

#define RITE_BINARY_EOF "END\0"

section_header = (const struct rite_section_header *)bin;
// ...
else if (memcmp(section_header->section_ident, RITE_BINARY_EOF, sizeof(section_header->section_ident)) != 0) {
  break;
}

こういう感じなので、 struct rite_section_header としてキャストできればOKという番兵な感じ。


mruby のバイナリとOPCODEを解説した本として、Kishima先生の「mrubyバイトコードハンドブック」があるが mruby 2.0.0 対応版である。それでも大変参考になる。

booth.pm


過去の自分を見ていると定期的にバイナリを解析している。次はWASMバイナリを解析するんだと思う。

udzura.hatenablog.jp

ということで明日からのRubyKaigi、皆様楽しんでください(唐突)。

*1:\b = \u0008