要約 JVMは任意の言語で作ることができるので、Perlで書いてみました github.com このようにクラスファイルを読み取り、それを実行することができます 今回は読者がJVMを書き始められるようにクラスファイルの読み取り方に焦点をおいて解説します(あまりPerlの話はしません) 目次 自己紹介 JVMの基本 クラスファイルの解説 オペコードの実行 まとめ 自己紹介 駅メモにて主にバックエンドを担当している id:toricor です。 仕事ではサーバがPerl実装なので、Perlでいろいろな機能を実装したりパフォーマンスチューニングをしたりしています。 JVMをつくろう 残念ながら今のところ仕事ではほぼJVMと縁がないので、まずは基本を確認します JVMとは Java Virtual Machine(Java仮想マシン)の略です Java仮想マシン - Wikipedia JVMはJavaプログラムのどこを担うか MyProgram.java というjavaのソースコードがあった場合、これを実行するためには以下のような操作になります % javac -encoding UTF-8 MyProgram.java # コンパイルしてクラスファイルを生成 % java MyProgram # JVMがクラスファイルを読み取り実行 コンパイラが生成したクラスファイルを読み取り実行するのがJVMです なぜJVMを実装する? いわゆる スタックマシン が現実でどのように動くのか知りたかった 「堅い仕様書」を元に実装してみる経験がしたかった 年末年始にTwitterを見ていたらJVMを実装するのが流行っていた(?) HelloWorldするための詳細な解説スライドが流れてきたので実装できそうな気がした https://speakerdeck.com/memory1994/phperkaigi-2019 とか 「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 ............. おぼろげながらクラス名などがいくつかあるのはわかります フォーマットの仕様に基づき解読しよう 以下の仕様書を見ながら解読します docs.oracle.com 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: 74 00 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 0a 00 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 0c 07 .......4........ class_indexが 0x0003 、続くname_and_type_indexが 0x000c (=12)となりjavap結果と一致していそうです(Constant pool内の要素のindexを示しています)。 access_flags: u2 000000a0: 74 00 20 00 0200 0300 0000 0000 0200 0000 t. ............. このクラスやインターフェースのpublic等のアクセス修飾子です(和で表されます)。 今回は 0x20 です。 this_class: u2 000000a0: 7400 20 00 02 00 0300 0000 0000 0200 0000 t. ............. このクラスの情報です。constant_poolの2番目を指していて JustAddIntです。 #2 = Class #13 // JustAddInt super_class: u2 000000a0: 7400 2000 02 00 03 00 0000 0000 0200 0000 t. ............. 親クラスはconstant_poolの3番目で、java/lang/Object です。 #3 = Class #14 // java/lang/Object interfaces_count: u2 000000a0: 7400 2000 0200 03 00 00 00 0000 0200 0000 t. ............. インターフェース数は0です interfaces[interfaces_count]: u2 今回はインターフェースがないのでデータなしです。 fields_count: u2 000000a0: 7400 2000 0200 0300 00 00 00 00 0200 0000 t. ............. フィールド数は0です。 field_info: fields[fields_count] フィールドがないのでデータなしです。 methods_count: u2 000000a0: 7400 2000 0200 0300 0000 00 00 02 00 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 など もっと本格実装する話 セルフホストで学ぶJVM入門 - k0kubun's blog