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. How to extend TracePoint
    TokyuRuby
    会議13
    @joker1007

    View Slide

  2. またTracePoint

    やあ (´
    ・ω
    ・‵)
    ようこそ、プレモルハウスへ。
    このプレモルはサービスだから、まず飲んで落ち着いて欲しい。
    うん、「また」なんだ。済まない。
    仏の顔もって⾔うしね、謝って許してもらおうとも思っていない。
    でも、このLT
    を⾒たとき、君は、きっと⾔葉では⾔い表せない
    「ときめき」みたいなものを感じてくれたと思う。
    殺伐とした世の中で、そういう気持ちを忘れないで欲しい
    そう思って、このLT
    を⽴てたんだ。
    じゃあ、注⽂を聞こうか。

    View Slide

  3. 今回はTracePoint
    ⾃体を拡張する話

    View Slide

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

    View Slide

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

    View Slide

  6. 納得いかないので取れる様にしてみた

    View Slide

  7. DEMO

    View Slide

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

    View Slide

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

    View Slide

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

    理を登録する
    各イベントに対応した箇所に EXEC_EVENT_HOOK
    というマクロがあり、有効なhook
    処理があればそこからhook
    が実⾏される

    View Slide

  11. 修正箇所の探し⽅
    TracePoint
    の各イベントは RUBY_EVENT_
    という形式で表現されている。
    修正したい対象のevent
    でgrep
    すればすぐに⾒つかる。
    今回のターゲットは RUBY_EVENT_C_CALL
    と RUBY_EVENT_CALL

    View Slide

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

    View Slide

  13. 引数はどこか
    val = (*cfunc->invoker)(recv, argc, reg_cfp->sp + 1, cfunc->func);
    実際のC
    関数を呼び出す処理はここ。
    val
    は戻り値であり、 RUBY_EVENT_C_RETURN
    に付随データとして渡されている。
    引数にあたるのは reg_cfp->sp + 1
    。ここに引数情報がある。

    View Slide

  14. sp
    って?
    多分、スタックポインタの略。
    RubyVM
    はスタックマシンであり、⼤体以下の様な仕組みで動作している。
    スタックにオブジェクトを積む
    ISeq
    を取得する
    ISeq
    に対応した数だけスタックからオブジェクトをpop
    して命令を実⾏する
    戻り値をスタックに積む
    これを延々繰り返す。
    sp
    は VALUE
    のポインタであり、つまりRuby
    のスタックはオブジェクトとして表現可能
    なものが連なった単なる連続したメモリ領域である。

    View Slide

  15. Ruby
    におけるメソッド実⾏
    改めてISeq
    を確認する。
    String.new("hoge")
    % ruby --dump=insns tokyu_experiment3.rb
    == disasm: #@tokyu_experiment3.rb:1 (1,0)-(1,18)> (catch: FALSE)
    0000 opt_getinlinecache 7, ( 1)[Li]
    0003 getconstant :String
    0005 opt_setinlinecache
    0007 putstring "hoge"
    0009 opt_send_without_block ,
    0012 leave
    ISeq
    のsend
    命令のバリエーションでメソッドが呼び出される。
    opt_send_without_block
    の直前の putstring
    に注⽬。これが引数。
    その上にある getconstant :String
    がレシーバ。

    View Slide

  16. vm_call_cfunc_with_frame
    を再確認
    val = (*cfunc->invoker)(recv, argc, reg_cfp->sp + 1, cfunc->func);
    sp + 1
    しているのはレシーバの位置にsp
    があるからであることが分かる。

    View Slide

  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
    にあたる。

    View Slide

  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
    に任意のオ
    ブジェクトを渡すことが出来る。

    View Slide

  19. 引数の取り⽅と、TracePoint
    への渡し⽅が分かった
    後は配列を作って渡すだけ

    View Slide

  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,

    View Slide

  21. そして作ってから考えた

    View Slide

  22. ユースケースが思い付かない!
    せっかく書いたんだけど、このままではパッチを送るだけの説得⼒が無い……

    というわけでユースケースを募集しております。
    ( :call
    の⽅はsorbet
    の型定義を⾃動⽣成するとかに利⽤できるかもしれないけど。)
    ⽬的と⼿段が⼊れ替わるのはプログラミングではよくありますよね。

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide