argius note

プログラミング関連

JavaでJVM言語を作ってみる(2) - バイトコード解析

前回の続きです。
足し算プログラム"Adder"を、直接バイトコードを出力して作ります。
その前に、JVM機械語について少し触れてみたいと思います。


"Adder"は、mainとaddの2つのstatic関数だけのクラスで成っています。
まずは、Javaで同様のプログラムを書いてみます。

最初は一部を単純にするために限定バージョンにします。

// バージョン1
class Adder {
    static int add(int... a) {
        int r = 0;
        for (int i : a) {
            r += i;
        }
        return r;
    }
    public static void main(String... args) {
        System.out.println(add(8, 5));
    }
}

コンパイルして実行すると、"13"と出力されます。
これをバイトコードレベルでどうなっているのかを解析してみましょう。

$ javap -c Adder.class
Compiled from "Adder.java"
class Adder {
  Adder();
    Code:
       0: aload_0       
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return        

  static int add(int...);
    Code:
       0: iconst_0      
       1: istore_1      
       2: aload_0       
       3: astore_2      
       4: aload_2       
       5: arraylength   
       6: istore_3      
       7: iconst_0      
       8: istore        4
      10: iload         4
      12: iload_3       
      13: if_icmpge     33
      16: aload_2       
      17: iload         4
      19: iaload        
      20: istore        5
      22: iload_1       
      23: iload         5
      25: iadd          
      26: istore_1      
      27: iinc          4, 1
      30: goto          10
      33: iload_1       
      34: ireturn       

  public static void main(java.lang.String...);
    Code:
       0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
       3: iconst_2      
       4: newarray       int
       6: dup           
       7: iconst_0      
       8: bipush        8
      10: iastore       
      11: dup           
      12: iconst_1      
      13: iconst_5      
      14: iastore       
      15: invokestatic  #3                  // Method add:([I)I
      18: invokevirtual #4                  // Method java/io/PrintStream.println:(I)V
      21: return        
}

クラスAdderのメソッドの定義が出力されました。上から順に、暗黙のデフォルトコンストラクタ、addメソッド、mainメソッドとなっています。



ひとつずつ見ていきます。命令コードの詳細については、Java仮想マシン - Wikipediaなどを参照してください。

  Adder();
    Code:
       0: aload_0       
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return        

JVMはスタックマシンで、スタックに積まれた値で計算を行います。スタックへは定数やローカル変数の値を積む(loadする)ことができます。スタックの先頭の値をローカル変数へ保存(storeする)こともできます。メソッド呼び出しの無限ループなどでこのスタックが溢れると、StackOverFlowになります。
このコンストラクタについては、暗黙のsuper()でObjectクラスのコンストラクタを呼んでいるだけです。"aload_0"は変数0から(スタックに)参照値(参照型の値)を取り出す命令で、ここではthisを取り出しています。"invokespecial"は特殊インスタンスメソッド(ここではインスタンス初期化メソッド)の呼び出し命令です。
つまり、thisを引数にして特殊インスタンスメソッドjava/lang/Object.を呼ぶ、という意味です。

  • addメソッド

当然ながら、コンストラクタより少し複雑になっています。# の後ろは私が入れたコメントです。

  # 変数0: 参照型 パラメータ a
  # 変数1: int    total
  # 変数2: 参照型 無名(パラメータaのコピー?)
  # 変数3: int    無名(a.lengthの値)
  # 変数4: int    無名(配列カウンタ)
  # 変数5: int    i

  static int add(int...);
    Code:
       0: iconst_0
       1: istore_1              # 定数0 → stack → 変数1(total)
       2: aload_0
       3: astore_2              # パラメータa → stack → 変数2
       4: aload_2
       5: arraylength
       6: istore_3              # 変数2 → stack → 配列の長さ取得 → stack → 変数3
       7: iconst_0
       8: istore        4       # 定数0 → stack → 変数4
      10: iload         4       # 変数4 → stack
      12: iload_3               # 変数3 → stack
      13: if_icmpge     33      # stack → v1, stack → v2; if v1(変数3) > v2(変数4) then goto 33
      16: aload_2               # 変数2 → stack
      17: iload         4       # 変数4 → stack
      19: iaload                # stack → v1, stack → v2; v1[v2] (変数4[変数2]) → 変数5
      20: istore        5
      22: iload_1               # 変数1(total) → stack
      23: iload         5       # 変数5(i) → stack
      25: iadd                  # stack → v1, stack → v2; v1 + v2 (total + i) → stack
      26: istore_1              # stack → 変数1(total)
      27: iinc          4, 1    # 変数4 += 1
      30: goto          10      # goto 10
      33: iload_1               # 変数1 → stack → return
      34: ireturn               

まず、ローカル変数について。Javaソース上では、a,total,iの3個しか宣言していませんが、無名の変数があと3つあります。
これはおそらく、

    int 変数3 = a.length;
    for (int 変数4 = 0; 変数4 < 変数3; 変数4++) {
        // ...
    }

というコードに変換されたんだと思います。
(変数2はパラメータだからだと思いますが、これは今回は無視します。)
その他、いくつか抜粋して説明します。


命令コードは、名前が型+命令(+"_")+番号という体系のものがあります。
たとえば、iconst_0は、型i=int、命令const=定数をスタックに積む、番号=定数の値で「intの定数0をスタックに積む」ことを示しています。
同様に、aload_2は、型a=参照型、命令load=変数の値をスタックに積む、番号=変数の番号で「変数2から参照型の値を取り出しスタックに積む」ことを示しています。
コメントの「→ stack」は、スタック(の先頭)に積む、「stack →」はスタック(の先頭)から取り出すことを示しています。
命令セットを単純化するため、ひとつの操作で出来ることが限られています。total=0を行うには、まず定数0をスタックに積み、スタックから変数1に取り出すとしなければなりません。
この辺りを踏まえてコメントも合わせて読んでいただければ、コードの意味が分かってくるのではないかと思います。

  • mainメソッド
  public static void main(java.lang.String...);
    Code:
       0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
       3: iconst_2              # 定数2 → stack
       4: newarray       int    # new int[2] → stack (intarray)
       6: dup                   # copy stack(intarray[]) → stack
       7: iconst_0              # 定数0 → stack
       8: bipush        8       # int 8 → stack
      10: iastore               # stack x 3 → intarray[0]
      11: dup                   # copy stack(intarray[]) → stack
      12: iconst_1              # 定数1 → stack
      13: iconst_5              # 定数5 → stack
      14: iastore               # stack x 3 → intarray[1]
      15: invokestatic  #3                  // Method add:([I)I
      18: invokevirtual #4                  // Method java/io/PrintStream.println:(I)V
      21: return                # return void

こちらはローカル変数を使用していないため、スタックマシンならではの動きになっています。
スタックの状態をトレースしてみましょう。
アドレス15を実行する時点で、スタックの先頭がintarray、2番目が"Field java/lang/System.out:Ljava/io/PrintStream;"になり、"Method add:([I)I"が実行された直後(アドレス18)では、スタックの先頭がintの13、2番目が"Field java/lang/System.out:Ljava/io/PrintStream;"になるはずです。
順番が逆のように思われるかもしれませんが、これは、スタックだけで計算が済む場合にローカル変数が不要になるので、合理的なしくみと言えます。


Java言語がどのようなマシンコードに変換され、このコードでJVMがどのように動いているかが少し分かってきました。
JVMをスタックマシンとして操作するイメージが沸いてくれば、直接JVMのマシンコード、すなわちバイトコードJava言語のソースコード無しで生成するのはそれほど難しくないのかも知れません。


それに、Java言語とは関係なくバイトコードを生成できるということは、Java言語ではできない、もしくはまだサポートされていない機能を独自に実装できるかもしれないということです。例えば文字列のswitchは、APIJVMを変えることなく実現可能でした(Java SE 7 (1) - 文字列switchのからくり参照)。
そう考えてみると、もっと面白くなりそうです。


次回は、"Adder"のクラスファイルをjavacを使わずにBCELで作っていきます。

(つづく)