クラスファイル全体の構造
Java のクラスファイルの構成は次のようになっています。
class_file {
magic // 0xcafebabe
minor // 2 byte
major // 2 byte
constants
modifiers // 2 byte
thisClass // 2 byte
superClass // 2 byte
interfaces
fields
methods
attributes
}
クラスファイルの先頭には必ず 0xcafebabe (カフェベイビー) が入っています。
minor, major はクラスファイルのバージョン番号が入っています。
バージョン番号の次は 定数プール と呼ばれる構造体です。
定数プールはクラスファイルの大部分を占めており、クラスファイルの様々な場所から参照されます。
modifiers はクラスのアクセス修飾子等を表現するフラグの集合です。
thisClass に入っている値は定数プールのインデックスで、このクラスファイルが表現するクラスを示しています。
superClass も定数プールのインデックスで、このクラスの親クラスを表現する定数への参照となっています。
ただし、こちらは正しい定数プールのインデックスの代わりに 0 が入ることがあります。これは親クラスが存在しないクラス (つまり、java.lang.Object) であることを表します。
interfaces, fields, methods, attributes はいずれも先頭 2 バイトに個数が入っており、その後に指定された個数だけ情報が格納されているという構造をしています。
interfaces {
count // 2 byte
interface // 2 byte
... // } count 個
interface // 2 byte
}
interface は定数プールのインデックスで、このクラスが直接実装しているインターフェースを表現する定数を示します。
fields の各エントリ field_info は以下の様な構造となっています。
methods の各エントリ method_info も同様の構造となっています。
field_info {
modifiers // 2 byte
name // 2 byte
descriptor // 2 byte
attributes
}
name はフィールド/メソッド名を表現する定数への参照で、descriptor は型情報を表現する定数への参照です。
これらは共に定数プールのインデックスとなっています。
descriptor が参照する文字列は 型記述子 と呼ばれる形式で表現されます。
class_file, field_info の双方にある attributes (属性) には様々な情報が格納されています。
各エントリ attribute_info の構造は以下のようになっています。
attribute_info {
tag // 2 byte
length // 4 byte
data // length byte
}
tag は属性名を表す定数の定数プール上でのインデックスです。
length はそれに続く data の部分の長さを示します。
data の内容をどう解釈するかは tag によって変化します。
クラスファイルを解釈するソフトウェア (javac や jvm) は自身の知らない属性名の属性は無視します。
そのため、クラスファイルの作成者はここに自身のツールが利用する情報を自由に付加することができます。
例えば、Scalaコンパイラ は ScalaSig という属性を class_file の属性として追加しています。
定数プール
定数プールの構造は Java の設計ミスの1つと言われています。
定数プールは interfaces などと同じく先頭 2 バイトに count が格納されており、それに続いてエントリが記述されるといった構造となっています。
しかし、定数プールの場合は続くエントリの個数が count 個ではありません。
count に格納されている値は、
(エントリ数) + (long及びdoubleのエントリ数) + 1
です。
これは long や double が int の倍のバイト長であるためだと思われますが、文字列定数のような可変長のエントリもあるため、解析を複雑にするだけの全く無意味なものとなっています。
定数プールの各エントリは先頭の 1 byte がタグとなっており、それによって解釈方法が変化します。
以下ではそのそれぞれについて説明をしていきます。
先頭が1 : 文字列定数
可変長サイズのエントリです。
タグに続く構造は次のようになっています。
constant_utf8 {
length // 2 byte
bytes // length byte
}
bytes には文字列が UTF-8 の形式で格納されています。
そのため、Java で解析器を作るのであれば、
new String(bytes, "UTF-8")
とすることで文字列を取り出すことができます。
先頭が3 : int 定数
タグに続いて 4 バイトの int 定数が格納されています。
short や char, byte, boolean 定数も int 定数として格納されます。
先頭が4 : float 定数
タグに続いて 4 バイトの float 定数が格納されています。
先頭が5 : long 定数
タグに続いて 8 バイトの long 定数が格納されています。
2 エントリ分としてカウントされるため注意が必要です。
先頭が6 : double 定数
タグに続いて 8 バイトの double 定数が格納されています。
2 エントリ分としてカウントされるため注意が必要です。
先頭が7 : クラスを表現する定数
タグに続いて 2 バイトの数値が格納されています。
この値は定数プール上の文字列定数のインデックスです。
参照先の文字列は internal form と呼ばれる形式にエンコードされた、クラスを表現する文字列です。
internal form では '.' の代わりに '/' でパッケージ名を区切ります。
また、表現するクラスが配列であった場合は、参照先の文字列は配列型の型記述子となります。
先頭が8 : 文字列リテラルを表現する定数
タグに続いて 2 バイトの数値が格納されています。
この値は定数プール上の文字列定数のインデックスです。
先頭が9 : フィールドを表現する定数
フィールドの所属する型とフィールド名、型情報を保持する定数です。
タグに続く構造は次のようになっています。
constant_field_info {
class // 2 byte
name_and_type // 2 byte
}
class は定数プール上のクラスを表現する定数のインデックスで、この定数が表現するフィールドが所属するクラスを示します。
name_and_type は名前と型の組を表現する定数のインデックスで、フィールド名とフィールドの型を表します。
先頭が10 : メソッドを表現する定数
フィールドと概ね同様です。
こちらの場合は name_and_type はフィールド名と型の代わりにメソッド名と型(返り値の型と引数の型の両方)を表します。
先頭が11 : インターフェースメソッドを表現する定数
メソッドを表現する定数とほぼ同じですが、class が参照するクラスを表現する定数がインターフェース型である点が異なります。
先頭が12 : 名前と型の組を表現する定数
フィールドやメソッドの名前と型を表現する定数です。
タグに続く構造は次のようになっています。
name_and_type_info {
name // 2 byte
type // 2 byte
}
name は定数プール上の文字列定数のインデックスです。
type も定数プール上の文字列定数のインデックスで、参照される文字列定数は型記述子と呼ばれる形式にエンコードされています。
先頭が15 : メソッドハンドルを表現する定数
先頭が16 : メソッド型を表現する定数
先頭が18 : invokedynamic に利用される定数
この3つについては Java7 から追加されたものです。
あまり詳しく知らないので、知ったかぶりはしないでおこうと思います。
あとがき
結構長くなったので、型指定子や属性の詳しい説明は又の機会にすることにします。
ある程度クラスファイルの構造を知っていると、javap -verbose の出力結果が読めて面白いですよ。
ここに書いた情報は全てJavaの仮想マシン仕様書に書いてあります。
詳しく知りたい方はそちらを読みましょう。