Vim: 「*」キーでハイライト検索した時にカーソルが次の候補に移動しないようにする方法

検索する際に,"hlsearch" オプションを有効にして,検索語をハイライトし,視認性を高める."*" コマンドでカーソル位置の単語をサクッと検索する.
Peace Pipe: 効率的なテキスト編集の7つの習慣

確かに「*」キーでハイライト検索するとサクッと検索できて便利です。しかし、Vimのデフォルトの設定では、ヒットした文字列が複数ある場合、カーソルが次の候補に移動してしまい、ちょっとビックリします。カーソルは次の候補に移動しないで単に、現在のカーソルにあるキーワードをハイライトしてくれるだけで十分です。

.vimrcに以下の設定を書いておけば、勝手に次の候補にカーソルが移動しなくなります。

% vim ~/.vimrc
[...]
nmap * *N

仕掛けは簡単で、「*」キーで次の候補に移動してしまったカーソルを「N」コマンドで強制的に戻しているだけです。

Vim: GNU Screenの環境下でctagsによるタグジャンプの操作を快適にする設定

ctagsとvimを連携させて使うと、C言語などのプログラムをコーディング中に、関数など定義位置に簡単にジャンプできて便利です。デフォルトのvimの設定では、"Ctrl + ]"で関数などの定義位置に移動し、"Ctrl + t"で元の位置に戻れます。

GNU Screenで新しいウインドウを開いたりする時に使うprefixキーは、デフォルトではCtrl + aです。しかし、それをカスタマイズしてCtrl + tに変更している方も多いはずです。僕もCtrl + tに変更しています。(zshではCtrl + aを押した時に、カーソルが先頭に行くのですが、そのキーバインドGNU Screenのprefixキーがかぶらないないようにするために、変更しています。)

ここで少し困ったことが起きます。prefixキーをCtrl + tに変更したGNU Screenvimを一緒に使うと、vimでタグジャンプから戻るためのコマンド(Ctrl + t)が使えなくなります。Ctrl + tを押すと、それはGNU Screenでのprefixキーとして認識されてしまいます。GNU Screenvimでのキーバインドの衝突を回避する必要があります。

そこで、.vimrcに以下の設定を追加します。この設定をすると、"Ctrl + t"の代わりに、"Ctrl + ["でタグジャンプから戻れるようになります。"Ctrl + ]"でタグジャンプするので、その逆の操作は"Ctrl + ["でできれば直感的だと考えました。

% vim ~/.vimrc
[...]
nmap <c-[>  :pop<CR>

YARV: GDBとちょっとしたHackによる仮想マシン内のスタックの状態変化の観察

はじめに

仮想マシンが導入されたRubyの動作を理解するには、仮想マシン内のスタックの状態変化を頭の中でイメージできるようになることが重要です。そこで今回は、仮想マシン内のスタックの状態変化を追いやすくする小さなツールを作り、そのツールをGDBと組み合わせて使うことで、仮想マシン内のスタックの状態変化を観察できるようにする方法を紹介します。なお、使用するソースコードはr11701です。

ツールの作成

仮想マシン内のスタックの状態変化を追いやすくするために、以下の機能をrubyに追加します。

  • breakpoint: RubyプログラムからSIGTRAPを発生させる
  • dump_stack: スレッドに関連づけられているスタックの中身をinspectした状態でダンプする

前者を実装すると、GDB上でRubyプログラムを実行し、「breakpoint」と書かれた所が実行された時に、制御がGDBに移るようにできます。これによって、関心のある位置におけるスタックの状態を、GDBで簡単に調査できるようになります。

vm_dump.cで定義されているvm_stack_dump_raw関数を使うと、スレッドに関連づけされている、スタックと制御フレームをダンプできます。しかし、この関数は、スタックの中身を生のVALUEでダンプするため、何のデータがスタックに格納されているのかわかりづらいという問題があります。そこで、後者を実装し、スタックに何が格納されているのか簡単に確認できるようにします。

ちなみに、vm_stack_dump_raw関数は以下のようなダンプを出力します。stack frameの一番右側の列が、VALUEを16進数で表現した値です。

(gdb) p vm_stack_dump_raw(th, th->cfp)
-- stack frame ------------
0000 (0x605000): 00000004
0001 (0x605004): 00000005
0002 (0x605008): 004ce4d8
0003 (0x60500c): 004ce4c4
0004 (0x605010): 004ce4b0
0005 (0x605014): 00000004
0006 (0x605018): 00000001
0007 (0x60501c): 00000004
0008 (0x605020): 00000004
0009 (0x605024): 00000001 <- lfp <- dfp
-- control frame ----------
c:0004 p:---- s:0010 b:0010 l:000009 d:000009 CFUNC  :breakpoint
c:0003 p:0063 s:0007 b:0007 l:000006 d:000006 TOP    stack.rb:21
c:0002 p:---- s:0002 b:0002 l:000001 d:000001 FINISH 
c:0001 p:---- s:0000 b:-001 l:000000 d:000000 ------ 
---------------------------

作成したツールのソースコードは、以下の通りです。

前者については、int3ソフトウェア割り込みが発生するようにしています。rb_define_global_functionのAPIを使って、rb_f_breakpoint関数を、Rubyプログラムからbreakpoint関数として利用できるようにしています。簡単のため、object.cのInit_Object関数の最後で、その関連付けをするようにしました。

後者については、rb_p関数を使って、スタックの中身をinspectした形でダンプするようにしています。スタックの底からtopに向かってダンプします。vm_dump.cに定義します。

% svn diff
Index: object.c
===================================================================
--- object.c    (revision 11701)
+++ object.c    (working copy)
@@ -2284,6 +2284,13 @@
  *  <code>Symbol</code> (such as <code>:name</code>).
  */
 
+VALUE
+rb_f_breakpoint(void)
+{
+    asm volatile("int3");
+    return 0;
+}
+
 void
 Init_Object(void)
 {
@@ -2465,4 +2472,6 @@
     id_eql = rb_intern("eql?");
     id_inspect = rb_intern("inspect");
     id_init_copy = rb_intern("initialize_copy");
+
+    rb_define_global_function("breakpoint", rb_f_breakpoint, 0);
 }
Index: vm_dump.c
===================================================================
--- vm_dump.c   (revision 11701)
+++ vm_dump.c   (working copy)
@@ -608,3 +608,14 @@
     }
 #endif
 }
+
+void
+dump_stack(rb_thread_t *th)
+{
+    VALUE *p = th->stack;
+    while (p < th->cfp->sp) {
+        fprintf(stderr, "%p: ", p);
+        rb_p(*p);
+        p++;
+    }
+}

GDBによる観察

以下のような、RubyプログラムとGDBを使って、仮想マシン内のスタックの状態変化を観察してみます。このプログラムに与えられた引数によって、breakpointが実行される位置が変更できるようにしてあります。また、トップレベルとfooメソッドとbarメソッドで、ローカル変数が作られています。

% vim stack.rb
$arg = ARGV.shift.to_i || 1

def foo(x, y, z)
  a = "a (foo)"
  breakpoint if $arg == 2
  bar(x, y)
  breakpoint if $arg == 4
end

def bar(x, y)
  b = "b (bar)"
  puts x
  puts y 
  breakpoint if $arg == 3
end

x = "x (top)"
y = "y (top)"
z = "z (top)"

breakpoint if $arg == 1

foo(x, y, z)

それでは、このstack.rbをGDB上で動作させてみます。
まず、stack.rbに引数「1」を与えてrunしてみます。
そうすると、SIGTRAPシグナルでGDBに制御が移ります。SIGTRAPシグナルが発生したのは、先ほど作ったrb_f_breakpoint関数の中でint3ソフトウェア割り込みが発生させたためです。

% ./ruby -v
ruby 1.9.0 (2007-02-11 patchlevel 0) [i686-darwin8.8.1]
% gdb --quiet ./ruby
Reading symbols for shared libraries ... done
(gdb) run stack.rb 1
Starting program: /Users/ysano/ruby-trunk/ruby stack.rb 1
Reading symbols for shared libraries .... done

Program received signal SIGTRAP, Trace/breakpoint trap.
rb_f_breakpoint () at object.c:2292
2292    }
(gdb) bt
#0  rb_f_breakpoint () at object.c:2292
#1  0x0008788f in th_eval (th=0x2ce000, initial=0) at insns.def:1289
#2  0x0008a47b in th_eval_body (th=0x2ce000) at vm.c:1598
#3  0x0008a76f in rb_thread_eval (th=0x2ce000, iseqval=5040120) at vm.c:1804
#4  0x00002e91 in ruby_exec_internal () at eval.c:228
#5  0x00002ebc in ruby_exec () at eval.c:241
#6  0x00006fc0 in ruby_run () at eval.c:260
#7  0x00002533 in main (argc=3, argv=0xbffffa18, envp=0xbffffa28) at main.c:47

フレーム#1に移動して、先ほど作ったdump_stack関数で、th_eval_body関数の第一引数に渡されたスレッド(th)に関連付けされたスタックをダンプしてみます。ダンプされた結果を見ると、0x605008〜0x605010に、トップレベルで定義したローカル変数がスタックに積まれていることがわかります。(その他の値がなぜこのように積まれているのかは、まだ理解できていません。。。)

(gdb) f 1
#1  0x0008788f in th_eval (th=0x2ce000, initial=0) at insns.def:1289
1289        macro_eval_invoke_method(recv, klass, id, num, mn, blockptr);
(gdb) whatis th
type = rb_thread_t *
(gdb) p dump_stack(th)
0x605000: nil
0x605004: 2
0x605008: "x (top)"
0x60500c: "y (top)"
0x605010: "z (top)"
0x605014: nil
0x605018: 0
0x60501c: nil
0x605020: nil
0x605024: 0
$1 = void
(gdb) 

次に、stack.rbの引数に2を与えて、再度観察してみます。
fooメソッドが呼ばれた時点で、実行が停止します。
スタックが拡張されていることがわかります。
0x605020〜0x605028は、fooメソッドに渡された引数で、0x60502cは、fooメソッド内で定義したローカル変数aです。
それらの前にある0x60501cのnilは、foo(関数風メソッド)を呼び出した時に積まれた、ダミーのレシーバオブジェクトです。
0x605018と0x605034では0が出力されていますが、これは恐らくspecvalです。(specvalは論理的な固まりの境界を表現しているのかな?)

(gdb) run stack.rb 2
Starting program: /Users/ysano/ruby-trunk/ruby stack.rb 2
Reading symbols for shared libraries .... done

Program received signal SIGTRAP, Trace/breakpoint trap.
rb_f_breakpoint () at object.c:2292
2292    }
(gdb) f 1
#1  0x0008788f in th_eval (th=0x2ce000, initial=0) at insns.def:1289
1289        macro_eval_invoke_method(recv, klass, id, num, mn, blockptr);
(gdb) p dump_stack(th)
0x605000: nil
0x605004: 2
0x605008: "x (top)"
0x60500c: "y (top)"
0x605010: "z (top)"
0x605014: nil
0x605018: 0
0x60501c: nil
0x605020: "x (top)"
0x605024: "y (top)"
0x605028: "z (top)"
0x60502c: "a (foo)"
0x605030: nil
0x605034: 0
0x605038: nil
0x60503c: nil
0x605040: 0
$1 = void
(gdb)

しつこく、stack.rbの引数に3を与えて、再度観察してみます。
スタックがさらに拡張されました。規則性が見えてきました。

(gdb) run stack.rb 3
Starting program: /Users/ysano/ruby-trunk/ruby stack.rb 3
Reading symbols for shared libraries .... done
x (top)
y (top)

Program received signal SIGTRAP, Trace/breakpoint trap.
rb_f_breakpoint () at object.c:2292
2292    }
(gdb) f 1
#1  0x0008788f in th_eval (th=0x2ce000, initial=0) at insns.def:1289
1289        macro_eval_invoke_method(recv, klass, id, num, mn, blockptr);
(gdb) p dump_stack(th)
0x605000: nil
0x605004: 2
0x605008: "x (top)"
0x60500c: "y (top)"
0x605010: "z (top)"
0x605014: nil
0x605018: 0
0x60501c: nil
0x605020: "x (top)"
0x605024: "y (top)"
0x605028: "z (top)"
0x60502c: "a (foo)"
0x605030: nil
0x605034: 0
0x605038: nil
0x60503c: "x (top)"
0x605040: "y (top)"
0x605044: "b (bar)"
0x605048: nil
0x60504c: 0
0x605050: nil
0x605054: nil
0x605058: 0
$1 = void
(gdb) 

最後に、stack.rbの引数に4を与えて、実行すると、引数に2を渡した時とスタックが同じ状態になります。(試してみて確認してみて下さい。) このような結果になったのは、両者のbreakpointが同じスコープで実行されたからです。

おわりに

今回は、仮想マシン内のスタックの状態変化を観察してみました。観察をしやすくするために、rubyGDBと連携して利用するツールを組み込みました。観察を通して、スタックの状態変化を頭の中で少しイメージできるようになりました。
スタックの状態変化は、仮想マシン内の状態変化のごく一部に過ぎません。今後は、他の状態変化についてもGDBで動的解析できるような工夫をして、理解を深めたいと思います。

参考文献

今回紹介したbreakpointメソッドの実装には、Binary Hacksで解説されているHack #92 「Cのプログラムの中でブレークポイントを設定する」を用いました。

Binary Hacks ―ハッカー秘伝のテクニック100選

Binary Hacks ―ハッカー秘伝のテクニック100選

YARV: 命令列のシリアライズによるRubyプログラムの難読化

はじめに

YARVコアの入ったrubyでは、YARVの命令列のシリアライズ機構を用いることでRubyプログラムを簡単に難読化できます。今回は、hello worldRubyプログラムを難読化してみます。なお、今回の説明ではリビジョン11607を使い、YARVコアの入ったrubyがビルドした状態を前提にしています。また、YARV Maniacs 【第 8 回】 命令列のシリアライズを予め読んでおくと、良いと思います。

難読化ツールの利用

tool/compile.rbを使うと簡単にRubyプログラムの難読化ができます。ここでは、hello worldRubyプログラムを難読化してみます。-oオプションで難読化処理されたRubyプログラムのファイル名を指定します。このオプションが無いと、デフォルトでは「a.rb」というファイルが生成されます。

% cat hello.rb
puts "hello, world"
% ./ruby -I./lib tool/compile.rb -o obfuscated.rb hello.rb

難読化されたhello worldなプログラムは以下の通りです。YARV命令列がシリアライズされ、それがBase64エンコードされています。

% cat obfuscated.rb
VM::InstructionSequence.load(Marshal.load(<<EOS____.unpack('m*')[0])).eval
BAhbEiIkWUFSVkluc3RydWN0aW9uU2ltcGxlZGF0YUZvcm1hdGkGaQZpBjAi
CzxtYWluPiINaGVsbG8ucmJbADoNdG9wbGV2ZWxbAFsKaQBpAFsAaQBpAFsA
WwlbBjoLcHV0bmlsWwc6DnB1dHN0cmluZyIRaGVsbG8sIHdvcmxkWws6CXNl
bmQ6CXB1dHNpBjBpDTBbBjoKbGVhdmU=
EOS____

この難読化されたRubyプログラムを実行すると、全く同じ結果が得られます。

% ./ruby obfuscated.rb
hello, world
% diff =(./ruby hello.rb) =(./ruby obfuscated.rb)

またもやパッチ

実は、最新のソースコードでは、難読化したRubyプログラムは期待通りに動作しません。以下のパッチを適用する必要があります。

% svn diff
Index: ruby.h
===================================================================
--- ruby.h      (revision 11607)
+++ ruby.h      (working copy)
@@ -771,6 +771,8 @@
     else if (!RTEST(obj)) {
        if (obj == Qnil) return T_NIL;
        if (obj == Qfalse) return T_FALSE;
+    } else {
+        if (SYMBOL_P(obj)) return T_SYMBOL;
     }
     return BUILTIN_TYPE(obj);
 }

このパッチを適用していないと、以下のようなエラーが出ます。

% ./ruby obfuscated.rb
obfuscated.rb:1:in `load': can't convert Symbol to Symbol (Symbol#to_sym gives Symbol) (TypeError)
        from obfuscated.rb:1:in `<main>'

パッチ作成までの過程

まず、エラーメッセージを出力している場所をgrepで特定しました。

% grep -rn "can't convert" . | grep -v svn | grep gives                                                                      
./object.c:1856:        rb_raise(rb_eTypeError, "can't convert %s to %s (%s#%s gives %s)",
./object.c:1873:        rb_raise(rb_eTypeError, "can't convert %s to %s (%s#%s gives %s)",
./object.c:1886:        rb_raise(rb_eTypeError, "can't convert %s to Integer (%s#%s gives %s)",

object.cの1856行目のrb_raise関数の呼び出しが怪しそうだったので、以下のようにブレークポイントを配置しました。bp()はマクロで、debug.cで定義されているdebug_breakpoint関数に置換されます。bp()マクロは、「make gdb」と組み合わせて使います。これは結構便利です。

Index: object.c
===================================================================
--- object.c    (revision 11607)
+++ object.c    (working copy)
@@ -1853,6 +1853,7 @@
     v = convert_type(val, tname, method, Qtrue);
     if (TYPE(v) != type) {
    char *cname = rb_obj_classname(val);
+        bp();
    rb_raise(rb_eTypeError, "can't convert %s to %s (%s#%s gives %s)",
         cname, tname, cname, method, rb_obj_classname(v));
     }

makeでビルドします。make gdbを実行すると、GDBが起動して先ほどbp()と書いた所で実行が停止します。make gdbではtest.rbを対象にしてデバッグするため、cpコマンドでobfuscated.rbをtest.rbにコピーしておきました。

% make
% cp obfuscated.rb test.rb
% make gdb                                                                                                             
gdb -x run.gdb --quiet --args ./miniruby  -I./lib ./test.rb
Using host libthread_db library "/lib/libthread_db.so.1".
Breakpoint 1 at 0x80cfb13: file debug.c, line 71.
[Thread debugging using libthread_db enabled]
[New Thread -1208252752 (LWP 2815)]
[New Thread -1208255584 (LWP 2819)]
[Switching to Thread -1208252752 (LWP 2815)]

Breakpoint 1, debug_breakpoint () at debug.c:71
71      }

ちなみに、GDBの-xオプションで指定しているrun.gdbには以下のような記述が書かれています。GDBは、-xオプションで指定したスクリプトを実行してくれます。このスクリプトを見た時、こんなテクニックがあるのかと、ちょっと感動しました。

% cat run.gdb
b debug_breakpoint
run

GDBのセッションでbtコマンドを実行して、バックトレースを調査しました。

(gdb) bt
#0  debug_breakpoint () at debug.c:71
#1  0x0807bc81 in rb_convert_type (val=3085928060, type=20, tname=0x811a823 "Symbol", method=0x8105156 "to_sym") at object.c:1856
#2  0x080d25bf in iseq_load (self=3085975580, data=3085928180, parent=0, opt=4) at iseq.c:333
#3  0x080d2956 in iseq_s_load (argc=1, argv=0xb7e2d01c, self=3085975580) at iseq.c:380
#4  0x080d3016 in call_cfunc (func=0x80d2900 <iseq_s_load>, recv=3085975580, len=Variable "len" is not available.
) at call_cfunc.ci:24
#5  0x080d6ade in th_eval (th=0x91a0e40, initial=0) at ./insns.def:1278
#6  0x080d8cbc in th_eval_body (th=0x91a0e40) at vm.c:1521
#7  0x080dadb8 in yarv_th_eval (th=0x91a0e40, iseqval=3085928580) at yarvcore.c:462
#8  0x080590fb in ruby_exec_internal () at eval.c:233
#9  0x08059136 in ruby_exec () at eval.c:246
#10 0x0805ca21 in ruby_run () at eval.c:266
#11 0x0805686f in main (argc=135429892, argv=0xb, envp=0x1) at main.c:46
(gdb)

フレーム#1を見てみると、TYPEマクロが正しい型を返していないことに気が付きました。printデバッグをしていると、object.cの1856行目の「TYPE(v)」は、obfuscated.rbで起動した時には、常に0x07つまりT_STRINGが返っていることがわかりました。

(gdb) f 1
#1  0x0807bc81 in rb_convert_type (val=3085928060, type=20, tname=0x811a823 "Symbol", method=0x8105156 "to_sym") at object.c:1856
1856            bp();
(gdb) list
1846
1847    VALUE
1848    rb_convert_type(VALUE val, int type, const char *tname, const char *method)
1849    {
1850        VALUE v;
1851
1852        if (TYPE(val) == type) return val;
1853        v = convert_type(val, tname, method, Qtrue);
1854        if (TYPE(v) != type) {
1855            char *cname = rb_obj_classname(val);
1856            bp();
1857            rb_raise(rb_eTypeError, "can't convert %s to %s (%s#%s gives %s)",
1858                     cname, tname, cname, method, rb_obj_classname(v));
1859        }
1860        return v;
1861    }
1862
1863    VALUE
1864    rb_check_convert_type(VALUE val, int type, const char *tname, const char *method)
1865    {
(gdb)

それで、TYPEマクロはrb_type関数に展開されるので、rb_type関数のコードを見ました。調べていると、「objの型が明らかにT_SYMBOLである時、IMMEDIATE_P(obj)は常に真である」という命題は成り立たないことがわかりました。つまり、IMMEDIATE_P(obj)が真でなくても、SYMBOL_P(obj)が真になるケースがあることに気が付きました。また、もともとこのif文は問題があって、else ifのelseが無いので、ロジックに抜けがある可能性が非常に高いと思いました。

static inline int
rb_type(VALUE obj)
{
    if (IMMEDIATE_P(obj)) {
        if (FIXNUM_P(obj)) return T_FIXNUM;
        if (obj == Qtrue) return T_TRUE;
        if (SYMBOL_P(obj)) return T_SYMBOL;
        if (obj == Qundef) return T_UNDEF;
    }
    else if (!RTEST(obj)) {
        if (obj == Qnil) return T_NIL;
        if (obj == Qfalse) return T_FALSE;
    }
    return BUILTIN_TYPE(obj);
}

以上がパッチ作成までの流れです。

おわりに

今回は、Rubyプログラムを難読化する方法を紹介しました。最新のソースコードでは、難読化したRubyプログラムが期待通りに動作しなかったので、パッチを作成しました。また、パッチ作成時に、ささださんオリジナルのbp()マクロとmake gdbを組み合わせて、効率よくデバッグできることを確認しました。

追記(2007/02/02 23:05)

r11615から、ここで紹介したパッチを適用しなくても、Rubyプログラムを難読化できるようになりました。

matz    2007-02-02 22:19:44 +0900 (Fri, 02 Feb 2007)

 New Revision: 11615

 Modified files:
   trunk/ChangeLog
   trunk/compile.c
   trunk/parse.y
   trunk/ruby.h
   trunk/string.c

 Log:
   * ruby.h (SYMBOL_P): make Symbol immediate again for performance.

   * string.c: redesign symbol methods.

   * parse.y (rb_id2str): store Strings for operator symbols.
     [ruby-dev:30235]

YARV: hello worldなRubyプログラムをVMの命令列に変換する実験

はじめに

Rubyのtrunkでは最近YARVがマージされました。YARVが入ったrubyで遊んでみたい人も多いはず。そこで、今回はバージョン管理されているRubyの最新版のソースコードをゲットして、おなじみのhello worldなプログラムをYARV仮想マシンの命令列に変換して、それをダンプする実験をしてみます。なお、今回の説明ではリビジョン11585を使います。

YARVコアが入ったrubyのビルド

以下の手順で、YARVコアが入ったrubyをビルドできます。

% svn co http://svn.ruby-lang.org/repos/ruby/trunk ruby-trunk
% cd ruby-trunk
% autoconf
% ./configure && make

configureスクリプトを生成するために、autoconfを予めインストールしておく必要があります。僕の環境では2.59がインストールしてあります。
また、Fedora Core 5でビルドできることを確認しました。残念ながら、Mac OS X 10.4.8では、うまくビルドできませんでした。*1

% autoconf --version
autoconf (GNU Autoconf) 2.59
Written by David J. MacKenzie and Akim Demaille.

Copyright (C) 2003 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

% uname -a
Linux devel 2.6.15-1.2054_FC5smp #1 SMP Tue Mar 14 16:05:46 EST 2006 i686 i686 i386 GNU/Linux
% 

無事、ビルドが成功すると、ruby-trunkディレクトリ以下に「ruby」という実行ファイルが生成されます。
これが、YARVコアが入ったrubyです。

YARV仮想マシンの命令列をダンプする

ruby-trunkディレクトリ以下に、「tool」というディレクトリがあります。このディレクトリの中にparse.rbというプログラムが入っています。parse.rbを使うと、parse.rbに与えられたRubyプログラムを、YARV仮想マシンの命令列に変換して、それをわかりやすくダンプしてくれます。

リビジョン11585のソースコードでは、実はparse.rbが動作しません。ちょっとしたパッチを適用する必要があります。以下がそのパッチです。YARVCoreというクラス名をVMに変更するだけです。

% svn diff
Index: tool/parse.rb
===================================================================
--- tool/parse.rb       (revision 11585)
+++ tool/parse.rb       (working copy)
@@ -6,7 +6,7 @@
 puts $str
 puts '# ' + '-' * 70
 
-$parsed = YARVCore::InstructionSequence.compile_file($file)
+$parsed = VM::InstructionSequence.compile_file($file)
 puts "# disasm result: "
 puts '# ' + '-' * 70
 puts $parsed.disasm

今回は、おなじみのhello worldなプログラムをYARV仮想マシンの命令列に変換してみます。一応、1+2の計算結果も出力するコードも追加しました。

% cat hello.rb
puts "hello, world"
puts 1+2

以下は、parse.rbを使ってhello.rbをYARV仮想マシンの命令列に変換して、その結果をダンプした様子です。
うまくダンプできたみたいですね。

YARV仮想マシンの命令セットについては、YARV Maniacs 【第 4 回】 命令セット (1) YARV 命令セットの初歩の初歩なんかが参考になると思います。

% ./ruby tool/parse.rb hello.rb
# ----------------------------------------------------------------------
# target program: 
# ----------------------------------------------------------------------
puts "hello, world"
puts 1+2
# ----------------------------------------------------------------------
# disasm result: 
# ----------------------------------------------------------------------
== disasm: <ISeq:<main>@hello.rb>=======================================
local scope table (size: 1, argc: 0)

0000 putnil                                                           (   1)
0001 putstring        "hello, world"
0003 send             :puts, 1, nil, 8, <ic>
0009 pop              
0010 putnil                                                           (   2)
0011 putobject        1
0013 putobject        2
0015 opt_plus         
0016 send             :puts, 1, nil, 8, <ic>
0022 leave            
# ----------------------------------------------------------------------

寄り道: tool/parse.rbに対するパッチ作成までの過程

ここで、tool/parse.rbに対するパッチをどのようにして作成したか、その過程をちょっと書いておきます。

まず、tool/parse.rbがそのままでは動作しませんでした。

% ./ruby tool/parse.rb hello.rb
# ----------------------------------------------------------------------
# target program: 
# ----------------------------------------------------------------------
puts "hello, world"
puts 1+2
# ----------------------------------------------------------------------
tool/parse.rb:9:in `<main>': uninitialized constant YARVCore (NameError)

tool/parse.rbのソースコードを見ると、YARV命令列のダンプには、YARVCore::InstructionSequence#disasmを使っているようだったので、それに関連するソースコード(iseq.c)を見ました。iseq.cを見ると、InstructionSequenceクラスはrb_cVMで定義されたクラスの配下の名前空間に位置することがわかりました。

% vim tool/parse.rb
[...]
$parsed = YARVCore::InstructionSequence.compile_file($file)
[...]
puts $parsed.disasm
[...]
% vim iseq.c
[...]
void
Init_ISeq(void)
{
    /* declare YARVCore::InstructionSequence */
    rb_cISeq = rb_define_class_under(rb_cVM, "InstructionSequence", rb_cObject);
    rb_define_alloc_func(rb_cISeq, iseq_alloc);
    rb_define_method(rb_cISeq, "inspect", iseq_inspect, 0);
    rb_define_method(rb_cISeq, "disasm", iseq_disasm, 0);
[...]

そこで、rb_cVMを定義しているyarvcore.cを見てみました。Init_vm()関数を見てみると、rb_cVMには「VM」というクラス名がつけられていることがわかりました。従って、正しい名前空間は「YARVCore::InstructionSequence」ではなく、「VM::InstructionSequence」であることが判明しました。

% vim yarvcore.c
[...]
void
Init_vm(void)
{
    Init_ISeq();

    /* ::VM */
    rb_cVM = rb_define_class("VM", rb_cObject);
[...]

何が間違っていたのかわかったので、tool/parse.rbを上で示したパッチのように修正を加えました。そうすると、tool/parse.rbが
動作するようになりました。

以上がパッチ作成までの流れです。

おわりに

今回、rubyの最新のソースコードの中にあるtool/parse.rbを使って、hello worldRubyプログラムをYARV仮想マシンの命令列に変換し、その結果をダンプする実験を行いました。YARVアーキテクチャを理解するには、RubyプログラムをYARV仮想マシンの命令列に変換した結果をイメージできるようになる必要があると思います。rubySubversionリポジトリにある最新版は、動作が不安定ですが、面白いネタがいっぱいありそうです。これからも、また面白いネタを紹介できたらと思います。

追記#1(2007/01/30 00:10)

matzさんにパッチ取り込んでもらえました!!
本当に感激です。
従って、最新のソースコードでは、このエントリで説明したパッチを適用する必要がなくなりました。

% svn diff -r11585:11586
Index: ChangeLog
===================================================================
--- ChangeLog   (リビジョン 11585)
+++ ChangeLog   (リビジョン 11586)
@@ -1,3 +1,8 @@
+Mon Jan 29 14:14:35 2007  Yukihiro Matsumoto  <matz@ruby-lang.org>
+
+       * tool/parse.rb: replace YARVCore by VM class.
+         http://d.hatena.ne.jp/ysano2005/20070128
+
 Sun Jan 28 08:41:49 2007  Masaki Suketa  <masaki.suketa@nifty.ne.jp>
 
        * ext/win32ole/win32ole.c: refactoring.
Index: version.h
===================================================================
--- version.h   (リビジョン 11585)
+++ version.h   (リビジョン 11586)
@@ -1,7 +1,7 @@
 #define RUBY_VERSION "1.9.0"
-#define RUBY_RELEASE_DATE "2007-01-28"
+#define RUBY_RELEASE_DATE "2007-01-29"
 #define RUBY_VERSION_CODE 190
-#define RUBY_RELEASE_CODE 20070128
+#define RUBY_RELEASE_CODE 20070129
 #define RUBY_PATCHLEVEL 0
 
 #define RUBY_VERSION_MAJOR 1
@@ -9,7 +9,7 @@
 #define RUBY_VERSION_TEENY 0
 #define RUBY_RELEASE_YEAR 2007
 #define RUBY_RELEASE_MONTH 1
-#define RUBY_RELEASE_DAY 28
+#define RUBY_RELEASE_DAY 29
 
 RUBY_EXTERN const char ruby_version[];
 RUBY_EXTERN const char ruby_release_date[];
Index: tool/parse.rb
===================================================================
--- tool/parse.rb       (リビジョン 11585)
+++ tool/parse.rb       (リビジョン 11586)
@@ -6,7 +6,7 @@
 puts $str
 puts '# ' + '-' * 70
 
-$parsed = YARVCore::InstructionSequence.compile_file($file)
+$parsed = VM::InstructionSequence.compile_file($file)
 puts "# disasm result: "
 puts '# ' + '-' * 70
 puts $parsed.disasm

追記#2(2007/02/02 18:55)

r11609からMac OS Xでもrubyをビルドできるようになりました。

matz    2007-02-02 18:32:28 +0900 (Fri, 02 Feb 2007)

 New Revision: 11609

 Modified files:
   trunk/ChangeLog
   trunk/eval.c
   trunk/version.h

 Log:
   * eval.c: remove duplicated global variables rb_cProc and
     rb_cBinding.  [ruby-dev:30242]

パッチをruby-devに送っておいて良かったです。
http://blade.nagaokaut.ac.jp/cgi-bin/scat.rb/ruby/ruby-dev/30242

*1:追記#2を参照

Rubyを改造してAOPに対応した実装「あそびぃ」を公開しました

Rubyの中をHackして、アスペクト指向プログラミング(AOP)をネーティブに対応させてみました。実は去年の9月くらいから今までの4ヶ月間、これをずっとをやってました。(開発の途中でも、ブログでアウトプットすれば良かったのですが、中々見せられるような状態に持っていくのが大変だったので。) 別にAOPに凄いこだわりがあるわけでは無いです。Rubyの中がどのように動作しているのかを理解したくて、色々さわっていたらその延長線上にたまたまAOPがあったというだけです。

ここで、今回、言語処理系のように低いレイヤーの技術になぜ時間を投資してきたか、ちょっと書いておきます。ここ2年くらい、割と上位レイヤーの技術に関心があって(といか下位レイヤーは難しすぎた)、上位レイヤーの方ばかりさわってきました。例えば、Webアプリケーションの開発なんかです。しかし、1年くらい前から、上位レイヤーだけさわれても、いずれすぐに自分の能力に限界がくると考えるようになりました。特にWebアプリケーションが単に組めるだけでは、技術のコモディティー化の波にすぐにのみこまれてしまう可能性が非常に高いです。そして、何とか自分の独自性を主張できるようになりたい、と思うようになりました。

そのために、普通の人ができないこと(普通は「自分はできない」と決めつけてやろうともしない)を、自分にしかできないものにしてやれば、いずれ自分の独自性を主張できるようになると考えました。(考えが甘いかもしれませんが、その積み重ねは後に非常に大きな結果をもたらすものと信じています) その答えの1つが、今回やったHackなんだと思います。

Rubyに興味があって、手元にLinuxな環境がある方は、一度お試し下さい。Rubyがビルドできる環境なら、大抵うまくビルドできると思います。「ちょっと遊んでやるか」という感覚で試してみて下さい。何か少しでも輝いた所が見つかれば、このエントリにコメントでもして頂ければ幸いです。

http://asoby.redirectme.net/

新年のごあいさつとお知らせ

あけましておめでとうございます。
今年は低レイヤー技術をもっと勉強したいなぁと思っています。
Webプログラマーバイナリアンな人になれるよう頑張ります。


さて、1つお知らせがあります。
実は去年の9月頃から、ある新しい機能を組み込んだRubyの研究をこっそりしてきました。
まだ未熟で実用にはほど遠いですが、それをオープンソースの形で近日中に公開したいと考えています。
個人的にはRubyのHackから色々な勉強ができ、本当に良かったと思っています。
具体的な機能拡張については、また公開してからということで。
あんまり期待しすぎないでください(汗