Upgrade to Pro — share decks privately, control downloads, hide ads and more …

How to extend TracePoint

How to extend TracePoint

TokyuRuby会議13

Tomohiro Hashidate

June 29, 2019
Tweet

More Decks by Tomohiro Hashidate

Other Decks in Programming

Transcript

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

    でも、このLT を⾒たとき、君は、きっと⾔葉では⾔い表せない 「ときめき」みたいなものを感じてくれたと思う。 殺伐とした世の中で、そういう気持ちを忘れないで欲しい そう思って、このLT を⽴てたんだ。 じゃあ、注⽂を聞こうか。
  2. そもそもTracePoint とは vm_trace.c に実装がある。 rb_tp_t という構造体が情報を保持している enable を呼ぶとvm ポインタを辿って global_event_hooks

    という箇所にhook 処 理を登録する 各イベントに対応した箇所に EXEC_EVENT_HOOK というマクロがあり、有効なhook 処理があればそこからhook が実⾏される
  3. :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; }
  4. 引数はどこか val = (*cfunc->invoker)(recv, argc, reg_cfp->sp + 1, cfunc->func); 実際のC

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

    して命令を実⾏する 戻り値をスタックに積む これを延々繰り返す。 sp は VALUE のポインタであり、つまりRuby のスタックはオブジェクトとして表現可能 なものが連なった単なる連続したメモリ領域である。
  6. 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 がレシーバ。
  7. vm_call_cfunc_with_frame を再確認 val = (*cfunc->invoker)(recv, argc, reg_cfp->sp + 1, cfunc->func);

    sp + 1 しているのはレシーバの位置にsp があるからであることが分かる。
  8. 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 に任意のオ ブジェクトを渡すことが出来る。
  9. 最終的なパッチ 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,
  10. 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--; } } }
  11. 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); + } + } +
  12. 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; }
  13. 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); } }