Javaアセンブラ「Jasmin」でバイトコードの世界を覗いてみよう(1) - 「Jasmin」でプログラミング
- 「Jasmin」で遊ぶ
- 「Jasmin」でプログラミング
- 「Jasmin」で制御構文を操る
Javaバイトコードについて知ることで、Javaについてより深く学ぶことができるはずです。どのようなコードが javac によって生成されているのか、実際にバイトコードに近いアセンブラを書いてみます。今回は、足し算やメソッド呼び出しについて紹介します。
前回のおさらい~Jasmin で Hello, World
前回は、「Jasmin」というJavaアセンブラを利用して、Hello, World! という簡単なコードを書いてみました。今回も、Jasmin を使ってアセンブラを書いていきます。
その前に、Java で書かれているJavaアセンブラをいくつか紹介したいと思います。
- Jasmin — http://jasmin.sourceforge.net/
- Javaのバイトコードに関する本を書くために作ったのだそうです。前回から紹介しています。
- BCEL — http://bcel.jakarta.jp/
- Apach Jakarta Project で開発されたライブラリです。クラスファイルやメソッドの解析に加えて動的な書き換えに対応しています。
- ASM — http://asm.objectweb.org/
- BECLに似た機能を提供するライブラリです。動的なクラス生成や動的なクラスデータの編集などに対応しています。JRuby もこのライブラリを採用しています。
- Javassist — http://www.csg.is.titech.ac.jp/~chiba/javassist/
- BECLやASMと似た用途に使えます。直接バイトコードを書かなくても、Javaのコードをコンパイルしてバイトコードに直してくれる機能を持っています。
今回は、単純な足し算や、メソッド呼び出しについて詳しく見ていきます。
Hello, World を改良して足し算の結果を表示させてみる
それでは、前回作った、Hello, World のコードを改良して、簡単な足し算をやって、その結果を表示するものを作ってみます。(以下のプログラムでは、3 + 8 を計算し、これを画面に表示するというものになっています。)
.class public Add
.super java/lang/Object
; --- 初期化メソッド ---
.method public <init>()V
aload_0
invokenonvirtual java/lang/Object/<init>()V
return
.end method
; --- 静的 main メソッド ---
.method public static main([Ljava/lang/String;)V
.limit stack 2
.limit locals 3
; --- 足し算をしてローカル変数3に代入 --
ldc 3
ldc 8
iadd
istore_2
; --- 表示 ---
; java.lang.System.out("Hello, World!") と同等
getstatic java/lang/System/out Ljava/io/PrintStream;
iload_2
invokevirtual java/io/PrintStream/println(I)V
return
.end method
上記のソースコードを「Add.j」という名前で保存し「jasmin.jar」と同じフォルダにコピーして、コマンドラインから次のように入力します。
>java -jar jasmin.jar Add.j
すると、Add.class が生成されます。これを実行するには、次のように入力します。
>java Add 11
ほとんどが、初期化メソッドなど定型的なコードなので、ポイントだけ抜粋してみます。
ldc 3 ; 定数の値 3 をスタックに積む ldc 8 ; 定数の値 8 をスタックに積む iadd ; スタック上の整数を足して結果をスタックに積む istore_2; スタック上の整数を、ローカル変数2に代入
「ldc (定数)」というのは、定数(int, float, String)をスタックに積む命令です。class ファイルでは、定数は、コンスタントプールという領域に格納されます。そのため実際のバイトコードでは、ldc の後の値は、定数ではなくコンスタントプールへのエントリ番号となります。しかし、コンスタントプールのエントリ番号を指定するのは大変なので、Jasmin では、実際の定数値を指定できるようになっています。
つまり、「ldc 3」と書けば、整数の値 3 がスタックに積まれ、「ldc "hoge"」と書けば、文字列の値「hoge」がスタックに積まれます。
もし、定数ではなく、整数を直接指定するなら、次のように書き直すことができます。
bipush 3 ; byte型の値 3 をスタックに積む bipush 8 ; byte型の値 8 をスタックに積む iadd istore_2
なお、bipush 3 では、バイトコードは 0×10 0×03 と2バイトになりますが、定数の3をスタックに積むという動作は、よく使われるので、これを1バイトで表す命令 iconst_3(0×06) が定義されています。他にも、iconst_m1 (-1をスタックに積む)、iconst_0(0をスタックに積む) .. iconst_5 までが定義されています。
もう、お分かりかと思いますが、これを使ってさらに書き直すなら以下のようになるでしょう。
iconst_3 bipush 8 iadd istore_2
また、プログラム中に .limit という奇妙な宣言が出てきたことにもお気づきでしょう。これは、メソッド中で利用するスタックの長さと、ローカル変数の数を指定するものです。
.limit stack 2 .limit locals 3
上記のプログラムだと、スタックサイズを2、ローカル変数を3つ利用するという意味になります。多めに宣言する文には問題になりませんが、必要な数より少ないと実行時にエラーになります。このくらい、Jasmin が自動的にカウントしても良さそうなものですが、そこは、学習用に書かれたコンパイラだけあります。
スタックの働きがよく分からない場合:
ちなみに、前回紹介したJDK標準の逆アセンブラの「javap」ですが、こちらも「javap -c -verbose クラス名」のように -verbose オプションをつけると、スタックサイズやローカル変数の個数を表示します。
ところで、スタックマシンの働きが、よく分からないと言う方は、以前、筆者が執筆した以下の記事を参考にしてみてください。(こちらは、Java でなく、Flash ですが、Java のバイトコードとよく似ています。)
クラスにメソッドを定義する
次に、クラスのメソッドを自分で定義してみて、これを呼び出すプログラム「CallFunc.j」を作ってみます。
.class public CallFunc .super java/lang/Object ; --- 初期化メソッド --- .method public <init>()V aload_0 invokenonvirtual java/lang/Object/<init>()V return .end method ; --- 独自 test メソッド --- .method public test()V .limit stack 2 getstatic java/lang/System/out Ljava/io/PrintStream; ldc "call test" invokevirtual java/io/PrintStream/println(Ljava/lang/String;)V return .end method ; --- main メソッド --- .method public static main([Ljava/lang/String;)V .limit stack 2 new CallFunc dup invokespecial CallFunc/<init>()V invokevirtual CallFunc/test()V return .end method
ポイントとなるのは、独自メソッドの定義の部分と、メソッドを呼び出す部分です。まず、メソッドの定義についてみます。メソッドを定義するには、「.method」から「.end method」を記述します。「.method」に続いて、public や static などを記述し、メソッドの名前、そして、引数と戻り値の定義を記述します。
; --- public void test() { .. }
.method public test()V
.limit stack 2
getstatic java/lang/System/out Ljava/io/PrintStream;
ldc "call test"
invokevirtual java/io/PrintStream/println(Ljava/lang/String;)V
return
.end method
ここで定義したのは、Javaで言うと「public void test()」と書いたのと同じものです。「.method public test()V」と書きます。メソッドの中身は、Hello, World と同じものとなっています。
それから、呼び出し側ですが、こちらは、少し複雑です。インスタンスを生成し、初期化メソッドを呼び出してから、test メソッドを呼び出しています。
new CallFunc dup invokespecial CallFunc/<init>()V invokevirtual CallFunc/test()V
インスタンスを生成するのが「new」です。生成したインスタンスのポインタがスタックに乗せられます。次の「dup」では、スタックトップ(つまり、インスタンスのポインタ)を複製します。なぜ、複製するのかというと、その直後で、invokespecial と invokevirtual を実行するのにインスタンスへのポインタが必要になるからです。
メソッドの呼び出しには、次の種類が用意されています。
- invokevirtual — インスタンスメソッドを呼び出す。
- invokespecial — インスタンス初期化メソッド、privateメソッド、スーパークラスのインスタンスメソッドを呼び出す。
- invokestatic — staticメソッドを呼び出す
- invokeinterface — インターフェースのメソッドを呼び出す
これらは、スタックにあるインスタンスを取り出し、続くメソッドを実行するものとなっています。このとき、メソッドを呼び出す時には、メソッドの戻り値の型を指定する必要があります。ここでは「invokevirtual CallFunc/test()V」となっており、「V」つまり「void」型のメソッドを呼ぶという意味になります。
そして、「System.out.println()」の例で十分見ていますが、スタックに積む順番は、次のように行う必要があります。
- インスタンスを積む
- メソッドの引数1を積む
- メソッドの引数2を積む
- メソッドの引数3を積む・・・
- メソッドを呼び出す
足し算を行うメソッドを定義する
そして、次に、足し算を行うメソッド「int addInt(int,int)」を定義して、これを呼び出すプログラムを作ってみます。「addInt()」の定義と、これを呼び出すプログラムは下記の通りです。
.class public CallFunc .super java/lang/Object ; --- 初期化メソッド --- .method public <init>()V aload_0 invokenonvirtual java/lang/Object/<init>()V return .end method ; --- 独自 addInt メソッド --- .method public addInt(II)I .limit stack 2 .limit locals 3 iload_1 iload_2 iadd ireturn .end method ; --- main メソッド --- .method public static main([Ljava/lang/String;)V .limit stack 4 .limit locals 4 ; --- new instance new CallFunc dup invokespecial CallFunc/<init>()V astore_2 ; --- call addInt() aload_2 ldc 100 ldc 13 invokevirtual CallFunc/addInt(II)I istore_3 ;--- print getstatic java/lang/System/out Ljava/io/PrintStream; iload_3 invokevirtual java/io/PrintStream/println(I)V return .end method
まずは、「public int addInt(int, int)」のメソッドを定義した部分を見てみます。
.method public addInt(II)I .limit stack 2 .limit locals 3 iload_1 iload_2 iadd ireturn .end method
さて、ここで押さえておきたいのは、メソッドの宣言型、ireturn 命令、ローカル変数が3個必要という部分です。
まず、メソッドの宣言ですが、こちらは、見ての通り「(II)I」で、引数が2つ、戻り値の全てが int 型であることを表しています。
そして、忘れてはならないのが、整数型を返すメソッドでは、必ず、戻り型に応じた「?return」を使わなくてはならないという部分です。メソッドの宣言で「I」型と宣言しているなら「ireturn」を記述し、「Ljava/lang/String」なら「areturn」を記述するということになっています。
そして、見たところ、addInt()メソッドでは、ローカル変数を1つも使っていないように見えます。しかし、インスタンスのメソッドでは、必ずローカル変数を1つ以上、持っています。それは、自身のインスタンスで、ローカル変数0番には、自動的にインスタンスが保持される仕組みになっています。
それから、addInt() メソッドは、2つの引数を取ります。この引数が、自動的にローカル変数に割り当てられてます。つまり、ローカル変数3つの内訳は次のようになります。
- 「addInt(II)I」のローカル変数
- 0番:インスタンス変数
- 1番:第一引数
- 2番:第二引数
まとめ
以上、今回は、単純な足し算や、メソッド呼び出しについて詳しく見てみました。スタックを用いて、どのように計算を行うのか、また、どのようにメソッド呼び出しが行われるのかを理解することができました。
次回は、値の比較や条件分岐について紹介します。
TrackBack URL :
