BCEL を使い倒す

Copyright (C) 1997-2004 by Haruaki TAMADA All rights reserved.
Last Modified: Mon Jan 10 11:07:28 JST 2005

目次

ページのトップへ

はじめに

BCEL とは

BCEL たぁ(typo だがそのままにしとこ)、Byte Code Engineering Library の略で Java のクラスファイルをいじくることのできるライブラリです。 クラスファイルを読み込んで、ちょいといじくって、またクラスファイルに書き出すことも簡単にできます。

また、逆コンパイラやアセンブラなど BCEL が使われているライブラリはたくさんあります。 BCEL が提供している機能を自分で作ろうとすると確実に死ねるので、 バイトコードをいじくる際には BCEL を使いましょう。

BCEL を使い倒すと、バイトコードから nop(何もしないコード) を削除するなんてことは 朝飯前ですし、クラス名を public にしちゃうなんてこともできてしまいます。

ただ、Java バイトコードがわからないとにっちもさっちもいかなくなります。 クラスファイルの構造など、JVM Specification は読んで理解しておかないと、 何もできないのが問題点でしょう。

しかし、それさえわかってしまえばクラス名を「 」(半角スペース)にすることも できてしまうのです。(クラスファイルの仕様上、これは正しい。Java 言語の仕様としては間違っている)

このページについて

このページは Jakarta BCEL を使い倒す方法について書きます。 BCEL のインストール方法など初歩的なことや、 バイトコードの詳細については他のページや仕様、書籍を御覧下さい。

私のリンクポリシーは トップページ に載せていますので、 そちらを御覧下さい。

ページのトップへ

まずは基本

クラスファイルを読み込む

基本中の基本、BCEL での基本型、JavaClass を得る方法です。 この JavaClass クラスは java.lang.Class よりも もっと低レベルな情報を得ることができます。 BCEL を使う時には一番最初に取得するクラスです。 この JavaClass を取得できないと BCEL では何もできないと思って良いでしょう。

そんな JavaClass クラスですが、取得方法はいくつかあります。 それをこれから説明します。

一番簡単な方法

まずは一番簡単な方法です。読みたいクラスが実行時のクラスパスに含まれている場合にこの方法が使えます。 以下のようにやれば取得できます。

JavaClass javaClass = Repository.lookupClass("java.lang.Object");

これだけです。以下のようにもできます。

JavaClass javaClass = Repository.lookupClass(Class.forName("java.lang.System"))

ここで間違えてはいけないのは、この Repositoryorg.apache.bcel.Repository で、 org.apache.bcel.util.Repository ではないということです。

これさえ気を付ければ何の問題もなく、JavaClass を取得することができます。

取得したいクラスがクラスパスに含まれていない場合

では次に、取得したい JavaClass がクラスパスに含まれていない場合はどうしましょう。 アプリケーション実行時に java.lang.Class オブジェクトが取得できない場合です。

実はそれも特に問題なく JavaClass オブジェクトを取得することができます。 BCEL には ClassParser なるいたれりつくせりのユーティリティがあるのです。

この ClassParserInputStream やファイルからクラスファイルを読み込むためのユーティリティです。 以下のようにして ClassParser により JavaClass を構築することができます。

try{
    ClassParser parser = new ClassParser(new FileInputStream("hoge.class"));
    JavaClass javaClass = parser.parse();
} catch(ClassFormatException e){
    e.printStackTrace();
}

以上の処理でクラスファイルがどこにあっても JavaClass を構築することができます。

クラス名を変更する

クラス名を変更するために、BCEL の API をつらつらと見ていくと、 JavaClasssetClassName というメソッドを見つけるでしょう。

しかし、このメソッドは JavaClass のフィールドの値を変更するだけです。 setClassName でクラス名を変更した後、dump メソッドでクラスを書き出したとしても、変更されません。 この辺りが BCEL の困った点ですが、まぁ、仕方ありません。 別の方法があります。

実はクラス名は constant_pool(仕様上の表現) と呼ばれる部分に格納されています。 だから、JavaClass から getConstantPoolconstant_pool を取り出して、その中からクラス名を表すものを取り出して、 それを書き換えればいいのですが、メンドウです。

その辺りのことをやってくれるクラスが存在します。ClassGen です。 クラスの情報を書き換えるには JavaClass ではなく、 この ClassGen を使います。ちなみに、org.apache.bcel.generic パッケージに含まれています。

さて、ClassGen の取得方法ですが、既に JavaClass オブジェクトを手に入れていれば簡単です。

ClassGen gen = new ClassGen(javaClass);

で取得できます。 この org.apache.bcel.generic パッケージにはクラスの情報を書き換えるためのクラスが含まれています。 org.apache.bcel.classfile パッケージはクラスの情報を取得するためのクラス、 org.apache.bcel.generic パッケージにはクラスの情報を変更するためのクラス、 と覚えておけば良いでしょう。

さて、実際の処理です。

import java.io.*;

import org.apache.bcel.*;
import org.apache.bcel.classfile.*;
import org.apache.bcel.generic.*;

public class ChangeClassName{
    public static void main(String[] args) throws Exception{
        JavaClass jc = Repository.lookupClass(Class.forName(args[0]));
        ClassGen c = new ClassGen(jc);
        c.setClassName(args[1]);
        jc = c.getJavaClass();
        jc.dump(new File(args[1] + ".class"));
    }
}

まず第一引数で 変更前のクラス名 を取得し、 それを第二引数の 変更後のクラス名 に変更します。 その後、第二引数の 変更後のクラス名 に .class という拡張子を与え、 ファイルに書き出しています。

クラス名を変更する処理は ClassGen、ファイルに書き出す処理は JavaClass なので、上記の様に JavaClass を得た後、 ClassGen を作成し、クラス名を変更してから、また JavaClass を得ています。

こんな数行でクラス名を変更できます。しかも、変更後に書き出されたクラスはコンパイルを通していないので、 コンパイル時に弾かれる、Java 言語仕様にのっとっていない名称にすることも可能です。 例えば、クラス名を 1 にすることも可能です。 Java 言語仕様上正しくない、この 1 というクラスでも問題なく動作します。 もちろん、Java 言語仕様上正しくないので、JVM によっては動かない可能性もあることはあります。 実際にそのような名前にする場合はチェックが必須ですが、Sun の JVM では動くので、 ほとんどの場合は大丈夫でしょう。

上記のソースをコンパイルしたクラスファイルの名前を 1 に変更した物は 1.class からダウンロードできます。 以下のような結果になります。

$ javac -classpath bcel-5.1.jar ChangeClassName.java
$ java -classpath bcel-5.1.jar:. ChangeClassName ChangeClassName 1
$ java -classpath bcel-5.1.jar:. 1 ChangeClassName a
$ javap -classpath bcel-5.1.jar:. 1
Compiled from ChangeClassName.java
public class 1 extends java.lang.Object {
    public 1();
    public static void main(java.lang.String[]) throws java.lang.Exception;
}

デバッグ情報を削除する

と書いてしまっても何がデバッグ情報なのかどうかを説明しなければなりません。 何も知らないとデバッグ情報が残ったままであるとか、必要なものも消してしまうということが起こるかもしれません。 なので、最初にデバッグ情報の種類を説明し、それがクラスファイル中のどこに含まれているかを説明した上で、 削除する方法を説明します。

種類

クラスファイルには以下の 4 つのデバッグ情報を含めることができます。

ソースファイル情報はどのようなソースファイルからコンパイルされたかという情報が含まれます。 javap した時に出力される一番上の行からこれがわかります。 また、スタックトレース表示の時にもソースファイル名が表示されます。 この情報は実行時には必要ないので、削除することができます。

行番号は主にスタックトレースで使われます。J2SDK 1.4 移行は java.lang.StackTraceElement を使って例外が投げられた行番号を得ることも可能になっています。 この情報も実行時には必要ありません。削除することができます。 ただし、削除してしまうと java.lang.StackTraceElement を使って得た行番号で何か処理をするようなプログラムの場合、例外が発生する場合がありますし、 スタックトレースが表示されても行番号というデバッグ情報を得ることはできません。

実は Java はクラス変数やインスタンス変数の名前をクラスファイル中に持っています。 このクラス変数、インスタンス変数の名前をクラスファイルから削除してしまうと、 クラスファイルのフォーマットから外れてしまい、正常にクラスがロードできなくなります。 対してローカル変数名は実行時には番号で管理されるので、消すことができます。 この情報は上記 2 つのデバッグ情報と異なり、普通に使っている限りは表に出てきません。 自らクラスファイルを書き換えるような場合にしか使われないので、デフォルトではクラスファイルに含まれず、 この情報をクラスファイルに含めるにはコンパイル時に明示的に指定する必要があります。

推奨されないメソッドの情報は実はデバッグ情報ではありません。 しかし、コンパイル時に

推奨されないメソッドを使っています

云々と言われたことはありませんか? これを表示するためのもので、実行時には必要ありません。 これを付けるためにはメソッドの javadoc コメントで以下のように指定します。

/**
 * @deprecated このメソッドは使われていません。hogeHoge に置き換えられました
 */
public void deprecatedMethod(){
    ....
}

のように使われます。javac はこの deprecated javadoc タグのみを特別に読みとり推奨されないメソッドとしてマークを付けたクラスファイルを作成します。

上記のような 4 種類のデバッグ情報が存在しますが、どれも実行時には必要ないもので、 極力クラスファイルのサイズを小さくしたい場合には削除することができます。

デバッグ情報を含んだクラスファイルの作り方

デバッグ情報を削除するにはまずデバッグ情報を含んだクラスファイルを作成する必要があります。 また、普通にコンパイルしただけではローカル変数名のデバッグ情報を含むクラスファイルは作成されません。 これを作るにはどうするか、というのが最初の課題です。

答えは簡単、コンパイル時に -g オプションを付けます。

$ javac -g SomeClass.java

とすれば良いのです。簡単ですね。Apache Ant を使ってコンパイルしている場合は、javac タスクに debug="yes" を付け加えることで 3 つのデバッグ情報が含まれたクラスファイルが作成されます。 まぁ、Apache Ant の場合は debug="no" としていると、 全くデバッグ情報が付け加えられないんで、debug="yes" としている人が多いでしょうが。

そして、javac は便利なオプションが付いています。-g:{keyword list} というオプションです。 keyword list にコンマ区切りで source, lines, vars のどれかを指定することで、それぞれソースファイル名、行番号、 ローカル変数名デバッグ情報を含むクラスファイルが作成されます。

$ javac -g:source,lines SomeClass.java

とするとソースファイル名、行番号のデバッグ情報のみを含むクラスファイルが、

$ javac -g:vars,source SomeClass.java

とするとローカル変数名、ソースファイル名のデバッグ情報のみを含むクラスファイルが作成されます。

また、コンパイル時に明示的にデバッグ情報を含めなくすることもできます。

$ javac -g:none SomeClass.java

とすればデバッグ情報を全く含まないクラスファイルが出来上がります。 これでこのセクションの目的は達成されてしまうのですが、 一応 BCEL の使い方としてデバッグ情報を含むクラスファイルから削除するということで、続きます。

デバッグ情報のありか

ソースファイル情報

さて、クラスファイル中のどこにデバッグ情報が収まっているのかを知らなければ削除することはできません。 その辺りが自動化できるほど BCEL は万能ではありません。

まず、ソースファイル情報ですが、これは constant_pool に含まれています。 また、constant_pool 内のエントリを指すために Attribute という属性が一つ含まれています。 この 2 つの情報を消せばソースファイル情報を削除することができます。 と言いたいところですが、もう一つ消す情報があります。

Attribute はクラスの情報を収めている場所で、 ソースファイル情報の他にも推奨されないメソッドやフィールドの情報や定数値の情報などが格納されています。 それらの種類を区別するための文字列も constant_pool 内に含まれているのです。 なので、ソースファイル情報を完全に削除するには、ソースファイル情報であると識別するために constant_pool 内に含まれている文字列も削除する必要があります。

行番号

力尽きた・・・。 続きはまた今度。

ローカル変数名
推奨されないメソッド

ページのトップへ