Mobile Factory Tech Blog

技術好きな方へ!モバイルファクトリーのエンジニアたちが楽しい技術話をお届けします!

PerlでJVMをつくろう

要約

JVMは任意の言語で作ることができるので、Perlで書いてみました

github.com

HelloWorldのDEMO このようにクラスファイルを読み取り、それを実行することができます

今回は読者が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を実装するのが流行っていた(?)

「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: 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

など

もっと本格実装する話

セルフホストで学ぶJVM入門 - k0kubun's blog