全文検索エンジンのGroongaでmrubyを使っているのですが、たまにCRubyと異なる挙動に遭遇することがあります。このようなときはmrubyに問題があります。特定のメソッドの挙動がおかしいときはmrubyのライブラリーの実装に問題があります。構文の使い方で挙動がおかしいときはmrubyのVMに問題があります。
mrubyの問題に遭遇するときは、なぜかmrubyのVMに問題があるケースばかり1なのですが、久しぶりにmrubyのVMにある問題を直そうとしたらデバッグの仕方を忘れていました。忘れても後から思い出せるようにメモしておきます。
詳細は後述しますが、基本的な流れは次の通りです。
-
問題を再現するスクリプトを作る。
-
mrubyをデバッグビルドする。
-
mrbc -v
でスクリプトの構文木(?)を確認する。 -
GDB上で
mruby XXX.rb
を走らる。 -
src/vm.c
の中で問題のラベルがあるところにブレークポイントを設定する。 -
問題があるデータを見つけて直す。
問題を再現するスクリプトを作る
すでに問題を見つけているはずなので、それを独立させ、できるだけ小さいコードで再現するようにします。このあいだ遭遇した問題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.rb
にenable_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をアプリケーションに組み込みたいので技術支援して欲しい、という場合はご相談ください。