ククログ

株式会社クリアコード > ククログ > mrubyのVMのデバッグ方法

mrubyのVMのデバッグ方法

全文検索エンジンのGroongaでmrubyを使っているのですが、たまにCRubyと異なる挙動に遭遇することがあります。このようなときはmrubyに問題があります。特定のメソッドの挙動がおかしいときはmrubyのライブラリーの実装に問題があります。構文の使い方で挙動がおかしいときはmrubyのVMに問題があります。

mrubyの問題に遭遇するときは、なぜかmrubyのVMに問題があるケースばかり1なのですが、久しぶりにmrubyのVMにある問題を直そうとしたらデバッグの仕方を忘れていました。忘れても後から思い出せるようにメモしておきます。

詳細は後述しますが、基本的な流れは次の通りです。

  1. 問題を再現するスクリプトを作る。

  2. mrubyをデバッグビルドする。

  3. mrbc -vでスクリプトの構文木(?)を確認する。

  4. GDB上でmruby XXX.rbを走らる。

  5. src/vm.cの中で問題のラベルがあるところにブレークポイントを設定する。

  6. 問題があるデータを見つけて直す。

問題を再現するスクリプトを作る

すでに問題を見つけているはずなので、それを独立させ、できるだけ小さいコードで再現するようにします。このあいだ遭遇した問題2は次のスクリプトで再現しました。

class A
  def x
    yield
  ensure
    y
  end

  def y
  end
end

# Work
A.new.x do
end

# Not work
# trace:
#   [2] /tmp/a.rb:5:in A.x
#   [0] /tmp/a.rb:15
# /tmp/a.rb:5: undefined method 'y' for main (NoMethodError)
A.new.x do
  break
end

# trace:
#   [2] /tmp/a.rb:5:in A.call
#   [0] /tmp/a.rb:19
# /tmp/a.rb:5: undefined method 'y' for main (NoMethodError)
lambda do
  A.new.x do
    return
  end
end.call

yieldで呼ばれたブロックの中からbreakまたはreturnすると、ensureの中のselfがおかしくなるという問題です。

特定のメソッドがおかしな結果を返すという類のものではないので、mruby本体の問題である可能性が高いです。

問題を再現するスクリプトはできたので次へ進みます。

mrubyをデバッグビルドする

mruby本体の問題である場合、mrubyをGDB上で実行し、内部状態を確認しながら原因を調べる必要があります。GDBで実行する場合、-g3オプション付きでビルドするとマクロも展開できて便利です。

以前は手動でtasks/toolchains/gcc.rakeを変更して-g3-O0を追加していましたが、面倒になったのでpull requestを送って取り込んでもらいました。今はbuild_config.rbenable_debugと書いておくと勝手に-g3-O0オプションをつけてくれます。

なお、デフォルトのbuild_config.rbにはenable_debugが入っているので特に変更せずにビルドするだけでよいです。

mrbc -vでスクリプトの構文木(?)を確認

GDBでどのあたりを確認すればよいかのアタリをつけるために、mrubyのVMがどのようにスクリプトを実行するかを確認します。

mrbc -v(またはmruby -v)でRubyスクリプトを実行すると構文木(?)3が出力されます。

例えば、次のRubyスクリプトを入力にするとします。

class A
  def x
    yield
  ensure
    y
  end

  def y
  end
end

A.new.x do
  break
end

これをmrbc -vに渡すと次のような出力になります。

mruby 1.1.0 (2014-11-19) 
00001 NODE_SCOPE:
00001   NODE_BEGIN:
00001     NODE_CLASS:
00010       :A
00010       body:
00002         NODE_BEGIN:
00002           NODE_DEF:
00006             x
00006             NODE_ENSURE:
00006               body:
00003                 NODE_BEGIN:
00003                   NODE_YIELD:
00006               ensure:
00005                 NODE_BEGIN:
00005                   NODE_CALL:
00005                     NODE_SELF
00005                     method='y' (220)
00008           NODE_DEF:
00009             y
00009             NODE_BEGIN:
00012     NODE_CALL:
00012       NODE_CALL:
00012         NODE_CONST A
00012         method='new' (11)
00012       method='x' (219)
00014       args:
00014       block:
00012         NODE_BLOCK:
00014           body:
00013             NODE_BEGIN:
00013               NODE_BREAK:
irep 0xe7b640 nregs=3 nlocals=1 pools=0 syms=3 reps=2
file: /tmp/a.rb
    1 000 OP_LOADNIL	R1		
    1 001 OP_LOADNIL	R2		
    1 002 OP_CLASS	R1	:A
    1 003 OP_EXEC	R1	I(+1)
   12 004 OP_GETCONST	R1	:A
   12 005 OP_SEND	R1	:new	0
   12 006 OP_LAMBDA	R2	I(+2)	2
   12 007 OP_SENDB	R1	:x	0
   12 008 OP_STOP

irep 0xe7bd70 nregs=3 nlocals=1 pools=0 syms=2 reps=2
file: /tmp/a.rb
    2 000 OP_TCLASS	R1		
    2 001 OP_LAMBDA	R2	I(+1)	1
    2 002 OP_METHOD	R1	:x
    8 003 OP_TCLASS	R1		
    8 004 OP_LAMBDA	R2	I(+2)	1
    8 005 OP_METHOD	R1	:y
    8 006 OP_LOADSYM	R1	:y
    8 007 OP_RETURN	R0	return

irep 0xe962d0 nregs=3 nlocals=2 pools=0 syms=1 reps=1
file: /tmp/a.rb
    2 000 OP_ENTER	0:0:0:0:0:0:0
    6 001 OP_EPUSH	:I(+1)
    3 002 OP_BLKPUSH	R2	0:0:0:0
    3 003 OP_SEND	R2	:call	0
    3 004 OP_EPOP	1
    3 005 OP_RETURN	R2	return

irep 0xe9c0f0 nregs=3 nlocals=1 pools=0 syms=1 reps=0
file: /tmp/a.rb
    5 000 OP_LOADSELF	R1		
    5 001 OP_SEND	R1	:y	0
    5 002 OP_RETURN	R0	return

irep 0xe9d4d0 nregs=3 nlocals=2 pools=0 syms=0 reps=0
file: /tmp/a.rb
    8 000 OP_ENTER	0:0:0:0:0:0:0
    9 001 OP_LOADNIL	R2		
    9 002 OP_RETURN	R2	return

irep 0xe9d600 nregs=2 nlocals=1 pools=0 syms=0 reps=0
file: /tmp/a.rb
   13 000 OP_RETURN	R1	break

まずは以下のツリー状の方を確認します。

00001 NODE_SCOPE:
...

問題が発生するときに実行するコードは以下でした。

A.new.x do
  break
end

これに対応するサブツリーを見つけます。breakなどコードの中にあるキーワードに注目して見つけます。見つけやすいようにコードの中にリテラルを入れてもいいでしょう。

今回の場合は以下のサブツリーが対応します。

00012     NODE_CALL:
00012       NODE_CALL:
00012         NODE_CONST A
00012         method='new' (11)
00012       method='x' (219)
00014       args:
00014       block:
00012         NODE_BLOCK:
00014           body:
00013             NODE_BEGIN:
00013               NODE_BREAK:

今回はbreakが怪しいので次の部分に注目します。

00013             NODE_BEGIN:
00013               NODE_BREAK:

特に左側の13の部分に注目します。これに対応する数値を、ツリーの後に出力されている次の部分の中から見つけます。

irep 0xe7b640 nregs=3 nlocals=1 pools=0 syms=3 reps=2
...

今回の場合は次の箇所です。

irep 0xe9d600 nregs=2 nlocals=1 pools=0 syms=0 reps=0
file: /tmp/a.rb
   13 000 OP_RETURN	R1	break

ここにあるOP_XXXに注目します。今回の場合はOP_RETURNです。これがわかったら次に進みます。

GDB上でmruby XXX.rbを走らる

GDB上で問題のあるスクリプトを実行します。

% gdb --args bin/mruby XXX.rb
...
(gdb)

プログラムを実行する前にブレークポイントを設定します。

src/vm.cの中で問題のラベルがあるところにブレークポイントを設定する

どこにブレークポイントを設定すればよいかというと、src/vm.cの中のOP_XXXがある場所です。

今回だと次の箇所です。

1440      CASE(OP_RETURN) {
1441        /* A B     return R(A) (B=normal,in-block return/break) */

GDB上でブレークポイントを設定します。

(gdb) b vm.c:1440

ブレークポイントを設定したら実行します。

(gdb) r

ブレークポイントで止まるので後は問題があるデータを見つけて、正しいデータになるようにします。

なお、GDB上でC-x C-aを実行するとソースコード表示あり・なしを切り替えられるので必要に応じて使いわけます。出力したい内容が多い場合はソースコード表示をなしにして表示領域を広くとり、ステップ実行をする場合はソースコードを表示しながら流れを追いやすくします。

問題があるデータを見つけて直す

問題箇所の付近をステップ実行できるようになったので、mrb_p()でデータを確認しながら問題を見つけます。

(gdb) call mrb_p(mrb, v)
Array

問題を見つけるやり方の1つは、正常なケースのスクリプトも用意して動作を比較する方法です。期待する結果がわかるので問題のあるデータを見つけやすくなります。

別のやり方は、再現スクリプトからわかった問題の値を探す方法です。今回の場合は次のようなエラーメッセージがでているため、selfがおかしくなっていることがわかります。

# trace:
#   [2] /tmp/a.rb:5:in A.x
#   [0] /tmp/a.rb:15
# /tmp/a.rb:5: undefined method 'y' for main (NoMethodError)
A.new.x do
  break
end

問題があるデータを見つけたら後は直すだけです。

まとめ

久しぶりにmrubyを直そうとするとやり方を忘れていたので後で思い出せるようにやり方をまとめました。

必要ならmruby本体の問題も修正できるので、mrubyをアプリケーションに組み込みたいので技術支援して欲しい、という場合はご相談ください。

  1. 普通にRubyで書いているつもりなのですが、特殊な書き方をしているのかもしれません。

  2. pull requestはマージ済みなのでmasterでは直っています。

  3. なんと呼ぶのがよいのでしょうか。