CLOVER🍀

That was when it all began.

バイトコード操作ツール、Bytemanを試す

前々から気になっていたのと、少し前に購入したこちらの本にも紹介されていたので、JBoss ProjectsのBytemanを試してみることにしました。

JBoss Enterprise Application Platform6 構築・運用パーフェクトガイド

JBoss Enterprise Application Platform6 構築・運用パーフェクトガイド

そもそもBytemanってなんですか?ってところですが、javaagentを使用してバイトコードの変更を行うツールです。これを利用して、アプリケーションの動作を変更したりできます。

Byteman
https://www.jboss.org/byteman

使い方としては、

  • 対象のJavaアプリケーションにByteman用の設定を仕込んで起動する
  • 起動済みのJavaアプリケーションに、Bytemanが提供するスクリプトを使ってアタッチする

の2つがあり、どのような処理を入れるかは、Bytemanが提供するルールに沿って記述する必要があります。

インストール

まずはこちらからダウンロード。

https://www.jboss.org/byteman/downloads

現時点での最新バージョンは、2.1.3です。

とりあえず、展開します。

$ unzip byteman-download-2.1.3-bin.zip

最低限は、ここまで。なお、スクリプトでアタッチをかける場合には、インストールディレクトリに対して環境変数BYTEMAN_HOMEを設定しておく必要があります。

$ export BYTEMAN_HOME=/path/to/byteman-download-2.1.3

ルールを書く

バイトコード操作を行うためには、ルールを書く必要があります。おおまかには、

RULE [ルール名]
CLASS(またはINTERFACE) [クラス名またはインターフェース名]
METHOD [メソッド名]
[適用タイミング]
IF [ルールを実行する条件]
DO [実行されるアクション]
ENDRULE

端折ってるところもありますが、大まかにはこんな感じです。1ファイル中に、複数のルールを記述することもできます。コメントは、「#」で始めて書きます。

ルールファイルの拡張子は、「.btm」とするようです。

最終的には、このあたりのドキュメントにいきつくことになると思います…。

https://community.jboss.org/wiki/ABytemanTutorial#top
http://downloads.jboss.org/byteman/2.1.3/ProgrammersGuideSinglePage.2.1.3.1.html

Javaプログラムにルールを適用する

2つの適用方法を、それぞれ紹介します。

Javaアプリケーションの起動時に適用する

javaコマンドのオプションに、javaagentとかを設定します。

$ java -javaagent:$BYTEMAN_HOME/lib/byteman.jar=script:[ルールファイル] [メインクラス名]

もしくは

$ java -javaagent:$BYTEMAN_HOME/lib/byteman.jar=script:[ルールファイル],boot:$BYTEMAN_HOME/lib/byteman.jar
-Dorg.jboss.byteman.transform.all [メインクラス名]

下の場合は、Javaが提供するクラスにアタッチする場合に使います。

なお、この方法で実行する場合は、環境変数BYTEMAN_HOMEは必須ではありません。

起動中のアプリケーションにスクリプトで適用する

こちらでは、環境変数BYTEMAN_HOMEの設定は必須となります。

まずは、アタッチするJavaアプリケーションのPIDを特定します。

$ jps

特定したら、そのPIDに対してbminstall.shを実行します。

$ $BYTEMAN_HOME/bin/bminstall.sh [PID]

ルールファイルの適用には、bmsubmit.shを「-l」オプションで使用します。

$ $BYTEMAN_HOME/bin/bmsubmit.sh -l [ルールファイル]
install rule [ルール名]

ルールをアンロードするには、bmsubmit.shを「-u」オプションで使用します。

$ $BYTEMAN_HOME/bin/bmsubmit.sh -u [ルールファイル名]
uninstall RULE [ルール名]

「-b」オプションでbootstrap classpathに加えることができ、「-Dname=value」でシステムプロパティを設定できるようです。

.batファイルもあるので、Windowsでも使えそうな感じ?

使ってみる

では、実際に使ってみましょう。スケープゴートとなっていただくために、このようなクラスを用意しました。

// sample/MainClass.java 
package sample;

public class MainClass {
    public static void main(String[] args) {
        while (true) {
            execute();
        }
    }

    private static void execute() {
        SimpleImpl si = new SimpleImpl();
        si.echo();
        si.echo("Message From Main");
        System.out.println("count = " + si.getCount());
        System.out.println("totalCount = " + si.getTotalCount());
        System.out.println("myMethod = " + si.myMethod("Call MyMethod"));

        try { si.throwException(); } catch (Exception e) { }

        try {
            Thread.sleep(3 * 1000L);
        } catch (InterruptedException e) { }
    }
}

// sample/SimpleInterface.java 
package sample;

public interface SimpleInterface {
    void echo();

    void echo(String message);
}

// sample/SimpleImpl.java 
package sample;

public class SimpleImpl implements SimpleInterface {
    private static int totalCount = 0;

    private int count = 0;

    public String myMethod(String message) {
        return "***" + message + "***";
    }

    public void echo() {
        System.out.println("Default Message");
        count++;
        totalCount++;
    }

    public void echo(String message) {
        System.out.println("Echo = " + message);
        count++;
        totalCount++;

        int localCount = count;
        int localCount2 = localCount;
    }

    public int getCount() {
        return count;
    }

    public static int getTotalCount() {
        return totalCount;
    }

    public void throwException() {
        throw new RuntimeException("Oops!!");
    }
}

ずっと起動したまま、無限ループし続けるプログラムです。しれっとインターフェースも入れています。

では、これに実際にルールを適用してみます。

いきなりルール2つですが、こんなのを書いてみます。
sample.btm

# コメントは、「#」で記述

RULE trace entry getCount
CLASS sample.SimpleImpl
METHOD getCount
AT ENTRY
IF TRUE
  DO traceln("========== called getCount =========="), traceStack()
ENDRULE

# ルール名を分ければ、複数ルールは記述可能

RULE trace entry getTotalCount
CLASS sample.SimpleImpl
METHOD getTotalCount
AT EXIT
IF TRUE
  DO traceln("========== called getTotalCount =========="), traceStack()
ENDRULE

それぞれ、SimpleSimple#getCountの呼び出し時とgetTotalCountの終了時にprintlnして、スタックトレースを出力します。

普通に実行すると

$ java sample.MainClass
Default Message
Echo = Message From Main
count = 2
totalCount = 2
myMethod = ***Call MyMethod***
Default Message
Echo = Message From Main
count = 2
totalCount = 4
myMethod = ***Call MyMethod***

みたいな結果になりますが、Bytemanを使ってこのルールを適用すると

$ java -javaagent:$BYTEMAN_HOME/lib/byteman.jar=script:sample.btboot:$BYTEMAN_HOME/lib/byteman.jar -Dorg.jboss.byteman.transform.all sample.MainClass
Default Message
Echo = Message From Main
========== called getCount ==========
Stack trace for thread main
sample.SimpleImpl.getCount(SimpleImpl.java:-1)
sample.MainClass.execute(MainClass.java:14)
sample.MainClass.main(MainClass.java:6)
count = 2
========== called getTotalCount ==========
Stack trace for thread main
sample.SimpleImpl.getTotalCount(SimpleImpl.java:32)
sample.MainClass.execute(MainClass.java:15)
sample.MainClass.main(MainClass.java:6)
totalCount = 2
myMethod = ***Call MyMethod***
Default Message
Echo = Message From Main
========== called getCount ==========
Stack trace for thread main
sample.SimpleImpl.getCount(SimpleImpl.java:-1)
sample.MainClass.execute(MainClass.java:14)
sample.MainClass.main(MainClass.java:6)
count = 2
========== called getTotalCount ==========
Stack trace for thread main
sample.SimpleImpl.getTotalCount(SimpleImpl.java:32)
sample.MainClass.execute(MainClass.java:15)
sample.MainClass.main(MainClass.java:6)
totalCount = 4
myMethod = ***Call MyMethod***

のように、コンソールにメッセージとスタックトレースの出力が追加されます。

もちろん、

$ java sample.MainClass

で実行した後に

## PID特定
$ jps
22913 MainClass
22928 Jps

## アタッチ
$ $BYTEMAN_HOME/bin/bminstall.sh 22913

## ルールインストール
$ $BYTEMAN_HOME/bin/bmsubmit.sh -l sample.btm
install rule trace entry getCount
install rule trace entry getTotalCount

でもいいですからね。

では、いくつかルールのサンプルを記載して終わりにしますね。

オーバーロードを含む特定のメソッドに対して、ルールを適用する

メソッド名の後に()を付けないと、同じ名前のメソッドに対してルールが適用されます。

RULE trace all echo
CLASS sample.SimpleImpl
# ()を省略した場合は、引数有り無しのすべてのメソッドに適用される
# ちなみに、コンストラクタに対して適用したい場合は「<init> 」で指定
METHOD echo
AT ENTRY
IF TRUE
  DO traceln("========== call echo method START ==========")
ENDRULE
メソッドの引数を指定して、ルールを適用する

メソッド名の後に()を付けて、その中に引数の型を指定(もしくは書かずに引数なしのメソッドを指定)します。

RULE trace param echo
CLASS sample.SimpleImpl
METHOD echo(String)
# $0は自分自身(this)、$1以降でメソッド呼び出しパラメータ
BIND this = $0, param = $1
AT ENTRY
IF TRUE
  DO traceln("========== this[" + this.getClass().getName() + "] call echo method[" + param + "] START ==========")

# BINDせずに、いきなり$0や$1などを使ってもOK
#  DO traceln("========== this[" + $0.getClass().getName() + "] call echo method[" + $1 + "] START ==========")
ENDRULE

ここでは、BINDを使った例を書いています。$0がthisで、$1から始まる$Nはメソッドの引数を表します。もちろん、BIND自体はメソッドの引数を使う場合に限ったことではありません。

メソッドの戻り値を取得する

$!でメソッドの戻り値をキャプチャすることができます。ちなみに、「RETURN」はEXITのシノニムらしいです。まあ、いずれもメソッドの終了時ですね。

RULE trace method return
CLASS sample.SimpleImpl
METHOD getCount()
AT RETURN
IF TRUE
  # $!でメソッドの戻り値
  DO traceln("========== getCount Return Value[" + $! + "] ==========")
ENDRULE
インターフェースに対して、ルールを適用する

CLASSキーワードではなくて、INTERFACEキーワードでインターフェース名を指定することで、インターフェースの実装クラスに対してルールを適用することができます。

RULE trace interface
INTERFACE sample.SimpleInterface
METHOD echo()
AT ENTRY
IF TRUE
  DO traceln("========== call Interface echo method START ==========")
ENDRULE
メソッドから例外がスローされた時に、ルールを適用する

AT THROWで例外スロー時、$^でスローされた例外を使うことができます。

RULE trace throw exception
CLASS sample.SimpleImpl
METHOD throwException
AT THROW
IF TRUE
  # $^で投げられた例外
  DO traceln("========== in throwException() e = [" + $^ + "] =========="), traceStack()
ENDRULE

今回の例では、例外を握りつぶしている行儀の悪いコードなので、スタックトレースを出力しています。

フィールドの読み書き

「AT READ」や「AFTER READ」でフィールドを読む時や読んだ後、

RULE trace field read
CLASS sample.SimpleImpl
METHOD echo
AT READ count
IF TRUE
  DO traceln("========== in echo() read count[" + $0.count + "] ==========")
ENDRULE

「AT WRITE」や「AFTER WRITE」でフィールドに書き込む時や、書いた後

RULE trace field write
CLASS sample.SimpleImpl
METHOD echo
AFTER WRITE count
IF TRUE
  DO traceln("========== in echo() writed count[" + $0.count + "] ==========")
ENDRULE

それぞれに対して、ルールを適用できます。

staticフィールドに対して適用する

実は、フィールドと一緒です。

RULE trace static field read
CLASS sample.SimpleImpl
METHOD echo
AFTER WRITE totalCount
IF TRUE
  DO traceln("========== in all echo totalCount writed = [" + sample.SimpleImpl.totalCount + "] ==========")
ENDRULE
ローカル変数に対して、適用する

条件付きで、「AT READ」や「AFTER WRITE」などをローカル変数に対しても適用することができます。

条件は、コンパイル時に「-g」オプションを指定するか、

$ javac -g [ソースファイル]

もしくは、「-g」オプションに少なくとも「vars」を加えておく必要があります。

$ javac -g:vars [ソースファイル]

ルールのサンプル。

# ローカル変数を使うには、
#「javac -g」もしくは「javac -g:vars」を含めてコンパイルしておく必要がある
RULE trace local var write
CLASS sample.SimpleImpl
METHOD echo(String)
# $ローカル変数名で参照可能
AFTER READ $localCount
IF TRUE
  # $ローカル変数名で参照可能
  DO traceln("========== in echo(String) writed localVar[" + $localCount + "] ==========")
ENDRULE

ローカル変数は、「$ローカル変数名」で参照することができます。

実際使ってみて、やっぱりなかなか面白かったです。機会を見つけて、活用していこうと思います。