YARV Maniacs 【第 4 回】 命令セット (1) YARV 命令セットの初歩の初歩

書いた人: ささだ

はじめに

YARV: Yet Another RubyVM を解説するこの連載。連載 4 回目にして、やっと YARV を触ります。

YARV は Ruby プログラムをコンパイラによって YARV 命令列に変換して、その命令列を実行します。今回は、Ruby プログラムがどのような YARV 命令列に変換されるか、簡単なところを紹介します。

その前に:YARV 0.3.0、0.3.1 - YARV と Ruby のマージ

今まで YARV は Ruby の拡張ライブラリとして開発してきましたが、先日 Ruby から従来の評価器を取り除き、YARV だけを使うバージョンを作りました (YARV 0.3.0)。今後はこれをさらに拡張し、レガシーなコードを取り除いていく作業をやっていきます。

今回の連載はこいつを使って解説していきたいので、ビルド方法を紹介します。ただし、今現在のビルド方法を解説しますので、将来はこの方法ではビルド出来ないかもしれません。いや、多分出来ないっぽい。

YARV のビルド方法

まず、YARV を取ってきます。現在の最新版は YARV 0.3.1 です。また、Subversion リポジトリを公開していますので、Subversion がインストールされている環境では以下のコマンドで最新版を取ってくることができます。

 $ svn checkout http://www.atdot.net/svn/yarv/trunk yarv

このコマンドで、yarv というディレクトリにソースコード一式がダウンロードされます。

さて、その状態で YARV をビルドしましょう。

 $ mkdir build
 $ cd build
 $ ../yarv/configure
 $ make

ふつーに Ruby のソースコードをビルドする方法と同じですね。Windows で VC な人は ../yarv/win32/configure.bat を configure の代わりに実行してください (make の代わりに nmake にするのも忘れずに)。

では、テストしましょう。

 $ make yarv-test-all

テストがガンガン走り出すはずです。問題がなければ

 147 tests, 378 assertions, 0 failures, 0 errors

みたいなメッセージが表示されると思います。そうじゃなかったら、バグですのでご連絡ください。

これで準備は整いました。

YARV を使ってみる

では、ちょっと使ってみましょう。yarv/test.rb というファイルに、とてつもなく簡単な Ruby プログラムを書いてみましょう。

 puts "Hello World!"

その上で、YARV をビルドしたディレクトリ (build/) で、make run と入力してみてください。

 $ make run
 ./miniruby.exe ../trunk/test.rb
 Hello World!

実行されました。成功です。miniruby 云々は make コマンドが実行するコマンドラインを表示しているだけですので無視してください。ちなみに、この実行は cygwin 上で行いました (だから、".exe" が付いている)。

ここで、miniruby を動かしていることに注目してください。ruby コマンドじゃないです。これは、現在の YARV の実装では、ruby コマンドを作るにはちょっと足りない部分があるので、とりあえず miniruby だけ動かしている、という意味です。

さて、もうちょっと頑張りましょう。make run の代わりに、make runp と入力してみてください。

$ make runp
./miniruby.exe ../trunk/rb/parse.rb ../trunk/test.rb
puts "Hello World!"

== disasm: <ISeq:<main>@../trunk/test.rb>===============================
local scope table (size: 1, argc: 0)

0000 putself                                                          (   1)
0001 putstring       "Hello World!"
0003 send            :puts, 1, false, 0, <ic>
0009 end
./miniruby.exe ../trunk/test.rb
Hello World!

なにやらずらずらと出てきました。ここで表示されるのは、「yarv/test.rb に記述した Ruby プログラム」と、「Ruby プログラムを YARV 命令セットに変換した命令列」、そして「実際にそれを実行した結果」です。

今回は、この「ずらずらと出てきた命令列」についてご紹介します。

YARV 命令セットの初歩の初歩

では、上記の「ずらずらと出てきた命令列」の例を解説します。

puts "Hello World!"

という Ruby プログラムをコンパイルした結果、

== disasm: <ISeq:<main>@../yarv/test.rb>===============================
local scope table (size: 1, argc: 0)

0000 putself                                                          (   1)
0001 putstring       "Hello World!"
0003 send            :puts, 1, false, 0, <ic>
0009 end

このような出力が得られたのでした。

この例の最初の 3 行はいわゆるヘッダで、命令列に関する付加的な情報を表示しています。たとえば、1 行目に <ISeq:<main>@../yarv/test.rb> と書いてあるのは、この命令列が ../yarv/test.rb というファイルに書いてあるトップレベルのプログラム (メソッド内などではない) であることを表しています。

まぁ、そういうのはおいといて、4 行目の表示を見てみましょう。

0000 putself                                                          (   1)

ここには、命令番地、命令名、命令オペランド、そして行番号が表示されます。ただし、putself 命令の場合、命令オペランドは無いのでこの例では表示されていません。

命令番地は 0 から数えるため、0000 と表示されています。行番号はもちろん 1 行目から数えるので (1) になっています。

さて、putself というのが、もちろん命令名なのですが、何をするんでしょうね。名前から想像すると、self と関係ありそうです。でも、元のプログラムには self なんて単語は一切書かれていません。

そもそも、put ってなんでしょうね。どこに put するんでしょうか。次の命令は putstring ですね。こっちも put。命令オペランドは "Hello World!" という文字列なので、きっとプログラム中に現れた文字列をあらわしていると思うのですが。

スタックマシン

putself 命令はどこに何を put するのか、ですが、まずは「どこに put するのか」という疑問に先に答えておきましょう。YARV はスタックマシンなので、そのものずばり、スタックトップに置きます。

putself は、ご想像のとおり、スタックに self を積みます。putstring は文字列を dup してスタックに積みます。

pushself のほうがわかりやすいじゃん、と思われるかもしれませんが、...あまり考えて命名しませんでした。

Ruby のコードで書いてみると、こんな感じです。

 VMStack.push(self)
 VMStack.push("Hello World!".dup)

というか、まさにこれだけです。

さて、積むだけ積んでみました。では、誰かが pop すると思うんですが、誰が pop するんですかね。

メソッドの起動

0003 send            :puts, 1, false, 0, <ic>

という行が 3 番目に来ています。:puts という命令オペランドがあるので、きっと puts メソッドの起動を表しているんでしょう。よくみると、命令名は send です。Ruby でメソッドを起動するメソッドは Object#send でした。まぁ、そのまんまですね。そういうわけで、これがメソッドを起動する YARV の命令です。

命令オペランドが :puts の後にもまだ続いていますね。1 は、引数を 1 つで起動、という意味です。そのほかは、面倒なんで今は説明しません。

この命令は、引数の数分だけの値と、レシーバをスタック上から取って (pop して)、メソッドを起動する、という意味になります。つまり、先ほど push した値は、ここで利用され、pop されます。

ちょっとわかりづらいですね。Ruby で書いてみましょう。

 recv = VMStack[-argc-1]      # スタックの奥のほうからレシーバを取得
 args = VMStack[-argc...-1]   # 引数の数だけスタックから取得
 VMStack = VMStack[0...-argc] # スタックから値を pop
 recv.send(*args)             # メソッドの起動

ということを、send 命令 1 つでやっています (実際にはこんな複雑なことはやっていませんが、Ruby で書くとこんな感じ、という意味です)。

ちょっとおさらいしてみましょう。

0000 putself                                                          (   1)
0001 putstring       "Hello World!"
0003 send            :puts, 1, false, 0, <ic>

という命令列は、

                ## stack : [] # 実行前は stack は空
 putself        # self を積む
                ## stack : [self]
 putstring ".." # 文字を積む
                ## stack : [self, "Hello World!"]
 send :puts, 1  # メソッドを起動する。
                # stack 上の 2 つの値 (引数とレシーバ) はポップされる
                # スタックには返り値 (puts の返り値は nil) が詰まれる
                ## stack : [nil]

というようになります。

最後に end という命令がありますが、これはこのスコープから抜けろ、という意味です。スコープについては次号で紹介する予定です。まぁ、これで終わり、と思っていただければ間違いじゃないです。積まれているスタックトップを持って前のスコープに戻ります。

ちなみに、self を最初に積んでいるのは、Ruby では self.puts も self (レシーバ) を書かないで puts だけ記述するのも、「ほぼ」同じ意味だからです。厳密にはほんのちょっとだけ違うんですが、このレベルでは同じです。なので、self.puts に合わせている、ということです。

ほかの例を試してみる

ほかの例を試して見ましょう。

 p 1+2

人を馬鹿にしてるのか、というくらい簡単な Ruby プログラムですが、この命令列を見てみると、こんな感じです。

0000 putself
0001 putobject       1
0003 putobject       2
0005 send            :+, 1, false, 0, <ic>
0011 send            :p, 1, false, 0, <ic>
0017 end

1+2 を実行しているのが 1番地から5番地の

0001 putobject       1
0003 putobject       2
0005 send            :+, 1, false, 0, <ic>

この部分です。1+2 は、Ruby では立派なメソッド呼び出しですから (1.+(2)と同じ意味) 、このようにコンパイルされるのです。このとき、1 がレシーバオブジェクト、2 が引数として、:+ メソッドが呼ばれています。さっきの例と同じですね。

そして、最後に self.p(arg) を実行 (arg は 1+2 の結果) して、end でその返り値 (p の返り値は nil) を返します。

命令の一覧

さて、では他にどんな命令があるんでしょうか。

YARV: Yet another RubyVM / Instruction Table というページがあります。ここに、命令一覧をまとめてあります。ただし、このページの内容は最新の YARV に追従していない場合もありますのでご注意ください。

この命令一覧の見方を説明します。

type はどんな命令か、を示しています。Index は、とりあえず命令に番号を付けています。Instruction には命令名、Operands は命令オペランドが示されています。Stacks には、その命令を実行したら、スタックの状態がどのように変化するかを示しています。

たとえば、putnil という命令なら、Stacks には

 => val

と、記述されていますが、これは val (なんらかの値) が putnil 命令実行後にスタックに詰まれる、という意味です。val はもちろん nil です。スタックサイズは一個増えました。

たとえば、putnot という命令では、Stacks には

 obj =>  val

とありますが、これはスタックトップに積んである値を取り出し (pop)、その値を obj として、!obj を val として、val をスタックトップに積みます (push)。pop して push したので、スタックサイズの増減はありません。

Ruby で擬似コードを書いてみるとこんな感じです。

 obj = VMStack.pop
 val = !obj
 VMStack.push(val)

各命令の詳細

各命令がどんなことをするのか、というのは名前をじーっと見るとわかる気がするんですが、わからないような気もします。

各命令が具体的に何をやっているかを詳しく知りたい人は、YARV ソースツリーの yarv/insns.def を見てください。このファイルに、色々と細かいことが書いてあります。具体的には、日本語、英語での簡単な説明と、C プログラムでその命令が何をするか詳細に記述してあります。これを見れば、わかる人にはわかるんじゃないかと思います。

yarv/insns.def の書式は、普通の C 言語の書式ではないし、そもそも拡張子が ".def" という怪しげなものになっていますが、このファイルは YARV のビルド時に Ruby プログラムによって C 言語プログラムに変換されます。これについては、いつか解説します。

おわりに

本稿では YARV の命令セットの紹介のさわりを書きました。次号からは、もうちょっと大きなプログラムを YARV 命令セットに変換してみて、その内容を解説していこうと思っています。

YARV のビルドが出来た方は、今回紹介した方法を使っていろいろな Ruby プログラムを YARV 命令列に変換してみてください (いわゆる逆アセンブル)。YARV がどんなことをするのか、少しずつ見えてくると思います。

参考文献

今回は YARV に特化した内容でしたので、他に参照するところはありません。

YARV 命令の一覧は YARV: Yet another RubyVM / Instruction Table に用意してあります。

各命令の詳細は本文中で紹介したソースコード yarv/insns.def を見てください。

著者について

ささだこういち。学生。

最近マシンを新しくしたので、家で YARV の開発ができるぜー、と思っていたら、なんか事務処理とかばかりしている気がする。LLDN の準備とか研究会の準備とか若手の会の準備とか色々。まぁいいか。