semtax의 개발 일지

ASM Library Guideline 챕터3 정리 본문

개발/Java

ASM Library Guideline 챕터3 정리

semtax 2020. 1. 10. 18:43
반응형

1. JVM은 어떻게 클래스 파일을 실행하는가?

일단 자바가 어떠한 방식으로 컴파일된 클래스 파일을 실행하는지 알아보자.

먼저, JVM(자바 가상 머신)이 컴파일된 클래스파일을 읽어서 메모리에 로딩을 한다. 그 다음에, JVM이 클래스 파일에 있는 바이트코드를 순차적으로 읽으면서 그에 맞는 동작을 수행한다(사실 요즘에는 JVM이 속도를 위해서 자주 사용하는 부분을 파악해서 Just in time 방식을 이용해서 해당 부분을 미리 Native Code로 변환을 해서 실행을 한다).

이때 자바에서는 이러한 바이트코드들을 실행하는 기본 단위가 스레드(Thread)이다. 또한, 자바에서는 각 스레드 별로 Execution Stack(JVM Stack)을 1개씩 가지고 있다. 또한 각 스레드에서 함수를 호출할시 각 함수에 필요한 데이터(지역변수 저장공간, 피연산자 저장공간 등)들을 프레임(Frame)이라는 단위로 관리한다.

그리고 이때 자바가 사용하는 메모리 영역의 구성은 아래 그림과 같다.

JVMInternal4

위 그림에서 보여지는, 런타임 데이터 영역의 세부적인 구조는 아래와 같이 이루어져 있다.

  • 메서드 영역: 메서드 영역은 모든 스레드가 공유하는 영역으로 JVM이 시작될 때 생성된다. JVM이 읽어 들인 각각의 클래스와 인터페이스에 대한 런타임 상수 풀, 필드와 메서드 정보, Static 변수, 메서드의 바이트코드 등을 보관한다.
  • 런타임 상수 풀: 각 클래스와 인터페이스의 상수 값들과, 메서드와 필드에 대한 모든 레퍼런스까지 담고 있는 테이블이다. 즉, 어떤 메서드나 필드를 참조할 때 JVM은 런타임 상수 풀을 통해 해당 메서드나 필드의 실제 메모리상 주소를 찾아서 참조한다.
  • 힙: 실제 클래스의 인스턴스 혹은 객체를 저장하는 공간이다.

아래와 같이 자바 스레드에서 함수가 호출될때마다 스택프레임이 새로 생성되고. 해당 스레드의 JVM Stack(Execution Stack)에 스택프레임이 push되고 함수가 끝나면 스택프레임이 다시 pop이 되어 사라진다.

JVMInternal5

위 그림에서 보여지는 스택 프레임의 세부적인 내용은 아래와 같이 이루어져 있다.

  • 지역 변수 배열: 0부터 시작하는 인덱스를 가진 배열이다. 0은 메서드가 속한 클래스 인스턴스의 this 레퍼런스이고, 1부터는 메서드에 전달된 파라미터들이 저장되며, 메서드 파라미터 이후에는 메서드의 지역 변수들이 저장된다.
  • Operand Stack : 메서드의 실제 작업 공간이다. 각 메서드는 피연산자 스택과 지역 변수 배열 사이에서 데이터를 교환하고, 다른 메서드 호출 결과를 추가하거나(push) 꺼낸다(pop). 피연산자 스택 공간이 얼마나 필요한지는 컴파일할 때 결정할 수 있으므로, 피연산자 스택의 크기도 컴파일 시에 결정된다. 여담으로, JVM 6 이후부터는 Stack Map Frame이란것을 통해 이러한 스택을 검증하는 부분이 새로 생성되어서 BCI(바이트코드 조작)시에 이부분을 유념해서 조작을 해야한다.
  • 네이티브 메서드 스택: 자바 외의 언어로 작성된 네이티브 코드를 위한 스택이다. 즉, JNI(Java Native Interface)를 통해 호출하는 C/C++ 등의 코드를 수행하기 위한 스택으로, 언어에 맞게 C 스택이나 C++ 스택이 생성된다.

자세한 내용은 https://d2.naver.com/helloworld/1230 문서를 참고하면 더욱 좋다.

2. Java Bytecode Opcode

자바 바이트 코드의 종류는 크게 2가지로 나누어진다.

  1. Operand Stack과 local variable간의 데이터 이동을 하는 명령어들
  2. Operand Stack에만 값을 넣고 빼는 명령어들

조금 더 세부적으로 나누면 아래와 같다.

  • Operand Stack 조작 명령어
    • Operand Stack의 상위값 복제, 스택 top과 그 다음 원소를 바꾸는 명령어 등이 존재
  • 상수값들을 로딩하는 명령어
    • 이때, Operand Stack이나 local variable에는 constant pool의 index가 들어간다
  • 산술 연산
    • Operand Stack에서 값을 꺼내서 산술연산 명령어에 맞게 연산한후 다시 스택에 집어넣는 방식으로 동작
  • 타입 캐스팅
    • Operand Stack에서 값을 꺼내서 타입 캐스팅후, 다시 스택에 집어넣는 방식으로 동작
  • 필드 값 조작
    • Operand Stack 에서 레퍼런스 주소를 해당 주소에서 값을 읽고 해당필드의 이름을 스택에 집어넣거나. 레퍼런스 주소와 필드명을 꺼내서 해당 필드에 값을 쓰는방식으로 동작
  • 메서드 호출
    • Operand Stack 에서 함수 주소와 해당 함수의 인자개수만큼의 데이터를 스택에서 꺼낸뒤 함수를 호출하고 반환값을 집어넣는 명령어들로 구성되어있음.
  • 배열 조작
    • Operand Stack 에서 인덱스를 꺼내서 값을 가져오거나, 스택에서 인덱스와 값을 꺼내서 배열에 저장하는 명령어로 구성되어있다.
  • 분기 명령어
    • Operand Stack 에서 레이블 주소를 꺼내서 해당 주소로 점프하는 명령어들로 구성되어 있다.
  • 메서드 값 반환
    • 함수를 종료하는 역할과 함수가 실행되고 난 뒤의 값을 반환하는 명령어들로 구성되어 있다.

바이트 코드 예제를 한가지 보도록 하자.

먼저 아래와 같은 자바코드가 있다고 가정을 하자

package pkg;
public class Bean {
        private int f;
        public int getF() { return this.f; }
        public void setF(int f) { this.f = f;} 
}

위의 코드를 바이트코드로 변환하면 아래와 같이 된다.

ALOAD 0
GETFIELD pkg/Bean f I
IRETURN

먼저, ALOAD 명령어를 통해서 this객체를 스택에 저장한다 (지역변수 배열에서 0번 인덱스에 들어있는 값은 this객체를 의미한다). 그 다음에, GETFIELD 명령어를 이용해서 this 객체에서 필드 이름과 해당 필드에 저장된 값을 꺼낸다. 그 다음에 IRETURN 명령어를 이용해서 해당 필드의 값을 반환하는 방식으로 이루어 진다.

Java6 이후부터는, 자바 스택 프레임 검증의 효율성을 위해 스택 맵 프레임(Stack Map Frame)이라는 것을 도입하였다. 스택 맵 프레임은 각 명령어들이 실행될때마다 Operand Stack의 변화를 전부 저장하는 자료구조인데, Operand Stack에 저장되는 변수들의 정보를 모두 저장하면 비효율적이므로 해당 변수들의 타입만을 저장한다.

3. ASM MethodVisitor

ASM라이브러리에서는, 메서드를 조작하기 위한 MethodVisitor를 제공하고 있다.

또한 위에서 언급한 스택 맵 프레임 크기를 계산하는것도 ClassWriter에서 COMPUTE_MAX와 같은 옵션을 이용해서 자동으로 계산되게 설정이 가능하다.

대략적으로 아래와 같은 흐름으로 MethodVisitor가 사용된다고 보면 된다.

ClassVisitor cv = ...; 
cv.visit(...);
MethodVisitor mv1 = mv1.visitCode(); 
mv1.visitInsn(...); 
...
mv1.visitMaxs(...); 
mv1.visitEnd(); 
MethodVisitor mv2 = mv2.visitCode(); 
mv2.visitInsn(...); 
... 
mv2.visitMaxs(...); 
mv2.visitEnd(); 
cv.visitEnd();

MethodVisitor에서 실제 코드를 조작하는것을 대략적으로 아래와 같은 방식으로 이용된다고 보면된다.

mv.visitCode();
mv.visitVarInsn(ILOAD, 1);
Label label = new Label();
mv.visitJumpInsn(IFLT, label); 
mv.visitVarInsn(ALOAD, 0);
mv.visitVarInsn(ILOAD, 1); 
mv.visitFieldInsn(PUTFIELD, "pkg/Bean", "f", "I"); 
Label end = new Label();
mv.visitJumpInsn(GOTO, end);

....

mv.visitLabel(label);
mv.visitFrame(F_SAME, 0, null, 0, null); mv.visitTypeInsn(NEW, "java/lang/IllegalArgumentException"); mv.visitInsn(DUP);
mv.visitMethodInsn(INVOKESPECIAL,
"java/lang/IllegalArgumentException", "<init>", "()V"); mv.visitInsn(ATHROW);
mv.visitLabel(end);
mv.visitFrame(F_SAME, 0, null, 0, null); mv.visitInsn(RETURN);
mv.visitMaxs(2, 2); mv.visitEnd();

4. Method 변경 예제

이번 장에서는, Method를 변경하는 4가지 예제를 보고 위에서 언급한 MethodVistor를 어떤식으로 사용하는지 알아보도록 하겠다.

4-1. 예제 1 : NopRemoveAdapter

이번 예제는 MethodVisitor를 이용해서, 특정 메서드의 NOP명령어를 제거하는 예제이다.(사실 별 의미는 없지만 MethodVisitor가 어떤방식으로 동작하는지 알아보기 위해 만들어보는 예제이다.)

예제 코드는 아래와 같다.

package BCITest;

import org.objectweb.asm.MethodVisitor;

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

public class RemoveNopAdapter extends MethodVisitor {

    public RemoveNopAdapter(MethodVisitor mv) {
        super(ASM6, mv);
    }

    @Override
    public void visitInsn(int opcode) {
        if(opcode != NOP){
            mv.visitInsn(opcode);
        }
    }
}

=======

package BCITest;

import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.MethodVisitor;

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

public class RemoveNopClassAdapter extends ClassVisitor {

    public RemoveNopClassAdapter(ClassVisitor cv) {
        super(ASM6, cv);
    }

    @Override
    public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
        MethodVisitor mv;
        mv = cv.visitMethod(access, name, desc, signature, exceptions);
        if(mv != null && !name.equals("<init>")){
            mv = new RemoveNopAdapter(mv);
        }
        return mv;
    }
}

아래는 위에서 선언한 RemoveNopAdapter와 RemoveNopClassAdapter를 실제로 사용하는 예제이다.

package BCITest;

import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.util.Textifier;
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;

public class TransformingMethodExample {

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

    public static class MyClassLoader extends ClassLoader{
        public Class defineClass(String name, byte[] b){
            return defineClass(name, b,0,b.length);
        }
    }
    private static byte[] removeNopClass() throws IOException {
        ClassReader cr = new ClassReader("BCITest.TestTargetClass");
        ClassWriter cw = new ClassWriter(cr, 0);
        TraceClassVisitor tcv = new TraceClassVisitor(cw,printWriter);
        ClassVisitor cv = new RemoveNopClassAdapter(tcv);
        cr.accept(cv, 0);
        return cw.toByteArray();
    }

    public static void main(String[] args) throws Exception {
      removeNopClass();
    }
}

아래는 실행흐름을 조작할 클래스의 코드이다.

package BCITest;

public class TestTargetClass {

    public int kkk = 10;

    public int function1(){
        kkk = kkk;
        return 0;
    }

    public String function2(){
        return "Hello!";
    }

    public void function3(String args){
        System.out.println("Hello!");
    }

    public int function4() {
        int a = 0;
        a = a + 0;
        return 10;
    }
}

4-2. 예제 2 : TimerAdapter

이번 예제는 MethodVisitor를 이용해서, 특정 메서드의 실행시간을 측정하는 예제이다. 이번 예제 에서는 지역변수를 추가하고 해당 값을 읽고 쓰는 명령어들을 추가해야되기 때문에 visitMax 함수등을 이용해서 스택 프레임의 크기를 계산해주는 로직을 추가해야한다.

예제코드는 아래와 같다.

package BCITest;

import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.FieldVisitor;
import org.objectweb.asm.MethodVisitor;

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

public class AddTimerAdapter extends ClassVisitor {

    private String owner;
    private boolean isInterface;

    public AddTimerAdapter(ClassVisitor cv) {
        super(ASM6, cv);
    }

    @Override
    public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
        cv.visit(version, access, name, signature, superName, interfaces);
        owner = name;
        isInterface = (access & ACC_INTERFACE) != 0;
    }

    @Override
    public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
        MethodVisitor mv = cv.visitMethod(access, name, desc, signature, exceptions);
        if(!isInterface && mv != null && !name.equals("<init>")){
            mv = new AddTimerMethodAdapter(mv);
        }
        return mv;
    }

    @Override
    public void visitEnd() {
        if(!isInterface){
            FieldVisitor fv = cv.visitField(ACC_PUBLIC + ACC_STATIC, "timer", "J", null, null);
            if(fv != null){
                fv.visitEnd();
            }
        }
        cv.visitEnd();
    }

    public class AddTimerMethodAdapter extends MethodVisitor {

        public AddTimerMethodAdapter(MethodVisitor mv) {
            super(ASM6, mv);
        }

        @Override
        public void visitCode() {
            mv.visitCode();
            mv.visitFieldInsn(GETSTATIC, owner, "timer", "J");
            mv.visitMethodInsn(INVOKESTATIC, "java/lang/System", "currentTimeMillis","()J");
            mv.visitInsn(LSUB);
            mv.visitFieldInsn(PUTSTATIC, owner, "timer", "J");
        }

        @Override
        public void visitInsn(int opcode) {
            if((opcode >= IRETURN && opcode <=RETURN) || opcode == ATHROW){
                mv.visitFieldInsn(GETSTATIC, owner,"timer","J");
                mv.visitMethodInsn(INVOKESTATIC, "java/lang/System","currentTimeMillis","()J");
                mv.visitInsn(LADD);
                mv.visitFieldInsn(PUTSTATIC, owner, "timer","J");
            }
            mv.visitInsn(opcode);
        }

    }
}

아래는 위에서 선언한 AddTimerAdapter를 실제로 사용하는 예제이다.

package BCITest;

import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.util.Textifier;
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;

public class TransformingMethodExample {

    public static class MyClassLoader extends ClassLoader{
        public Class defineClass(String name, byte[] b){
            return defineClass(name, b,0,b.length);
        }
    }

    private static byte[] getTickCountMethod() throws IOException {
        ClassReader cr = new ClassReader("BCITest.C");
        ClassWriter cw = new ClassWriter(cr, ClassWriter.COMPUTE_MAXS);
        ClassVisitor cv = new AddTimerAdapter(cw);
        cr.accept(cv, 0);
        return cw.toByteArray();
    }


    private static void addtimerFirstExample() throws IOException,
            NoSuchMethodException, InvocationTargetException,
            IllegalAccessException, NoSuchFieldException,
            InstantiationException {
        Class transformedClass = new MyClassLoader().defineClass("BCITest.TestTargetClass",removeNopClass());
        Class clsTickCount = new MyClassLoader().defineClass("BCITest.C",getTickCountMethod());

        Object testClass = clsTickCount.newInstance();

        Method m = clsTickCount.getDeclaredMethod("m");
        m.invoke(testClass);

        Field f = clsTickCount.getDeclaredField("timer");
        long elapsedTime = f.getLong(testClass);

        System.out.printf("%d Elapsed!\n",elapsedTime);
    }

    public static void main(String[] args) throws Exception {
        addtimerFirstExample();
    }
}

아래는 실행흐름을 조작할 클래스의 코드이다.

package BCITest;

public class C {
    public void m() throws Exception{
        Thread.sleep(100);
    }
}

실행하면 100~104초가 걸린다는것을 확인 할 수 있다.

4-3. 예제 3 : IConstRemoveAdapter

이번 예제는 MethodVisitor를 이용해서, 특정 메서드에 들어있는 "a+=0" 과 같은 불필요한 구문을 삭제하는 예제이다. 이번 예제 에서도 역시나 값을 읽고 쓰는 명령어들을 삭제해야되기 때문에 visitMax 함수등을 이용해서 스택 프레임의 크기를 계산해주는 로직을 추가해야한다.

예제코드는 아래와 같다.

package BCITest;

import org.objectweb.asm.Label;
import org.objectweb.asm.MethodVisitor;

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

public abstract class PatternMethodAdapter extends MethodVisitor {

    protected final static int SEEN_NOTHING = 0;
    protected int state;

    public PatternMethodAdapter(MethodVisitor mv) {
        super(ASM6, mv);
    }

    @Override
    public void visitInsn(int opcode) {
        visitInsn();
        mv.visitInsn(opcode);
    }

    @Override
    public void visitIntInsn(int opcode, int operand) {
        visitInsn();
        mv.visitIntInsn(opcode, operand);
    }

    @Override
    public void visitFrame(int type, int nLocal, Object[] local, int nStack, Object[] stack) {
        visitInsn();
        mv.visitFrame(type, nLocal, local, nStack, stack);
    }

    @Override
    public void visitLabel(Label label) {
        visitInsn();
        mv.visitLabel(label);
    }

    @Override
    public void visitMaxs(int maxStack, int maxLocals) {
        visitInsn();
        mv.visitMaxs(maxStack,maxLocals);
    }

    protected abstract void visitInsn();
}

====

package BCITest;

import org.objectweb.asm.MethodVisitor;

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

public class RemoveAddZeroAdapter extends PatternMethodAdapter {

    private static int SEEN_ICONST_0 = 1;

    public RemoveAddZeroAdapter(MethodVisitor mv) {
        super(mv);
    }

    @Override
    public void visitInsn(int opcode) {
        if(state == SEEN_ICONST_0){
            if(opcode == IADD){
                state = SEEN_NOTHING;
                return ;
            }
        }
        visitInsn();
        if(opcode == ICONST_0){
            state = SEEN_ICONST_0;
            return ;
        }
        mv.visitInsn(opcode);
    }

    @Override
    protected void visitInsn() {
        if(state == SEEN_ICONST_0){
            mv.visitInsn(ICONST_0);
        }
        state = SEEN_NOTHING;
    }
}


====

package BCITest;

import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.util.Printer;
import org.objectweb.asm.util.Textifier;
import org.objectweb.asm.util.TraceMethodVisitor;

import java.io.PrintWriter;

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

public class RemoveAddZeroClassAdapter extends ClassVisitor {

    public RemoveAddZeroClassAdapter( ClassVisitor cv) {
        super(ASM6, cv);
    }

    @Override
    public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
        MethodVisitor mv = cv.visitMethod(access, name, desc, signature, exceptions);
        if(mv != null) {
            mv = new RemoveAddZeroAdapter(mv);
        }
        return mv;
    }
}

해당 클래스를 사용하는 예제는 아래와 간다.

package BCITest;

import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.util.Textifier;
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;

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

    public static class MyClassLoader extends ClassLoader{
        public Class defineClass(String name, byte[] b){
            return defineClass(name, b,0,b.length);
        }
    }

    private static byte[] removeAddZeroMethod() throws IOException{
        ClassReader cr = new ClassReader("BCITest.TestTargetClass");
        ClassWriter cw = new ClassWriter(cr, 0);
        TraceClassVisitor tcv = new TraceClassVisitor(cw,printWriter);
        ClassVisitor cv = new RemoveAddZeroClassAdapter(tcv);
        cr.accept(cv, 0);

        return cw.toByteArray();
    }

    public static void main(String[] args) throws Exception {
        removeAddZeroMethod();
    }
}

타겟 클래스는 아래와 같다.

package BCITest;

public class TestTargetClass {

    public int kkk = 10;

    public int function1(){
        kkk = kkk;
        return 0;
    }

    public String function2(){
        return "Hello!";
    }

    public void function3(String args){
        System.out.println("Hello!");
    }

    public int function4() {
        int a = 0;
        a = a + 0;
        return 10;
    }
}

실행해보면 "a+=0" 에 해당하는 명령어들(ICONST_0 IADD)이 삭제가 된것을 확인 할 수 있다.

4-4. 예제 4 : SelfAssignRemoveAdapter

이번 예제는 MethodVisitor를 이용해서, 특정 메서드에 들어있는 "f = f" 과 같은 불필요한 구문을 삭제하는 예제이다. 이번 예제 에서도 역시나 값을 읽고 쓰는 명령어들을 삭제해야되기 때문에 visitMax 함수등을 이용해서 스택 프레임의 크기를 계산해주는 로직을 추가해야한다. 또한 해당 명령어들을 패턴 매칭하기 위해 StateMachine을 구현해서 사용하였다.

아래 그림은 이번 예제에서 구현한 StateMachine의 도식도 이다.

예제코드는 아래와 같다.

package BCITest;

import org.objectweb.asm.MethodVisitor;

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

public class RemoveGetFieldPutFieldAdapter extends PatternMethodAdapter {

    private final static int SEEN_ALOAD_0 = 1;
    private final static int SEEN_ALOAD_0ALOAD_0 = 2;
    private final static int SEEN_ALOAD_0ALOAD_0GETFIELD = 3;
    private String fieldOwner;
    private String fieldName;
    private String fieldDesc;

    public RemoveGetFieldPutFieldAdapter(MethodVisitor mv) {
        super(mv);
    }

    @Override
    public void visitVarInsn(int opcode, int var) {
        switch (state){
            case SEEN_NOTHING:
                if(opcode == ALOAD && var == 0){
                    state = SEEN_ALOAD_0;
                    return;
                }
                break;
            case SEEN_ALOAD_0:
                if (opcode == ALOAD && var == 0) {
                    state = SEEN_ALOAD_0ALOAD_0;
                    return;
                }
                break;
            case SEEN_ALOAD_0ALOAD_0:
                if(opcode == ALOAD && var == 0){
                    mv.visitVarInsn(ALOAD, 0);
                }
                break;
        }
        visitInsn();
        mv.visitVarInsn(opcode, var);
    }

    @Override
    public void visitFieldInsn(int opcode, String owner, String name, String desc) {
        switch (state){
            case SEEN_ALOAD_0ALOAD_0:
                if(opcode == GETFIELD){
                    state = SEEN_ALOAD_0ALOAD_0GETFIELD;
                    fieldOwner = owner;
                    fieldName = name;
                    fieldDesc = desc;
                    return ;
                }
                break;
            case SEEN_ALOAD_0ALOAD_0GETFIELD:
                if(opcode == PUTFIELD && name.equals(fieldName)){
                    state = SEEN_NOTHING;
                    return ;
                }
                break;
        }
        visitInsn();
        mv.visitFieldInsn(opcode, owner, name, desc);
    }

    @Override
    protected void visitInsn() {
        switch(state){
            case SEEN_ALOAD_0:
                mv.visitVarInsn(ALOAD, 0);
                break;
            case SEEN_ALOAD_0ALOAD_0:
                mv.visitVarInsn(ALOAD, 0);
                mv.visitVarInsn(ALOAD, 0);
                break;
            case SEEN_ALOAD_0ALOAD_0GETFIELD:
                mv.visitVarInsn(ALOAD, 0);
                mv.visitVarInsn(ALOAD, 0);
                mv.visitFieldInsn(GETFIELD, fieldOwner, fieldName, fieldDesc);
                break;
        }
        state = SEEN_NOTHING;
    }
}

=====

package BCITest;

import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.MethodVisitor;

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

public class RemoveGetFieldPutFieldClassAdapter extends ClassVisitor {
    public RemoveGetFieldPutFieldClassAdapter(ClassVisitor cv) {
        super(ASM6, cv);
    }

    @Override
    public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
        MethodVisitor mv = cv.visitMethod(access, name, desc, signature, exceptions);
        if(mv != null) {
            mv = new RemoveGetFieldPutFieldAdapter(mv);
        }
        return mv;
    }
}

해당 클래스를 사용하는 예제는 아래와 간다.

package BCITest;

import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.util.Textifier;
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;

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

    public static class MyClassLoader extends ClassLoader{
        public Class defineClass(String name, byte[] b){
            return defineClass(name, b,0,b.length);
        }
    }
    private static byte[] removeGetFieldPutFieldMethod() throws IOException{
        ClassReader cr = new ClassReader("BCITest.TestTargetClass");
        ClassWriter cw = new ClassWriter(cr, 0);
        TraceClassVisitor tcv = new TraceClassVisitor(cw,printWriter);
        ClassVisitor cv = new RemoveGetFieldPutFieldClassAdapter(tcv);
        cr.accept(cv, 0);

        return cw.toByteArray();
    }

    public static void main(String[] args) throws Exception {
        removeGetFieldPutFieldMethod();
    }
}

타겟 클래스는 아래와 같다.

package BCITest;

public class TestTargetClass {

    public int kkk = 10;

    public int function1(){
        kkk = kkk;
        return 0;
    }

    public String function2(){
        return "Hello!";
    }

    public void function3(String args){
        System.out.println("Hello!");
    }

    public int function4() {
        int a = 0;
        a = a + 0;
        return 10;
    }
}

실행해보면 "f = f" 에 해당하는 명령어들(ALOAD 0 ALOAD 0 GETFIELD f PUTFIELD f)이 삭제가된것을 확인 할 수 있다.

5. 미리 정의된 MethodAdapter 들

ASM에서는 위에서 했던 번거로운 작업들을 덜어주기위해 미리 정의된 어뎁터 클래스들을 라이브러리 차원에서 제공을 해주고 있다. 아래는 ASM에서 제공하는 Adapter들의 목록들이다.

5-1. AnalyzerAdapter

함수나 프레임이 새로 생성될때마다 스택 프레임 크기를 자동으로 계산해주는 Adapter이다. 5-3에서의 AdviceAdapter를 주로 사용하기 때문에 사실 크게 쓸 일을 존재하지 않는다.

5-2. LocalVariableSorter

해당 어뎁터를 이용해서 로컬변수를 새로 생성하는 경우 해당 어뎁터에서 알아서 스택 프레임 크기를 자동으로 계산해준다. 5-3에서의 AdviceAdapter를 주로 사용하기 때문에 사실 크게 쓸 일을 존재하지 않는다.

5-3. AdviceAdapter

주로 메서드가 실행되기 이전/ 실행된 이후의 실행흐름을 조작하고 싶거나, 메서드의 파라미터 값이나 반환값을 조작하고 싶을때 사용한다.

예제 코드는 아래와 같다.

package BCITest;

import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.commons.AdviceAdapter;

public class AddTimerAdapter6 extends AdviceAdapter {

    private String owner = "";

    public AddTimerAdapter6(int access, String name, String desc, MethodVisitor mv){
        super(ASM6, mv, access, name, desc);
        owner = desc;
    }

    @Override
    protected void onMethodEnter() {
        mv.visitFieldInsn(GETSTATIC, owner, "timer", "J");
        mv.visitMethodInsn(INVOKESTATIC, "java/lang/System",
                "currentTimeMillis", "()J");
        mv.visitInsn(LSUB);
        mv.visitFieldInsn(PUTSTATIC, owner, "timer", "J");
    }

    @Override
    protected void onMethodExit(int opcode) {
        mv.visitFieldInsn(GETSTATIC, owner, "timer","J");
        mv.visitMethodInsn(INVOKESTATIC, "java/lang/System",
                "currentTimeMillis", "()J");
        mv.visitInsn(LADD);
        mv.visitFieldInsn(PUTSTATIC, owner,"timer","J");
    }

    @Override
    public void visitMaxs(int maxStack, int maxLocals) {
        super.visitMaxs(Math.max(maxStack,4), maxLocals);
    }
}

아래는 위에서 선언한 AdviceAdapter를 실제로 사용하는 예제이다.

package BCITest;

import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.util.Textifier;
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;

public class TransformingMethodExample {

    public static class MyClassLoader extends ClassLoader{
        public Class defineClass(String name, byte[] b){
            return defineClass(name, b,0,b.length);
        }
    }

    private static byte[] getTickCountMethod6() throws IOException {
        ClassReader cr = new ClassReader("BCITest.C");
        ClassWriter cw = new ClassWriter(cr, ClassWriter.COMPUTE_MAXS);
        ClassVisitor cv = new AddTimerAdapter6(cw);
        cr.accept(cv, 0);
        return cw.toByteArray();
    }

private static void addtimer6thExample() throws IOException,
            NoSuchMethodException, InvocationTargetException,
            IllegalAccessException, NoSuchFieldException,
            InstantiationException {
        Class transformedClass = new MyClassLoader().defineClass("BCITest.TestTargetClass",removeNopClass());
        Class clsTickCount = new MyClassLoader().defineClass("BCITest.C",getTickCountMethod6());

        Object testClass = clsTickCount.newInstance();

        Method m = clsTickCount.getDeclaredMethod("m");
        m.invoke(testClass);

        Field f = clsTickCount.getDeclaredField("timer");
        long elapsedTime = f.getLong(testClass);

        System.out.printf("%d Elapsed!\n",elapsedTime);
    }

    public static void main(String[] args) throws Exception {
        addtimer6thExample();
    }
}

아래는 실행흐름을 조작할 클래스의 코드이다.

package BCITest;

public class C {
    public void m() throws Exception{
        Thread.sleep(100);
    }
}

출처 :

  1. Naver D2(https://d2.naver.com/helloworld/1230)
  2. ASM 4.0 guideline
반응형
Comments