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

How to extend TracePoint

How to extend TracePoint

TokyuRuby会議13

Avatar for Tomohiro Hashidate

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); } }