追記: Ruby 2.7からはこのgemは必要ないです
Ruby 2.7でNumbered parametersが追加されました。最高!
http://bugs.ruby-lang.org/issues/4475
% ruby -ve 'p [1,2,3].map { @1 * 2 }'
ruby 2.7.0dev (2019-03-18 trunk 67289) [x86_64-darwin17]
[2, 4, 6]
TL;DR
groovyやKotlinのit
のようにブロックに渡された値をit
で参照できるthats_it
gemの紹介です。
require "thats_it"
[1, 2, 3].map { it * 2 }
# => [2, 4, 6]
便利ですね。
了
Kotlinやgroovyのitとは
ラムダ式が1つしかパラメーターを取らない場合、そのパラメーターを宣言しなくても暗黙的に宣言された「it」変数でそのパラメーターを参照できる便利機能。
参考
Higher-Order Functions and Lambdas - Kotlin Programming Language it: implicit name of a single parameter
The Apache Groovy programming language - Closures 2.2. Implicit parameter
忙しい人向けの実装方法解説
ブロックが取る引数が0かつit
の呼び出しがある場合にブロックのISeqを書き換えてブロックが1つ引数を取ることにする。1
# 書き換え前
1.yield_self { it }
# 書き換え後
1.yield_self {|it| it }
了
どのタイミングでブロックを書き換えるか
TracePointを使いメソッド呼び出しのタイミングでメソッドに渡されたブロックのISeqを取得し書き換える。
# Rubyで書かれたメソッドの呼び出し時に呼ばれる
TracePoint.trace(:call) do |tp|
# ここで書き換える
end
# Cで書かれたメソッドの呼び出し時に呼ばれる
TracePoint.trace(:c_call) do |tp|
# ここで書き換える
end
ブロックが呼び出されるタイミングで呼び出されるb_call
というイベントもあるがそこではISeqを書き換えられない。理由は後述。
ブロックが持っている情報
ブロックのISeqにはブロックが取る引数の情報が保持されている。
以下のような引数を取る場合のISeqを確認してみる。
42.yield_self {|it| it }
rubyではオプションで--dump=insns
を渡すとISeqを確認できる。
% ruby -v --dump=insns -e '42.yield_self {|it| it }'
ruby 2.5.1p57 (2018-03-29 revision 63029) [x86_64-linux]
== disasm: #<ISeq:<main>@-e:1 (1,0)-(1,24)>=============================
== catch table
| catch type: break st: 0000 ed: 0006 sp: 0000 cont: 0006
== disasm: #<ISeq:block in <main>@-e:1 (1,14)-(1,24)>===================
== catch table
| catch type: redo st: 0001 ed: 0003 sp: 0000 cont: 0001
| catch type: next st: 0001 ed: 0003 sp: 0000 cont: 0003
|------------------------------------------------------------------------
local table (size: 1, argc: 1 [opts: 0, rest: -1, post: 0, block: -1, kw: -1@-1, kwrest: -1])
[ 1] it<Arg>
0000 nop ( 1)[Bc]
0001 getlocal_OP__WC__0 it[Li]
0003 leave [Br]
|------------------------------------------------------------------------
0000 putobject 42 ( 1)[Li]
0002 send <callinfo!mid:yield_self, argc:0>, <callcache>, block in <main>
0006 leave
ブロックのISeqを確認してみると以下のように引数を1つ取ることが分かる。
local table (size: 1, argc: 1 [opts: 0, rest: -1, post: 0, block: -1, kw: -1@-1, kwrest: -1])
一方ブロックが引数を取らない場合はローカル変数が無いので以下のようにlocal tableが省略して表示される。
表示されていないだけで、実際にはlocal tableの情報は引数を渡さない場合もブロックのISeqに保持されており、size: 0, argc: 0
となっている。
% ruby -v --dump=insns -e '42.yield_self { it }'
ruby 2.5.1p57 (2018-03-29 revision 63029) [x86_64-linux]
== disasm: #<ISeq:<main>@-e:1 (1,0)-(1,20)>=============================
== catch table
| catch type: break st: 0000 ed: 0006 sp: 0000 cont: 0006
== disasm: #<ISeq:block in <main>@-e:1 (1,14)-(1,20)>===================
== catch table
| catch type: redo st: 0001 ed: 0005 sp: 0000 cont: 0001
| catch type: next st: 0001 ed: 0005 sp: 0000 cont: 0005
|------------------------------------------------------------------------
0000 nop ( 1)[Bc]
0001 putself [Li]
0002 opt_send_without_block <callinfo!mid:it, argc:0, FCALL|VCALL|ARGS_SIMPLE>, <callcache>
0005 leave [Br]
|------------------------------------------------------------------------
0000 putobject 42 ( 1)[Li]
0002 send <callinfo!mid:yield_self, argc:0>, <callcache>, block in <main>
0006 leave
ブロック呼び出し周りのRubyVMの動作
ブロックを呼び出すコードを読めばブロックのISeqを取る方法やどう書き換えればいいかがが分かる。
以下のようなブロックを呼び出す内容のプログラムのISeqを確認する。
def foo
yield 42
end
% ruby -v --dump=insns -e 'def foo; yield 42; end'
ruby 2.5.1p57 (2018-03-29 revision 63029) [x86_64-linux]
== disasm: #<ISeq:<main>@-e:1 (1,0)-(1,22)>=============================
0000 putspecialobject 1 ( 1)[Li]
0002 putobject :foo
0004 putiseq foo
0006 opt_send_without_block <callinfo!mid:core#define_method, argc:2, ARGS_SIMPLE>, <callcache>
0009 leave
== disasm: #<ISeq:foo@-e:1 (1,0)-(1,22)>================================
0000 putobject 42 ( 1)[LiCa]
0002 invokeblock <callinfo!argc:1, ARGS_SIMPLE>
0004 leave [Re]
invokeblock
命令で呼び出しているよう。ruby/rubyをcloneしてgit grepする2。
% git grep invokeblock
bootstraptest/test_insns.rb: [ 'invokeblock', <<~'},', ], # {
compile.c: ADD_INSN1(ret, line, invokeblock, new_callinfo(iseq, 0, FIX2INT(argc), flag, keywords, FALSE));
doc/ChangeLog-1.9.3: * insns.def (invokeblock): fix of splat argument.
doc/ChangeLog-1.9.3: * insns.def (invokeblock): check block is created by lambda
doc/ChangeLog-2.0.0: * insns.def (send, invokesuper, invokeblock, opt_*), vm_core.h:
insns.def:invokeblock
YARVの命令はinsns.def
ファイルで定義されているのでinsns.def
のinvokeblock
を読む。3
invokeblock
コメントから見てわかるようにyield
に対応している。
vm_invoke_block
を実行してますね。
この辺でgdbで止めてデバッグします。
/**
@c method/iterator
@e yield(args)
@j yield を実行する。
*/
DEFINE_INSN
invokeblock
(CALL_INFO ci)
(...)
(VALUE val) // inc += 1 - ci->orig_argc;
{
struct rb_calling_info calling;
calling.argc = ci->orig_argc;
calling.block_handler = VM_BLOCK_HANDLER_NONE;
calling.recv = GET_SELF();
val = vm_invoke_block(ec, GET_CFP(), &calling, ci);
if (val == Qundef) {
RESTORE_REGS();
NEXT_INSN();
}
}
vm_invoke_block
ブロックとして渡された引数があるか確認、ブロックが渡されてなければno block givenの例外を投げる。
ふつうにrubyでyield
した際はvm_invoke_iseq_block
が呼ばれるようです。
static VALUE
vm_invoke_block(rb_execution_context_t *ec, rb_control_frame_t *reg_cfp, struct rb_calling_info *calling, const struct rb_call_info *ci)
{
VALUE block_handler = VM_CF_BLOCK_HANDLER(reg_cfp);
VALUE type = GET_ISEQ()->body->local_iseq->body->type;
int is_lambda = FALSE;
if ((type != ISEQ_TYPE_METHOD && type != ISEQ_TYPE_CLASS) ||
block_handler == VM_BLOCK_HANDLER_NONE) {
rb_vm_localjump_error("no block given (yield)", Qnil, 0);
}
again:
switch (vm_block_handler_type(block_handler)) {
case block_handler_type_iseq:
{
const struct rb_captured_block *captured = VM_BH_TO_ISEQ_BLOCK(block_handler);
return vm_invoke_iseq_block(ec, reg_cfp, calling, ci, is_lambda, captured);
}
case block_handler_type_ifunc:
{
const struct rb_captured_block *captured = VM_BH_TO_IFUNC_BLOCK(block_handler);
return vm_invoke_ifunc_block(ec, reg_cfp, calling, ci, captured);
}
case block_handler_type_proc:
is_lambda = block_proc_is_lambda(VM_BH_TO_PROC(block_handler));
block_handler = vm_proc_to_block_handler(VM_BH_TO_PROC(block_handler));
goto again;
case block_handler_type_symbol:
return vm_invoke_symbol_block(ec, reg_cfp, calling, ci, VM_BH_TO_SYMBOL(block_handler));
}
VM_UNREACHABLE(vm_invoke_block: unreachable);
return Qnil;
}
vm_invoke_iseq_block
引数周りのスタックを調整してからYARVのcontrol frameを積んでる。
/* ruby iseq -> ruby block */
static VALUE
vm_invoke_iseq_block(rb_execution_context_t *ec, rb_control_frame_t *reg_cfp,
struct rb_calling_info *calling, const struct rb_call_info *ci,
int is_lambda, const struct rb_captured_block *captured)
{
const rb_iseq_t *iseq = rb_iseq_check(captured->code.iseq);
const int arg_size = iseq->body->param.size;
VALUE * const rsp = GET_SP() - calling->argc;
int opt_pc = vm_callee_setup_block_arg(ec, calling, ci, iseq, rsp, is_lambda ? arg_setup_method : arg_setup_block);
SET_SP(rsp);
vm_push_frame(ec, iseq,
VM_FRAME_MAGIC_BLOCK | (is_lambda ? VM_FRAME_FLAG_LAMBDA : 0),
captured->self,
VM_GUARDED_PREV_EP(captured->ep), 0,
iseq->body->iseq_encoded + opt_pc,
rsp + arg_size,
iseq->body->local_table_size - arg_size, iseq->body->stack_max);
return Qundef;
}
struct rb_captured_block *
からブロックのISeqが取得できることが分かりました。
vm_push_frame
まで見ます。
const rb_iseq_t *iseq = rb_iseq_check(captured->code.iseq);
vm_push_frame
vm_push_frame
のシグネチャは以下
static inline rb_control_frame_t *
vm_push_frame(rb_execution_context_t *ec,
const rb_iseq_t *iseq,
VALUE type,
VALUE self,
VALUE specval,
VALUE cref_or_me,
const VALUE *pc,
VALUE *sp,
int local_size,
int stack_max)
vm_push_frame
の後ろから3番目の引数がspになっているのがわかる。sp
にはrsp + arg_size
が入るよう。
vm_push_frame(ec, iseq,
VM_FRAME_MAGIC_BLOCK | (is_lambda ? VM_FRAME_FLAG_LAMBDA : 0),
captured->self,
VM_GUARDED_PREV_EP(captured->ep), 0,
iseq->body->iseq_encoded + opt_pc,
rsp + arg_size, /* ここがスタックの底になる */
iseq->body->local_table_size - arg_size, iseq->body->stack_max);
rsp
とarg_size
は以下のように計算されていますね。
const int arg_size = iseq->body->param.size;
VALUE * const rsp = GET_SP() - calling->argc;
arg_size
にはブロックのISeqの引数の数の情報が入る。
ブロックの引数を書かない場合ここのarg_size
が0
になる。
calling->argc
にはyield
に渡した引数の数が入っている、なので1
。
calling
の型はstruct rb_calling_info *
で対応するのはこの部分かな?
0002 invokeblock <callinfo!argc:1, ARGS_SIMPLE>
rsp
には現在のスタックポインターから呼び出し側でyield
に渡して積んだ引数を引いた部分が入る。
つまり引数0のブロックを呼び出す場合いくらyield
に引数を渡してもブロックの実行前に破棄されてしまうよう。4
ブロック呼び出し時の動作雑な理解
- cfpからblock_handlerが取れる
- block_handlerは4種類ある
- iseq
- ifunc
- proc
- symbol
- block_handlerは4種類ある
- iseqのblock_handlerからはiseqが取れる
-
iseq->body->param.size
が0だとyield 1, 2, 3, 4, ...
といくらyield
に引数を渡しても捨てられてしまう
b_callでブロックのISeqを書き換えても意味なさ
以下のようなプログラムをgdbで実行します。
TracePoint.trace(:b_call) do |tp|
1.yield_self
end
def foo
yield 42
end
foo {}
vm_invoke_iseq_block
とrb_obj_yield_self
にbreakpointを仕掛けて呼び出される順序を確認。
% gdb $(rbenv which ruby)
GNU gdb (Gentoo 8.1 p1) 8.1
Copyright (C) 2018 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law. Type "show copying"
and "show warranty" for details.
This GDB was configured as "x86_64-pc-linux-gnu".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
<https://bugs.gentoo.org/>.
Find the GDB manual and other documentation resources online at:
<http://www.gnu.org/software/gdb/documentation/>.
For help, type "help".
Type "apropos word" to search for commands related to "word"...
Reading symbols from /home/sei/.rbenv/versions/2.5.1/bin/ruby...done.
(gdb) break vm_invoke_iseq_block if rb_vm_top_self() == calling->recv
Breakpoint 1 at 0x1850fd: file vm_insnhelper.c, line 2630.
(gdb) break rb_obj_yield_self
Breakpoint 2 at 0x909da: file object.c, line 577.
(gdb) run --disable-gems -e 'TracePoint.trace(:b_call) {|tp| 1.yield_self }; def foo; yield 42; end; foo
{}'
Starting program: /home/sei/.rbenv/versions/2.5.1/bin/ruby --disable-gems -e 'TracePoint.trace(:b_call) {|tp| 1.yield_self }; def foo; yield 42; end; foo {}'
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib64/libthread_db.so.1".
[New Thread 0x7ffff7ff6700 (LWP 15626)]
Error in testing breakpoint condition:
Couldn't get registers: そのようなプロセスはありません.
An error occurred while in a function called from GDB.
Evaluation of the expression containing the function
(rb_vm_top_self) will be abandoned.
When the function is done executing, GDB will silently stop.
Selected thread is running.
(gdb) frame 0
#0 vm_invoke_iseq_block (ec=0x555555a79258, reg_cfp=0x7ffff7fc5f80, calling=0x7fffffffd8b0,
ci=0x555555babf60, is_lambda=0, captured=0x7ffff7fc5fc8) at vm_insnhelper.c:2630
2630 const rb_iseq_t *iseq = rb_iseq_check(captured->code.iseq);
(gdb) c
Continuing.
Thread 1 "ruby" hit Breakpoint 2, rb_obj_yield_self (obj=3) at object.c:577
577 RETURN_SIZED_ENUMERATOR(obj, 0, 0, rb_obj_size);
(gdb) frame 0
#0 rb_obj_yield_self (obj=3) at object.c:577
577 RETURN_SIZED_ENUMERATOR(obj, 0, 0, rb_obj_size);
(gdb) c
Continuing.
[Thread 0x7ffff7ff6700 (LWP 15626) exited]
[Inferior 1 (process 15622) exited normally]
見ての通りvm_invoke_block
が呼ばれてからrb_obj_yield_self
が呼び出されている。
-
yield
の引数をスタックに積む -
invokeblock
が呼ばれる -
vm_invoke_block
が呼ばれる -
vm_invoke_iseq_block
が呼ばれる -
vm_push_frame
が呼ばれる(このタイミングでスタックに積んだ引数が捨てられる) - TracePointの
b_call
が呼ばれる
6の段階でISeqを書き換えても5の段階で既にyield
に渡された値がすてられてしまう。
なので書き換えるなら4を呼ぶ前にISeqを書き換えないといけない。
6の段階のcontrol frameのスタックを見てみると以下のようになっている。
0006がyield_selfの呼び出しで、0005がTracePoint作成時に渡したブロック、0004がfooに渡したブロックとなっており、既に制御フレームが積まれているのが分かる。
(gdb) call rb_vmdebug_stack_dump_raw_current()
-- Control frame information -----------------------------------------------
c:0006 p:---- s:0021 e:000020 CFUNC :yield_self
c:0005 p:0005 s:0017 e:000016 BLOCK -e:1 [FINISH]
c:0004 p:0001 s:0013 e:000012 BLOCK -e:1
c:0003 p:0004 s:0010 e:000009 METHOD -e:1
c:0002 p:0029 s:0006 E:002660 EVAL -e:1 [FINISH]
c:0001 p:0000 s:0003 E:000460 (none) [FINISH]
ブロックのISeqをメソッド呼び出し時に取得する
以下のようなコードをgdbで実行してrb_obj_yield_self
にbreakポイントを仕掛けブロックのISeqを取る方法を確認する。
TracePoint.trace(:call) do |tp|
1.yield_self
end
def foo
yield 42
end
foo { 42 }
とりあえず止まった
% gdb $(rbenv which ruby)
GNU gdb (Gentoo 8.1 p1) 8.1
Copyright (C) 2018 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law. Type "show copying"
and "show warranty" for details.
This GDB was configured as "x86_64-pc-linux-gnu".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
<https://bugs.gentoo.org/>.
Find the GDB manual and other documentation resources online at:
<http://www.gnu.org/software/gdb/documentation/>.
For help, type "help".
Type "apropos word" to search for commands related to "word"...
Reading symbols from /home/sei/.rbenv/versions/2.5.1/bin/ruby...done.
(gdb) source ~/src/github.com/ruby/ruby/.gdbinit
(gdb) break rb_obj_yield_self
Breakpoint 1 at 0x909da: file object.c, line 577.
(gdb) run --disable-gems -e 'TracePoint.trace(:call) {|tp| 1.yield_self }; def foo; yield 42; end; foo { 42 }
'
Starting program: /home/sei/.rbenv/versions/2.5.1/bin/ruby --disable-gems -e 'TracePoint.trace(:call) {|tp| 1.yield_self }; def foo; yield 42; end; foo {}'
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib64/libthread_db.so.1".
[New Thread 0x7ffff7ff6700 (LWP 16662)]
Thread 1 "ruby" hit Breakpoint 1, rb_obj_yield_self (obj=3) at object.c:577
577 RETURN_SIZED_ENUMERATOR(obj, 0, 0, rb_obj_size);
メソッドは2つ上のフレームのよう
(gdb) call rb_vmdebug_stack_dump_raw_current()
-- Control frame information -----------------------------------------------
c:0005 p:---- s:0018 e:000017 CFUNC :yield_self
c:0004 p:0005 s:0014 e:000013 BLOCK -e:1 [FINISH]
c:0003 p:0001 s:0010 e:000009 METHOD -e:1
c:0002 p:0029 s:0006 E:002660 EVAL -e:1 [FINISH]
c:0001 p:0000 s:0003 E:000460 (none) [FINISH]
今のcontrol frameはruby_current_execution_context_ptr
で取れる。5
(gdb) p *(ruby_current_execution_context_ptr->cfp)
$40 = {pc = 0x0, sp = 0x7ffff7ec60a0, iseq = 0x0, self = 3, ep = 0x7ffff7ec6098, block_code = 0x0}
(gdb) call rb_p((ruby_current_execution_context_ptr->cfp)->self)
1
cfpは--
して伸びる。試しに-したCFPを見ると使われてない空のフレームがあるのが分かる。
(gdb) p *(ruby_current_execution_context_ptr->cfp - 1)
$49 = {pc = 0x0, sp = 0x7ffff7ec60b8, iseq = 0x0, self = 3, ep = 0x7ffff7ec60b0, block_code = 0x0}
(gdb) p *(ruby_current_execution_context_ptr->cfp - 2)
$50 = {pc = 0x0, sp = 0x0, iseq = 0x0, self = 0, ep = 0x0, block_code = 0x0}
(gdb) p *(ruby_current_execution_context_ptr->cfp - 3)
$51 = {pc = 0x0, sp = 0x0, iseq = 0x0, self = 0, ep = 0x0, block_code = 0x0}
なので2つ上のメソッド呼び出しのcfpを見るときは+ 2
すると見れる。
(gdb) p *(ruby_current_execution_context_ptr->cfp + 2)
$23 = {pc = 0x555555babee8, sp = 0x7ffff7ec6060, iseq = 0x555555a95d80, self = 93824998023880,
ep = 0x7ffff7ec6058, block_code = 0x0}
さきほど見たvm_invoke_block
でブロックハンドラーを取得していた関数VM_CF_BLOCK_HANDLER
を呼んでみる。
(gdb) p VM_CF_BLOCK_HANDLER((ruby_current_execution_context_ptr->cfp + 2))
$52 = 140737353899977
vm_invoke_block
同様VM_BH_TO_ISEQ_BLOCK
を呼んでstruct rb_captured_block *
に変換する。
(gdb) p VM_BH_TO_ISEQ_BLOCK(VM_CF_BLOCK_HANDLER((ruby_current_execution_context_ptr->cfp + 2)))
$54 = (const struct rb_captured_block *) 0x7ffff7fc5fc8
(gdb) p *VM_BH_TO_ISEQ_BLOCK(VM_CF_BLOCK_HANDLER((ruby_current_execution_context_ptr->cfp + 2)))
$55 = {self = 93824998023880, ep = 0x555555bb0090, code = {iseq = 0x555555a95c90,
ifunc = 0x555555a95c90, val = 93824997743760}}
vm_invoke_block_iseq
のようにiseqにアクセスしてみる。
(gdb) p VM_BH_TO_ISEQ_BLOCK(VM_CF_BLOCK_HANDLER((ruby_current_execution_context_ptr->cfp + 2)))->code.iseq
$56 = (const rb_iseq_t *) 0x555555a95c90
(gdb) p *VM_BH_TO_ISEQ_BLOCK(VM_CF_BLOCK_HANDLER((ruby_current_execution_context_ptr->cfp + 2)))->code.iseq
$57 = {flags = 28698, reserved1 = 0, body = 0x555555babfb0, aux = {compile_data = 0x8, loader = {
obj = 8, index = 0}, trace_events = 8}}
このままだとよくわからないのでdisasmした結果を見てみる。
(gdb) call rb_p(rb_iseq_disasm(VM_BH_TO_ISEQ_BLOCK(VM_CF_BLOCK_HANDLER((ruby_current_execution_context_ptr->cfp + 2)))->code.iseq))
"== disasm: #<ISeq:block in <main>@-e:1 (1,74)-(1,80)>===================\n== catch table\n| catch type: redo st: 0001 ed: 0003 sp: 0000 cont: 0001\n| catch type: next st: 0001 ed: 0003 sp: 0000 cont: 0003\n|------------------------------------------------------------------------\n0000 nop ( 1)[Bc]\n0001 putobject 42[Li]\n0003 leave [Br]\n"
目的のブロックが取れている。
ブロックのISeqを書き換える
この辺のうちflagsはそれぞれの種類の引数があるかどうかをあらわしている。
(gdb) p VM_BH_TO_ISEQ_BLOCK(VM_CF_BLOCK_HANDLER((ruby_current_execution_context_ptr->cfp + 2)))->code.iseq->body.param
$70 = {flags = {has_lead = 0, has_opt = 0, has_rest = 0, has_post = 0, has_kw = 0, has_kwrest = 0,
has_block = 0, ambiguous_param0 = 0}, size = 0, lead_num = 0, opt_num = 0, rest_start = 0,
post_start = 0, post_num = 0, block_start = 0, opt_table = 0x0, keyword = 0x0}
foo {|x| x }
のような、1つ引数を取るブロックの場合は以下のようになっている。
(gdb) p VM_BH_TO_ISEQ_BLOCK(VM_CF_BLOCK_HANDLER((ruby_current_execution_context_ptr->cfp + 2)))->code.iseq->body.param
$71 = {flags = {has_lead = 1, has_opt = 0, has_rest = 0, has_post = 0, has_kw = 0, has_kwrest = 0,
has_block = 0, ambiguous_param0 = 1}, size = 1, lead_num = 1, opt_num = 0, rest_start = 0,
post_start = 0, post_num = 0, block_start = 0, opt_table = 0x0, keyword = 0x0}
それに合わせてflags
のhas_lead
とsize
とambiguous_param0
を1に書き換えていく。
(gdb) p VM_BH_TO_ISEQ_BLOCK(VM_CF_BLOCK_HANDLER((ruby_current_execution_context_ptr->cfp + 2)))->code.iseq->body.param.flags.ambiguous_param0 = 1
$75 = 1
(gdb) p VM_BH_TO_ISEQ_BLOCK(VM_CF_BLOCK_HANDLER((ruby_current_execution_context_ptr->cfp + 2)))->code.iseq->body.param.flags.has_lead = 1
$76 = 1
(gdb) p VM_BH_TO_ISEQ_BLOCK(VM_CF_BLOCK_HANDLER((ruby_current_execution_context_ptr->cfp + 2)))->code.iseq->body.param.size = 1
$77 = 1
(gdb) p VM_BH_TO_ISEQ_BLOCK(VM_CF_BLOCK_HANDLER((ruby_current_execution_context_ptr->cfp + 2)))->code.iseq->body.param
$78 = {flags = {has_lead = 1, has_opt = 0, has_rest = 0, has_post = 0, has_kw = 0, has_kwrest = 0,
has_block = 0, ambiguous_param0 = 1}, size = 1, lead_num = 0, opt_num = 0, rest_start = 0,
post_start = 0, post_num = 0, block_start = 0, opt_table = 0x0, keyword = 0x0}
これでスタックに値が積まれているはず、continueしてみる。
(gdb) c
Continuing.
-e:1: [BUG] Stack consistency error (sp: 14, bp: 13)
ruby 2.5.1p57 (2018-03-29 revision 63029) [x86_64-linux]
-- Control frame information -----------------------------------------------
c:0004 p:0004 s:0014 e:000013 BLOCK -e:1
c:0003 p:0004 s:0010 e:000009 METHOD -e:1
c:0002 p:0029 s:0006 E:002680 EVAL -e:1 [FINISH]
c:0001 p:0000 s:0003 E:000460 (none) [FINISH]
エラーが出る。git grep
でエラーメッセージを調べると以下がヒット。
static void
vm_stack_consistency_error(const rb_execution_context_t *ec,
const rb_control_frame_t *cfp,
const VALUE *bp)
{
const ptrdiff_t nsp = VM_SP_CNT(ec, cfp->sp);
const ptrdiff_t nbp = VM_SP_CNT(ec, bp);
static const char stack_consistency_error[] =
"Stack consistency error (sp: %"PRIdPTRDIFF", bp: %"PRIdPTRDIFF")";
#if defined RUBY_DEVEL
VALUE mesg = rb_sprintf(stack_consistency_error, nsp, nbp);
rb_str_cat_cstr(mesg, "\n");
rb_str_append(mesg, rb_iseq_disasm(cfp->iseq));
rb_exc_fatal(rb_exc_new3(rb_eFatal, mesg));
#else
rb_bug(stack_consistency_error, nsp, nbp);
#endif
}
vm_stack_consistency_error
で探すとleave
する際にしかこのエラーは出ないよう。
/**
@c method/iterator
@e return from this scope.
@j このスコープから抜ける。
*/
DEFINE_INSN
leave
()
(VALUE val)
(VALUE val)
{
if (OPT_CHECKED_RUN) {
const VALUE *const bp = vm_base_ptr(reg_cfp);
if (reg_cfp->sp != bp) {
vm_stack_consistency_error(ec, reg_cfp, bp);
}
}
RUBY_VM_CHECK_INTS(ec);
if (vm_pop_frame(ec, GET_CFP(), GET_EP())) {
#if OPT_CALL_THREADED_CODE
rb_ec_thread_ptr(ec)->retval = val;
return 0;
#else
return val;
#endif
}
else {
RESTORE_REGS();
}
}
vm_base_ptr
の計算を見てみる
static VALUE *
vm_base_ptr(const rb_control_frame_t *cfp)
{
const rb_control_frame_t *prev_cfp = RUBY_VM_PREVIOUS_CONTROL_FRAME(cfp);
if (cfp->iseq && VM_FRAME_RUBYFRAME_P(cfp)) {
VALUE *bp = prev_cfp->sp + cfp->iseq->body->local_table_size + VM_ENV_DATA_SIZE;
if (cfp->iseq->body->type == ISEQ_TYPE_METHOD) {
/* adjust `self' */
bp += 1;
}
#if VM_DEBUG_BP_CHECK
if (bp != cfp->bp_check) {
fprintf(stderr, "bp_check: %ld, bp: %ld\n",
(long)(cfp->bp_check - GET_EC()->vm_stack),
(long)(bp - GET_EC()->vm_stack));
rb_bug("vm_base_ptr: unreachable");
}
#endif
return bp;
}
else {
return NULL;
}
}
local_table_size
の書き換えが必要。
もう一度同じところまで進めてlocal_table_sizeも合わせて書き換える
(gdb) p VM_BH_TO_ISEQ_BLOCK(VM_CF_BLOCK_HANDLER((ruby_current_execution_context_ptr->cfp + 2)))->code.ise
q->body.local_table_size
$82 = 0
(gdb) p VM_BH_TO_ISEQ_BLOCK(VM_CF_BLOCK_HANDLER((ruby_current_execution_context_ptr->cfp + 2)))->code.iseq->body.local_table_size = 1
(gdb) c
Continuing.
[Thread 0x7ffff7ff6700 (LWP 20326) exited]
[Inferior 1 (process 20325) exited normally]
正常終了するようになった
積まれた引数を取得する
ブロックを書き換えて、CFPにyield
に渡された値が捨てられずにスタックに積まれたままになった。
vm_invoke_iseq_block
を呼び出したcfpではSET_SP(rsp);
でspがyield
で積んだとこを指してるので、vm_invoke_block
を呼び出したcfpのspにアクセスすると取れる。
スタックの様子
ブロック呼び出し時のスタックの様子
static VALUE
vm_invoke_iseq_block(rb_execution_context_t *ec, rb_control_frame_t *reg_cfp,
struct rb_calling_info *calling, const struct rb_call_info *ci,
int is_lambda, const struct rb_captured_block *captured)
{
const rb_iseq_t *iseq = rb_iseq_check(captured->code.iseq);
const int arg_size = iseq->body->param.size;
VALUE * const rsp = GET_SP() - calling->argc;
int opt_pc = vm_callee_setup_block_arg(ec, calling, ci, iseq, rsp, is_lambda ? arg_setup_method : arg_setup_block);
この時点ではこういう感じ、cfp->sp
が引数の1つ上を指してる。引数の個数が1つなんでrspはspの1つ下
SET_SP(rsp);
SET_SP
呼び出すとcfp->sp
がここに移動する
vm_push_frame呼び出す
vm_push_frame(ec, iseq,
VM_FRAME_MAGIC_BLOCK | (is_lambda ? VM_FRAME_FLAG_LAMBDA : 0),
captured->self,
VM_GUARDED_PREV_EP(captured->ep), 0,
iseq->body->iseq_encoded + opt_pc,
rsp + arg_size,
iseq->body->local_table_size - arg_size, iseq->body->stack_max);
return Qundef;
}
vm_push_frame
では新しいcfpが作られる
static inline rb_control_frame_t *
vm_push_frame(rb_execution_context_t *ec,
const rb_iseq_t *iseq,
VALUE type,
VALUE self,
VALUE specval,
VALUE cref_or_me,
const VALUE *pc,
VALUE *sp,
int local_size,
int stack_max)
{
rb_control_frame_t *const cfp = ec->cfp - 1;
int i;
vm_check_frame(type, specval, cref_or_me, iseq);
VM_ASSERT(local_size >= 0);
/* check stack overflow */
CHECK_VM_STACK_OVERFLOW0(cfp, sp, local_size + stack_max);
ec->cfp = cfp;
/* setup new frame */
cfp->pc = (VALUE *)pc;
cfp->iseq = (rb_iseq_t *)iseq;
cfp->self = self;
cfp->block_code = NULL;
/* setup vm value stack */
/* initialize local variables */
for (i=0; i < local_size; i++) {
*sp++ = Qnil;
}
/* setup ep with managing data */
VM_ASSERT(VM_ENV_DATA_INDEX_ME_CREF == -2);
VM_ASSERT(VM_ENV_DATA_INDEX_SPECVAL == -1);
VM_ASSERT(VM_ENV_DATA_INDEX_FLAGS == -0);
*sp++ = cref_or_me; /* ep[-2] / Qnil or T_IMEMO(cref) or T_IMEMO(ment) */
*sp++ = specval /* ep[-1] / block handler or prev env ptr */;
*sp = type; /* ep[-0] / ENV_FLAGS */
cfp->ep = sp;
cfp->sp = sp + 1;
#if VM_DEBUG_BP_CHECK
cfp->bp_check = sp + 1;
#endif
if (VMDEBUG == 2) {
SDR();
}
return cfp;
}
このときISeqのブロックの引数が0の時と引数1のときではvm_push_frame
に渡されるspの位置がこういう感じでずれている。
ISeqの引数0のときはこういう感じでspに積まれていた42が上書きされてしまう。
ISeqの引数が1のときはこういう感じで上書きされず残っている
積まれた引数を取るメソッドを生やす
こういうプログラムだと
def foo
yield 42
end
foo { it }
it
メソッド呼び出した際のcontrol frameのスタックはこうなっている
なのでit
で積まれた引数を取るには以下のような感じの2通りのやり方で取れる
def it
# ここで1つ上のcfpを参照するとブロックのcfpが取れる、1つ上のcfp->ep - 3にアクセスする
# or
# ここで2つ上のcfpを参照するとブロックを呼び出したときのcfpが取れる、メソッド呼び出し時のcfp->spにアクセスする
end
実装
rb_control_frame_t
の型などはrubyのAPIとしてinclude出来るヘッダーが公開されていないので必要な部分をコピーしてくる。
バージョンごとに構造体が違うのでバージョンごとの定義を用意する。
https://github.com/hanachin/thats_it/blob/master/ext/thats_it/thats_it_20500.h
https://github.com/hanachin/thats_it/blob/master/ext/thats_it/thats_it_20600.h
RUBY_API_VERSION
で分岐して各バージョンにあった定義を読み込む。
#if RUBY_API_VERSION_CODE >= 20600
#include "thats_it_20600.h"
#elif RUBY_API_VERSION_CODE >= 20500
#include "thats_it_20500.h"
#endif
現在実行中のcontrol frameはこんな感じでexternしておいて
extern rb_execution_context_t *ruby_current_execution_context_ptr;
こうやれば取れる
ruby_current_execution_context_ptr->cfp
control frameやiseq書き換え周りはCで書いている。
https://github.com/hanachin/thats_it/blob/master/ext/thats_it/thats_it.c
TracePoint仕掛けるのはrubyの方が楽に書けそうなのでrubyで書いた。
https://github.com/hanachin/thats_it/blob/master/lib/thats_it.rb
まとめ
C拡張を書けばTracePointでメソッド呼び出し時に強引にブロックのISeqを取得できる。
メソッド呼び出し時のタイミングであれば渡されたブロックのISeqを書き換えられる。
ブロックは呼び出されるまで評価されないためISeqを実行する前にISeqを書き換えられる。
マクロだーーー!!!
完
-
実際には書き換え後のコードと同様の動きをするわけではない。ローカル変数
it
には何も入っておらずit
は変数参照ではなくメソッド呼び出しとなっている。RubyVMとISeqの動作はRubyのコードで表現できないので雰囲気だけ掴んでもらえれば。 ↩ -
RubyVMの設計についてのドキュメント参考 https://github.com/ruby/ruby/blob/v2_5_1/template/yarvarch.ja ↩
-
例えば呼び出す際に
yield 1, 2, 3
するとcalling->argc
は3
、なのでスタックに3つ値が積まれた状態だとrsp
は積まれていない底を指している。新しいスタックの底はrsp + arg_size
で決定されるので引数0の場合は積んである1, 2, 3は後からスタックを積む際に上書きされて消えてしまう。ブロックが3つ引数を取る場合はrsp + 3
で3つの値が積まれた状態を指すようになる。このへん説明難しいのでRubyのしくみを読みましょう... ↩ -
Rubyのオブジェクトをプリント出来るデバッグ用のC関数 https://github.com/ruby/ruby/blob/v2_5_1/io.c#L7655 ↩