要約
JVMは任意の言語で作ることができるので、Perlで書いてみました
このようにクラスファイルを読み取り、それを実行することができます
今回は読者がJVMを書き始められるようにクラスファイルの読み取り方に焦点をおいて解説します(あまりPerlの話はしません)
目次
- 自己紹介
- JVMの基本
- クラスファイルの解説
- オペコードの実行
- まとめ
自己紹介
駅メモにて主にバックエンドを担当しているid:toricorです。 仕事ではサーバがPerl実装なので、Perlでいろいろな機能を実装したりパフォーマンスチューニングをしたりしています。
JVMをつくろう
残念ながら今のところ仕事ではほぼJVMと縁がないので、まずは基本を確認します
JVMとは
Java Virtual Machine(Java仮想マシン)の略です
JVMはJavaプログラムのどこを担うか
MyProgram.java
というjavaのソースコードがあった場合、これを実行するためには以下のような操作になります
% javac -encoding UTF-8 MyProgram.java # コンパイルしてクラスファイルを生成 % java MyProgram # JVMがクラスファイルを読み取り実行
コンパイラが生成したクラスファイルを読み取り実行するのがJVMです
なぜJVMを実装する?
- いわゆる
スタックマシン
が現実でどのように動くのか知りたかった - 「堅い仕様書」を元に実装してみる経験がしたかった
- 年末年始にTwitterを見ていたらJVMを実装するのが流行っていた(?)
- HelloWorldするための詳細な解説スライドが流れてきたので実装できそうな気がした
「JVMを実装する」とは
To implement the Java Virtual Machine correctly, you need only be able to read the class file format and correctly perform the operations specified therein.
The Java Virtual Machine Specification 第2章冒頭より引用
「JVMを正しく実装するにはクラスファイルを読んでそこの指示を正しく実行できればよい」
詳細な仕様書 があるのでそれにそって実装すればよいです。
仕様 を満たせばどの言語で書いてもそれはJVMです。
JVM 実装した
主に年末年始に実装しました https://github.com/toricor/p5-jvmtiny
HelloWorldの他、FizzBuzzくらいなら出力できます
Java実行環境
手元のMacで動かします
% javac -version javac 1.8.0_144 // 昔インストールしたままのjava
今回はJava 8を使います。以下に示すクラスファイルの形式はJava8でコンパイルした場合のものになります(そういえばJavaは14まで出ていますがJava界のシェアは8が依然トップらしいですね)。
簡単なプログラムを実行しよう
よくある例はHelloWorldですが、HelloWorldのような標準出力に何かを出すようなプログラムには実は結構難しい命令が含まれるので、今回は簡単のため次のような足すだけのプログラムを見ます
class JustAddInt { public static void main(String[] args) { int a = 1; int b = 2; int c = a + b; } }
これをコンパイルしたJustAddInt.classを読み取り、実行しましょう
クラスファイルの中身をみる
% javac -encoding UTF-8 JustAddInt.java // コンパイルしてクラスファイルをつくる
バイナリを読む
xxdが便利です。
xxd - make a hexdump or do the reverse.
% xxd JustAddInt.class 00000000: cafe babe 0000 0034 000f 0a00 0300 0c07 .......4........ 00000010: 000d 0700 0e01 0006 3c69 6e69 743e 0100 ........<init>.. 00000020: 0328 2956 0100 0443 6f64 6501 000f 4c69 .()V...Code...Li 00000030: 6e65 4e75 6d62 6572 5461 626c 6501 0004 neNumberTable... 00000040: 6d61 696e 0100 1628 5b4c 6a61 7661 2f6c main...([Ljava/l 00000050: 616e 672f 5374 7269 6e67 3b29 5601 000a ang/String;)V... 00000060: 536f 7572 6365 4669 6c65 0100 0f4a 7573 SourceFile...Jus 00000070: 7441 6464 496e 742e 6a61 7661 0c00 0400 tAddInt.java.... 00000080: 0501 000a 4a75 7374 4164 6449 6e74 0100 ....JustAddInt.. 00000090: 106a 6176 612f 6c61 6e67 2f4f 626a 6563 .java/lang/Objec 000000a0: 7400 2000 0200 0300 0000 0000 0200 0000 t. ............. 000000b0: 0400 0500 0100 0600 0000 1d00 0100 0100 ................ 000000c0: 0000 052a b700 01b1 0000 0001 0007 0000 ...*............ 000000d0: 0006 0001 0000 0001 0009 0008 0009 0001 ................ 000000e0: 0006 0000 002d 0002 0004 0000 0009 043c .....-.........< 000000f0: 053d 1b1c 603e b100 0000 0100 0700 0000 .=..`>.......... 00000100: 1200 0400 0000 0300 0200 0400 0400 0600 ................ 00000110: 0800 0700 0100 0a00 0000 0200 0b .............
おぼろげながらクラス名などがいくつかあるのはわかります
フォーマットの仕様に基づき解読しよう
以下の仕様書を見ながら解読します
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]; }
クラスファイルはこのような並びで記述されています
u1, u2, u4はそれぞれ符号なし(unsigned)の1バイト、2バイト、4バイトのことです
適宜参照しやすいようにxxdの結果の一部と比較しながら順番にみていきます
magic: u4
00000000: cafe babe 0000 0034 000f 0a00 0300 0c07 .......4........
0xCAFEBABE
この冒頭4byteはクラスファイルであることを宣言しています
バイナリファイルの冒頭にこれが見えたらJavaのクラスファイルだとわかりますね
minor_version, major_version : u2, u2
00000000: cafe babe 0000 0034 000f 0a00 0300 0c07 .......4........
0x0000: minor_version
0x0034: major_version
2byteと2byteのこの組み合わせで、今回はJava 8のクラスファイルだとわかります
Javaのバージョンとの対応表はこちらをみてください( 表があるのはJava 11のドキュメントですが)
constant_pool_count: u2
00000000: cafe babe 0000 0034 000f 0a00 0300 0c07 .......4........
constant_pool(後述)の要素数+1 (=15)
constant_pool[constant_pool_count-1]: cp_info
クラス名やメソッド名、メソッドの型情報などをまとめたものです。 以降の処理で必要な情報は適宜constant_poolの配列へindexでアクセスして取得します。
位置 | 16進表示 | ascii表示 |
---|---|---|
00000000: | cafe babe 0000 0034 000f 0a00 0300 0c07 | .......4........ |
00000010: | 000d 0700 0e01 0006 3c69 6e69 743e 0100 | ........ |
00000020: | 0328 2956 0100 0443 6f64 6501 000f 4c69 | .()V...Code...Li |
00000030: | 6e65 4e75 6d62 6572 5461 626c 6501 0004 | neNumberTable... |
00000040: | 6d61 696e 0100 1628 5b4c 6a61 7661 2f6c | main...([Ljava/l |
00000050: | 616e 672f 5374 7269 6e67 3b29 5601 000a | ang/String;)V... |
00000060: | 536f 7572 6365 4669 6c65 0100 0f4a 7573 | SourceFile...Jus |
00000070: | 7441 6464 496e 742e 6a61 7661 0c00 0400 | tAddInt.java.... |
00000080: | 0501 000a 4a75 7374 4164 6449 6e74 0100 | ....JustAddInt.. |
00000090: | 106a 6176 612f 6c61 6e67 2f4f 626a 6563 | .java/lang/Objec |
000000a0: | 7400 2000 0200 0300 0000 0000 0200 0000 | t. ............. |
表: xxd結果を整形したもの。緑色部分がconstant_poolの情報をもつ部分。
このままでは見にくいのでjavap -v JustAddInt
の結果の一部をはります(javap: classファイルの逆アセンブルコマンド)
以下のように14個の要素が並んでおり、たしかにconstant_pool_count-1個あることがわかります(JVMをつくるときはjavapの結果を見ながら作業するとスムーズです)。
Constant pool: #1 = Methodref #3.#12 // java/lang/Object."<init>":()V #2 = Class #13 // JustAddInt #3 = Class #14 // java/lang/Object #4 = Utf8 <init> #5 = Utf8 ()V #6 = Utf8 Code #7 = Utf8 LineNumberTable #8 = Utf8 main #9 = Utf8 ([Ljava/lang/String;)V #10 = Utf8 SourceFile #11 = Utf8 JustAddInt.java #12 = NameAndType #4:#5 // "<init>":()V #13 = Utf8 JustAddInt #14 = Utf8 java/lang/Object
constant poolの各要素はtag(上記のMethodRefやUtf8などに対応する)とその詳細の組として表されます。下に示すu1 info[];
はtagによって示すものが変わります。
cp_info { u1 tag; u1 info[]; }
ここでConstant poolの要素についてどう読み取っていくか見てみましょう。 たとえば先頭の#1はjavap結果だとMethodref_infoということですが確認します。
00000000: cafe babe 0000 0034 000f 0a00 0300 0c07 .......4........
1byteのtagが0x0a
(=10)なので定義から、たしかにMethodref_infoです。
Methodref_infoの定義は以下のような形式になります。
CONSTANT_Methodref_info { u1 tag; u2 class_index; u2 name_and_type_index; }
00000000: cafe babe 0000 0034 000f 0a00 0300 0c07 .......4........
class_indexが0x0003
、続くname_and_type_indexが0x000c
(=12)となりjavap結果と一致していそうです(Constant pool内の要素のindexを示しています)。
access_flags: u2
000000a0: 7400 2000 0200 0300 0000 0000 0200 0000 t. .............
このクラスやインターフェースのpublic等のアクセス修飾子です(和で表されます)。 今回は 0x20
です。
this_class: u2
000000a0: 7400 2000 0200 0300 0000 0000 0200 0000 t. .............
このクラスの情報です。constant_poolの2番目を指していて JustAddIntです。
#2 = Class #13 // JustAddInt
super_class: u2
000000a0: 7400 2000 0200 0300 0000 0000 0200 0000 t. .............
親クラスはconstant_poolの3番目で、java/lang/Object です。
#3 = Class #14 // java/lang/Object
interfaces_count: u2
000000a0: 7400 2000 0200 0300 0000 0000 0200 0000 t. .............
インターフェース数は0です
interfaces[interfaces_count]: u2
今回はインターフェースがないのでデータなしです。
fields_count: u2
000000a0: 7400 2000 0200 0300 0000 0000 0200 0000 t. .............
フィールド数は0です。
field_info: fields[fields_count]
フィールドがないのでデータなしです。
methods_count: u2
000000a0: 7400 2000 0200 0300 0000 0000 0200 0000 t. .............
メソッド数は2です。mainだけでなくコンパイラが <init>
もつくるので2つです。
At the level of the Java Virtual Machine, every constructor written in the Java programming language (JLS §8.8) appears as an instance initialization method that has the special name
. This name is supplied by a compiler.
https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-2.html#jvms-2.9
method_info: methods[methods_count]
ようやくメソッドの具体的内容です。定義は以下のようになります。
method_info { u2 access_flags; u2 name_index; u2 descriptor_index; u2 attributes_count; attribute_info attributes[attributes_count]; }
しかし、
書くのが大変になってきたのでPerlのプログラム上でmethod_infoをダンプした結果で代用します。
2つ目の要素がmainメソッドについての情報です。
[ [0] { access_flags 0, attribute_info [ [0] { attribute_length 29, attributes [ [0] { attribute_length 6, line_number_table_length 1, line_number_tables [ [0] { line_number 1, start_pc 0 } ], name "LineNumberTable" } ], attributes_count 1, code "*�\0�", code_length 5, exception_table_length 0, exception_tables [], max_locals 1, max_stack 1, name "Code" } ], descriptor_index "()V", name_index "<init>" }, [1] { # ここ以下がmainメソッドの情報 access_flags 9, # ACC_PUBLIC(0x0001), ACC_STATIC(0x0008) attribute_info [ [0] { attribute_length 45, attributes [ [0] { attribute_length 18, line_number_table_length 4, line_number_tables [ [0] { line_number 3, start_pc 0 }, [1] { line_number 4, start_pc 2 }, [2] { line_number 6, start_pc 4 }, [3] { line_number 7, start_pc 8 } ], name "LineNumberTable" } ], attributes_count 1, code "<=>�", # これが計算処理 code_length 9, # コードの長さは9 exception_table_length 0, exception_tables [], max_locals 4, max_stack 2, name "Code" } ], descriptor_index "([Ljava/lang/String;)V", name_index "main" } ]
mainメソッドのもつCodeアトリビュートがコードの長さ9からなるコードを持っています (後でこのコードを実行します)
attributes_count: u2
(省略)
attribute_info: attributes[attributes_count]
定義です。attribute_infoはmethod_infoでも使われていました。
attribute_info { u2 attribute_name_index; u4 attribute_length; u1 info[attribute_length]; }
これでクラスファイルの読み取りができました。
コードを実行しよう
クラスファイルの読み取り結果に基づき、どのような計算指示が格納されていたかをまず確認しましょう
code "<=>�", # これをいい感じに実行したい code_length 9,
このままではよくわからないので、mainメソッドがもつコード情報を16進表示します。すると以下のような配列であることがわかりました。
\ [ [0] "04", [1] "3c", [2] "05", [3] "3d", [4] "1b", [5] "1c", [6] 60, [7] "3e", [8] "b1" ]
これは オペコードとオペランド
があつまったものになります(今回はオペランドないですが)。
あとはオペコードの仕様を確認しながら、オペコードの指示に従ってスタックに入れたり出したり(+ローカル変数というものもあってそれに入れたり出したり)すれば計算ができます。
# JustAddIntプログラムは 1 + 2 = 3 を計算する [0] "04", # iconst_1: スタックに1を積む [1] "3c", # istore_1: ローカル変数1番にスタックからpopした値を入れる [2] "05", # iconst_2: スタックに2を積む [3] "3d", # istore_2: ローカル変数2番にスタックからpopした値を入れる [4] "1b", # iload_1: ローカル変数1番の値をスタックに積む [5] "1c", # iload_2: ローカル変数2番の値をスタックに積む [6] 60, # iadd: スタックから2つpopして足した値をスタックに積む [7] "3e", # istore_3: スタックからpopした値をローカル変数3番に入れる = 3 が入る [8] "b1" # return: voidを返す
(一応)計算できました!
ちなみにHelloWorldではこのようなオペコードになります。
# テストから抜粋 qw/ b2 00 02 /, # getstatic qw/ 12 03 /, # ldc qw/ b6 00 04 /, # invokevirtual qw/ b1 /, # return
最後は駆け足でしたが、ごく簡単なJVMをつくるために必要なことを紹介しました。
最初はクラスファイルを読み取る部分をつくるまでが大変なので、乗り越えやすいようにできるだけ詳しく紹介しました。
まとめ
JVMはPerlでもつくれます(簡単なものなら) 皆さんも
JVM + YOU, LET'S WRITE IT!
参考資料
https://speakerdeck.com/memory1994/phperkaigi-2019 など
仕様書
https://docs.oracle.com/javase/specs/jvms/se8/html/index.html
参考実装
いろいろとお手本実装が見つかったのでそのうち書き直したいですね
PHP: https://github.com/php-java/php-java
Java: https://github.com/k0kubun/jjvm
Rust: https://github.com/maekawatoshiki/ferrugo
Node.js: https://github.com/YaroslavGaponov/node-jvm
Python: https://github.com/gkbrk/python-jvm-interpreter
など