Rubyの大域脱出はどのように実現されているか

qiitaの下書きを整理していたら何か書きかけの奴が見つかったが、消すのももったいないというか自分もそのうち見返したいのでそのまま投下しておく。ので、変なところがあるかもしれない

# 背景
[前回の記事](https://qiita.com/k0kubun/items/c118a968065a48c1b873)による理解で、以下のコードのデバッグが多少進んだ。

```rb
def timeout(sec)
  bl = proc do
    y = Thread.start {
      sleep sec
    }
    result = yield(sec)
    y.kill
    y.join
    return result
  end
  bl.call
  caller(0)
end

timeout(100) do
  foo.bar
end
```

> ところが、実際にはbl.callでVMを起動する時のISeqがfoo.barのあるブロックになってしまい、そこでfoo.barを呼び出した後普通に次の行に抜け、本来大域脱出により到達しないはずのcaller(0)の呼び出しに到達してしまいました。

gdbで見てみると`bl.call`から`vm_call_opt_call`経由で`invoke_block`により積まれているcfpのiseqには正しいiseqがセットされ、実行され始めていた。なので上記の引用の内容は間違っていた。

一方、そこで起動されたVM内で`foo.bar`のブロックのiseqが実行されるのもまた事実のようである。

これは、例外処理のルーチンから`vm_exec_core`の手前にgotoされることで、一度の`vm_exec`呼び出しの中で`vm_exec_core`が二度実行されていることがprintデバッグによりわかった。

なので、このメソッドが本来どういう順番でVMに処理されるはずなのかを追いかけることにする。

# returnでの大域脱出の仕組み
## どのような時にexception_handlerに到達するか
長すぎるため全体の引用はしないのでこちらを参照: https://github.com/ruby/ruby/blob/9f68d0f59196f356b9c20f23a5299c3d32559eae/vm.c#L1765-L2005

まず、`goto vm_loop_start`があるのは`exception_handler:`ラベル以降なので、一度の`vm_exec`の中で複数回`vm_exec_core`が実行されるためには、`vm_exec`内の`exception_handler:`というラベルに到達しないといけない。

`exception_handler:`に来るパスには、最初の分岐の`EC_EXEC_TAG()`が`TAG_NONE`以外を返すケースと、`vm_exec_core`を抜けた後`_tag.state`が`TAG_NONE`以外を返すケースの2通りある。

全部の条件を見ていくのはダルめなので、今回の例のコードの大域脱出だとどちらに行きそうかだけ考える

## ブロック内returnでメソッドから抜ける時の処理
最初に出したコードのISeqはこれ

```
$ RBENV_VERSION=trunk-svn ruby --dump=insns b.rb
== disasm: #<ISeq:<main>@b.rb:0>========================================
== catch table
| catch type: break  st: 0010 ed: 0017 sp: 0000 cont: 0017
== disasm: #<ISeq:block in <main>@b.rb:15>==============================
== catch table
| catch type: redo   st: 0001 ed: 0008 sp: 0000 cont: 0001
| catch type: next   st: 0001 ed: 0008 sp: 0000 cont: 0008
|------------------------------------------------------------------------
0000 nop                                                              (  15)[Bc]
0001 putself                                                          (  16)[Li]
0002 opt_send_without_block <callinfo!mid:foo, argc:0, FCALL|VCALL|ARGS_SIMPLE>, <callcache>
0005 opt_send_without_block <callinfo!mid:bar, argc:0, ARGS_SIMPLE>, <callcache>
0008 leave            [Br]
|------------------------------------------------------------------------
0000 putspecialobject 1                                               (   1)[Li]
0002 putobject        :timeout
0004 putiseq          timeout
0006 opt_send_without_block <callinfo!mid:core#define_method, argc:2, ARGS_SIMPLE>, <callcache>
0009 pop
0010 putself                                                          (  15)[Li]
0011 putobject        100
0013 send             <callinfo!mid:timeout, argc:1, FCALL>, <callcache>, block in <main>
0017 leave
== disasm: #<ISeq:timeout@b.rb:1>=======================================
== catch table
| catch type: break  st: 0000 ed: 0005 sp: 0000 cont: 0005
== disasm: #<ISeq:block in timeout@b.rb:2>==============================
== catch table
| catch type: break  st: 0001 ed: 0012 sp: 0000 cont: 0012
== disasm: #<ISeq:block (2 levels) in timeout@b.rb:3>===================
== catch table
| catch type: redo   st: 0001 ed: 0008 sp: 0000 cont: 0001
| catch type: next   st: 0001 ed: 0008 sp: 0000 cont: 0008
|------------------------------------------------------------------------
0000 nop                                                              (   3)[Bc]
0001 putself                                                          (   4)[Li]
0002 getlocal         sec, 2
0005 opt_send_without_block <callinfo!mid:sleep, argc:1, FCALL|ARGS_SIMPLE>, <callcache>
0008 leave            [Br]
| catch type: redo   st: 0001 ed: 0036 sp: 0000 cont: 0001
| catch type: next   st: 0001 ed: 0036 sp: 0000 cont: 0036
|------------------------------------------------------------------------
local table (size: 2, argc: 0 [opts: 0, rest: -1, post: 0, block: -1, kw: -1@-1, kwrest: -1])
[ 2] y          [ 1] result
0000 nop                                                              (   2)[Bc]
0001 getinlinecache   8, <is:0>                                       (   3)[Li]
0004 getconstant      :Thread
0006 setinlinecache   <is:0>
0008 send             <callinfo!mid:start, argc:0>, <callcache>, block (2 levels) in timeout
0012 setlocal_OP__WC__0 y
0014 getlocal_OP__WC__1 sec                                           (   6)[Li]
0016 invokeblock      <callinfo!argc:1, ARGS_SIMPLE>
0018 setlocal_OP__WC__0 result
0020 getlocal_OP__WC__0 y                                             (   7)[Li]
0022 opt_send_without_block <callinfo!mid:kill, argc:0, ARGS_SIMPLE>, <callcache>
0025 pop
0026 getlocal_OP__WC__0 y                                             (   8)[Li]
0028 opt_send_without_block <callinfo!mid:join, argc:0, ARGS_SIMPLE>, <callcache>
0031 pop
0032 getlocal_OP__WC__0 result                                        (   9)[Li]
0034 throw            1
0036 leave            [Br]
|------------------------------------------------------------------------
local table (size: 2, argc: 1 [opts: 0, rest: -1, post: 0, block: -1, kw: -1@-1, kwrest: -1])
[ 2] sec<Arg>   [ 1] bl
0000 putself                                                          (   2)[LiCa]
0001 send             <callinfo!mid:proc, argc:0, FCALL>, <callcache>, block in timeout
0005 setlocal_OP__WC__0 bl
0007 getlocal_OP__WC__0 bl                                            (  11)[Li]
0009 opt_send_without_block <callinfo!mid:call, argc:0, ARGS_SIMPLE>, <callcache>
0012 pop
0013 putself                                                          (  12)[Li]
0014 putobject_OP_INT2FIX_O_0_C_
0015 opt_send_without_block <callinfo!mid:caller, argc:1, FCALL|ARGS_SIMPLE>, <callcache>
0018 leave            [Re]
```

`return`に該当しそうなところを探すと、

```
0034 throw            1
```

なんかこの後に無駄っぽいleaveあるけどまあ置いておく。

throwの定義を見る。

```c 
/**
  @c exception
  @e longjump
  @j 大域ジャンプを行う。
 */
DEFINE_INSN
throw
(rb_num_t throw_state)
(VALUE throwobj)
(VALUE val)
{
    RUBY_VM_CHECK_INTS(ec);
    val = vm_throw(ec, GET_CFP(), throw_state, throwobj);
    THROW_EXCEPTION(val);
    /* unreachable */
}
```

### vm_throw はどのように`vm_throw_data`を作るか

```c 
static VALUE
vm_throw(const rb_execution_context_t *ec, rb_control_frame_t *reg_cfp,
         rb_num_t throw_state, VALUE throwobj)
{
    const int state = (int)(throw_state & VM_THROW_STATE_MASK);
    const int flag = (int)(throw_state & VM_THROW_NO_ESCAPE_FLAG);
    const rb_num_t level = throw_state >> VM_THROW_LEVEL_SHIFT;

    if (state != 0) {
        return vm_throw_start(ec, reg_cfp, state, flag, level, throwobj);
    }
    else {
        return vm_throw_continue(ec, throwobj);
    }
}

enum ruby_vm_throw_flags {
    VM_THROW_NO_ESCAPE_FLAG = 0x8000,
    VM_THROW_LEVEL_SHIFT = 16,
    VM_THROW_STATE_MASK = 0xff
};


static VALUE
vm_throw_start(const rb_execution_context_t *ec, rb_control_frame_t *const reg_cfp, enum ruby_tag_type state,
               const int flag, const rb_num_t level, const VALUE throwobj)
{
    const rb_control_frame_t *escape_cfp = NULL;
    const rb_control_frame_t * const eocfp = RUBY_VM_END_CONTROL_FRAME(ec); /* end of control frame pointer */

    if (flag != 0) {
        /* do nothing */
    }
    else if (state == TAG_BREAK) {
        int is_orphan = 1;
        const VALUE *ep = GET_EP();
        const rb_iseq_t *base_iseq = GET_ISEQ();
        escape_cfp = reg_cfp;

        while (base_iseq->body->type != ISEQ_TYPE_BLOCK) {
            if (escape_cfp->iseq->body->type == ISEQ_TYPE_CLASS) {
                escape_cfp = RUBY_VM_PREVIOUS_CONTROL_FRAME(escape_cfp);
                ep = escape_cfp->ep;
                base_iseq = escape_cfp->iseq;
            }
            else {
                ep = VM_ENV_PREV_EP(ep);
                base_iseq = base_iseq->body->parent_iseq;
                escape_cfp = rb_vm_search_cf_from_ep(ec, escape_cfp, ep);
                VM_ASSERT(escape_cfp->iseq == base_iseq);
            }
        }

        if (VM_FRAME_LAMBDA_P(escape_cfp)) {
            /* lambda{... break ...} */
            is_orphan = 0;
            state = TAG_RETURN;
        }
        else {
            ep = VM_ENV_PREV_EP(ep);

            while (escape_cfp < eocfp) {
                if (escape_cfp->ep == ep) {
                    const rb_iseq_t *const iseq = escape_cfp->iseq;
                    const VALUE epc = escape_cfp->pc - iseq->body->iseq_encoded;
                    const struct iseq_catch_table *const ct = iseq->body->catch_table;
                    unsigned int i;

                    if (!ct) break;
                    for (i=0; i < ct->size; i++) {
                        const struct iseq_catch_table_entry * const entry = &ct->entries[i];

                        if (entry->type == CATCH_TYPE_BREAK &&
                            entry->iseq == base_iseq &&
                            entry->start < epc && entry->end >= epc) {
                            if (entry->cont == epc) { /* found! */
                                is_orphan = 0;
                            }
                            break;
                        }
                    }
                    break;
                }

                escape_cfp = RUBY_VM_PREVIOUS_CONTROL_FRAME(escape_cfp);
            }
        }

        if (is_orphan) {
            rb_vm_localjump_error("break from proc-closure", throwobj, TAG_BREAK);
        }
    }
    else if (state == TAG_RETRY) {
        rb_num_t i;
        const VALUE *ep = VM_ENV_PREV_EP(GET_EP());

        for (i = 0; i < level; i++) {
            ep = VM_ENV_PREV_EP(ep);
        }

        escape_cfp = rb_vm_search_cf_from_ep(ec, reg_cfp, ep);
    }
    else if (state == TAG_RETURN) {
        const VALUE *current_ep = GET_EP();
        const VALUE *target_lep = VM_EP_LEP(current_ep);
        int in_class_frame = 0;
        int toplevel = 1;
        escape_cfp = reg_cfp;

        while (escape_cfp < eocfp) {
            const VALUE *lep = VM_CF_LEP(escape_cfp);

            if (!target_lep) {
                target_lep = lep;
            }

            if (lep == target_lep &&
                VM_FRAME_RUBYFRAME_P(escape_cfp) &&
                escape_cfp->iseq->body->type == ISEQ_TYPE_CLASS) {
                in_class_frame = 1;
                target_lep = 0;
            }

            if (lep == target_lep) {
                if (VM_FRAME_LAMBDA_P(escape_cfp)) {
                    toplevel = 0;
                    if (in_class_frame) {
                        /* lambda {class A; ... return ...; end} */
                        goto valid_return;
                    }
                    else {
                        const VALUE *tep = current_ep;

                        while (target_lep != tep) {
                            if (escape_cfp->ep == tep) {
                                /* in lambda */
                                goto valid_return;
                            }
                            tep = VM_ENV_PREV_EP(tep);
                        }
                    }
                }
                else if (VM_FRAME_RUBYFRAME_P(escape_cfp)) {
                    switch (escape_cfp->iseq->body->type) {
                      case ISEQ_TYPE_TOP:
                      case ISEQ_TYPE_MAIN:
                        if (toplevel) goto valid_return;
                        break;
                      case ISEQ_TYPE_EVAL:
                      case ISEQ_TYPE_CLASS:
                        toplevel = 0;
                        break;
                      default:
                        break;
                    }
                }
            }

            if (escape_cfp->ep == target_lep && escape_cfp->iseq->body->type == ISEQ_TYPE_METHOD) {
                goto valid_return;
            }

            escape_cfp = RUBY_VM_PREVIOUS_CONTROL_FRAME(escape_cfp);
        }
        rb_vm_localjump_error("unexpected return", throwobj, TAG_RETURN);

      valid_return:;
        /* do nothing */
    }
    else {
        rb_bug("isns(throw): unsupport throw type");
    }

    ec->tag->state = state;
    return (VALUE)THROW_DATA_NEW(throwobj, escape_cfp, state);
}

enum ruby_tag_type {
    RUBY_TAG_NONE	= 0x0,
    RUBY_TAG_RETURN	= 0x1,
    RUBY_TAG_BREAK	= 0x2,
    RUBY_TAG_NEXT	= 0x3,
    RUBY_TAG_RETRY	= 0x4,
    RUBY_TAG_REDO	= 0x5,
    RUBY_TAG_RAISE	= 0x6,
    RUBY_TAG_THROW	= 0x7,
    RUBY_TAG_FATAL	= 0x8,
    RUBY_TAG_MASK	= 0xf
};

#define TAG_NONE	RUBY_TAG_NONE
#define TAG_RETURN	RUBY_TAG_RETURN
#define TAG_BREAK	RUBY_TAG_BREAK
#define TAG_NEXT	RUBY_TAG_NEXT
#define TAG_RETRY	RUBY_TAG_RETRY
#define TAG_REDO	RUBY_TAG_REDO
#define TAG_RAISE	RUBY_TAG_RAISE
#define TAG_THROW	RUBY_TAG_THROW
#define TAG_FATAL	RUBY_TAG_FATAL
#define TAG_MASK	RUBY_TAG_MASK

static inline struct vm_throw_data *
THROW_DATA_NEW(VALUE val, const rb_control_frame_t *cf, VALUE st)
{
    return (struct vm_throw_data *)rb_imemo_new(imemo_throw_data, val, (VALUE)cf, st, 0);
}

/*! THROW_DATA */
struct vm_throw_data {
    VALUE flags;
    VALUE reserved;
    const VALUE throw_obj;
    const struct rb_control_frame_struct *catch_frame;
    VALUE throw_state;
};

VALUE
rb_imemo_new(enum imemo_type type, VALUE v1, VALUE v2, VALUE v3, VALUE v0)
{
    VALUE flags = T_IMEMO | (type << FL_USHIFT);
    return newobj_of(v0, flags, v1, v2, v3, TRUE);
}

static inline VALUE
newobj_of(VALUE klass, VALUE flags, VALUE v1, VALUE v2, VALUE v3, int wb_protected)
{
    rb_objspace_t *objspace = &rb_objspace;
    VALUE obj;

#if GC_DEBUG_STRESS_TO_CLASS
    if (UNLIKELY(stress_to_class)) {
        long i, cnt = RARRAY_LEN(stress_to_class);
        const VALUE *ptr = RARRAY_CONST_PTR(stress_to_class);
        for (i = 0; i < cnt; ++i) {
            if (klass == ptr[i]) rb_memerror();
        }
    }
#endif
    if (!(during_gc ||
          ruby_gc_stressful ||
          gc_event_hook_available_p(objspace)) &&
        (obj = heap_get_freeobj_head(objspace, heap_eden)) != Qfalse) {
        return newobj_init(klass, flags, v1, v2, v3, wb_protected, objspace, obj);
    }
    else {
        return wb_protected ?
          newobj_slowpath_wb_protected(klass, flags, v1, v2, v3, objspace) :
          newobj_slowpath_wb_unprotected(klass, flags, v1, v2, v3, objspace);
    }
}

static VALUE
newobj_slowpath_wb_protected(VALUE klass, VALUE flags, VALUE v1, VALUE v2, VALUE v3, rb_objspace_t *objspace)
{
    return newobj_slowpath(klass, flags, v1, v2, v3, objspace, TRUE);
}

static inline VALUE
newobj_slowpath(VALUE klass, VALUE flags, VALUE v1, VALUE v2, VALUE v3, rb_objspace_t *objspace, int wb_protected)
{
    VALUE obj;

    if (UNLIKELY(during_gc || ruby_gc_stressful)) {
        if (during_gc) {
            dont_gc = 1;
            during_gc = 0;
            rb_bug("object allocation during garbage collection phase");
        }

        if (ruby_gc_stressful) {
            if (!garbage_collect(objspace, FALSE, FALSE, FALSE, GPR_FLAG_NEWOBJ)) {
                rb_memerror();
            }
        }
    }

    obj = heap_get_freeobj(objspace, heap_eden);
    newobj_init(klass, flags, v1, v2, v3, wb_protected, objspace, obj);
    gc_event_hook(objspace, RUBY_INTERNAL_EVENT_NEWOBJ, obj);
    return obj;
}

static inline VALUE
newobj_init(VALUE klass, VALUE flags, VALUE v1, VALUE v2, VALUE v3, int wb_protected, rb_objspace_t *objspace, VALUE obj)
{
    GC_ASSERT(BUILTIN_TYPE(obj) == T_NONE);
    GC_ASSERT((flags & FL_WB_PROTECTED) == 0);

    /* OBJSETUP */
    RBASIC(obj)->flags = flags;
    RBASIC_SET_CLASS_RAW(obj, klass);
    RANY(obj)->as.values.v1 = v1;
    RANY(obj)->as.values.v2 = v2;
    RANY(obj)->as.values.v3 = v3;

#if RGENGC_CHECK_MODE
    GC_ASSERT(RVALUE_MARKED(obj) == FALSE);
    GC_ASSERT(RVALUE_MARKING(obj) == FALSE);
    GC_ASSERT(RVALUE_OLD_P(obj) == FALSE);
    GC_ASSERT(RVALUE_WB_UNPROTECTED(obj) == FALSE);

    if (flags & FL_PROMOTED1) {
        if (RVALUE_AGE(obj) != 2) rb_bug("newobj: %s of age (%d) != 2.", obj_info(obj), RVALUE_AGE(obj));
    }
    else {
        if (RVALUE_AGE(obj) > 0) rb_bug("newobj: %s of age (%d) > 0.", obj_info(obj), RVALUE_AGE(obj));
    }
    if (rgengc_remembered(objspace, (VALUE)obj)) rb_bug("newobj: %s is remembered.", obj_info(obj));
#endif

#if USE_RGENGC
    if (UNLIKELY(wb_protected == FALSE)) {
        MARK_IN_BITMAP(GET_HEAP_WB_UNPROTECTED_BITS(obj), obj);
    }
#endif

#if RGENGC_PROFILE
    if (wb_protected) {
        objspace->profile.total_generated_normal_object_count++;
#if RGENGC_PROFILE >= 2
        objspace->profile.generated_normal_object_count_types[BUILTIN_TYPE(obj)]++;
#endif
    }
    else {
        objspace->profile.total_generated_shady_object_count++;
#if RGENGC_PROFILE >= 2
        objspace->profile.generated_shady_object_count_types[BUILTIN_TYPE(obj)]++;
#endif
    }
#endif

#if GC_DEBUG
    RANY(obj)->file = rb_source_location_cstr(&RANY(obj)->line);
    GC_ASSERT(!SPECIAL_CONST_P(obj)); /* check alignment */
#endif

    objspace->total_allocated_objects++;

    gc_report(5, objspace, "newobj: %s\n", obj_info(obj));

#if RGENGC_OLD_NEWOBJ_CHECK > 0
    {
        static int newobj_cnt = RGENGC_OLD_NEWOBJ_CHECK;

        if (!is_incremental_marking(objspace) &&
            flags & FL_WB_PROTECTED &&   /* do not promote WB unprotected objects */
            ! RB_TYPE_P(obj, T_ARRAY)) { /* array.c assumes that allocated objects are new */
            if (--newobj_cnt == 0) {
                newobj_cnt = RGENGC_OLD_NEWOBJ_CHECK;

                gc_mark_set(objspace, obj);
                RVALUE_AGE_SET_OLD(objspace, obj);

                rb_gc_writebarrier_remember(obj);
            }
        }
    }
#endif
    check_rvalue_consistency(obj);
    return obj;
}

#define RANY(o) ((RVALUE*)(o))

typedef struct RVALUE {
    union {
        struct {
            VALUE flags;                /* always 0 for freed obj */
            struct RVALUE *next;
        } free;
        struct RBasic  basic;
        struct RObject object;
        struct RClass  klass;
        struct RFloat  flonum;
        struct RString string;
        struct RArray  array;
        struct RRegexp regexp;
        struct RHash   hash;
        struct RData   data;
        struct RTypedData   typeddata;
        struct RStruct rstruct;
        struct RBignum bignum;
        struct RFile   file;
        struct RMatch  match;
        struct RRational rational;
        struct RComplex complex;
        union {
            rb_cref_t cref;
            struct vm_svar svar;
            struct vm_throw_data throw_data;
            struct vm_ifunc ifunc;
            struct MEMO memo;
            struct rb_method_entry_struct ment;
            const rb_iseq_t iseq;
            rb_env_t env;
            struct rb_imemo_alloc_struct alloc;
            rb_ast_t ast;
        } imemo;
        struct {
            struct RBasic basic;
            VALUE v1;
            VALUE v2;
            VALUE v3;
        } values;
    } as;
#if GC_DEBUG
    const char *file;
    int line;
#endif
} RVALUE;
```

`THROW_DATA_NEW`ってのをやるとRubyが管理しているヒープのページを取ってきて、そこにv1, v2, v3をセットするようです。ざっと見た感じだと以下のような構造になりそうです

```c 
struct vm_throw_data {
    VALUE flags; /* RBasic flags */
    VALUE reserved; /* RBasic klass, 多分0? */
    const VALUE throw_obj; /* as.values.v1, throwobj (YARVスタックの一番上にあったもの) */
    const struct rb_control_frame_struct *catch_frame; /* as.values.v2, escape_cfp (脱出先のcfp) */
    VALUE throw_state; /* as.values.v3, state (TAG_RETURN) */
};
```

脱出先のcfpは`reg_cfp->ep`からlepを探し、そのepを持つcfpを探しているようです。まあブロックを持つ親メソッドのフレームになりそうですね。
ここでimemoを作る前に`ec->tag->state = TAG_RETURN;`も行なわれていることに注意する。

### `THROW_EXCEPTION`は何をするか
```c 
#define OPT_CALL_THREADED_CODE       0

#define THROW_EXCEPTION(exc) return (VALUE)(exc)
```

ここで`vm_exec_core`は`struct vm_throw_data *`を返すようですね。

### `vm_exec_core`から`struct vm_throw_data *`が返された時の処理
```c 
    if ((state = EC_EXEC_TAG()) == TAG_NONE) {
      vm_loop_start:
        result = vm_exec_core(ec, initial);
        VM_ASSERT(ec->tag == &_tag);
        if ((state = _tag.state) != TAG_NONE) {
            err = (struct vm_throw_data *)result;
            _tag.state = TAG_NONE;
            goto exception_handler;
        }
    }
```

`ec->tag->state = TAG_RETURN`が行なわれているので、`struct vm_throw_data *err;`に`struct vm_throw_data *`が渡され、`ec->tag->state`を`TAG_NONE`に戻した上で`goto exception_handler`されます。

```c 
      exception_handler:
        cont_pc = cont_sp = 0;
        catch_iseq = NULL;

        while (ec->cfp->pc == 0 || ec->cfp->iseq == 0) {
            if (UNLIKELY(VM_FRAME_TYPE(ec->cfp) == VM_FRAME_MAGIC_CFUNC)) {
                EXEC_EVENT_HOOK(ec, RUBY_EVENT_C_RETURN, ec->cfp->self,
                                rb_vm_frame_method_entry(ec->cfp)->def->original_id,
                                rb_vm_frame_method_entry(ec->cfp)->called_id,
                                rb_vm_frame_method_entry(ec->cfp)->owner, Qnil);
                RUBY_DTRACE_CMETHOD_RETURN_HOOK(ec,
                                                rb_vm_frame_method_entry(ec->cfp)->owner,
                                                rb_vm_frame_method_entry(ec->cfp)->def->original_id);
            }
            rb_vm_pop_frame(ec);
        }

        cfp = ec->cfp;
        epc = cfp->pc - cfp->iseq->body->iseq_encoded;

        escape_cfp = NULL;
        if (state == TAG_BREAK || state == TAG_RETURN) {
            escape_cfp = THROW_DATA_CATCH_FRAME(err);

            if (cfp == escape_cfp) {
                if (state == TAG_RETURN) {
                    if (!VM_FRAME_FINISHED_P(cfp)) {
                        THROW_DATA_CATCH_FRAME_SET(err, cfp + 1);
                        THROW_DATA_STATE_SET(err, state = TAG_BREAK);
                    }
                    else {
                        ct = cfp->iseq->body->catch_table;
                        if (ct) for (i = 0; i < ct->size; i++) {
                            entry = &ct->entries[i];
                            if (entry->start < epc && entry->end >= epc) {
                                if (entry->type == CATCH_TYPE_ENSURE) {
                                    catch_iseq = entry->iseq;
                                    cont_pc = entry->cont;
                                    cont_sp = entry->sp;
                                    break;
                                }
                            }
                        }
                        if (catch_iseq == NULL) {
                            ec->errinfo = Qnil;
                            result = THROW_DATA_VAL(err);
                            THROW_DATA_CATCH_FRAME_SET(err, cfp + 1);
                            hook_before_rewind(ec, ec->cfp, TRUE, state, err);
                            rb_vm_pop_frame(ec);
                            goto finish_vme;
                        }
                    }
                    /* through */
                }
                else {
                    /* TAG_BREAK */
#if OPT_STACK_CACHING
                    initial = THROW_DATA_VAL(err);
#else
                    *ec->cfp->sp++ = THROW_DATA_VAL(err);
#endif
                    ec->errinfo = Qnil;
                    goto vm_loop_start;
                }
            }
        }
```

`ec->cfp->pc == 0 || ec->cfp->iseq == 0`でひたすら`rb_vm_pop_frame`しているwhileループはよくわかりませんが、Rubyで定義されたブロックを実行している場合はiseqが存在しpcも動いているはずなのでとりあえず今回は無視します。

後半の分岐に関しては、`TAG_RETURN`の方にひっかかりそうですが、throw時点での`ec->cfp`はthrowがあるフレームになっていそうなので、`cfp == escape_cfp`を満たさずスルーされそうです。

その後に進みます。

```c 
        else {
            ct = cfp->iseq->body->catch_table;
            if (ct) for (i = 0; i < ct->size; i++) {
                entry = &ct->entries[i];
                if (entry->start < epc && entry->end >= epc) {

                    if (entry->type == CATCH_TYPE_ENSURE) {
                        catch_iseq = entry->iseq;
                        cont_pc = entry->cont;
                        cont_sp = entry->sp;
                        break;
                    }
                }
            }
        }
```

ここでthrowしたフレームのcatch tableにensureがないか走査されます。上の方のdisasmを見る限りでは`catch type: ensure`はないので、これもスルーでしょう。したがって、`catch_iseq`はNULLのままのはず。

```c 
        if (catch_iseq != NULL) { /* found catch table */
            /* enter catch scope */
            const int arg_size = 1;

            rb_iseq_check(catch_iseq);
            cfp->sp = vm_base_ptr(cfp) + cont_sp;
            cfp->pc = cfp->iseq->body->iseq_encoded + cont_pc;

            /* push block frame */
            cfp->sp[0] = (VALUE)err;
            vm_push_frame(ec, catch_iseq, VM_FRAME_MAGIC_RESCUE,
                          cfp->self,
                          VM_GUARDED_PREV_EP(cfp->ep),
                          0, /* cref or me */
                          catch_iseq->body->iseq_encoded,
                          cfp->sp + arg_size /* push value */,
                          catch_iseq->body->local_table_size - arg_size,
                          catch_iseq->body->stack_max);

            state = 0;
            ec->tag->state = TAG_NONE;
            ec->errinfo = Qnil;
            fprintf(stderr, "found catch;\n");
            goto vm_loop_start;
        }
        else {
            hook_before_rewind(ec, ec->cfp, FALSE, state, err);

            if (VM_FRAME_FINISHED_P(ec->cfp)) {
                rb_vm_pop_frame(ec);
                ec->errinfo = (VALUE)err;
                EC_TMPPOP_TAG();
                EC_JUMP_TAG(ec, state);
            }
            else {
                rb_vm_pop_frame(ec);
                goto exception_handler;
            }
        }
```

```c 
static void
hook_before_rewind(rb_execution_context_t *ec, const rb_control_frame_t *cfp, int will_finish_vm_exec, int state, struct vm_throw_data *err)
{
    if (state == TAG_RAISE && RBASIC_CLASS(err) == rb_eSysStackError) {
        return;
    }
    switch (VM_FRAME_TYPE(ec->cfp)) {
      case VM_FRAME_MAGIC_METHOD:
        RUBY_DTRACE_METHOD_RETURN_HOOK(ec, 0, 0);
        EXEC_EVENT_HOOK_AND_POP_FRAME(ec, RUBY_EVENT_RETURN, ec->cfp->self, 0, 0, 0, frame_return_value(err));
        THROW_DATA_CONSUMED_SET(err);
        break;
      case VM_FRAME_MAGIC_BLOCK:
        if (VM_FRAME_BMETHOD_P(ec->cfp)) {
            EXEC_EVENT_HOOK(ec, RUBY_EVENT_B_RETURN, ec->cfp->self, 0, 0, 0, frame_return_value(err));

            if (!will_finish_vm_exec) {
                /* kick RUBY_EVENT_RETURN at invoke_block_from_c() for bmethod */
                EXEC_EVENT_HOOK_AND_POP_FRAME(ec, RUBY_EVENT_RETURN, ec->cfp->self,
                                              rb_vm_frame_method_entry(ec->cfp)->def->original_id,
                                              rb_vm_frame_method_entry(ec->cfp)->called_id,
                                              rb_vm_frame_method_entry(ec->cfp)->owner,
                                              frame_return_value(err));
            }
            THROW_DATA_CONSUMED_SET(err);
        }
        else {
            EXEC_EVENT_HOOK_AND_POP_FRAME(ec, RUBY_EVENT_B_RETURN, ec->cfp->self, 0, 0, 0, frame_return_value(err));
            THROW_DATA_CONSUMED_SET(err);
        }
        break;
      case VM_FRAME_MAGIC_CLASS:
        EXEC_EVENT_HOOK_AND_POP_FRAME(ec, RUBY_EVENT_END, ec->cfp->self, 0, 0, 0, Qnil);
        break;
    }
}

#define EXEC_EVENT_HOOK_AND_POP_FRAME(ec_, flag_, self_, id_, called_id_, klass_, data_) \
  EXEC_EVENT_HOOK_ORIG(ec_, flag_, ruby_vm_event_flags, self_, id_, called_id_, klass_, data_, 1)

#define EXEC_EVENT_HOOK_ORIG(ec_, flag_, vm_flags_, self_, id_, called_id_, klass_, data_, pop_p_) do { \
    const rb_event_flag_t flag_arg_ = (flag_); \
    if (UNLIKELY(vm_flags_ & (flag_arg_))) { \
	/* defer evaluating the other arguments */ \
	rb_exec_event_hook_orig(ec_, flag_arg_, self_, id_, called_id_, klass_, data_, pop_p_); \
    } \
} while (0)

static inline void
rb_exec_event_hook_orig(rb_execution_context_t *ec, const rb_event_flag_t flag,
			VALUE self, ID id, ID called_id, VALUE klass, VALUE data, int pop_p)
{
    struct rb_trace_arg_struct trace_arg;

    VM_ASSERT(rb_ec_vm_ptr(ec)->event_hooks.events == ruby_vm_event_flags);
    VM_ASSERT(rb_ec_vm_ptr(ec)->event_hooks.events & flag);

    trace_arg.event = flag;
    trace_arg.ec = ec;
    trace_arg.cfp = ec->cfp;
    trace_arg.self = self;
    trace_arg.id = id;
    trace_arg.called_id = called_id;
    trace_arg.klass = klass;
    trace_arg.data = data;
    trace_arg.path = Qundef;
    trace_arg.klass_solved = 0;
    rb_exec_event_hooks(&trace_arg, pop_p);
}

void
rb_exec_event_hooks(rb_trace_arg_t *trace_arg, int pop_p)
{
    rb_execution_context_t *ec = trace_arg->ec;
    rb_vm_t *vm = rb_ec_vm_ptr(ec);

    if (trace_arg->event & RUBY_INTERNAL_EVENT_MASK) {
        if (ec->trace_arg && (ec->trace_arg->event & RUBY_INTERNAL_EVENT_MASK)) {
            /* skip hooks because this thread doing INTERNAL_EVENT */
        }
        else {
            rb_trace_arg_t *prev_trace_arg = ec->trace_arg;
            ec->trace_arg = trace_arg;
            exec_hooks_unprotected(ec, vm, &vm->event_hooks, trace_arg);
            ec->trace_arg = prev_trace_arg;
        }
    }
    else {
        if (ec->trace_arg == NULL && /* check reentrant */
            trace_arg->self != rb_mRubyVMFrozenCore /* skip special methods. TODO: remove it. */) {
            const VALUE errinfo = ec->errinfo;
            const VALUE old_recursive = ec->local_storage_recursive_hash;
            int state = 0;

            ec->local_storage_recursive_hash = ec->local_storage_recursive_hash_for_trace;
            ec->errinfo = Qnil;
            ec->trace_arg = trace_arg;
            state = exec_hooks_protected(ec, vm, &vm->event_hooks, trace_arg);
            if (!state) {
                ec->errinfo = errinfo;
            }
            ec->trace_arg = NULL;

            ec->local_storage_recursive_hash_for_trace = ec->local_storage_recursive_hash;
            ec->local_storage_recursive_hash = old_recursive;

            if (state) {
                if (pop_p) {
                    if (VM_FRAME_FINISHED_P(ec->cfp)) {
                        ec->tag = ec->tag->prev;
                    }
                    rb_vm_pop_frame(ec);
                }
                EC_JUMP_TAG(ec, state);
            }
        }
    }
}
```

`hook_before_rewind`はTracePointのフックとかを呼びつつ、ブロックの場合はframeをpopするようです。ここで多分throwしたフレームを抜ける。

その後`VM_FRAME_FINISHED_P`で分岐するようですが、どういうフレームに`VM_FRAME_FLAG_FINISH`が立つのか調べるのはちょっと面倒くさいですね。とりあえず`invoke_block`には`VM_FRAME_FLAG_FINISH`が立つようなのでそちらを見ます。

(ここで下書きが終わっていた)