SHIROBAKO大好き人間のブログ

SHIROBAKOが好きなエンジニアによる技術ブログ

pythonでシンプルなJVM作ってみた

概要

ふと思い立ってpythonで動くJVMを作ってみました(実行できるのは今のところHello Worldのみ)

github.com

この記事では、これを作る際に調べたことをざっくりまとめていきます

何を作ったの?

皆さんご存知の通り、Javaのコードをコンパイルするとclassファイルが生成されます
そしてclassファイルをJVMで実行すると、プログラムの実行結果が得られます
今回は図で言うところの②の部分をpythonで作りました(①のコンパイル部分は作っていません)

f:id:phoro3:20200211173117p:plain
java実行の流れ

処理の流れ

JVMを書く際に、処理の流れを色々調べたのでそれをここにもまとめておきます
なお、今回の実装はHello Worldを実行することに主眼を置いたため、実際のJVMの仕様を無視・簡略化している部分があります
そこを承知の上で読んでいただければ幸いです

大まかな流れはこんな形です

  1. JVMの仕様を確認する
  2. classファイルの内容を読み取る
  3. mainメソッドに含まれている命令を確認する
  4. 命令をもとに実行する

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の値と一致します

f:id:phoro3:20200211181911p:plain
cafababe

・・・といった感じで、仕様書に沿ってバイナリファイルを読んでいきます
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ファイルの読み込みに関しては、こちらの記事によくまとまっていたので、参考にさせていただきました

dev.classmethod.jp

3. mainメソッドに含まれている命令を確認する

先ほどのclassファイルの項目の中で、今回特に重要な項目が2つあります
それが、constant_poolmethodsの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_indexconstant_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の先頭にある0xb2getstatic命令を表しており、仕様書を見ると次のように書いてあります

f:id:phoro3:20200215085656p:plain
https://docs.oracle.com/javase/specs/jvms/se11/html/jvms-6.html#jvms-6.5.getstatic

Formatの部分にgetstatic indexbyte1 indexbyte2とあります
これはこのgetstatic命令が2バイトの引数を取ることを表します
つまり、こういうことです
f:id:phoro3:20200215090955p:plain

一度codesとそれに対応するJVMの仕様の読み方が分かれば、その次の0x12ldc命令を表していて・・・と次々と読み進めることができます

4. 命令をもとに実行する

mainメソッドに含まれている命令は確認できるようになったので、あとはそれを実行していくだけです
JVMの命令の実行の際にはオペランドスタックというものが必要になります
これは、命令を実行している途中の結果を保存しておく場所です

例えば、先ほどのgetstatic命令は、引数の値をconstant_poolから取ってきて、それをオペランドスタックにpushするという命令になります
オペランドスタックからいくつ値を取るか、どんな値をプッシュするかも命令ごとに仕様書に書かれています
getstaticの場合はこのような表記です

f:id:phoro3:20200215093608p:plain
https://docs.oracle.com/javase/specs/jvms/se11/html/jvms-6.html#jvms-6.5.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

作ってみた感想

言語の処理系は作ったことなかったので新鮮でした
最初は分からないことだらけですが、仕様を読んでいけば少しずつ分かってくる感覚が面白かったです