Javaのクラスファイルをjavapとバイナリエディタで読む

java
128件のシェア(ちょっぴり話題の記事)

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

はじめに

こんにちは、虎塚です。

この記事はJava Advent Calendar 2014 の22日目の記事です。昨日はすふぃあ (@empressia) さんの「JavaEEなWebアプリケーションを作ろうとしたときのお話: すふぃあの記憶」でした。

この記事では、「Javaクラスファイルの読み方・増補版」と題しまして、12月20日(土)に開催したJavaクラスファイル入門という勉強会でお話しした内容の補足をお届けします。なお、勉強会のターゲットは、

  • Javaプログラムは書いたことがあるけど、JVMのことは全然知らない
  • Javaクラスファイルのバイナリを見たことがない

といった初心者の方や新人さんでした。なので、Javaに興味さえあれば、どなたでもお読みいただける内容かと思います。

JVM仕様とは

JavaとJVM

Javaプログラム(.java)をコンパイルすると、中間コードと呼ばれるJavaクラスファイル(.class)が生成されます。

Javaの実行イメージ

Javaプログラムが、OSやハードウェアなどが異なるプラットフォームでも実行できるのは、中間コードのおかげです。

JVMのイメージ

JVMの内部に、中間コードを特定のプラットフォームで動かすために必要なプラットフォーム依存の機能があり、中間コードを読み込んで決められた振る舞いをするように処理してくれます。

JVM仕様とは何か

JVM仕様を一言でいうと、「入力であるクラスファイルのデータ構造と、出力である振る舞いのルールを定義するもの」です。

このルールを満たした実装が、JVMと呼ばれます。逆に言うと、JVM仕様とは、実在する特定のJVMプロダクトの仕様を定めたものではありません。

JVM仕様では、上記定義を実現するための実装の詳細については、踏み込みません。

そして、JVM仕様の一部であるクラスファイルのデータ構造、つまりclassファイルフォーマットとは、Javaのクラスファイルを表現するためのバイナリの並びを定義したものです。

クラスファイルを見るツール

Javaのクラスファイルを見るためのツールとして、逆アセンブルツールとバイナリエディタがあります。

Javaの逆アセンブルツール
バイナリデータをJavaのclassファイルとして解釈し,JVM仕様で定義されたデータ構造との対応を表示する
入力として想定するのは、Javaのクラスファイルだけ
バイナリエディタ
バイナリエディタを読みやすく整形して表示する
入力として想定するのは、ファイルなら何でも

この記事では、逆アセンブルのツールとして、javapを扱います。

javap

javapとは、OpenJDKベースのJDKに付属している標準ツールです。コマンドラインから利用します。

javapを使う場面としては、次のようなケースが考えられます。

  • 複数のコンパイラを使う状況で,コンパイラごとに出力される中間コードの差分を見たいとき
  • 処理系を作る際に,期待する入力と実際の中間コードの差分を見たいとき

要は、バイナリとは異なる粒度でJavaクラスファイル間の差分を見たい時に使うと思います。

クラスファイルをjavapで見る

たとえば、「hello」と出力するJavaプログラムHello.javaを書いて、コンパイルします。

% vi Hello.java
class Hello{
    public static void main(String[] args){
        System.out.pintln(“hello”);
    }
}
% javac Hello.java

このクラスファイルをjavapの引数に渡して実行します。情報量を増やすため、「-v」オプションをつけましょう。

次のような出力が得られました。


% javap -v Hello.class
Classfile /Users/torigatayuki/src/javap-sample/1220/Hello.class
  Last modified 2014/12/20; size 409 bytes
  MD5 checksum 786366c9c8962af2a9d3e1cf3284d69c
  Compiled from "Hello.java"
class Hello
  SourceFile: "Hello.java"
  minor version: 0
  major version: 52
  flags: ACC_SUPER
Constant pool:
   #1 = Methodref          #6.#15         //  java/lang/Object."<init>":()V
   #2 = Fieldref           #16.#17        //  java/lang/System.out:Ljava/io/PrintStream;
   #3 = String             #18            //  hello
   #4 = Methodref          #19.#20        //  java/io/PrintStream.println:(Ljava/lang/String;)V
   #5 = Class              #21            //  Hello
   #6 = Class              #22            //  java/lang/Object
   #7 = Utf8               <init>
   #8 = Utf8               ()V
   #9 = Utf8               Code
  #10 = Utf8               LineNumberTable
  #11 = Utf8               main
  #12 = Utf8               ([Ljava/lang/String;)V
  #13 = Utf8               SourceFile
  #14 = Utf8               Hello.java
  #15 = NameAndType        #7:#8          //  "<init>":()V
  #16 = Class              #23            //  java/lang/System
  #17 = NameAndType        #24:#25        //  out:Ljava/io/PrintStream;
  #18 = Utf8               hello
  #19 = Class              #26            //  java/io/PrintStream
  #20 = NameAndType        #27:#28        //  println:(Ljava/lang/String;)V
  #21 = Utf8               Hello
  #22 = Utf8               java/lang/Object
  #23 = Utf8               java/lang/System
  #24 = Utf8               out
  #25 = Utf8               Ljava/io/PrintStream;
  #26 = Utf8               java/io/PrintStream
  #27 = Utf8               println
  #28 = Utf8               (Ljava/lang/String;)V
{
  Hello();
    descriptor: ()V
    flags:
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 1: 0

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=1, args_size=1
         0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
         3: ldc           #3                  // String hello
         5: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
         8: return
      LineNumberTable:
        line 3: 0
        line 4: 8
}

バイナリエディタ

バイナリエディタは、GUIツールもコマンドラインツールも非常に豊富な種類があります。

この記事では、Mac上で0xEDを使います。

# 勉強会当日に、コマンドラインツールのhexdumpも便利だと教えていただきました。こちらはMacやLinuxで使えます。Windowsの方には、BZeditがおすすめです。

クラスファイルをバイナリエディタで見る

バイナリエディタでクラスファイルを開くだけです。0xEDの場合、次のように表示されます。

バイナリエディタ(0xED)のイメージ

クラスファイルを読んでみよう

それでは、Javaプログラムのクラスファイルを読む手順を説明します。javap、バイナリエディタ、そしてJVM仕様書を参照します。

最新のJVM仕様書は、次のページで確認できます。

ClassFile

ClassFileは、JVM仕様書の「4.1 The ClassFile Structure」を参照すると、次のような構造になっています。

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];
}

クラス、あるいはインタフェース1つに対して、この構造が1つ存在します。

u4、u2といった値は、を表わします。u4は、符号なし4バイトの型です。magic、minor_versionといった値は、項目です。

これらの項目を読む方法をウォークスルーで説明します。

magic

ClassFile構造の最初の項目は、magicです。

これは、Javaクラスファイルを識別するための定数値「CAFEBABE」です。Javaのクラスファイルには、先頭に必ずこのバイナリ列がくる決まりになっています。

CAFEBABEは16進数の表現です。magicはu4型なので、4バイトです。実際のバイナリは「11001010111111101011101010111110」ですね。

CAFEBABEという文字列の由来は、James Gosling氏によると次のようなエピソードがあるそうです。興味のある方はどうぞ。

javapで見る

javapの出力項目にmagicはありません。そもそも、javapの入力はJavaクラスファイルだけのはずだからですね。

magicが異なるファイルを読み込むと、ClassFormatErrorが発生します。OpenJDKのjavapの実装では、次のようになっています。

      magic = in.readInt();
             if (magic != JAVA_MAGIC) {
                 throw new ClassFormatError("wrong magic: " +
                                            toHex(magic) + ", expected " +
                                            toHex(JAVA_MAGIC));

バイナリエディタで見る

先頭が「CAFEBABE」となっていることが確認できます。

magic

minor_version、major_version

次は、minor_version、major_versionです。これらは、classファイルフォーマットのバージョンを表わします。minor_versionとmajor_version、2項目合わせてバージョンを特定します。

javapで見る

javap出力内容の次の箇所が、minor_version、major_versionです。

  minor version: 0
  major version: 52

バイナリエディタで見る

バイナリエディタでは、次のように見えます。

バイナリエディタでminor_versionとmajor_versionを確認する

major_versionが34となっていますが、これは16進数です。10進数に変換すると52なのでで、javapの出力結果と一致しますね。

constant_pool_count

constant_pool_countは、続くconstant_poolテーブルのエントリ数です。正確には、constant_poolのエントリ数は、この値よりも1少なくなります。これについては、後ほど説明します。

バイナリエディタで見る

バイナリエディタでは、次のように見えます。

バイナリエディタでconstant_pool_countの値を確認する

16進数で1Dとあるので、10進数に直すと29。つまり、constant_poolには、29-1=28個のエントリがあると分かります。

constant_pool[constant_pool_count-1]

constan_poolは、可変長のテーブルです。上記のconstant_pool_countによって、要素の数は分かりますが、中身の構成を見るまで、何バイトあるかは分かりません。

なぜなら、constant_pool要素の型であるcp_infoには、複数の種類があるからです。JVM仕様書の「4.4 The Constant Pool」によると、cp_infoは次のような構造をしています。

cp_info {
    u1 tag;
    u1 info[];
}

1つ目の項目である「tag」の種類によって、cp_infoの種類(Constant Type)が特定されます。JVM仕様書の「Table 4.4-A. Constant pool tags」には、14種類のConstant Typeが列挙されています。

javapで見る

constant_poolは、javap出力内容の次の箇所です。Methodref、Fieldref、String、……といった項目が、Constant Typeに当たります。


Constant pool:
   #1 = Methodref          #6.#15         //  java/lang/Object."<init>":()V
   #2 = Fieldref           #16.#17        //  java/lang/System.out:Ljava/io/PrintStream;
   #3 = String             #18            //  hello
   #4 = Methodref          #19.#20        //  java/io/PrintStream.println:(Ljava/lang/String;)V
   #5 = Class              #21            //  Hello
   #6 = Class              #22            //  java/lang/Object
   #7 = Utf8               <init>
   #8 = Utf8               ()V
   #9 = Utf8               Code
  #10 = Utf8               LineNumberTable
  #11 = Utf8               main
  #12 = Utf8               ([Ljava/lang/String;)V
  #13 = Utf8               SourceFile
  #14 = Utf8               Hello.java
  #15 = NameAndType        #7:#8          //  "<init>":()V
  #16 = Class              #23            //  java/lang/System
  #17 = NameAndType        #24:#25        //  out:Ljava/io/PrintStream;
  #18 = Utf8               hello
  #19 = Class              #26            //  java/io/PrintStream
  #20 = NameAndType        #27:#28        //  println:(Ljava/lang/String;)V
  #21 = Utf8               Hello
  #22 = Utf8               java/lang/Object
  #23 = Utf8               java/lang/System
  #24 = Utf8               out
  #25 = Utf8               Ljava/io/PrintStream;
  #26 = Utf8               java/io/PrintStream
  #27 = Utf8               println
  #28 = Utf8               (Ljava/lang/String;)V

左端のインデックスが1から28まであり、constant_pool_countの数より1つ少ないことが確認できます。

じつは、インデックスは本当は0から始まります。しかし、出力には「#0」がありません。

これらのインデックス番号は、ClassFile構造のconstant_pool以外の場所から参照されます。ところが、0という数値は「データが存在しない」ことを意味します。そのため、0をインデックスとして使わないように欠番になっているのだそうです。

バイナリエディタで見る

constant_poolの1つ目のエントリ(javap出力の#1に当たる要素)を確認します。

バイナリエディタでcontant_poolテーブルを見る

まず、最初の1バイト「tag」を見ると、「0A」です。10進数では10にあたります。JVM仕様書のTable 4.4-A. Constant pool tagsを参照すると、tag「10」のcp_infoはCONSTANT_Methodrefだと分かります。

次に、JVM仕様書でCONSTANT_Methodref_infoの構造を調べると、次のように書いてあります。

CONSTANT_Methodref_info {
    u1 tag;
    u2 class_index;
    u2 name_and_type_index;
}

「tag」は、先ほど読んだ「0A」のことです。続いて、2バイトの要素が2つあります。CONSTANT_Methodref_infoは、全体で5バイトの構造であることが分かりました。

これを図示したのが、上のスクリーンショットです。このように、バイナリとJVM仕様書を参照する作業を、constant_poolテーブルのエントリの数だけ繰り返します。

constant_poolの28個の要素を図示すると、次のようになります。

constant_poolの中身を腑分け

枠の色が黒の要素は、tag01、つまりStringの要素を示しています。バイナリ列の右側に表示された文字と見比べてみてください。

access_flags

access_flagsは、アクセス許可やクラスの属性を表わすu2型の項目です。フラグの種別は、最新のJVM仕様では8種類あります。

Javaがバージョン1.2の頃には5種類でしたが、アノテーションやEnumを表わすフラグが後から追加されました。

javapで見る

javapの出力結果にも、「flags」という項目名で表示されていました。

  flags: ACC_SUPER

javapで見る

バイナリエディタで見ると、次の部分がaccess_flagsです。

バイナリエディタでaccess_flagsを見る

16進数で0020が表示されています。JVM仕様書の「Table 4.1-A. Class access and property modifiers」を参照すると、0020はACC_SUPERを意味することが分かります。

this_class、super_class

this_classとsuper_classは、先ほど登場したconstant_poolテーブルへの参照値です。

その名のとおり、this_classはそのクラス自身、super_classは直接のsuper classの名前を指します。なお、Objectクラスの場合はsuper classが存在しないため、参照ではなく値0が入ります。

バイナリエディタで見る

この項目は、javap出力内容より先にバイナリで見た方が分かりやすいかと思います。

バイナリエディタでthis_classを見る

  1. this_class: 16進数の「5」、つまりconstant_poolの5番目の要素への参照です
  2. constant_poolの5番目の要素: 16進数の「15」、つまりconstant_poolの21番目の要素への参照です
  3. constant_poolの21番目の要素: 16進数の「48656C6C6F」。これは文字列「Hello」で、クラス名です。

this_classの参照をたどると、クラス名の文字列に行き着きました。

javapで見る

javapの出力で、constant_pool内の該当する要素を見てみましょう。

   #5 = Class              #21            //  Hello
(略)
  #21 = Utf8               Hello

参照先要素のインデックス(と値)が、右側に書かれていますね。

interfaces_count、interfaces[interfaces_count]

interfaces_countは、このクラス、あるいはインタフェースの直接のsuper interfaceの数です。

また、interfaces[interfaces_count]は、CONSTANT_Class_info要素を持つ配列です。interfacesは、constant_poolのような可変長のテーブルではなく、固定長の配列です。

バイナリエディタで見る

バイナリエディタでは、(2行に渡っていてちょっと見づらいですが)次のようになります。

バイナリエディタでinterfaces_countを見る

「0」ですね。Helloクラスはインタフェースを実装していないため、interfaces_countは0になります。

interfaces_countが0の場合、interfaces配列は存在しないことに注意してください。バイナリ列でも、項目丸ごと飛ばされます。つまり、interfaces_countの次には、interfaces配列のさらに次の項目が続くことになります。

fields_count、fields[fields_count]

fields_countは、このクラス,あるいはインタフェースで定義されているフィールドの数です。

また、fields[fields_count]は、field_info構造を持つ可変長のテーブルです。

field_infoは、JVM仕様書の「4.5 Fields」によると、次のような構造をしています。

field_info {
    u2            access_flags;
    u2            name_index;
    u2            descriptor_index;
    u2            attributes_count;
    attribute_info attributes[attributes_count];
}

フィールドにもaccess_flagsがありますね。これは、先ほど登場したクラス用のaccess_flagsとは別物のフィールド用のaccess_flagsです。

attribute_infoについては後述します。

今回のHelloクラスでは、fields_countも(先ほどのinterfaces_countと同じく)「0」なので、fieldsテーブルは存在しません。次の項目へ進みましょう。

methods_count、methods[methods_count]

methods_countは、このクラス,あるいはインタフェースで定義されているメソッドの数です。

また、methods[methods_count]は、method_info構造を持つ可変長のテーブルです。

method_infoは、JVM仕様書の「4.6 Methods」によると、次のような構造をしています。

method_info {
    u2            access_flags;
    u2            name_index;
    u2            descriptor_index;
    u2            attributes_count;
    attribute_info attributes[attributes_count];
}

fields_infoの構造とそっくりですね。ここでも、メソッド専用のaccess_flagsが先頭に配置されています。

バイナリエディタで見る

バイナリエディタで、methods_countに当たる部分を見てみましょう。

バイナリエディタでmethods_countを見る

「2」になっています。Helloクラスにはmainメソッドしか定義しなかったのに、なぜメソッド数が2になるのでしょうか。それは、Helloクラスのデフォルトコンストラクタがカウントされているからです。

前掲のHelloクラスのサンプルコードでは、コンストラクタを定義していませんでした。そのため、コンパイル時にデフォルトコンストラクタが自動生成されました。

クラス全体でのメソッド数は、デフォルトコンストラクタとmainメソッドの2個になります。

attributes_count、attributes[attributes_count]

ついに最後の項目です。

attributes_countは、JVM仕様を見ても、attribute_info構造の数としか書かれていません。続く項目であるattributes[attributes_count]の要素数を表わします。

では、attribute_infoとは何でしょうか。JVM仕様書の「4.7 Attributes」によると、次のような構造をしています。

attribute_info {
    u2 attribute_name_index;
    u4 attribute_length;
    u1 info[attribute_length];
}

attribute_infoは、attribute_name_indexの種類によって構造の種類が変わります。constant_poolの要素が、tagによって異なる構造を持っていたのと似ていますね。現時点では、23種類のattributeが定義されています。(「Table 4.7-A. Predefined class file attributes」を参照)

attribute_infoは、ClassFile構造の中に繰り返し登場します。ClassFile構造の末尾の項目でありつつ、その手前のfieldsテーブルやmethodsテーブルの内部にも現れます。さらに、Attributeの構造の1つであるCode_attributeの中に、入れ子の形で登場することもあります。

また、attributeの面白いところは、定義済み以外の値を新規に追加できることです。つまり、Javaの言語機能に何か追加があった時、何か増えるならこの部分ということです。実際、Java 8では、タイプアノテーションが言語機能に追加されたため、Attributeに新しくRuntimeVisibleTypeAnnotationsとRuntimeInvisibleTypeAnnotationsが追加されました。

バイナリエディタで見る

上記の構造を踏まえて、methodsテーブル内のメソッド構造を1個確認してみましょう。Attributesが含まれています。

バイナリエディタでAttribute構造を見る

まず、method_info構造の前から2バイト×4個は、 access_flags、name_index、descriptor_index、attributes_countで固定長です(図中の枠がピンク色の部分)。

次に、attribute_infoの本体です。こちらも前から2バイト、4バイトは、attribute_name_index、attribute_lengthで固定長です(図中の枠が水色の部分)。

attribute_name_indexの値に注目してください。これは、constant_poolテーブルのインデックスを参照しています。ここでは「09」なので、constant_poolの9番目の要素を見ると、「Code」という文字列です。つまり、このattribute_infoは、Attributeの1つであるCode_attributeであることが分かります。

Code_attributeは、JVM仕様書の「4.7.3 The Code attribute」によると、次のような構造になっています。

Code_attribute {
    u2 attribute_name_index;
    u4 attribute_length;
    u2 max_stack;
    u2 max_locals;
    u4 code_length;
    u1 code[code_length];
    u2 exception_table_length;
    {  u2 start_pc;
        u2 end_pc;
        u2 handler_pc;
        u2 catch_type;
    } exception_table[exception_table_length];
    u2 attributes_count;
    attribute_info attributes[attributes_count];
}

attribute_name_indexとattribute_lengthまでは、attribute_infoすべてに共通で、すでに確認した部分です。続く2バイト×2と4バイトが、max_stack、max_locals、code_lengthで固定長です。code_lengthの値は「5」です。続くcode配列に5個の要素があることが分かります。

code[code_length]の部分は、次のようになっています。

2A B7 00 01 B1

この意味は、JVM仕様書の「Chapter 7. Opcode Mnemonics by Opcode」を参照すると確認できます。これらのバイナリと対応するニーモニックを抜粋すると、次のようになります。

  • (0x2a) aload_0
  • (0xb7) invokespecial
  • (0x00) nop
  • (0x01) aconst_null
  • (0xb1) return

これは、javap出力内容の次の部分に対応しています。

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

さて、classファイルのバイナリを読むために必要な最小限の流れは、ここまでで記述できたかと思います。そろそろおしまいにしますが、後続の要素をあともう少しだけ見てみましょう。

code[code_length]の次には、exception_table_lengthという2バイトの項目があります。この値が「0」なので、その後のexception_table[exception_table_length]は飛ばされて、attributes_count項目が続きます(図中の枠が黄緑色の部分)。attributeの入れ子が現れました。

ここからは、先ほどCode_attributeを特定した時と同様の作業です。attribute_name_indexの値を見て、参照先のconstant_poolの10番目の要素を確認すると、「LineNumberTable」という文字列です(図中の枠がオレンジの部分)。

Code_attributeの中に入れ子になっているattribute_infoは、LineNumberTable_attributeであることが分かりました。

続きが気になる方は、JVM仕様書を参照しながら、Hello.classを末尾まで読んでみてください。

おわりに

JVM仕様書を参照しながら、javapとバイナリエディタでJavaのクラスファイルを読む方法を説明しました。構造が入れ子になっていたり、参照があったりするため、仕様書のあちこちを飛び回ることになりますが、すべての構造が仕様書に書かれているので、ゆっくり読めば必ず読めます。

今年のクリスマスはジングルベルを聴きながら、普段実行しているJavaのクラスファイルをバイナリエディタで鑑賞してみるのも楽しいかもしれませんね。

Java Advent Calendar 2014、明日は@nagaseyasuhitoさんの予定です。

それでは、また。

参考資料