semtax의 개발 일지

ASM Library Guideline 챕터6 정리 본문

개발/Java

ASM Library Guideline 챕터6 정리

semtax 2020. 1. 13. 20:38
반응형

1. Tree API

이번 포스팅에서는 ASM에서 제공하는 Tree API를 이용해서, 클래스 파일을 조작해보도록 하겠습니다.

ASM에서는 클래스 조작을 위한 Tree API인 ClassNode 클래스를 제공합니다. 대략적인 생김새는 아래와 같습니다.

public class ClassNode ... { 
    public int version;
    public int access;
    public String name;
    public String signature;
    public String superName;
    public List<String> interfaces;
    public String sourceFile;
    public String sourceDebug;
    public String outerClass;
    public String outerMethod;
    public String outerMethodDesc;
    public List<AnnotationNode> visibleAnnotations;
    public List<AnnotationNode> invisibleAnnotations;
    public List<Attribute> attrs;
    public List<InnerClassNode> innerClasses;
    public List<FieldNode> fields;
    public List<MethodNode> methods;
}

ASM Core API와는 다르게, 자식들의 타입이 XXXNode로 끝난다는 것을 알 수 있습니다.

다음으로는 FieldNode 클래스의 구조 입니다.

public class FieldNode ... 
{ 
  public int access;
    public String name;
    public String desc;
  public String signature;
  public Object value;
     public FieldNode(int access, String name, String desc, String signature, Object value) {                 ...
    } 
     ...
}

MethodNode 구조도 아래와 같습니다.

public class MethodNode ... 
{ 
    public int access;
  public String name;
  public String desc;
  public String signature;
  public List<String> exceptions;
    ...
  public MethodNode(int access, String name, String desc, String signature, 
                    String[] exceptions) {
        ...
  } 
}

역시나 FieldNode와 비슷하게 생긴것을 알 수 있습니다.

2. 클래스 생성

아래 코드는 TreeAPI를 이용해서 클래스를 생성하는 예제입니다.

package BCITestTree;

import BCITest.GeneratingClassExampleMain;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.Type;
import org.objectweb.asm.tree.ClassNode;
import org.objectweb.asm.tree.FieldNode;
import org.objectweb.asm.tree.MethodNode;
import org.objectweb.asm.util.TraceClassVisitor;

import java.io.IOException;
import java.io.PrintWriter;

import static org.objectweb.asm.Opcodes.*;

public class TransformTreeClassMain {

    public static PrintWriter printWriter = new PrintWriter(System.out);

    public static void classNodeWrite() throws IOException {
        ClassNode cn = new ClassNode(ASM6);
        cn.version = V1_8;
        cn.access = ACC_PUBLIC + ACC_ABSTRACT + ACC_INTERFACE;
        cn.name = "BCITestTree/Comparable";
        cn.superName = "java/lang/Object";
        cn.interfaces.add("BCITestTree/Measurable");
        cn.fields.add(new FieldNode(ACC_PUBLIC + ACC_FINAL + ACC_STATIC, "LESS", "I", null, new Integer(-1)));
        cn.fields.add(new FieldNode(ACC_PUBLIC + ACC_FINAL + ACC_STATIC, "EQUAL", "I", null, new Integer(0)));
        cn.fields.add(new FieldNode(ACC_PUBLIC + ACC_FINAL + ACC_STATIC, "GREATER", "I", null, new Integer(1)));
        cn.methods.add(new MethodNode(ACC_PUBLIC + ACC_STATIC,"compareTo","(Ljava/lang/Object;)I",null,null));

        ClassReader cr = new ClassReader("BCITestTree/Measurable");
        ClassWriter cw = new ClassWriter(0);
        TraceClassVisitor tcv = new TraceClassVisitor(cw,printWriter);
        cn.accept(tcv);
        byte[] b = cw.toByteArray();
    }

    public static void main(String[] args) throws IOException {
        classNodeWrite();
    }
}

위에 코드를 확인 해보면, ASM Core API를 사용할때와는 다르게 xxxNode 클래스를 생성 해서 메서드나 필드를 추가하는 것을 알 수 있습니다. 그리고 API 이름대로 마치 트리구조를 가지고 있다는것을 알 수 있습니다.

사실, TreeAPI를 사용하여 클래스를 조작/생성 하는 경우 Core API에 비해서 30%정도 시간이 더 소요되기는 합니다. 하지만 무조건 생성 순서를 지켜서 만들어야 했던 Core API와는 다르게 Tree API는 순서에 상관없이 각 요소들을 추가할 수 있다는 장점이 있습니다.

3. 클래스 조작

아래 예제는 클래스에서 메서드를 삭제하는 예제입니다.

package BCITestTree;

import org.objectweb.asm.tree.ClassNode;

public abstract class ClassTransformer {
    protected ClassTransformer ct;

    public ClassTransformer(ClassTransformer ct){
        this.ct = ct;
    }

    public void transform(ClassNode cn) {
        if (ct != null) {
            ct.transform(cn);
        }
    }
}
package BCITestTree;

import org.objectweb.asm.tree.ClassNode;
import org.objectweb.asm.tree.MethodNode;

import java.util.Iterator;

public class RemoveMethodTransformer extends ClassTransformer {

    private String methodName;
    private String methodDesc;

    public RemoveMethodTransformer(ClassTransformer ct, String methodName, String methodDesc){
        super(ct);
        this.methodName = methodName;
        this.methodDesc = methodDesc;
    }

    @Override
    public void transform(ClassNode cn) {
        Iterator<MethodNode> i = cn.methods.iterator();
        while(i.hasNext()){
            MethodNode mn = i.next();
            if(methodName.equals(mn.name) && methodDesc.equals(mn.desc)){
                i.remove();
            }
        }
        super.transform(cn);
    }
}

위의 코드를 보면 Core API와는 다르게, transform 함수에서 바로 Iterator를 이용하여 모든 메서드 목록을 조회 한 뒤, 삭제하는 것을 알 수 있습니다.

아래 예제는 클래스에서 필드를 추가하는 예제입니다.

package BCITestTree;

import org.objectweb.asm.tree.ClassNode;
import org.objectweb.asm.tree.FieldNode;

public class AddFieldTransformer extends ClassTransformer {

    private int fieldAccess;
    private String fieldName;
    private String fieldDesc;

    public AddFieldTransformer(ClassTransformer ct, int fieldAccess, String fieldName, String fieldDesc){
        super(ct);
        this.fieldAccess = fieldAccess;
        this.fieldName = fieldName;
        this.fieldDesc = fieldDesc;
    }

    @Override
    public void transform(ClassNode cn) {
        boolean isPresent = false;
        for(FieldNode fn : cn.fields){
            if(fieldName.equals(fn.name)){
                isPresent = true;
                break;
            }
        }

        if(!isPresent){
            cn.fields.add(new FieldNode(fieldAccess, fieldName, fieldDesc, null, null));
        }

        super.transform(cn);
    }
}

사용예시는 아래와 같습니다.

package BCITestTree;

import BCITest.GeneratingClassExampleMain;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.Type;
import org.objectweb.asm.tree.ClassNode;
import org.objectweb.asm.tree.FieldNode;
import org.objectweb.asm.tree.MethodNode;
import org.objectweb.asm.util.TraceClassVisitor;

import java.io.IOException;
import java.io.PrintWriter;

import static org.objectweb.asm.Opcodes.*;

public class TransformTreeClassMain {

    public static PrintWriter printWriter = new PrintWriter(System.out);

    public static void removeMethod() throws IOException {
        ClassReader cr = new ClassReader("BCITestTree/TestTargetClass");
        ClassNode cn = new ClassNode(ASM6);
        cr.accept(cn, 0);
        //RemoveMethodTransformer rt = new RemoveMethodTransformer(null,"function1","()I");
        RemoveMethodTransformer rt = new RemoveMethodTransformer(null,"function1", Type.INT_TYPE.getDescriptor());
        rt.transform(cn);
        ClassWriter cw = new ClassWriter(0);
        TraceClassVisitor tcv = new TraceClassVisitor(cw,printWriter);
        cn.accept(tcv);
        byte[] b = cw.toByteArray();
    }

    public static void addFieldMethod() throws IOException {
        ClassReader cr = new ClassReader("BCITest/TestTargetClass");
        ClassNode cn = new ClassNode(ASM6);
        cr.accept(cn, 0);
        AddFieldTransformer rt = new AddFieldTransformer(null,ACC_PUBLIC,"ttt" ,Type.INT_TYPE.getDescriptor());
        rt.transform(cn);
        ClassWriter cw = new ClassWriter(0);
        TraceClassVisitor tcv = new TraceClassVisitor(cw,printWriter);
        cn.accept(tcv);
        byte[] b = cw.toByteArray();
    }

    public static void main(String[] args) throws IOException {
        removeMethod();
        addFieldMethod();
    }
}

사실, Core API를 쓰면 되지 굳이 Tree API를 써야되냐는 의문도 들 수 있습니다. 하지만, Tree API를 사용하는 경우 Core API와는 다르게 한번에(one pass) 처리가 가능한 경우가 있습니다.

대표적인 예시로, 클래스 안에 있는 필드나 메소드의 내용에 따라 어노테이션을 동적으로 붙여주는 예제를 들 수 있습니다.

Core API를 이용하는 경우, 클래스 정보를 전부 읽어서 어노테이션을 붙일지 말지를 판단하고 다시 Signature와 관련된 어뎁터(Transformer)를 거쳐서 어노테이션을 붙여주어야 합니다. 즉, 두 번 일해야 하는것이죠.

그러나, Tree API를 사용하는 경우 한번에 이러한 처리가 가능합니다.

하지만, 바이트코드 난독화와 같은 경우 Core API를 사용하는것이 더 좋습니다. 결국 적절한 용도로 쓰는것이 최고입니다.)

4. 기타

사실 Tree API에 있는 xxxNode 클래스 들은 아래코드와 같이 내부적으로 ClassVisitor를 상속받고 있습니다.

public class ClassNode extends ClassVisitor { 
      ...
    public void visit(int version, int access, String name,
            String signature, String superName, String[] interfaces[]) {
            this.version = version; 
      this.access = access; 
      this.name = name; 
      this.signature = signature; 
      ...
    }

        ...

        public void accept(ClassVisitor cv) {
                cv.visit(version, access, name, signature, ...); 
          ...
        } 
}

따라서, ClassVisitor에 있는 함수들도 오버라이드 한뒤, Core API와 섞어서 사용할 수 도 있습니다.

반응형
Comments