Javaアセンブラ「Jasmin」でバイトコードの世界を覗いてみよう(3) - 「Jasmin」で制御構文を操る
- 「Jasmin」で遊ぶ
- 「Jasmin」でプログラミング
- 「Jasmin」で制御構文を操る
Javaバイトコードについて知ることで、Javaについてより深く学ぶことができるはずです。アセンブラの Jasmin を使って実際にバイトコードに近いアセンブラを書いてみます。今回は、制御構造について紹介します。
前回のまでのおさらい~Jasmin で Hello, World
Jasminは、Javaのバイトコードに近いコードを記述することでJavaのクラスファイルを生成することのできるコンパイラです。以下のサイトより入手することができます。
- Jasmin のダウンロードページ
- http://sourceforge.net/project/showfiles.php?group_id=100746
プログラムを「hello.j」という名前で保存したならば、コマンドラインから、以下のように入力することでコンパイルすることができます。
> java -jar jasmin.jar hello.j
簡単な使い方に関しては、連載の1回目をご覧ください。
前回は、Jasminで足し算の計算をしたり、また、クラスのメソッドを定義し、呼び出す方法を紹介しました。
さて、今回は、if 文や for 文など制御構文について紹介します。
if 文について
連載の1回目の最後でも紹介しましたが、Java のバイトコードにどんなものがあるのかを知るには、JVMの命令セット(Wikipedia)がまとまっています。
Wikipediaの条件分岐を見ると、条件分岐と言いつつも、if が1つあるだけでなく、「ifeq」や「ifnull」「iflt」「if_icmpeq」「if_acmpeq」など、多くのオペコードが用意されていることが分かります。
使い分けについて
いろいろな ifXXX のオペコードがありますが、大きく分けて3種類用意されていることが分かります。
(1)スタックに積まれている値1つに対して
まず、スタックに積まれている値によって、処理を分岐するものがあります。
- ifeq, ifnull, iflt, ifle, ifne, ifnonnull, ifgt, ifge
- スタックの値が0, null, 0未満, 0以下, 0以外, null以外, 0より大きい, 0以上ならジャンプする
(2)スタックに積まれている2つの値に対して
次に整数2つを比較して、処理を分岐するものがあります。
- if_icmpeq, if_icmpne, if_icmplt, if_icmpgt, if_icmple, if_icmpge
- 2つのint値が等しい、等しくない、<、>、≦、≧ならジャンプする
(3)スタックに積まれている2つのオブジェクトに対して
最後に、スタックに積まれている2つのオブジェクトが等しいかどうかを判定して、処理を分岐するものがあります。
- if_acmpeq, if_acmpne
- 2つのオブジェクトが同一、異なるオブジェクトを参照していたらジャンプする。
Jasmin で簡単な条件分岐のプログラム
それでは、Jasmin で簡単な条件分岐のプログラムを書いてみます。ここでは、ユーザーが入力した値が1ならば「ワン」、値が2ならば「ツー」と表示するという簡単なものを作ってみます。
以下のプログラムを「OneTwo.j」という名前で保存します。
; file:"OneTwo.j" ; --- クラス OneTwo の定義 --- .class public OneTwo .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 .throws java/io/IOException .limit stack 3 .limit locals 2 ; --- 入力 --- getstatic java/lang/System/in Ljava/io/InputStream; invokevirtual java/io/InputStream/read()I istore_1 ; --- 分岐 --- iload_1 bipush 49 ; <--- '1' if_icmpeq LABEL_PRINT_ONE iload_1 bipush 50 ; <--- '2' if_icmpeq LABEL_PRINT_TWO return LABEL_PRINT_ONE: getstatic java/lang/System/out Ljava/io/PrintStream; ldc "One" invokevirtual java/io/PrintStream/println(Ljava/lang/String;)V return LABEL_PRINT_TWO: getstatic java/lang/System/out Ljava/io/PrintStream; ldc "Two" invokevirtual java/io/PrintStream/println(Ljava/lang/String;)V return .end method
そして、以下のように入力してコンパイルします。
> java -jar jasmin.jar OneTwo.j
コンパイルが無事に完了したら、1や2を入力してみてください。1を入力すると"One"と表示され、2を入力すると"Two"が表示されます。
このプログラムのポイント(if_icmpeq)
このプログラムでポイントとなるのは、なんと言っても、条件分岐を行う部分「if_cmpeq」コマンドのある部分です。iload_1 でスタックに1番目のローカル変数の値を乗せます。そして、次に bipush でint型の整数をスタックに乗せます。そして、if_icmpeq で、スタックにある2つの変数を下ろして比較します。そして、値が等しければラベル LABEL_PRINT_ONE へジャンプします。
iload_1 ; <--- 変数1番の値をスタックに乗せる bipush 49 ; <--- '1'の文字コード49をスタックに乗せる if_icmpeq LABEL_PRINT_ONE
「if_icmpeq」は、if + int + compare + equal を短くしたものです。2つの整数を比較して等しければジャンプするという意味になるのです。
使っているローカル変数は2つ(?)
ところで、このプログラムで、main メソッドの定義を以下のように 1 つにしてしまうと、java コマンドでプログラムを実行すると時にエラーが出て、実行できません。
.method public static main([Ljava/lang/String;)V .throws java/io/IOException .limit stack 3 .limit locals 1 ; <=== 問題の箇所
比較のプログラムを見直しても、ローカル変数は1つしか使っていないように見えます。ところが、main メソッドをよく見ると、メソッドに String 型の引数があり、ここへ自動的にローカル変数が割り振られるようになっています。
そして、main メソッドは静的メソッド(static)なので、引数分の変数を考慮するだけでよいのですが、これがインスタンスのメソッド場合は、変数0番にインスタンス、変数1番からメソッドの引数が自動的に代入されるのです。
Jasmin 側にローカル変数の数を自動的にチェックする機能はないのですが、java コマンド側にチェックする機能があるので、実行時に以下のような java.lang.VerifyError のエラーが出ます。
>java OneTwo Exception in thread "main" java.lang.VerifyError: (class: OneTwo, method: main s ignature: ([Ljava/lang/String;)V) Illegal local variable number Could not find the main class: OneTwo. Program will exit.
for / while文について
次に、for や while 文について考えてみたいと思います。JVMの命令セット(Wikipedia)を見てください。for や while に相当するオペコードはありません。これは、どういうことかと言うと、goto や ifXXX 命令を工夫してループを作ることを意味します。
つまり、条件が満たされなければ、ifXXX 命令で、繰り返したい範囲にジャンプするという意味です。
次のプログラムは、3回「Hello」と表示するプログラムです。
for (int a = 0; a < 3; a++) {
System.out.println("Hello");
}
これを、Jasmin のプログラムで書いてみます。ちょっと長いですが、1行ずつ読んでみてください。
; file:"ThreeHello.j" ; --- クラス OneTwo の定義 --- .class public ThreeHello .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 2 ; ---> a = 0 bipush 0 istore_1 LOOP_START: ; ---> if (a >= 3) goto LOOP_END iload_1 bipush 3 if_icmpge LOOP_END ; ---> print "Hello" getstatic java/lang/System/out Ljava/io/PrintStream; ldc "Hello" invokevirtual java/io/PrintStream/println(Ljava/lang/String;)V ; ---> a = a + 1 iload_1 bipush 1 iadd istore_1 goto LOOP_START LOOP_END: return .end method
ここでは、(a >= 3)の条件を記述するので、if_icmpge を利用しています。ge というのは、greater than or equal で「以上」の意味になります。Perl でも文字列の比較に ge 演算子を使うので分かりやすいのではないでしょうか。
goto 命令では、指定したラベルにジャンプします。フローチャートで、for や while を無理矢理書こうとしたとき、どのようにするのかを考えてみると、Jasmin のプログラム ThreeHello.j のようになるのではないでしょうか。
まとめ
以上、3回にわたり、Java のバイトコードがどのようになっているのか、Jasmin を使って、Class ファイルを作成することを通して、確認してみました。Java のバイトコードの数はそれほど多くありません。これまで紹介した命令を組み合わせることで様々な処理を実現しているのです。
さらに研究してみたい場合は、クラスファイルを「javap」を使って逆アセンブルし、それを参考にJasminのプログラムを書いてみると良いでしょう。実用用途でバイトコードを扱う場合には、前回紹介した BCEL や ASM、Javassist を使うのが良いと思いますが、学習用とでは、Jasmin を使うのが簡単です。次の機会には、BCEL や ASM を使って、独自言語などを作る方法も紹介しようと思いますので、お楽しみに。
このサイトについて
TrackBack URL :

