トップ » 技術記事 » Javaアセンブラ「Jasmin」でバイトコードの世界を覗いてみよう(3) - 「Jasmin」で制御構文を操る

Javaアセンブラ「Jasmin」でバイトコードの世界を覗いてみよう(3) - 「Jasmin」で制御構文を操る はてなブックマーク数 このエントリーをブックマークに追加

Javaバイトコードについて知ることで、Javaについてより深く学ぶことができるはずです。アセンブラの Jasmin を使って実際にバイトコードに近いアセンブラを書いてみます。今回は、制御構造について紹介します。

前回のまでのおさらい~Jasmin で Hello, World

Jasminは、Javaのバイトコードに近いコードを記述することでJavaのクラスファイルを生成することのできるコンパイラです。以下のサイトより入手することができます。

プログラムを「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"が表示されます。

http://aoikujira.com/demo/hakkaku/rc/20090315aJYI01-OneTwo.png
このプログラムのポイント(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 を使って、独自言語などを作る方法も紹介しようと思いますので、お楽しみに。

Series Navigation«「Jasmin」でプログラミング

このサイトについて

八角研究所
株式会社八角研究所のWEBサイトですよー。 いろんなものを創り出すことのできる環境をコツコツ構築中。 いったい、いつになったらできるのか。 この技術情報サイトもそのための活動の一環のつもり。

執筆者紹介

クジラ飛行机

クジラ飛行机

くじらはんど(http://kujirahand/)にて、日本語プログラミング言語「なでしこ」(IPA未踏ユース採択)、テキスト音楽「サクラ」(OSPオンラインソフト大賞入賞)など多くのオンラインソフトを開発。著書に「Flexプロフェッショナルガイド」「なでしこ公式バイブル」、「一週間でマスターするActionScript3.0」など。

TrackBack URL :