kagamihogeの日記

kagamihogeの日記です。

ClassLoader#defineClassでクラスのインスタンスを作ってみる

Java Advent Calendar 2011 : ATND 用のエントリです。今回の個人テーマは、使ったことのない無いJavaAPIを使ってみる、です。

java.lang.ClassLoaderを見るとdefineClass(String name, byte[] b, int off, int len)というメソッドがあります。javadocによると「バイトの配列を Class クラスのインスタンスに変換します。」とある。これはちょっと面白そうなので、少し試してみることにします。

まずは適当なクラスを用意します。

package classloadertest;

public class Hoge{
    public int getHogeMethod() {
        return 10;
    }
}

コンパイルしたあとバイナリエディタ*1でHoge.classファイルを覗いてみます。下図のうち、赤線で囲んだアドレスはあとで使います。

クラスファイルの構造や意味とかはJava仮想マシン仕様などに譲るとしてともかく、なんとなーく「このようにしてクラスファイルは出来てるのだ」という雰囲気は分かります。

次に、上図の内容をJavaのクラスの中でバイト配列として定数値で埋め込んでしまい、そのバイト配列からクラスを作る、ということをやってみます。

public class HogeMain {
    public static void main(String[] args) throws Exception {
        HogeClassLoader cl = new HogeClassLoader();// A
        Class hogeClass = cl.getHogeClass();// B
        Object hogeInstance = hogeClass.newInstance();//C
        Method hogeMethod = hogeClass.getMethod("getHogeMethod", null);//D
        System.out.println(hogeMethod.invoke(hogeInstance, null));
    }

    public static class HogeClassLoader extends ClassLoader {
        final int[] hogeByte = {
            0xCA,0xFE,0xBA,0xBE, 0x0, 0x0, 0x0,0x31, 0x0,0x12, 0x7, 0x0, 0x2, 0x1, 0x0,0x14,
            0x63,0x6C,0x61,0x73,0x73,0x6C,0x6F,0x61,0x64,0x65,0x72,0x74,0x65,0x73,0x74,0x2F,
            0x48,0x6F,0x67,0x65, 0x7, 0x0, 0x4, 0x1, 0x0,0x10,0x6A,0x61,0x76,0x61,0x2F,0x6C,
            0x61,0x6E,0x67,0x2F,0x4F,0x62,0x6A,0x65,0x63,0x74, 0x1, 0x0, 0x6,0x3C,0x69,0x6E,
            0x69,0x74,0x3E, 0x1, 0x0, 0x3,0x28,0x29,0x56, 0x1, 0x0, 0x4,0x43,0x6F,0x64,0x65,
             0xA, 0x0, 0x3, 0x0, 0x9, 0xC, 0x0, 0x5, 0x0, 0x6, 0x1, 0x0, 0xF,0x4C,0x69,0x6E,
            0x65,0x4E,0x75,0x6D,0x62,0x65,0x72,0x54,0x61,0x62,0x6C,0x65, 0x1, 0x0,0x12,0x4C,
            0x6F,0x63,0x61,0x6C,0x56,0x61,0x72,0x69,0x61,0x62,0x6C,0x65,0x54,0x61,0x62,0x6C,
            0x65, 0x1, 0x0, 0x4,0x74,0x68,0x69,0x73, 0x1, 0x0,0x16,0x4C,0x63,0x6C,0x61,0x73,
            0x73,0x6C,0x6F,0x61,0x64,0x65,0x72,0x74,0x65,0x73,0x74,0x2F,0x48,0x6F,0x67,0x65,
            0x3B, 0x1, 0x0, 0xD,0x67,0x65,0x74,0x48,0x6F,0x67,0x65,0x4D,0x65,0x74,0x68,0x6F,
            0x64, 0x1, 0x0, 0x3,0x28,0x29,0x49, 0x1, 0x0, 0xA,0x53,0x6F,0x75,0x72,0x63,0x65,
            0x46,0x69,0x6C,0x65, 0x1, 0x0, 0x9,0x48,0x6F,0x67,0x65,0x2E,0x6A,0x61,0x76,0x61,
             0x0,0x21, 0x0, 0x1, 0x0, 0x3, 0x0, 0x0, 0x0, 0x0, 0x0, 0x2, 0x0, 0x1, 0x0, 0x5,
             0x0, 0x6, 0x0, 0x1, 0x0, 0x7, 0x0, 0x0, 0x0,0x2F, 0x0, 0x1, 0x0, 0x1, 0x0, 0x0,
             0x0, 0x5,0x2A,0xB7, 0x0, 0x8,0xB1, 0x0, 0x0, 0x0, 0x2, 0x0, 0xA, 0x0, 0x0, 0x0,
             0x6, 0x0, 0x1, 0x0, 0x0, 0x0, 0x3, 0x0, 0xB, 0x0, 0x0, 0x0, 0xC, 0x0, 0x1, 0x0,
             0x0, 0x0, 0x5, 0x0, 0xC, 0x0, 0xD, 0x0, 0x0, 0x0, 0x1, 0x0, 0xE, 0x0, 0xF, 0x0,
             0x1, 0x0, 0x7, 0x0, 0x0, 0x0,0x2D, 0x0, 0x1, 0x0, 0x1, 0x0, 0x0, 0x0, 0x3,0x10,
             0xA,0xAC, 0x0, 0x0, 0x0, 0x2, 0x0, 0xA, 0x0, 0x0, 0x0, 0x6, 0x0, 0x1, 0x0, 0x0,
             0x0, 0x5, 0x0, 0xB, 0x0, 0x0, 0x0, 0xC, 0x0, 0x1, 0x0, 0x0, 0x0, 0x3, 0x0, 0xC,
             0x0, 0xD, 0x0, 0x0, 0x0, 0x1, 0x0,0x10, 0x0, 0x0, 0x0, 0x2, 0x0,0x11
        };
        
        public Class getHogeClass() {
            byte[] bytes = new byte[hogeByte.length];
            for (int i=0; i<bytes.length; i++) {
                bytes[i] = (byte)hogeByte[i];
            }
            return defineClass(null, bytes, 0, bytes.length);//B
        }
    }
}

*2

これを動かすと「10」と表示されます。動きとしては、まず自前のクラスローダを作り(A.、HogeのClassクラスのインスタンスがバイト配列から作られて(B.、そこからnewInstanceでインスタンスが生成され(C.、最後にgetHogeMethodメソッドを呼び出しています(D.

ここではクラスの中身を静的に持っていますが、ネットワークなりファイルなり何らかのストリームから、Javaのクラスとして成立するバイト列を持ってこられれれば、動的にクラスを作れるんだな、ということがわかります*3


コレで終わるのも勿体無いので、もうちょっと遊んでみます。

getHogeMethodメソッドの返り値が「10」なのを別の数値を返すように変更することを考えます。ここではクラスの構造は何も知らないものとして、最も原始的なアプローチでいきます。すなわち総当り式。値が10(0x0A)となってるところは全部で5箇所(上図の赤線で囲んだアドレス)あるので、このうちのどれかが返り値の10だろうと仮定します。よって、あるアドレスの値が10になっているのを別の値に変えたバイト列からクラスを作ろうとしたとき、動作がどのようになるか、試してみます。

000050番地 0x0A→0xFFにしてみる。


java.lang.ClassFormatError: Unknown constant tag 255 in class file

クラスファイルが壊れているか解釈できずに読み込むことが出来ないことを示すOracle Technology Network for Java Developers | Oracle Technology Network | Oracleになりました。命令コードが並んでそうなところをテキトーに変えてるので、まぁなんとなく理解できる動きです。

0000B9番地 0x0A→0xFFにしてみる。


java.lang.ClassFormatError: Truncated class file

エラーメッセージ的に、クラスファイルが切り詰められちゃったから実行できません、といったところでしょうか。ちなみに値を0xFFを0x0BにするとエラーがUnknown constant tagになる。0xFF(-1)はEOFだからということなんでしょう。

0000FC番地 0x0A→0xFFにしてみる。


ava.lang.ClassFormatError: Invalid code attribute name index 255 in class file classloadertest/Hoge

index 255は不正なコードだとかなんとかいうエラーになった。どっかへのポインタにでもなってるんですかね? せっかくなので0xFFにしたところを0x0Bに変えてみる。


java.lang.ClassFormatError: LocalVariableTable has wrong length in class file classloadertest/Hoge

LocalVariableTableの長さがおかしい、とかなんとか。まぁどちらにせよgetHogeMethodの返り値がこの番地に入ってるわけでは無い。

000130番地 0x0A→0xFFにしてみる。

これはまともに動いて「-1(0xFF)」が表示される。ここが「10」の置いてある場所ということですね。値を0x0Bに変えると、予想通り「11」と表示される。

最後の137番地を0x0A→0xFFは、やっぱりLocalVariableTable has wrong lengthとエラーになりました。

おわり

というわけでこのエントリでは、

  • ClassLoader#defineClassにバイト配列を渡すと、クラスのインスタンスが作れる。
  • 意図的に構造を崩したクラスを実行することで、壊れたときの動作の一例を見る。

ことができました。

*1:http://www2.lint.ne.jp/~lrc/ed_binary.htm Binary Editor Bzを使用

*2:最初からbyte[]で定義しないのは、リテラルだとintになるので(byte)0xCA,(byte)0xFE...とキャストで見た目がよろしくなくなるため

*3:実際には、javadocに書いてある通り「ネットワークからクラスをロードするために findClass メソッドと loadClassData メソッドを定義」というルールに従う必要があると思われる