pythonでシンプルなJVM作ってみた
概要
ふと思い立ってpythonで動くJVMを作ってみました(実行できるのは今のところHello Worldのみ)
この記事では、これを作る際に調べたことをざっくりまとめていきます
何を作ったの?
皆さんご存知の通り、Javaのコードをコンパイルするとclassファイルが生成されます
そしてclassファイルをJVMで実行すると、プログラムの実行結果が得られます
今回は図で言うところの②の部分をpythonで作りました(①のコンパイル部分は作っていません)
処理の流れ
JVMを書く際に、処理の流れを色々調べたのでそれをここにもまとめておきます
なお、今回の実装はHello Worldを実行することに主眼を置いたため、実際のJVMの仕様を無視・簡略化している部分があります
そこを承知の上で読んでいただければ幸いです
大まかな流れはこんな形です
- JVMの仕様を確認する
- classファイルの内容を読み取る
- mainメソッドに含まれている命令を確認する
- 命令をもとに実行する
1. JVMの仕様を確認する
そもそも仕様が分からないと実装が進められないので、まずは仕様を確認します
JVMの仕様はoracleのサイトで公開されています
Java11であればここです
docs.oracle.com
残念ながら全部英語です😱
一応日本語がないか探しましたが、「JVM 仕様 日本語」とgoogle先生に聞いてもそれらしきものは見当たりませんでした・・・
ただ、技術系のドキュメントなので難しい文法は使われてません
英語が超絶苦手な場合でもgoogle翻訳を駆使すれば対応できると思います
FAQ: すごい長いけど全部読んだの?
いいえ !
Hello Worldを実装する上で必要な部分は限られるので、そこを読むだけで対応可能です
実装しているときに「これどうなってるんだろう?」と気になることは多々あるので、各章の冒頭は読んでおくと作業がスムーズに進みます😀
例えば、JVMの実行の流れについては2章にあり、classファイルの仕様については4章にある・・・という感じです
2. classファイルの内容を読み取る
JVMを実行する際は、classファイルを入力として使うので、まずはclassファイルの中身を見ます
今回はこのシンプルなHello Worldのコードをコンパイルして確認します
class Hello { public static void main(String[] args) { System.out.println("Hello world!"); } }
classファイルはバイナリファイルなので、Hello Worldのコードをコンパイルした後、classファイルをhexdumpで確認してみます
$ javac Hello.java $ hexdump -C Hello.class 00000000 ca fe ba be 00 00 00 37 00 1d 0a 00 06 00 0f 09 |.......7........| 00000010 00 10 00 11 08 00 12 0a 00 13 00 14 07 00 15 07 |................| 00000020 00 16 01 00 06 3c 69 6e 69 74 3e 01 00 03 28 29 |.....<init>...()| 00000030 56 01 00 04 43 6f 64 65 01 00 0f 4c 69 6e 65 4e |V...Code...LineN| 00000040 75 6d 62 65 72 54 61 62 6c 65 01 00 04 6d 61 69 |umberTable...mai| 00000050 6e 01 00 16 28 5b 4c 6a 61 76 61 2f 6c 61 6e 67 |n...([Ljava/lang| 00000060 2f 53 74 72 69 6e 67 3b 29 56 01 00 0a 53 6f 75 |/String;)V...Sou| 00000070 72 63 65 46 69 6c 65 01 00 0a 48 65 6c 6c 6f 2e |rceFile...Hello.| 00000080 6a 61 76 61 0c 00 07 00 08 07 00 17 0c 00 18 00 |java............| 00000090 19 01 00 0c 48 65 6c 6c 6f 20 77 6f 72 6c 64 21 |....Hello world!| (長いので以下省略)
バイナリなのでかなり見にくいですが、よく確認してみると00000090
の行の末尾にHello World!
という文字列が存在します
なので、ちゃんとこのバイナリを読んであげればHello Worldは実行できそうです
このclassファイルは(当たり前ですが)ルールが決まっており、仕様書の4章にまとまっています
JVMの仕様書を確認すると、このようなルールになっています
ClassFile { u4 magic; u2 minor_version; u2 major_version; u2 constant_pool_count; cp_info constant_pool[constant_pool_count-1]; u2 access_flags; u2 this_class; u2 super_class; u2 interfaces_count; u2 interfaces[interfaces_count]; u2 fields_count; field_info fields[fields_count]; u2 methods_count; method_info methods[methods_count]; u2 attributes_count; attribute_info attributes[attributes_count]; }
このルール中で、u4
は符号なしの4バイトを表しています(同様に、u2
は2バイトです)
つまり、u4 magic
は先頭の4バイトはmagicナンバーとして使われる
ことを表します
実際に、先ほどのclassファイルの先頭を見てみましょう
$ hexdump -C Hello.class 00000000 ca fe ba be 00 00 00 37 00 1d 0a 00 06 00 0f 09 |.......7........|
先頭の4バイトを見るとcafababe
となっています
これは仕様書に書かれているmagic
の値と一致します
・・・といった感じで、仕様書に沿ってバイナリファイルを読んでいきます
pythonの場合、バイナリモードでファイルを読み込めば指定したバイトずつファイルを読み込めるので、実装は大変ではないです(地道な作業ではありますが・・・)
with open(filename, "rb") as f: f.read(4) #4バイト読む
classファイルの読み込みを実装すると、こんな風にJVMの実行に必要な各項目の情報が得られます
読み込み箇所の実装はリポジトリでいうとこの辺りです
constant_pool: [None, {'tag': 10, 'class_index': 6, 'name_and_type_index': 15}, {'tag': 9, 'class_index': 16, 'name_and_type_index': 17}, {'tag': 8, 'string_index': 18}, {'tag': 10, 'class_index': 19, 'name_and_type_index': 20}, ...(省略) interfaces: [] fields: [] methods: [{'access_flags': 0, 'name_index': 7, 'descriptor_index': 8, 'attributes_count': 1, 'attributes': {'attribute_name_index': 9, 'attribute_length': 29, 'max_stack': 1, 'max_locals': 1, 'code_length': 5, 'codes': [b'*', b'\xb7', b'\x00', b'\x01', b'\xb1'], 'exception_table_length': 0, 'exception_table': [], 'attributes_count': 1, 'attributes': [{'attribute_name_index': 10, 'attribute_length': 6, 'line_number_table_length': 1, 'line_number_table': [{'start_pc': 0, 'line_number': 1}]}]}}, ...(省略) attributes: [{'attribute_name_index': 13, 'attribute_length': 2, 'sourcefile_index': 14}] magic: b'\xca\xfe\xba\xbe' minor_version: 0 major_versio: 55 constant_pool_count: 29 access_flags: 32 this_class: 5 super_class: 6 interfaces_count: 0 fields_count: 0 methods_count: 2 attributes_count: 1
参考
classファイルの読み込みに関しては、こちらの記事によくまとまっていたので、参考にさせていただきました
3. mainメソッドに含まれている命令を確認する
先ほどのclassファイルの項目の中で、今回特に重要な項目が2つあります
それが、constant_pool
とmethods
の2つです
constant_pool
には、実行の際に必要な定数が格納されています
例えば、クラス名やHello World
といった文字列定数です
methods
はその名の通り、メソッドに関する情報が格納されています
Javaの実行はmainメソッドから始まるので、まずmainメソッドを探します
methodsの中身をよく見ると、今回は2つメソッドがあることが確認できます
//1つ目のmethod [{'access_flags': 0, 'name_index': 7, 'descriptor_index': 8, 'attributes_count': 1, 'attributes': {'attribute_name_index': 9, 'attribute_length': 29, 'max_stack': 1, 'max_locals': 1, 'code_length': 5, 'codes': [b'*', b'\xb7', b'\x00', b'\x01', b'\xb1'], 'exception_table_length': 0, 'exception_table': [], 'attributes_count': 1, 'attributes': [{'attribute_name_index': 10, 'attribute_length': 6, 'line_number_table_length': 1, 'line_number_table': [{'start_pc': 0, 'line_number': 1}]}]}}, //2つ目のmethod {'access_flags': 9, 'name_index': 11, 'descriptor_index': 12, 'attributes_count': 1, 'attributes': {'attribute_name_index': 9, 'attribute_length': 37, 'max_stack': 2, 'max_locals': 1, 'code_length': 9, 'codes': [b'\xb2', b'\x00', b'\x02', b'\x12', b'\x03', b'\xb6', b'\x00', b'\x04', b'\xb1'], 'exception_table_length': 0, 'exception_table': [], 'attributes_count': 1, 'attributes': [{'attribute_name_index': 10, 'attribute_length': 10, 'line_number_table_length': 2, 'line_number_table': [{'start_pc': 0, 'line_number': 3}, {'start_pc': 8, 'line_number': 4}]}]}}]
片方はHelloクラスのコンストラクタで、もう片方はmainメソッドです
どうやって判定するかというと、それぞれのmethodの中にあるname_index
を確認します
このname_index
はconstant_pool
のインデックスを示しています
1つ目のmethodのname_index
は7番で、2つ目のmethodのname_index
は11番です
これに対応するconstant_pool
は以下のようになっています
//constant_poolの7番目 {'tag': 1, 'length': 6, 'bytes': [b'<', b'i', b'n', b'i', b't', b'>']} //constant_poolの11番目 {'tag': 1, 'length': 4, 'bytes': [b'm', b'a', b'i', b'n']}
stringではなくbytesで出力しているので見にくいですが、7番目は<init>
、11番目はmain
となっています
<init>
はコンストラクタに対応しています
コンストラクタも重要ですが、今回のHello Worldでは関係ないのでmain
のみに注目していきます
main
の中身を見やすく出力するとこのような形になっています
access_flags: 9 name_index: 11 descriptor_index: 12 attributes_count: 1 attribute_name_index: 9 attribute_length: 37 max_stack: 2 max_locals: 1 code_length: 9 codes: [b'\xb2', b'\x00', b'\x02', b'\x12', b'\x03', b'\xb6', b'\x00', b'\x04', b'\xb1'] exception_table_length: 0 exception_table: [] attributes_count: 1 attributes: [{'attribute_name_index': 10, 'attribute_length': 10, 'line_number_table_length': 2, 'line_number_table': [{'start_pc': 0, 'line_number': 3}, {'start_pc': 8, 'line_number': 4}]}]
ここで重要なのは codes
です
ここにはJVMの命令セットが格納されており、このcodes
に沿ってJVMの命令を実行していけばmain
メソッドを実行したことになります
JVMの命令については仕様書の6章にまとまっています
JVMの命令は命令ごとに何バイトの引数を取るかが決まっています
例えば、codes
の先頭にある0xb2
はgetstatic
命令を表しており、仕様書を見ると次のように書いてあります
Format
の部分にgetstatic indexbyte1 indexbyte2
とあります
これはこのgetstatic
命令が2バイトの引数を取ることを表します
つまり、こういうことです
一度codes
とそれに対応するJVMの仕様の読み方が分かれば、その次の0x12
はldc
命令を表していて・・・と次々と読み進めることができます
4. 命令をもとに実行する
main
メソッドに含まれている命令は確認できるようになったので、あとはそれを実行していくだけです
JVMの命令の実行の際にはオペランドスタックというものが必要になります
これは、命令を実行している途中の結果を保存しておく場所です
例えば、先ほどのgetstatic
命令は、引数の値をconstant_pool
から取ってきて、それをオペランドスタックにpushするという命令になります
オペランドスタックからいくつ値を取るか、どんな値をプッシュするかも命令ごとに仕様書に書かれています
getstatic
の場合はこのような表記です
上の行(..., →
)は「オペランドスタックからいくつ値を取るか」を表しています
→
スタックトップを表しています
この..., →
という表記は、この命令においてスタックからは何も値を取らないのを意味します
下の行(..., value
)は「オペランドスタックにどんな値をプッシュするか」を表します
この場合はgetstatic
命令の中で取得した値をオペランドスタックにプッシュすることを表します
これらのオペランドスタックから値を取得する、あるいは値をプッシュする操作を組み合わせることでmain
メソッドの実行が可能になります
この辺りの実装はここにまとまっています
コード中のstack
がオペランドスタックを意味しているので、コードと照らし合わせながら読んでみてください
https://github.com/phoro3/python_jvm/blob/master/method_invoker.py
参考
codes
の読み込みやオペランドスタックの操作に関してはこちらを参考にしました
言語はPythonではなくPHPですが、JVMの実行の流れについてよくまとまっているのでとても助かりました
PHP で JVM を実装して Hello World を出力するまで - Speaker Deck
作ってみた感想
言語の処理系は作ったことなかったので新鮮でした
最初は分からないことだらけですが、仕様を読んでいけば少しずつ分かってくる感覚が面白かったです