水曜日, 23 2 月 2005

Cojenでバイトコード解析
この記事をクリップ!
このエントリーをはてなブックマークに追加

« 視力 | Main | サプリメント依存症 »
Javaで書かれたソースはコンパイルするとバイトコードと呼ばれるプラットホーム非依存の中間コードに変換される。
そのバイトコードを容易に解析、再構築するためのライブラリがCojenだ。
まずは解析するためのソースを用意する。今回用意したのは以下ような簡単なもの。
public class test{
  public static void main(String[] argv){
    test t=new test();
    t.print();
  }

  public test(){
  }

  public void print(){
    System.out.println("hello");
  }
}
これを普通にコンパイルする。
$ javac test.java
生成されたtest.classをCojenのディスアセンブラに通してみる。
$ java -cp cojen-1.1.0.jar:. cojen.classfile.DisassemblyTool test
すると以下のような出力が得られる。
/**
 * Disassembled on Wed Feb 23 01:53:47 JST 2005.
 * 
 * @target 1.2
 * @source test.java
 */
public class test extends java.lang.Object {
  public static void main(java.lang.String[]) {
    // max stack: 2
    // max locals: 1
    // line 5
    new test
    dup
    invokespecial void test.()
    astore_0
    // line 6
    aload_0
    invokevirtual void test.print()
    // line 7
    return
  }

  public void () {
    // max stack: 1
    // max locals: 1
    // line 10
    aload_0
    invokespecial void java.lang.Object.()
    // line 11
    return
  }

  public void print() {
    // max stack: 2
    // max locals: 1
    // line 15
    getstatic java.io.PrintStream java.lang.System.out
    ldc "hello"
    invokevirtual void java.io.PrintStream.println(java.lang.String)
    // line 16
    return
  }
}
上記の出力だけではjavapを使用して得られたバイトコードとほとんど変わらないが、以下の-f builderオプションを付けて実行するとクラスファイルを生成するためのJavaコードが得られる。
$ java -cp cojen-1.1.0.jar:. cojen.classfile.DisassemblyTool -f builder test
出力は以下の通り。
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.OutputStream;

import cojen.classfile.ClassFile;
import cojen.classfile.CodeBuilder;
import cojen.classfile.FieldInfo;
import cojen.classfile.Label;
import cojen.classfile.LocalVariable;
import cojen.classfile.Location;
import cojen.classfile.MethodInfo;
import cojen.classfile.Modifiers;
import cojen.classfile.Opcode;
import cojen.classfile.TypeDesc;

/**
 * Builds ClassFile for test
 *
 * @author auto-generated
 */
public class ClassFileBuilder {
  public static void main(String[] args) throws Exception {
    // public class test
    ClassFile cf = createClassFile();

    if (args.length > 0) {
      File file = new File(args[0]);
      if (file.isDirectory()) {
        writeClassFiles(cf, file);
      } else {
        OutputStream out = new BufferedOutputStream(new FileOutputStream(file));
        cf.writeTo(out);
        out.close();
      }
    }
  }

  private static void writeClassFiles(ClassFile cf, File dir) throws Exception {
    File file = new File(dir, cf.getClassName().replace('.', '/') + ".class");
    file.getParentFile().mkdirs();
    OutputStream out = new BufferedOutputStream(new FileOutputStream(file));
    cf.writeTo(out);
    out.close();

    ClassFile[] innerClasses = cf.getInnerClasses();
    for (int i=0; i<innerClasses.length; i++) {
      writeClassFiles(innerClasses[i], dir);
    }
  }

  public static ClassFile createClassFile() {
    ClassFile cf = new ClassFile("test", "java.lang.Object");
    cf.setTarget("1.2");
    cf.setSourceFile("test.java");

    //
    // Create constructors
    //

    // public void <init>()
    createConstructor_1(cf);

    //
    // Create methods
    //

    // public static void main(java.lang.String[])
    createMethod_1(cf);

    // public void print()
    createMethod_2(cf);

    return cf;
  }

  // public void <init>()
  private static void createConstructor_1(ClassFile cf) {
    MethodInfo mi = cf.addConstructor(Modifiers.PUBLIC, null);
    CodeBuilder b = new CodeBuilder(mi);

    b.mapLineNumber(10);
    b.loadThis();
    b.invokeSuperConstructor(null);

    b.mapLineNumber(11);
    b.returnVoid();
  }

  // public static void main(java.lang.String[])
  private static void createMethod_1(ClassFile cf) {
    MethodInfo mi = cf.addMethod(Modifiers.PUBLIC_STATIC, "main", null, new TypeDesc[] {TypeDesc.STRING.toArrayType()});
    CodeBuilder b = new CodeBuilder(mi);

    LocalVariable var_1 = b.getParameter(0);

    b.mapLineNumber(5);
    TypeDesc type_1 = TypeDesc.forClass("test");
    b.newObject(type_1);
    b.dup();
    b.invokeConstructor(null);
    b.storeLocal(var_1);

    b.mapLineNumber(6);
    b.loadLocal(var_1);
    b.invokeVirtual("print", null, null);

    b.mapLineNumber(7);
    b.returnVoid();
  }

  // public void print()
  private static void createMethod_2(ClassFile cf) {
    MethodInfo mi = cf.addMethod(Modifiers.PUBLIC, "print", null, null);
    CodeBuilder b = new CodeBuilder(mi);

    b.mapLineNumber(15);
    TypeDesc type_1 = TypeDesc.forClass("java.io.PrintStream");
    b.loadStaticField("java.lang.System", "out", type_1);
    b.loadConstant("hello");
    TypeDesc[] params_1 = new TypeDesc[] {TypeDesc.STRING};
    b.invokeVirtual("java.io.PrintStream", "println", null, params_1);     b.mapLineNumber(16);     b.returnVoid();   } }
やたら長くて難解なコードだがよく見るとb.dup();のようなバイトコードのニーモニックそのままメソッドがあるためバイトコードの動きをそのままJavaで実装?しているようだ。
このコードをClassFileBuilder.javaという名前で保存し、コンパイルしてクラスファイルを生成してみる。
$ javac -classpath cojen-1.1.0.jar ClassFileBuilder.java
$ java -cp cojen-1.1.0.jar:. ClassFileBuilder test.class
するとtest.classが作成されるのでこれをjavaで実行する。
$ java -cp . test
hello
見事元のクラスファイルと同様の動きが再現された。
最近はAOPなどバイトコードレベルで処理を行う場合もあるためそういった低レベルな部分を解析、実装する場合は非常に役に立つのではないだろうか。
Posted by tsujitako at 2:19 午前 in Java/