Hatena::ブログ(Diary)

プログラマーの脳みそ このページをアンテナに追加 RSSフィード

2010-02-07

Javaバイトコードの読み方

| 15:02 |  Javaバイトコードの読み方を含むブックマーク

 Javaデバッグをしていて、ステップ実行中にステップインを繰り返したらソースコードのないところに行き当たったことがあるだろう。あるいはEclipseでF3キーでクラスやメソッド・フィールドの宣言元を辿っていってソースコードのないところに行き当たったことがあるだろう。

 Eclipseの場合、"Class File Editor"というものが開く。そこにはJavaバイトコードニーモニックがズラズラと並んでいて、「これは読めないや、ワケが分からない」と投げ出してしまったりしていないだろうか。

f:id:Nagise:20100207144640p:image

 怖がることはない。ちょっとコツを掴めばすぐに読めるようになる。

Class File Editorの開き方

 自前のJavaクラスの場合、ビルドして出来上がったclassファイルを開く必要がある。"Package Explorer"だとclassファイルは隠されていて見えないのでWindow -> Show View -> Navigatorを選択し、"Navigator"を表示させる。あとはJavaプロジェクトを作った際に指定した出力先を探せばclassファイルがみつかるはずだ。

 classファイルがどこだか分からないなら"Package Explorer"でプロジェクト名を選択、右クリックでBuild Path -> Configure Build Pathを選択する。Java Build Pathを選び、Sourceタブを選択しよう。下にDefalut output folderが表示されているはずだ。特に凝った設定をしていなければここにclassファイルは出力される。

f:id:Nagise:20100207144641p:image

 classファイルをダブルクリックして開けば"Class File Editor"と題されたビューが表示される。

 ところで、classファイルを選択して右クリックした場合に出てくるメニューでOpen Withを選択した場合は"Class File Viewer"というのが候補として出てくる。しかし、開くと"Class File Editor"となってる。この食い違いは謎だが、気にしないことにしよう。

クラスのメンバを確認する

 "Class File Editor"を初めて見ると、第一感で拒絶反応を起こし、読みもせずに閉じてしまうかもしれない。しかし、落ち着いて見れば、特別な知識がなくても読める部分がある。フィールドやメソッドといったクラスのメンバだ。

package sample;

public class Hoge {
	public static String staticField;
	public String instanceField;
	public static void staticMethod() {}
	public void instanceMethod() {}
}

 このHogeクラスをコンパイルして作られるclassファイルをClass File Editorで見てみよう。まずはコメントすぐ下の宣言だけを読めばいい。

// Compiled from Hoge.java (version 1.6 : 50.0, super bit)
public class sample.Hoge {
  
  // Field descriptor #6 Ljava/lang/String;
  public static java.lang.String staticField;
  
  // Field descriptor #6 Ljava/lang/String;
  public java.lang.String instanceField;
  
  // Method descriptor #9 ()V
  // Stack: 1, Locals: 1
  public Hoge();
    0  aload_0 [this]
    1  invokespecial java.lang.Object() [11]
    4  return
      Line numbers:
        [pc: 0, line: 3]
      Local variable table:
        [pc: 0, pc: 5] local: this index: 0 type: sample.Hoge
  
  // Method descriptor #9 ()V
  // Stack: 0, Locals: 0
  public static void staticMethod();
    0  return
      Line numbers:
        [pc: 0, line: 6]
  
  // Method descriptor #9 ()V
  // Stack: 0, Locals: 1
  public void instanceMethod();
    0  return
      Line numbers:
        [pc: 0, line: 7]
      Local variable table:
        [pc: 0, pc: 1] local: this index: 0 type: sample.Hoge
}

 クラスの宣言、フィールド、メソッドが書かれているのがわかるだろう。そのクラスがどんな形をしているのかを見るだけなら"Class File Editor"で眺めるだけでわかる。ソースを追いかけていて、こうしたインターフェース部分さえ分かればいいというのであれば、すでに読める状態にあるんだ。読めると思えば読める

 ところでpublic Hoge();の部分。Javaソースコードでは書いていないのに存在している。これは暗黙に作られるデフォルトコンストラクタってヤツ。その辺を理解していないならまずJavaの仕様を理解しよう。

メソッドの中身

 さて、クラスの概観は特別な知識がなくても読めた。ここからメソッドの中を読もう。ごく単純なサンプルとしてHelloWorldを作って"Class File Editor"で覗いてみよう。

public class HelloWorld {
	public static void main(String[] args) {
		System.out.println("HelloWorld!");
	}
}

 mainメソッド部分だけを抜粋する。

 public static void main(java.lang.String[] args);
    0  getstatic java.lang.System.out : java.io.PrintStream [16]
    3  ldc <String "HelloWorld!"> [22]
    5  invokevirtual java.io.PrintStream.println(java.lang.String) : void [24]
    8  return
      Line numbers:
        [pc: 0, line: 5]
        [pc: 8, line: 6]
      Local variable table:
        [pc: 0, pc: 9] local: args index: 0 type: java.lang.String[]

 左側の数字はバイトコードインデックス。0からスタートするのだけど、命令によってバイト数が違うので飛びとびの値になる。この数字は"Line numbers:"で照合するとjavaファイルの行番号がわかる。[pc: 0, line: 5]とあるので0の部分がjavaファイルの5行目だ。javacでコンパイルする際に-g:noneオプションをつけると、この行番号対照表が生成されなくなるのでスタックトレースなどで行番号が表示されなくなる。

 getstatic, ldc, invokevirtual, returnらがJavaバイトコードの命令で、ニーモニックと呼ばれる。ニーモニックは人間が覚えやすいように命令に付けられた名前で、バイトコードとしての実体はそれぞれ 0xB2, 0x12, 0xB6, 0xB1となる。だけど、直接バイナリエディタclassファイルを解析しようというケースでもないかぎり、あまりこの値は関係ない。

メソッド呼び出しを捉える

 実際の動作を正確に読むには、ちゃんと命令を読まなくてはならないのだけど、いきなりそこまで読める必要はない。重要なのはinvoke系の命令だ。

ニーモニック動作
invokevirtualインスタンスのメソッド呼び出し
invokeinterfaceインターフェースのメソッド呼び出し
invokestaticstaticメソッドの呼び出し
invokespecialコンストラクタの呼び出し

 ざっくりこんな感じ。

    5  invokevirtual java.io.PrintStream.println(java.lang.String) : void [24]

 であれば、java.io.PrintStreamクラスの println(java.lang.String) : void を呼び出すよ、ということ。

引数の渡し方

 メソッドを呼ぶには引数を渡さなくてはならない。JavaVMの場合、値の受け渡しはスタックを使う。

 スタックというのはLIFOとか呼ばれたりするけども、イメージとしては本とかを積み上げるイメージ。順番に積み上げて、上から順番に取る。詳しくはWikipediaでも読んでくれ。情報工学的には基礎なのでここでは解説は省く。

 メソッド呼び出しをする場合は、引数のぶんだけスタックにデータを積み上げ、メソッドを呼び出す。メソッドの中ではスタックからデータを取り出す。そんな感じで動く。こうしたデータをスタックでやり取りするマシンはスタックマシンと呼ばれる。僕らがよく使っている8086互換CPU、つまるところPentiumとかCore2とかはレジスタで計算を行うのでレジスタマシンって呼ばれてる。

 というわけで、メソッド呼び出しの手前で、スタック引数を積み上げているので、そこを確認すれば引数に何を渡しているのかはわかる。

    3  ldc <String "HelloWorld!"> [22]
    5  invokevirtual java.io.PrintStream.println(java.lang.String) : void [24]

 ldcという命令の細かい動きはおいておくとして、とにかくどうやら"HelloWorld!"という文字列スタックに積んでからinvokevirtual命令でprintlnメソッドを呼んでいるということがわかる。

インスタンスメソッド

 インスタンスメソッドの場合は、隠れた1番目の引数としてインスタンスが渡される。

public class Foo {
	public void hoge() {
		bar();
	}
	public void bar() {
	}
}

というコードに対し、hoge()メソッドのバイトコードは以下のようになる。

  // Method descriptor #6 ()V
  // Stack: 1, Locals: 1
  public void hoge();
    0  aload_0 [this]
    1  invokevirtual sample.Foo.bar() : void [15]
    4  return
      Line numbers:
        [pc: 0, line: 5]
        [pc: 4, line: 6]
      Local variable table:
        [pc: 0, pc: 5] local: this index: 0 type: sample.Foo

 1:invokevirtual の手前で0:aload_0 [this]という命令を呼んでいる。aload_0命令の詳細はおいておいて、[this]ってのを見ておけばいい。引数がある場合も見てみよう。

public class Foo {
	public void hoge() {
		bar(3, "str");
	}
	public void bar(int i, String s) {
	}
}

と、barの引数にint型とString型を加えてみた。ニーモニック

  // Method descriptor #6 ()V
  // Stack: 3, Locals: 1
  public void hoge();
    0  aload_0 [this]
    1  iconst_3
    2  ldc <String "str"> [15]
    4  invokevirtual sample.Foo.bar(int, java.lang.String) : void [17]
    7  return
      Line numbers:
        [pc: 0, line: 5]
        [pc: 7, line: 6]
      Local variable table:
        [pc: 0, pc: 8] local: this index: 0 type: sample.Foo

となって、4:invokevirtual の手前で、0:aload_0でthisをスタックに積み、1:iconst_3で定数3をスタックに積み、2:ldc文字列を積んでいるのがわかる。

戻り値

 ここまで戻り値がvoidのメソッドばかり作っていた。returnという命令が書かれているのはすでに気付いているだろう。戻り値を返す場合はどうか。

	public int bar() {
		return 3;
	}
  // Method descriptor #15 ()I
  // Stack: 1, Locals: 1
  public int bar();
    0  iconst_3
    1  ireturn
      Line numbers:
        [pc: 0, line: 5]
      Local variable table:
        [pc: 0, pc: 2] local: this index: 0 type: sample.Foo

 0:iconst_3でスタックに定数3を積んで、ireturn命令を呼び出している。このireturn命令はboolean, byte, short, char, int型の戻り値のメソッドで用いられる。呼び出し元ではinvokevirtual の手前でスタックインスタンス引数を積む。invokevirtual の実行後、スタックからインスタンス引数が消え、代わりにこの戻り値が積まれた状態になる。

 ireturn命令以外の型の場合は代わりにareturn命令が使われる。

変数への格納

 戻り値スタックに積まれるところまで分かった。通常は戻り値変数に格納する。これはどうなるのか。

public class Foo {
	public void hoge() {
		int value = bar();
	}
	public int bar() {
		return 3;
	}
}
  // Method descriptor #6 ()V
  // Stack: 1, Locals: 2
  public void hoge();
    0  aload_0 [this]
    1  invokevirtual sample.Foo.bar() : int [15]
    4  istore_1 [value]
    5  return
      Line numbers:
        [pc: 0, line: 5]
        [pc: 5, line: 6]
      Local variable table:
        [pc: 0, pc: 6] local: this index: 0 type: sample.Foo
        [pc: 5, pc: 6] local: value index: 1 type: int

 4:istore_1命令でvalueというローカル変数に代入している。storeとついている命令はスタックから変数への代入と思えばいい。型によってstoreの手前にiとかlとかfとかdとか付くが型によって命令が異なる程度の話。ざっくり動きを読めればいいいいや程度なら細かいことは気にしなくても大丈夫。

 じゃあ、istore_1の_1って何よって話なんだが、これはローカル変数インデックスを表す。VM中では変数配列で管理していて、この変数インデックス1、この変数インデックス2みたいにしているんだ。

 単なるistore命令ってのがあってスタック変数インデックスを積んでから呼び出すのだけど、手前の数個がよく使われるので楽できるようにistore_0, istore_1, istore_2, istore_3という命令があらかじめ用意されている。istore命令でいちいちスタックインデックスを積む代わりにistore_1を呼べば一発でインデックス1の変数に値を格納できる。

 aload_0の_0とかも同じ。

まとめ

 この稿では簡単なバイトコードニーモニックの読み方を解説した。

  1. クラスの定義は簡単に読める
  2. メソッドの中身を読むときはまずinvoke系の命令を探す
  3. invoke系命令の前にインスタンス引数スタックに積まれているはず
  4. invoke系命令の後に戻り値変数に格納しているはず

 これぐらいを抑えておけば、ざっくりした動きぐらいは読めるようになるだろう。

参考資料

オンラインでJava仮想マシン仕様を読むことができる。ただし、これは英語。

Java SE Specifications

書籍でなら日本語訳が発売されている。

Amazon CAPTCHA

Wikipediaにも簡易な解説があるので役立つだろう。

Java仮想マシン - Wikipedia

最速マスターではない理由

 僕はジョークとパロディが好きだからエントリに Java変態文法最速マスター - プログラマーの脳みそなんてタイトルをつけたけど、「最速」ってのは誇大広告的で嫌いなフレーズだ。そんなわけで、最速ではないJavaバイトコードの読み方を書いた。もちろん、僕はこのエントリにエンジニアとしての意地を詰め込んでいて、必要最小限の知識・技能を手早く理解できるように工夫してある。出来れば「最速」と評価されたい気持ちもある。ただ、ここが世界の最速の到達点で、これ以上世界が進化しないなんてつまらないじゃないか。

 それではみなさん、楽しいデバッグライフを :-)

kmizushimakmizushima 2010/02/08 01:24 読んでいて、ちょっと気になった点について補足をば。

> メソッド呼び出しをする場合は、引数のぶんだけスタックにデータを積み上げ、メソッドを呼び出す。メソッドの中ではスタックからデータを取り出す。そんな感じで動く。こうしたデータをスタックでやり取りするマシンはスタックマシンと呼ばれる。

これだと、呼び出し時後に、仮引数が束縛されているデータがオペランド
スタックに積まれているような印象を受けそうな気がします。仮引数自体は
通常のローカル変数として管理されている、ということはどこかで明示した方
がいいのではないかと。

> 単なるistore命令ってのがあってスタックに変数のインデックスを積んでから呼び出すのだけど、手前の数個がよく使われるので楽できるようにistore_0, istore_1, istore_2, istore_3という命令があらかじめ用意されている。istore命令でいちいちスタックにインデックスを積む代わりにistore_1を呼べば一発でインデックス1の変数に値を格納できる。

たぶんケアレスミスだと思うのですが、store系命令のインデックスはオペランドスタックに積むのではなく、即値(命令列にエンコードされている)ですよね。

通りすがりの新米プログラマー通りすがりの新米プログラマー 2016/01/31 10:08 非常に参考になりました。

> 読めると思えば読める。
英語もそうですけど、考えずに「あ・・・(察し)」って読むのを辞めてしまうと世界が狭まってしまうんですね。

最初の一歩を教えて頂いたので、JVMの仕様書を片手に少しずつバイトコードにも慣れていきたいと思います。
ありがとうございました。

リンク元