semtax의 개발 일지

ASM Library Guideline 챕터7 정리 본문

개발/Java

ASM Library Guideline 챕터7 정리

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

1. Tree API를 이용한 메서드 조작

ASM에서 제공하는 MethodNode는 대략적으로 아래와 같이 생겼습니다.

public class MethodNode ... { 
  public int access;
  public String name;
  public String desc;
  public String signature;
  public List<String> exceptions;
  public List<AnnotationNode> visibleAnnotations;
  public List<AnnotationNode> invisibleAnnotations;
  public List<Attribute> attrs;
  public Object annotationDefault;
  public List<AnnotationNode>[] visibleParameterAnnotations;
  public List<AnnotationNode>[] invisibleParameterAnnotations;
  public InsnList instructions;
  public List<TryCatchBlockNode> tryCatchBlocks;
  public List<LocalVariableNode> localVariables;
  public int maxStack;
  public int maxLocals;
}

다른 부분은 Core API의 MethodVisitor와 별 차이가 없지만, 가장 차이가 나는 부분은 실제 바이트코드를 저장하는 InsnList클래스 입니다.

아래는 InsnList의 구조입니다.

public class InsnList { // public accessors omitted
  int size();
  AbstractInsnNode getFirst();
  AbstractInsnNode getLast();
  AbstractInsnNode get(int index);
  boolean contains(AbstractInsnNode insn);
  int indexOf(AbstractInsnNode insn);
  void accept(MethodVisitor mv);
  ListIterator iterator();
  ListIterator iterator(int index);
  AbstractInsnNode[] toArray();
  void set(AbstractInsnNode location, AbstractInsnNode insn);
  void add(AbstractInsnNode insn);
  void add(InsnList insns);
  void insert(AbstractInsnNode insn);
  void insert(InsnList insns);
  void insert(AbstractInsnNode location, AbstractInsnNode insn);
  void insert(AbstractInsnNode location, InsnList insns);
  void insertBefore(AbstractInsnNode location, AbstractInsnNode insn);
  void insertBefore(AbstractInsnNode location, InsnList insns);
  void remove(AbstractInsnNode insn);
  void clear();
}

위 코드를 보면 알 수 있지만, InstList는 AbstractInsnNode에 링크가 저장된 더블 링크드 리스트 구조를 가지고 있습니다.

따라서, 아래와 같은 주의 사항을 지켜야 합니다.

  • AbstractInsnNode 객체는 1번 이상 나올 수 없습니다.
  • AbstractInsnNode 객체는 동시에 여러개의 바이트코드 명령어를 가질 수 없습니다.
  • 따라서, AbstractInsnNode를 리스트에 추가할때, 기존 AbstractInsnNode(즉 바이트코드 명령어)를 삭제한 뒤에 추가해야 합니다.
  • 또한 인스트럭션 리스트의 원소들을 다른 인스트럭션 리스트에 복사하려면, 복사 후에 반드시 원본 리스트를 제거해야합니다.

AbstractInsnNode의 구조는 아래와 같습니다.

public abstract class AbstractInsnNode {
        public int getOpcode();
        public int getType();
        public AbstractInsnNode getPrevious();
        public AbstractInsnNode getNext();
        public void accept(MethodVisitor cv);
        public AbstractInsnNode clone(Map labels);
}

또한, 기존의 프레임 명령어나, 변수선언 명령어, 심지어 라인 넘버와 같은 정보도 전부 AbstractInsnNode를 상속받아서 구현되어 있습니다.

또한, 명령어 리스트가 이중 리스트 구조를 가지고 있기때문에, 기존 Core API에 비해서 명령어를 추가하거나 삭제하는것이 용이합니다.

2. 메서드 생성 예제

아래 코드는, MethodNode를 이용해서 메서드를 생성하는 예제입니다.

package BCITestTree;

import BCITest.TransformingMethodExample;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.Type;
import org.objectweb.asm.tree.*;
import org.objectweb.asm.util.TraceClassVisitor;

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

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

public class TransformTreeMethodMain {

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

    public static void methodNodeWrite() throws IOException {
        MethodNode mn = new MethodNode(ACC_PUBLIC,"function1","()I",null,new String[] {"java/lang/IllegalArgumentException"});
        InsnList il = mn.instructions;
        il.add(new VarInsnNode(ILOAD,1));
        LabelNode label = new LabelNode();
        il.add(new JumpInsnNode(IFLT, label));
        il.add(new VarInsnNode(ALOAD, 0));
        il.add(new VarInsnNode(ILOAD, 1));
        il.add(new FieldInsnNode(PUTFIELD, "BCITestTree/Bean","f","I"));
        LabelNode end = new LabelNode();
        il.add(new JumpInsnNode(GOTO, end));
        il.add(label);
        il.add(new FrameNode(F_SAME, 0, null, 0, null));
        il.add(new TypeInsnNode(NEW,"java/lang/IllegalArgumentException"));
        il.add(new InsnNode(DUP));
        il.add(new MethodInsnNode(INVOKESPECIAL, "java/lang/IllegalArgumentException","<init>","()V"));
        il.add(new InsnNode(ATHROW));
        il.add(end);
        il.add(new FrameNode(F_SAME, 0, null, 0, null));
        il.add(new InsnNode(RETURN));
        mn.maxStack = 2;
        mn.maxLocals = 2;

        ClassNode cn = new ClassNode(ASM6);
        cn.version = V1_8;
        cn.access = ACC_PUBLIC;
        cn.name = "BCITestTree/Bean";
        cn.superName = "java/lang/Object";
        cn.methods.add(mn);

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

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

역시나, CoreAPI와는 다르게 순서에 상관없이 추가할 수 있습니다.

3. 메서드 조작 예제

메서드 내의 명령어를 조작하는 흐름은 대략 아래와 같습니다.

InsnList il = new InsnList(); 
il.add(...);
...
il.add(...); 
mn.instructions.insert(i, il);

아래 코드는 클래스 내의 메서드에 시간측정 로직을 끼워넣는 예제입니다.

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.*;

import java.util.Iterator;
import java.util.List;

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

public class AddTimerTransformer extends ClassTransformer {

    public AddTimerTransformer(ClassTransformer ct){
        super(ct);
    }

    @Override
    public void transform(ClassNode cn) {

        for(MethodNode mn : (List<MethodNode>) cn.methods){
            if("<init>".equals(mn.name) || "<client>".equals(mn.name)){
                continue;
            }

            InsnList insns = mn.instructions;

            if(insns.size() == 0){
                continue;
            }

            Iterator<AbstractInsnNode> j = insns.iterator();
            while(j.hasNext()){
                AbstractInsnNode in = j.next();
                int op = in.getOpcode();
                if((op >= IRETURN && op <= RETURN) || op == ATHROW){
                    InsnList il = new InsnList();
                    il.add(new FieldInsnNode(GETSTATIC, cn.name, "timer", "J"));
                    il.add(new MethodInsnNode(INVOKESTATIC, "java/lang/System",
                            "currentTimeMillis", "()J"));
                    il.add(new InsnNode(LADD));
                    il.add(new FieldInsnNode(PUTSTATIC, cn.name, "timer", "J"));
                    insns.insert(in.getPrevious(), il);
                }
            }
            InsnList il = new InsnList();
            il.add(new FieldInsnNode(GETSTATIC, cn.name, "timer", "J")); il.add(new MethodInsnNode(INVOKESTATIC, "java/lang/System",
                    "currentTimeMillis", "()J"));
            il.add(new InsnNode(LSUB));
            il.add(new FieldInsnNode(PUTSTATIC, cn.name, "timer", "J")); insns.insert(il);
            mn.maxStack += 4;
        }

        int acc = ACC_PUBLIC + ACC_STATIC;
        cn.fields.add(new FieldNode(acc, "timer", "J", null, null));
        super.transform(cn);
    }
}

사실 위와 같이 stateless하게 메서드의 내용을 변경하는 경우에는, Core API와는 별 차이가 없습니다.

하지만, stateful하게 메서드의 내용을 변경을 변경하는 경우는 작성 해야하는 코드의 양이 현격하게 차이가 나게 됩니다.

아래 예제는, 메서드 내에서 f=f와 같은 의미없는 코드를 삭제하는 예제입니다.

package BCITestTree;

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

public class RemoveGetFieldPutFieldClassTransformer extends ClassTransformer {

    public RemoveGetFieldPutFieldClassTransformer(ClassTransformer ct){
        super(ct);
    }

    @Override
    public void transform(ClassNode cn) {
        RemoveGetFieldPutFieldTransformer remover = new RemoveGetFieldPutFieldTransformer(null);

        for(MethodNode mn : cn.methods){
            remover.transform(mn);
        }

        super.transform(cn);
    }
}
package BCITestTree;

import org.objectweb.asm.tree.*;

import java.util.Iterator;

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

public class RemoveGetFieldPutFieldTransformer extends MethodTransformer {

    public RemoveGetFieldPutFieldTransformer(MethodTransformer mt) {
        super(mt);
    }

    @Override
    public void transform(MethodNode mn) {
        InsnList insns = mn.instructions;
        Iterator<AbstractInsnNode> i = insns.iterator();
        while (i.hasNext()) {
            AbstractInsnNode i1 = i.next();
            if (isALOAD0(i1)) {
                AbstractInsnNode i2 = getNext(i1);
                if (i2 != null && isALOAD0(i2)) {
                    AbstractInsnNode i3 = getNext(i2);
                    if (i3 != null && i3.getOpcode() == GETFIELD) {
                        AbstractInsnNode i4 = getNext(i3);
                        if (i4 != null && i4.getOpcode() == PUTFIELD) {
                            if (sameField(i3, i4)) {
                                while (i.next() != i4) { }
                                insns.remove(i1); insns.remove(i2); insns.remove(i3); insns.remove(i4);
                            }
                        }
                    }
                }
            }
        }
        super.transform(mn);
    }

    private static AbstractInsnNode getNext(AbstractInsnNode insn) {
        do {
            insn = insn.getNext();
            if (insn != null && !(insn instanceof LineNumberNode)) {
                break;
            }
        } while (insn != null);
        return insn;
    }

    private static boolean isALOAD0(AbstractInsnNode i) {
        return i.getOpcode() == ALOAD && ((VarInsnNode) i).var == 0;
    }
    private static boolean sameField(AbstractInsnNode i, AbstractInsnNode j) {
        return ((FieldInsnNode) i).name.equals(((FieldInsnNode) j).name);
    }
}

아래 코드는 사용 예제입니다.

package BCITestTree;

import BCITest.AddTimerAdapter;
import BCITest.TransformingMethodExample;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.Type;
import org.objectweb.asm.tree.*;
import org.objectweb.asm.util.TraceClassVisitor;

import java.io.IOException;
import java.io.PrintWriter;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

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

public class TransformTreeMethodMain {

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

    public static void removeGetFieldPutFieldMethod() throws IOException {
        ClassReader cr = new ClassReader("BCITestTree/TestTargetClass");
        ClassNode cn = new ClassNode(ASM6);
        cr.accept(cn, 0);
        //RemoveMethodTransformer rt = new RemoveMethodTransformer(null,"function1","()I");
        RemoveGetFieldPutFieldClassTransformer rt = new RemoveGetFieldPutFieldClassTransformer(null);
        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, NoSuchMethodException, NoSuchFieldException, InstantiationException, IllegalAccessException, InvocationTargetException {
        removeGetFieldPutFieldMethod();
    }
}

Core API를 사용해서 작성한 예제보다 코드 내용이 더 간결하다는 것을 볼 수 있습니다.

또한 위의 예제는 아래와 같이 구현 할 수 도 있습니다.

package BCITestTree;

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

public class RemoveGetFieldPutFieldClassTransformer2 extends ClassTransformer {

    public RemoveGetFieldPutFieldClassTransformer2(ClassTransformer ct){
        super(ct);
    }

    @Override
    public void transform(ClassNode cn) {
        RemoveGetFieldPutFieldTransformer remover = new RemoveGetFieldPutFieldTransformer(null);

        for(MethodNode mn : cn.methods){
            remover.transform(mn);
        }

        super.transform(cn);
    }
}
package BCITestTree;

import org.objectweb.asm.tree.*;

import java.util.Iterator;

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

public class RemoveGetFieldPutFIeldTransformer2 extends MethodTransformer{

    public RemoveGetFieldPutFIeldTransformer2(MethodTransformer mt) {
        super(mt);
    }

    @Override
    public void transform(MethodNode mn) {
        InsnList insns = mn.instructions;
        Iterator i = insns.iterator();
        while (i.hasNext()) {
            AbstractInsnNode i1 = (AbstractInsnNode) i.next(); if (isALOAD0(i1)) {
                AbstractInsnNode i2 = getNext(i);
                if (i2 != null && isALOAD0(i2)) {
                    AbstractInsnNode i3 = getNext(i);
                    while (i3 != null && isALOAD0(i3)) {
                        i1 = i2;
                        i2 = i3;
                        i3 = getNext(i);
                    }
                    if (i3 != null && i3.getOpcode() == GETFIELD) { AbstractInsnNode i4 = getNext(i);
                        if (i4 != null && i4.getOpcode() == PUTFIELD) {
                            if (sameField(i3, i4)) { insns.remove(i1); insns.remove(i2); insns.remove(i3); insns.remove(i4);
                            }
                        }
                    }
                }
            }
        }

        super.transform(mn);
    }

    private static AbstractInsnNode getNext(Iterator i) {
        while (i.hasNext()) {
            AbstractInsnNode in = (AbstractInsnNode) i.next();
            if (!(in instanceof LineNumberNode)) {
                return in;
            }
        }
        return null;
    }


    private static boolean isALOAD0(AbstractInsnNode i) {
        return i.getOpcode() == ALOAD && ((VarInsnNode) i).var == 0;
    }

    private static boolean sameField(AbstractInsnNode i, AbstractInsnNode j) {
        return ((FieldInsnNode) i).name.equals(((FieldInsnNode) j).name);
    }
}

마지막으로, Instruction의 상대위치와는 관련없이 Global하게 메서드 내의 바이트코드를 조작하여 TestTargetClass의 checkAndSetF() 메서드의 GOTO명령어를 RETURN으로 변경하는 예제입니다.

package BCITestTree;

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

public class OptimizeJumpClassTransformer extends ClassTransformer {

    public OptimizeJumpClassTransformer(ClassTransformer ct){
        super(ct);
    }

    @Override
    public void transform(ClassNode cn) {
        OptimizeJumpTransformer optimizer = new OptimizeJumpTransformer(null);

        for(MethodNode mn : cn.methods){
            optimizer.transform(mn);
        }

        super.transform(cn);
    }

}
package BCITestTree;

import org.objectweb.asm.tree.*;

import java.util.Iterator;

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

public class OptimizeJumpTransformer extends MethodTransformer {

    public OptimizeJumpTransformer(MethodTransformer mt){
        super(mt);
    }

    @Override
    public void transform(MethodNode mn) {

        InsnList insns = mn.instructions;
        Iterator<AbstractInsnNode> i = insns.iterator();

        while(i.hasNext()){
            AbstractInsnNode in = i.next();
            if(in instanceof JumpInsnNode){
                LabelNode label = ((JumpInsnNode)in).label;
                AbstractInsnNode target;

                while(true){
                    target = label;
                    while(target != null && target.getOpcode() < 0){
                        target = target.getNext();
                    }

                    if(target != null && target.getOpcode() == GOTO){
                        label = ((JumpInsnNode)target).label;
                    }else{
                        break;
                    }
                }
                ((JumpInsnNode)in).label = label;

                if(in.getOpcode() == GOTO && target != null){
                    int op = target.getOpcode();
                    if ((op >= IRETURN && op <= RETURN) || op == ATHROW) {
                        // replace ’in’ with clone of ’target’
                        insns.set(in, target.clone(null));
                    }
                }
            }
        }

        super.transform(mn);
    }
}

아래 코드는 사용 예시입니다.

package BCITestTree;

import BCITest.AddTimerAdapter;
import BCITest.TransformingMethodExample;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.Type;
import org.objectweb.asm.tree.*;
import org.objectweb.asm.util.TraceClassVisitor;

import java.io.IOException;
import java.io.PrintWriter;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

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

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

    public static void optimizeJumpTransformMethod() throws IOException {
        ClassReader cr = new ClassReader("BCITestTree/TestTargetClass");
        ClassNode cn = new ClassNode(ASM6);
        cr.accept(cn, 0);
        //RemoveMethodTransformer rt = new RemoveMethodTransformer(null,"function1","()I");
        OptimizeJumpClassTransformer rt = new OptimizeJumpClassTransformer(null);
        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, NoSuchMethodException, NoSuchFieldException, InstantiationException, IllegalAccessException, InvocationTargetException {
        optimizeJumpTransformMethod();
    }
}
반응형
Comments