Javaバイトコードの読み方
先日、Javaクラスファイル入門 - connpassにてバイトコードの読み方について話してきたので、折角だからこちらにも書いてみます。
内容的にはあくまで入門ということで、サンプルとして取り上げたコードを読むために必要な範囲の知識のみ、加えてあまり厳密な事までには言及していません。とりあえずこんな感じで動いてるという雰囲気を掴む事を主眼に起きました。厳密な定義なんてわざわざ自分が言わなくても仕様書見れば書いてありますしね。
Javaバイトコードとは?
通常、コンピュータを動かすための命令は高級言語がなんであれ、最終的にはアセンブラ、ひいては機械語となって処理されます。
Javaでは様々なプラットフォーム上で動作するJava仮想マシンを用意し、その仮想マシン上で動作する、先述のアセンブラの位置に該当する言語を解釈して実行します。大雑把に言ってしまえば、それがバイトコードということですね。
バイトコードを学ぶ意義としてもアセンブラと同様の事が言えます。最終的にどういう挙動になるのかを理解することは、より上位のコードを書く上でも助けになるでしょう。
私よりもちゃんとしたエンジニアのお言葉を聞きたい人は下記のIBM developer worksの記事を読むといいと思います。
Javaバイトコードの閲覧方法
javapコマンドを使います。この辺りは同勉強会で自分よりも先に、バイナリの解析について講師をしていただいた虎塚さんの記事を見ていただいた方が早いので、そちらに譲ります。
実例その1
以下のコードについて考えます
このコードをコンパイルして、javapにかけると次の様なコードが出力されます
Constant poolはひとまず置いておきましょう。定数やらが定義されているところです。本稿で着目すべき対象はその下、ブラケットで囲まれている箇所になります。抜き出すと以下の通り。
前半部分がコンストラクタ、後半部分がmain関数のバイトコードに相当します。javaのコードにはコンストラクタを書いていませんでしたが、このバイトコードから自動的に補完されていることがわかりますね。
さて、更にmain関数のみに着目して、そのバイトコード本体を見てみましょう。それだけを抜き出したものが次のコードです。
左の番号は行番号ではなく、実際にクラスファイル上でバイトコードが含まれている構造の中で何バイト目の位置にあるかを表したものです。
さて、これらの処理について概要を見てみます。詳細については後述するなりしますので、一旦は意味不明な言葉が出てきても突き進んでください。
また、以降で述べる命令についての正確な定義はJava Virtual Machine Specification (PDF)を参照下さい。
getstatic命令
getstatic命令はクラスのstaticフィールドを取得します。今回のコードではConstant poolの#2に該当する部分、java/lang/System.out:Ljava/io/PrintStream;より、java/lang/System.outを取得しています。
ldc命令
実行時コンスタントプールから項目をpushします。何処にpushするのかは後述。今回のコードでは#3、つまり文字列hello
をpushする事を意味します。
invokevirutal命令
クラスに基づくディスパッチを行い、インスタンスメソッドを起動します。今回のコードでは#4、つまり java/io/PrintStream.println:(Ljava/lang/String;)V の型に一致するインスタンスメソッドを実行します。つまりはSystem.out.println(String)を実行することになります。
return命令
メソッドからvoidをreturnします。main関数の定義より、返値はvoidである必要があるのでこの命令が実行されます。
以上より、このmain関数では
- 使用するフィールド(System.out)の取得
- 引数(文字列hello)を渡す
- 関数(println)の実行
- voidをreturn
という手順で動作していることがバイトコードから読み取れます。
もうちょっと踏み込んでみる
さて、今の例では何をやっているかに軽く触れただけで、細かいところはスルーしてきました。ここで今のコードを理解するのに必要な最低限の事柄に触れておきます。
Java仮想マシンスタック
Java仮想マシンはプログラム実行時に必要なデータを保持する領域をいくつか持ちます。先程の処理を理解する為に、Java仮想マシンスタックについて見ていきます。
このスタックはスレッド作成と同時に作られる領域であり、フレーム(後述)を保持するためのものです。各スレッド毎にこのスタックを保持することになります。
こんな感じですね。各スレッド毎のスタックは独立しています。
フレーム
スタックに積まれている、このフレームというもの。これらはメソッド毎に作成され、そのメソッドのローカル変数やオペランドスタック(後述)などを保持します。メソッドの定義が同じものであっても、メソッドの実行毎にそれぞれがフレームを持つことになります。再帰処理などで同じ定義のメソッドを呼んでも状態がそれぞれ独立して保持されているのはこのためですね。
オペランドスタック
位置づけとしては前述の図の通り、フレームの中にあります。このスタックの役割は色々ありますが、ここで注目するのは
といった所です。例として、バイトコードのiadd命令を見てみましょう。この命令の概要は
という具合です。オペランドスタックの状況を図を交えて見てみます。
まず、スタックに和を計算する対象の2値、aとbが入っている状態とします。iadd命令はこれらをpopします。
iaddがpopした2値の和を計算します。popされているのでスタックは空になっています。
計算結果をpushします。この処理を経て、オペランドスタックには計算結果の値が1つだけ積まれている状態になります。
ローカル変数配列
ローカル変数配列はローカルで使われる変数を保持するのに使われる他、状況によってはメソッド起動時のパラメーター引き渡しに使われます。例えばクラスメソッドの起動では、配列のindexが0の位置から連続したローカル変数として引き渡される事になります。
例えば、クラスXのメソッドAから、このクラスのクラスメソッドBを引数 x,y,z を渡して呼び出すとします。適当に書くならこんな感じでしょうか
class X {
public static void A () {
X.B(x, y, z);
}
private static void B(int x, int y, int z) {
return x + y + z;
}
}
このとき、Bのフレームにはローカル変数配列のindexが0の位置から順にx, y, zが入っている状態として、Bが開始されることになります。
最後に
一応、勉強会の時はもう一歩複雑な例について、スタックの様子も見ながらバイトコードをトレースして挙動を観察すると言うことをやりました。それについては省きます。スタック周りがわかってれば、あとは仕様書でコードの挙動を追えばそれでわかる範囲のものですから。
コードだけ載せておきますと、こんな感じで変数の定義と計算、クラスメソッドの呼び出しが含まれるものになります。興味ある人は実際にコンパイルしてクラスファイルをjavapにかけてみるといいと思います。そんなに長くもないので、軽い気持ちで出来る練習になるかと。
バイトコードの命令について、詳細な仕様はOracleの公式ドキュメントで、"The Java Virtual Machine Specification, Java SE 8 Edition"の方を閲覧してもらえばよいと思います。命令の一覧はchapter6にあります。フレームやらオペランドスタックやらの話はchapter2の2.6です。