Javaのクラスファイルの読み方
日曜に、「夏休みバイナリ入門 2012 第1日目」に参加してきました。
色々と初心者丸出しな質問をしましたが、詳しい方が丁寧に教えてくださったおかげで、疑問を解決しながら進むことができました。ありがとうございます。
備忘のために、学習したことをメモします。
やったこと
- Javaで「hello」とコンソール出力するHello.javaを書く。
- コンパイルする。(javac Hello.java)
- 実行して、正しく動くことを確かめる。(java Hello)
- javapをかける。(javap -v Hello.class)
- バイナリエディタでHello.classを開いて、出力を読む。
Javaのバイトコードを読むための『Java仮想マシン仕様』の参照方法を学習しました。
また、スタッフの方がJavaScriptで実装されたスタックマシンのデモを見せてもらったり、加算と乗算が混じった計算がどのように処理されるかについての解説を聴いたりもしました。
バイナリエディタの見方
- バイナリエディタは、バイナリを整形して見せてくれる
- 本当は2進だけど、見やすいように16進にしてくれる
- 本当はひと続きだけど、見やすいように2ケタごとに区切ったり、改行したりしてくれる
- 上端一行(+0, +1, ...)と、左端一列(000000, 000010, ...)は、アドレスを表わす
- アドレスの読み方
- (例)「68」のアドレスは000094(キャプチャのオレンジと緑が掛け合わさった部分)
- 右側の点と文字が混じっている部分
- 文字は、数字に対応するASCII文字
- 点は、文字で表わせない制御コード
- 下の方の点だけが固まっている部分は、バイトコード(キャプチャの水色の部分)
# 上のスクリーンキャプチャは、主催の七誌さんがオススメとおっしゃっていた(気がする…)バイナリエディタのBZです。本家(http://www.vcraft.jp/soft/bz.html)に加えて、機能改良版(http://code.google.com/p/binaryeditorbz/)も公開されています。
javapの出力の読み方
javapの出力は、バイトコードを人間が読みやすい形に直したもの。
「hello」を出力する実行ファイル(class)をjavapにかけた出力の一部は、次のようになる。
public static void main(java.lang.String[]); 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
「Code:」から先に、mainメソッドの中身が表示されている。そのことは、「Code:」の数行上の「public static void main(java.lang.String[]);」という出力から判別できる。
#2、#3、#4は、(後ほど見る)constant_poolへの参照になっている。ここではひとまず、各行のコメントを参考に、参照している内容がそれぞれ次のものだと捉えればよい。
getstaticのコメント行にある「Ljava/io/PrintStream」は、getstaticで取得できるフィールドの型。
invokevirtualのコメント行にある「(Ljava/lang/String)」は、printlnの引数の型で、「V」は戻り値の型(void)。
なお、「return」は、コンパイラが付け加えてくれたもの。mainメソッドは戻り値がvoidなので、メソッドの終わりで明示的にreturnを書かなくてもコンパイルは通る。しかし、実際には、省略したreturnをコンパイラが補ってくれている。
対応するバイトコード
mainメソッドに対応するバイトコードは、次のようになっている。
B2 00 02 12 03 B6 00 04 B1
# これは、javapでなく、バイナリエディタで確認する。この箇所はバイトコードなので、右側に「main...」などと出力されているわけではない。ここでは、ひとまず数字の読み方の話をする。
上の数字は、javapの出力と次のように対応する。
B2 00 02 // getstatic #2 12 03 // ldc #3 B6 00 04 // invokevirtual #4 B1 // return
たとえば、1行目であれば、「B2」が「getstatic」に対応し、「00 02」が「#2」に対応している。
ところで、2行目の「#3」は、1バイトの「03」に対応する。1行目や3行目では、2バイトを使ってconstant_poolへのインデックスを表わしているのに、なぜ2行目では1バイトか。
これは、ldcのオペランドが1バイトと定められているため。後ろに続くオペランドが、1バイトより長いバイトを必要とする場合は、2バイトのオペランドを取れるldc_wが使われる(というか、ldc_wは短いバージョンのldcを持っているが、getstaticやinvokevirtualには短いバージョンがないのだそうです)。
(追記)上の2段落で、 1バイトを誤って2バイトと表記していたので、修正しました。ご指摘ありがとうございます。>七誌さん
バイトコードの読み方
クラスファイルの大まかな構造を知るには、『Java仮想マシン仕様』の第4章「The class File Format」を参照する。
Javaのクラスファイルでは、次の仕様に沿って、前から順にバイトが並んでいる。
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]; }
「u4」や「u2」は、バイトの数を表わす。たとえば、「u4 magic」は、「ファイルの先頭から4バイトはmagic」という意味になる。magicとは、ファイルのフォーマットを識別するための番号。
Javaのクラスファイルなら、magicに「cafebabe」という値が入っている。(バイナリエディタのスクリーンキャプチャで、赤枠で囲んだ部分)
この調子で、次々と読んでいく。続く2バイトはマイナバージョン、その次の2バイトはメジャーバージョン、という具合。
constant_pool_count
「constant_pool_count」の2バイトに、「00 1d」と入っていたとする。この数字は、続く「cp_info」の個数を表わしている。javapの出力にある#1, #2, ... と対応している。
1dは16進数なので、10進数でいうと29だが、「0オリジンだが、#0は存在しない」という理由で、29 - 1 = 28個になることに注意する。
cp_info
「cp_info constant_pool[constant_pool_count-1]」の部分を読むには、『Java仮想マシン仕様』の4.3「The Constant Pool」を参照する。
cp_infoでは、次の形式で数字が並んでいる。
cp_info { u1 tag; u1 info[]; }
上の構造が、constant_pool_countで指定された個数分だけ存在する。
tagは、Constant Typeを示しているので、まずtagを見る。次に、Constant TypeとValue(tagの部分の数字)の対応表「Constant pool tags」を参照する。
# ちなみに、最新の『Java仮想マシン仕様』には、翻訳書第2版の表にない15、16、18のValueがある。
たとえば、tagに「0a」と入っていたら、表で「10」のValueの行を参照する。すると、それがCONSTANT_Methodrefだと分かる。そうなれば、今度は、CONSTANT_Methodrefの仕様を参照する。CONSTANT_Methodrefの構造が、「u1 info[]」の部分に当たる。
こうして28個のcp_infoを読み終わると、クラスファイルでcp_infoの次に位置する「u2 access_flags」に至る。
以上の作業をひたすら繰り返すと、最後まで読める。
interfaces_count、fields_count
(クラスファイルの構造から抜粋)次の箇所に注意する。
u2 interfaces_count; u2 interfaces[interfaces_count]; u2 fields_count; field_info fields[fields_count];
interfaces_countやfields_countが0のとき、interfacesやfield_info fieldsは、それぞれバイトを持たない(飛ばされる)。
もし、interfaces_countの位置から、「00 00」と続いていたら、それはinterfaces_countとinterfacesではなく、interfaces_countとfields_countを表わしている。
methods_count
mainメソッドしか存在しないHello Worldのクラスファイルでも、メソッドの数は2つになる。なぜなら、デフォルトコンストラクタが、コンパイラによって自動生成されるため。
method_infoとattribute_info
『Java仮想マシン仕様』の4.6「Methods」を参照する。
method_infoの一部として記述されているattribute_infoと、method_infoと同列に記述されているクラスのattribute_infoは、(従う仕様の構造は同じだけれども)別物なので、混乱しないように。
attribute_infoには複数の種類があり、その構造は、仕様の中で分割して記述されている。
4.7「Attributes」の冒頭で、attribute_infoの構造が次のように書かれているが、
attribute_info { u2 attribute_name_index; u4 attribute_length; u1 info[attribute_length]; }
このinfoの中身は、後続の章で、(たとえばCode_attributeであれば)次のように記述されている。
Code_attribute { u2 attribute_name_index; u4 attribute_length; u2 max_stack; u2 max_locals; ...(中略)... }
これは、上のinfoが、下のmax_stack以下の部分に当たるという意味になる。
読み方のコツ
途中でhoge_infoが出てきたときのように、仕様の他の箇所を参照しなければいけない場合、その部分の長さは事前に与えられているので、まずはhoge_infoの中身を読み飛ばし、全体の把握を優先する、といった読み方ができるそうです。
method_info { u2 access_flags; u2 name_index; u2 descriptor_index; u2 attributes_count; // attribute_infoに要するバイト数はここで分かる attribute_info attributes[attributes_count]; // ドリルダウンせずにブロックごと読み飛ばす }
ということですよね。
というわけで
JVM仕様の辿り方が分かったおかげで、これからは仕様片手にJavaのバイナリファイルを自分で読んでいけそうで、とても嬉しいです。
主催の七誌さん、スタッフの皆さん、ありがとうございました。
(追記)
- (第2日目)jarファイルのバイナリの読み方 http://d.hatena.ne.jp/torazuka/20120826/zip