How to extend TracePoint

How to extend TracePoint

TokyuRuby会議13

A5e5ee2fb9e4ce3c728ed9e3ef6e916f?s=128

Tomohiro Hashidate

June 29, 2019
Tweet

Transcript

  1. How to extend TracePoint TokyuRuby 会議13 @joker1007

  2. またTracePoint か やあ (´ ・ω ・‵) ようこそ、プレモルハウスへ。 このプレモルはサービスだから、まず飲んで落ち着いて欲しい。 うん、「また」なんだ。済まない。 仏の顔もって⾔うしね、謝って許してもらおうとも思っていない。

    でも、このLT を⾒たとき、君は、きっと⾔葉では⾔い表せない 「ときめき」みたいなものを感じてくれたと思う。 殺伐とした世の中で、そういう気持ちを忘れないで欲しい そう思って、このLT を⽴てたんだ。 じゃあ、注⽂を聞こうか。
  3. 今回はTracePoint ⾃体を拡張する話

  4. TracePoint はメソッドの呼び出しとreturn をフックで きる そしてreturn は戻り値が取れる TracePoint#return_value

  5. しかし、call された時の引数が取れない Ruby レベルのメソッドの場合は以下を駆使すれば取ろうと思えば取れる。 TracePoint#binding Binding#local_variable_get Method#parameters しかし、C で実装されたものはどうやっても取得できない。 C

    で実装されたメソッドはマッピングされたC の関数を呼んでるだけで、引数名も binding もない。
  6. 納得いかないので取れる様にしてみた

  7. DEMO

  8. 時間切れまでどうやったかを話す

  9. 最初に -O0 でコンパイルしたRuby を⽤意する 簡単な機能だけでも gdb を使える様になっておく 正直gdb で⽌めて追っかけないと、相当詳しくない限りVM の処理を追うのは難しい。

  10. そもそもTracePoint とは vm_trace.c に実装がある。 rb_tp_t という構造体が情報を保持している enable を呼ぶとvm ポインタを辿って global_event_hooks

    という箇所にhook 処 理を登録する 各イベントに対応した箇所に EXEC_EVENT_HOOK というマクロがあり、有効なhook 処理があればそこからhook が実⾏される
  11. 修正箇所の探し⽅ TracePoint の各イベントは RUBY_EVENT_<name> という形式で表現されている。 修正したい対象のevent でgrep すればすぐに⾒つかる。 今回のターゲットは RUBY_EVENT_C_CALL

    と RUBY_EVENT_CALL 。
  12. :c_call の実⾏場所 vm_insnhelper.c の vm_call_cfunc_with_frame にある。 static VALUE vm_call_cfunc_with_frame(rb_execution_context_t *ec,

    rb_control_frame_t *reg_cfp, struct rb_calling_info *calling, const struct rb_call_info *ci, struct rb_call_cache *cc) { VALUE val; const rb_callable_method_entry_t *me = cc->me; const rb_method_cfunc_t *cfunc = vm_method_cfunc_entry(me); int len = cfunc->argc; VALUE recv = calling->recv; VALUE block_handler = calling->block_handler; int argc = calling->argc; RUBY_DTRACE_CMETHOD_ENTRY_HOOK(ec, me->owner, me->def->original_id); EXEC_EVENT_HOOK(ec, RUBY_EVENT_C_CALL, recv, me->def->original_id, ci->mid, me->owner, Qundef); vm_push_frame(ec, NULL, VM_FRAME_MAGIC_CFUNC | VM_FRAME_FLAG_CFRAME | VM_ENV_FLAG_LOCAL, recv, block_handler, (VALUE)me, 0, ec->cfp->sp, 0, 0); if (len >= 0) rb_check_arity(argc, len, len); reg_cfp->sp -= argc + 1; val = (*cfunc->invoker)(recv, argc, reg_cfp->sp + 1, cfunc->func); CHECK_CFP_CONSISTENCY("vm_call_cfunc"); rb_vm_pop_frame(ec); EXEC_EVENT_HOOK(ec, RUBY_EVENT_C_RETURN, recv, me->def->original_id, ci->mid, me->owner, val); RUBY_DTRACE_CMETHOD_RETURN_HOOK(ec, me->owner, me->def->original_id); return val; }
  13. 引数はどこか val = (*cfunc->invoker)(recv, argc, reg_cfp->sp + 1, cfunc->func); 実際のC

    関数を呼び出す処理はここ。 val は戻り値であり、 RUBY_EVENT_C_RETURN に付随データとして渡されている。 引数にあたるのは reg_cfp->sp + 1 。ここに引数情報がある。
  14. sp って? 多分、スタックポインタの略。 RubyVM はスタックマシンであり、⼤体以下の様な仕組みで動作している。 スタックにオブジェクトを積む ISeq を取得する ISeq に対応した数だけスタックからオブジェクトをpop

    して命令を実⾏する 戻り値をスタックに積む これを延々繰り返す。 sp は VALUE のポインタであり、つまりRuby のスタックはオブジェクトとして表現可能 なものが連なった単なる連続したメモリ領域である。
  15. Ruby におけるメソッド実⾏ 改めてISeq を確認する。 String.new("hoge") % ruby --dump=insns tokyu_experiment3.rb ==

    disasm: #<ISeq:<main>@tokyu_experiment3.rb:1 (1,0)-(1,18)> (catch: FALSE) 0000 opt_getinlinecache 7, <is:0> ( 1)[Li] 0003 getconstant :String 0005 opt_setinlinecache <is:0> 0007 putstring "hoge" 0009 opt_send_without_block <callinfo!mid:new, argc:1, ARGS_SIMPLE>, <callcache> 0012 leave ISeq のsend 命令のバリエーションでメソッドが呼び出される。 opt_send_without_block の直前の putstring に注⽬。これが引数。 その上にある getconstant :String がレシーバ。
  16. vm_call_cfunc_with_frame を再確認 val = (*cfunc->invoker)(recv, argc, reg_cfp->sp + 1, cfunc->func);

    sp + 1 しているのはレシーバの位置にsp があるからであることが分かる。
  17. TracePoint オブジェクトへのデータの受け渡し :c_return の return_value を参考にする。 EXEC_EVENT_HOOK(ec, RUBY_EVENT_C_RETURN, recv, me->def->original_id,

    ci->mid, me->owner, val); 最後の引数が return_value にあたる。
  18. TracePoint#return_value の実装 VALUE rb_tracearg_return_value(rb_trace_arg_t *trace_arg) { if (trace_arg->event & (RUBY_EVENT_RETURN

    | RUBY_EVENT_C_RETURN | RUBY_EVENT_B_RETURN)) { /* ok */ } else { rb_raise(rb_eRuntimeError, "not supported by this event"); } if (trace_arg->data == Qundef) { rb_bug("rb_tracearg_return_value: unreachable"); } return trace_arg->data; } つまり EXEC_EVENT_HOOK の最後の引数を使えば rb_trace_arg_t の data に任意のオ ブジェクトを渡すことが出来る。
  19. 引数の取り⽅と、TracePoint への渡し⽅が分かった 後は配列を作って渡すだけ

  20. 最終的なパッチ diff --git a/vm_insnhelper.c b/vm_insnhelper.c index 93b1ebfe7a..471395dc60 100644 --- a/vm_insnhelper.c

    +++ b/vm_insnhelper.c @@ -2200,7 +2200,13 @@ vm_call_cfunc_with_frame(rb_execution_context_t *ec, rb_control_frame_t *reg_cfp int argc = calling->argc; RUBY_DTRACE_CMETHOD_ENTRY_HOOK(ec, me->owner, me->def->original_id); - EXEC_EVENT_HOOK(ec, RUBY_EVENT_C_CALL, recv, me->def->original_id, ci->mid, me->owner, Qundef); + + VALUE argv = Qundef; + rb_hook_list_t *global_hooks = rb_vm_global_hooks(ec); + if (UNLIKELY(global_hooks->events & (RUBY_EVENT_C_CALL))) { + argv = rb_ary_new_from_values(argc, reg_cfp->sp - argc); + } + EXEC_EVENT_HOOK(ec, RUBY_EVENT_C_CALL, recv, me->def->original_id, ci->mid, me->owner, argv); vm_push_frame(ec, NULL, VM_FRAME_MAGIC_CFUNC | VM_FRAME_FLAG_CFRAME | VM_ENV_FLAG_LOCAL, recv, block_handler, (VALUE)me,
  21. そして作ってから考えた

  22. ユースケースが思い付かない! せっかく書いたんだけど、このままではパッチを送るだけの説得⼒が無い…… 。 というわけでユースケースを募集しております。 ( :call の⽅はsorbet の型定義を⾃動⽣成するとかに利⽤できるかもしれないけど。) ⽬的と⼿段が⼊れ替わるのはプログラミングではよくありますよね。

  23. Appendix ( 時間⾜りないと思う)

  24. :call イベントの場合 Ruby のメソッド定義は def と define_method で定義できる。 実はどちらで定義されたかによって呼び出しパスが異なる。 多分、

    define_method だとスコープが切り替わらないからだと思う。
  25. def の場合 trace_xxx というiseq の命令が vm_trace 関数を実⾏する。 最終的に vm_insnhelper.c の

    vm_trace_hook がhook を処理する。 static inline void vm_trace_hook(rb_execution_context_t *ec, rb_control_frame_t *reg_cfp, const VALUE *pc, rb_event_flag_t pc_events, rb_event_flag_t target_event, rb_hook_list_t *global_hooks, rb_hook_list_t *local_hooks, VALUE val) { rb_event_flag_t event = pc_events & target_event; VALUE self = GET_SELF(); VM_ASSERT(rb_popcount64((uint64_t)event) == 1); if (event & global_hooks->events) { /* increment PC because source line is calculated with PC-1 */ reg_cfp->pc++; vm_dtrace(event, ec); rb_exec_event_hook_orig(ec, global_hooks, event, self, 0, 0, 0 , val, 0); reg_cfp->pc--; } if (local_hooks != NULL) { if (event & local_hooks->events) { /* increment PC because source line is calculated with PC-1 */ reg_cfp->pc++; rb_exec_event_hook_orig(ec, local_hooks, event, self, 0, 0, 0 , val, 0); reg_cfp->pc--; } } }
  26. env ポインタから引数を取る diff --git a/vm_insnhelper.c b/vm_insnhelper.c index 93b1ebfe7a..471395dc60 100644 ---

    a/vm_insnhelper.c +++ b/vm_insnhelper.c @@ -4337,6 +4343,36 @@ vm_trace_hook(rb_execution_context_t *ec, rb_control_frame_t *reg_cfp, const VAL VM_ASSERT(rb_popcount64((uint64_t)event) == 1); + if (event & (RUBY_EVENT_CALL | RUBY_EVENT_B_CALL)) { + const rb_iseq_t *iseq = reg_cfp->iseq; + int local_table_size = iseq->body->local_table_size; + int not_keyword_arg_size = iseq->body->param.lead_num + iseq->body->param.opt_num + iseq->body->param.flags.has_rest + iseq->body->param.post_num; + + int keyword_size = 0; + int keyword_rest = 0; + if (iseq->body->param.keyword) { + keyword_size = iseq->body->param.keyword->num; + keyword_rest = iseq->body->param.keyword->rest_start; + } + + val = rb_ary_new_from_values(not_keyword_arg_size, reg_cfp->ep - (local_table_size + 2)); + + if (keyword_size > 0) { + const VALUE *keyword_args = reg_cfp->ep - (local_table_size + 2) + not_keyword_arg_size; + VALUE hash = rb_hash_new(); + int i; + for (i=0; i<keyword_size; i++) { + rb_hash_aset(hash, rb_id2sym(*(iseq->body->param.keyword->table + i)), *(keyword_args + i)); + } + rb_ary_push(val, hash); + } + + if (keyword_rest > 0) { + const VALUE *keyword_rest = reg_cfp->ep - (local_table_size + 2) + not_keyword_arg_size + keyword_size + 1; + rb_ary_push(val, *keyword_rest); + } + } +
  27. define_method の場合 vm.c の invoke_bmethod でhook を処理している。 static VALUE invoke_bmethod(rb_execution_context_t

    *ec, const rb_iseq_t *iseq, VALUE self, const struct rb_captured_block *captured, const rb_callable_method_entry_t *me, VALUE type, int opt_pc) { /* bmethod */ int arg_size = iseq->body->param.size; VALUE ret; rb_hook_list_t *hooks; VM_ASSERT(me->def->type == VM_METHOD_TYPE_BMETHOD); vm_push_frame(ec, iseq, type | VM_FRAME_FLAG_BMETHOD, self, VM_GUARDED_PREV_EP(captured->ep), (VALUE)me, iseq->body->iseq_encoded + opt_pc, ec->cfp->sp + arg_size, iseq->body->local_table_size - arg_size, iseq->body->stack_max); RUBY_DTRACE_METHOD_ENTRY_HOOK(ec, me->owner, me->def->original_id); EXEC_EVENT_HOOK(ec, RUBY_EVENT_CALL, self, me->def->original_id, me->called_id, me->owner, Qnil); if (UNLIKELY((hooks = me->def->body.bmethod.hooks) != NULL) && hooks->events & RUBY_EVENT_CALL) { rb_exec_event_hook_orig(ec, hooks, RUBY_EVENT_CALL, self, me->def->original_id, me->called_id, me->owner, Qnil, FALSE); } VM_ENV_FLAGS_SET(ec->cfp->ep, VM_FRAME_FLAG_FINISH); ret = vm_exec(ec, TRUE); EXEC_EVENT_HOOK(ec, RUBY_EVENT_RETURN, self, me->def->original_id, me->called_id, me->owner, ret); if ((hooks = me->def->body.bmethod.hooks) != NULL && hooks->events & RUBY_EVENT_RETURN) { rb_exec_event_hook_orig(ec, hooks, RUBY_EVENT_RETURN, self, me->def->original_id, me->called_id, me->owner, ret, FALSE); } RUBY_DTRACE_METHOD_RETURN_HOOK(ec, me->owner, me->def->original_id); return ret; }
  28. sp からでも取れるが、直前の関数まで argv が渡ってきているので、直接渡せそう。 diff --git a/vm.c b/vm.c index 7ad6bdd264..436f0aa4c8

    100644 --- a/vm.c +++ b/vm.c @@ -1031,7 +1031,7 @@ invoke_block(rb_execution_context_t *ec, const rb_iseq_t *iseq, VALUE self, cons } static VALUE -invoke_bmethod(rb_execution_context_t *ec, const rb_iseq_t *iseq, VALUE self, const struct rb_captured_block *captured, const rb_callable_method_entry_t *me, VALUE type, int opt_pc) +invoke_bmethod(rb_execution_context_t *ec, const rb_iseq_t *iseq, VALUE self, int argc, const VALUE *argv, const struct rb_captured_block *captured, const rb_callable_method_entry_t *me, VALUE type, int opt_pc) { /* bmethod */ int arg_size = iseq->body->param.size; @@ -1049,12 +1049,18 @@ invoke_bmethod(rb_execution_context_t *ec, const rb_iseq_t *iseq, VALUE self, co iseq->body->stack_max); RUBY_DTRACE_METHOD_ENTRY_HOOK(ec, me->owner, me->def->original_id); - EXEC_EVENT_HOOK(ec, RUBY_EVENT_CALL, self, me->def->original_id, me->called_id, me->owner, Qnil); + + VALUE data = Qundef; + rb_hook_list_t *global_hooks = rb_vm_global_hooks(ec); + if (UNLIKELY(global_hooks->events & (RUBY_EVENT_CALL))) { + data = rb_ary_new_from_values(argc, argv); + } + EXEC_EVENT_HOOK(ec, RUBY_EVENT_CALL, self, me->def->original_id, me->called_id, me->owner, data); if (UNLIKELY((hooks = me->def->body.bmethod.hooks) != NULL) && hooks->events & RUBY_EVENT_CALL) { rb_exec_event_hook_orig(ec, hooks, RUBY_EVENT_CALL, self, - me->def->original_id, me->called_id, me->owner, Qnil, FALSE); + me->def->original_id, me->called_id, me->owner, data, FALSE); } VM_ENV_FLAGS_SET(ec->cfp->ep, VM_FRAME_FLAG_FINISH); ret = vm_exec(ec, TRUE); @@ -1102,7 +1108,7 @@ invoke_iseq_block_from_c(rb_execution_context_t *ec, const struct rb_captured_bl return invoke_block(ec, iseq, self, captured, cref, type, opt_pc); } else { - return invoke_bmethod(ec, iseq, self, captured, me, type, opt_pc); + return invoke_bmethod(ec, iseq, self, argc, argv, captured, me, type, opt_pc); } }