semtax의 개발 일지

ASM Library Guideline 챕터 2 정리 본문

개발/Java

ASM Library Guideline 챕터 2 정리

semtax 2020. 1. 8. 20:50
반응형

1. 자바 클래스 구조

1-1. 자바 클래스 전체 구조

컴파일된 자바 클래스의 개략적인 구조는 아래와 같이 구성되어있다.

  1. 클래스 메타정보
    1. 버전, 식별자 등
    2. 소스파일 이름
    3. 상속관련 정보들
  2. 클래스 접근자에 대한 정보
  3. 클래스 내에 선언된 필드들의 목록
  4. 메서드와 생성자들의 목록
  5. 어노테이션 정보
  6. 상수 목록들(Constant Pool)
  7. 바이트 코드
  8. 내부 클래스 정보

그림으로 나타내면 아래와 같다.

그림 1. Java클래스 도식도 

1-2. Internal Name

컴파일된 클래스에서, 클래스 이름들은 소스코드와는 다르게 내부적으로 "java/lang/String"과 같은식으로 사용이 된다. 이를 Internal Name이라고 부른다. 참고로, 자바의 Class 클래스를 이용해서 해당 이름을 얻어올 수 있다.

1-3. 타입 및 메서드 명명 규칙

컴파일된 클래스들의 타입은 코드와는 다르게 아래 표와 같이 명세를 해준다.

타입 Type Descriptor
Boolean Z
char C
byte B
Short S
Int I
Float F
Long J
double D
Object Ljava/lang/Object;
int[] [I
object[][] [[Ljava/lang/Object;

Primitive타입과는 다르게, Object 형태는 "L<경로>;", "[타입" 과 같이 명시합니다.

컴파일된 클래스에서 메서드 들은 아래 표와 같이 정의가 됩니다.

메서드 정의 메서드 Descriptor
void m(int i, float f) (IF)V
int m(Object o) (Ljava/lang/Object;)I
int[] m(int i, String s) (ILjava/lang/String;)[I
Object m(int[] i) ([I)Ljava/lang/Object;

메서드는 괄호안에 파라미터 타입들이 순서대로 열거되어있고, 괄호 밖에는 리턴타입이 명시가 되어있습니다.

2. 클래스 읽어오기(ClassReader)

ASM에서는 클래스를 읽어올때 ClassReader를 통해 클래스를 읽어들일 수 있습니다. ClassReader는 클래스를 읽어서 파싱한뒤 AST로 변환하는 역할을 수행합니다. 해당 AST는, ClassReader에 ClassVisitor를 넘겨줌으로써 AST를 순회할때 어떤동작을 수행할지 결정 할 수 있습니다. 이번예제에서는 ClassReader와 ClassVisitor를 이용해서 javap형태로 함수를 출력하는 예제를 수행합니다.

주의사항 : 시스템 클래스를 읽거나 변경해야되는 경우 따로 권한이 필요 할 수 있습니다.

아래는 ClassPrinter의 예제코드입니다.

package BCITest;

import org.objectweb.asm.*;

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

public class ClassPrinter extends ClassVisitor{

    public ClassPrinter() {
        super(ASM6);
    }

    @Override
    public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
        System.out.println(name + " extends " + superName + " {");
    }

    @Override
    public void visitSource(String source, String debug) {

    }

    @Override
    public void visitOuterClass(String owner, String name, String desc) {

    }

    @Override
    public AnnotationVisitor visitAnnotation(String desc, boolean visible) {
        return null;
    }

    @Override
    public void visitAttribute(Attribute attr) {

    }

    @Override
    public FieldVisitor visitField(int access, String name, String desc, String signature, Object value) {
        System.out.println("    " + desc + " " + name);
        return null;
    }

    @Override
    public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
        System.out.println("    " + name + desc);
        return null;
    }

    @Override
    public void visitEnd() {
        System.out.println("}");
    }
}

아래 코드는 위에서 정의한 클래스를 ClassReader를 이용해서 사용하는 예제입니다.

package BCITest;

import org.objectweb.asm.ClassReader;
import java.io.IOException;

import java.lang.reflect.Method;

// TODO : 시스템 클래스 읽어오는거 나중에 추가하기;;;
public class AsmExampleMain {

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

        ClassPrinter cp = new ClassPrinter();
        ClassReader cr = new ClassReader("BCITest.HelloWorld");
        cr.accept(cp,0);
    }
}

3. 클래스 생성하기(ClassWriter)

이번예제는 ClassWriter를 이용하여 Class를 생성하는 예제입니다. ClassWriter를 선언한뒤, visitField와 같은 메서드를 호출해서 AST를 만든 뒤에 byte배열 형태로 직렬화 해서 반환하는 방식으로 동작합니다.

아래코드는 이번예제에서 생성할 인터페이스의 구조입니다.

package BCITest;
public interface Comparable extends Mesurable {
    int LESS = -1;
    int EQUAL = 0;
    int GREATER = 1;
    int compareTo(Object o);
}

아래는 위와 같은 인터페이스를 ClassWriter를 이용해서 생성하는 예제코드이다

package BCITest;

import org.objectweb.asm.ClassWriter;

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

public class GeneratingClassExampleMain {

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

    public static void main(String[] args) {
        ClassWriter cw = new ClassWriter(0);
        cw.visit(V1_8, ACC_PUBLIC + ACC_ABSTRACT + ACC_INTERFACE,
        "BCITest/Comparable", null, "java/lang/Object",
                new String[]{"BCITest/Measurable"});
        cw.visitField(ACC_PUBLIC + ACC_FINAL + ACC_STATIC, "LESS", "I",
                null, new Integer(-1)).visitEnd();

        cw.visitField(ACC_PUBLIC + ACC_FINAL + ACC_STATIC, "EQUAL", "I",
                null, new Integer(0)).visitEnd();

        cw.visitField(ACC_PUBLIC + ACC_FINAL + ACC_STATIC, "GREATER", "I",
                null, new Integer(1)).visitEnd();

        cw.visitMethod(ACC_PUBLIC + ACC_ABSTRACT, "compareTo", "(Ljava/lang/Object;)I", null, null).visitEnd();
        cw.visitEnd();

        Class c = new MyClassLoader().defineClass("BCITest.Comparable", cw.toByteArray());

        System.out.println(c.getName());
    }
}

4. 클래스 내용 변경하기(Transformer && Visitor)

4-1. 클래스 내용 변경하는 과정

BCI를 이용해서 클래스 내용을 변경하는 과정은 아래와 같습니다.

그림2. BCI를 이용한 컴파일 된 클래스 변경과정

위 그림과 같이, ClassReader에서 클래스를 읽어들이고 파싱하서 Adapter에서 적절한 형태로 가공한 뒤에, Writer를 이용해서 가공된 클래스를 생성하는 형태로 구성됩니다.

코드로 보면, 아래와 같은 흐름이 되는것입니다.

byte[] b1 = ...;
ClassWriter cw = new ClassWriter(0);
// cv forwards all events to cw
ClassVisitor cv = new ClassVisitor(ASM4, cw) { };
ClassReader cr = new ClassReader(b1);
cr.accept(cv, 0);
byte[] b2 = cw.toByteArray(); // b2 represents the same class as b1

4-2. Transform 예제1 : 클래스 메서드 제거 하는 예제

이번 예제는, 클래스에 메서드를 BCI를 이용하여 추가하는 예제이다. VisitMethod함수에서 해당 메서드가 선언되었는지 여부를 판단하고, 만약 선언된 경우, null을 반환함으로써. 해당 메서드를 제외한 AST를 돌려주는 방식으로 동작을 하는 예제입니다. 예제 코드는 아래와 같습니다.

먼저, 아래 두 예제에서 접근할 클래스의 코드이다

package BCITest;

public class TestTargetClass {

    public int kkk = 10;

    public int function1(){
        return 0;
    }

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

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

RemoveMethodAdapter의 코드는 아래와 같다.

package BCITest;

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

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

public class RemoveMethodAdapter extends ClassVisitor {

    private String mName;
    private String mDesc;

    public RemoveMethodAdapter(ClassVisitor cv, String mName, String mDesc) {
        super(ASM6, cv);
        this.mName = mName;
        this.mDesc = mDesc;
    }

    @Override
    public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {

        if(name.equals(mName) && desc.equals(mDesc)){
            return null;
        }

        return cv.visitMethod(access, name, desc, signature, exceptions);
    }
}

선언한 RemoveMethod Adapter는 아래 코드와 같이 사용 하면 된다.

package BCITest;

import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.Type;

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

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

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

    private static byte[] removeMethod() throws IOException {
        ClassReader cr = new ClassReader("BCITest.TestTargetClass");
        ClassWriter cw = new ClassWriter(cr,0);
        //ClassVisitor cv = new RemoveMethodAdapter(cw,"function1","()I");
        ClassVisitor cv = new RemoveMethodAdapter(cw,"function1", Type.INT_TYPE.getDescriptor());
        cr.accept(cv, 0);
        return cw.toByteArray();
    }

    public static void main(String[] args) throws IOException, IllegalAccessException, InstantiationException {

        Class transformedClass = new MyClassLoader().defineClass("BCITest.TestTargetClass",removeMethod());

        Method[] methods = transformedClass.getDeclaredMethods();

        for(Method m : methods){
            System.out.printf("Method : %s\n",m.getName());
        }
}

4-3. Transform 예제2 : 클래스 멤버 추가 하는 예제

이번 예제는, 클래스에 멤버를 BCI를 이용하여 추가하는 예제이다. VisitField함수에서 이미 멤버가 선언되었는지 여부를 판단하고, 만약 추가가 되지않는다면 AST를 전부 순회하였을때 (즉, visitEnd가 호출되었을때) 멤버를 생성하는 방식으로 동작합니다. 예제 코드는 아래와 같습니다.

package BCITest;

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

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

public class AddFieldAdapter extends ClassVisitor {

    private int fAcc;
    private String fName;
    private String fDesc;
    private boolean isFieldPresent;

    public AddFieldAdapter(ClassVisitor cv, int fAcc, String fName, String fDesc) {
        super(ASM6, cv);
        this.fAcc = fAcc;
        this.fName = fName;
        this.fDesc = fDesc;
    }

    @Override
    public FieldVisitor visitField(int access, String name, String desc, String signature, Object value) {
        if(name.equals(fName)){
            isFieldPresent = true;
        }
        return cv.visitField(access, name, desc, signature, value);
    }

    @Override
    public void visitEnd() {
        if(!isFieldPresent){
            FieldVisitor fv = cv.visitField(fAcc, fName, fDesc, null, new Integer(42));
            if(fv != null){
                fv.visitEnd();
            }
        }
        cv.visitEnd();
    }
}

선언한 AddFieldAdapter는 아래 코드와 같이 사용 하면 됩니다.

package BCITest;

import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.Type;

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

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

public class TransformMethodExample {

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

    private static byte[] addField() throws IOException {
        ClassReader cr = new ClassReader("BCITest.TestTargetClass");
        ClassWriter cw = new ClassWriter(cr, 0);
        //ClassVisitor cv = new AddFieldAdapter(cw, ACC_PUBLIC,"NiceValue","I");
        ClassVisitor cv = new AddFieldAdapter(cw, ACC_PUBLIC,"NiceValue", Type.INT_TYPE.getDescriptor());
        cr.accept(cv, 0);
        return cw.toByteArray();
    }

    public static void main(String[] args) throws IOException, IllegalAccessException, InstantiationException {

        Class transformedClass2 = new MyClassLoader().defineClass("BCITest.TestTargetClass",addField());

        Field[] fields = transformedClass2.getDeclaredFields();

        for(Field f : fields){
            System.out.printf("Field : %s, Value : %d\n",f.getName(),f.getInt(transformedClass2.newInstance()));
        }
    }
}

4.4 미리 정의된 유용한 클래스들

ASM라이브러리에는 미리 정의된 다양한 클래스들이 있습니다. 이번 챕터에서 소개할 함수들은 아래와 같습니다.

  1. Type클래스 : 특정 타입에 대한 Internal Name, 타입 정보 등을 간편하게 얻어올 수 있는 클래스 입니다.
  2. TraceClassVisitor : ClassReader와 ClassWriter를 이용해서 생성한 클래스가 제대로 되었는지 확인을 편하게 해주는 클래스입니다. (예쁘게 Javap 형식으로 코드를 찍어줍니다.)

그 외에도 CheckClassAdapter, ASMifier와 같은 것들을 사용할 수 있습니다.

TraceClassVisitor의 예제 코드는 아래와 같습니다.

package BCITest;

import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.Type;
import org.objectweb.asm.util.CheckClassAdapter;
import org.objectweb.asm.util.TraceClassVisitor;

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

public class TraceClassExampleMain {

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

    private static byte[] traceExample() throws IOException {
        ClassReader cr = new ClassReader("BCITest.TestTargetClass"); // 여기다가 확인하고 싶은 클래스 넣기
        ClassWriter cw = new ClassWriter(cr, ClassWriter.COMPUTE_MAXS);
        TraceClassVisitor cv = new TraceClassVisitor(cw, printWriter);
        cr.accept(cv, 0);
        return cw.toByteArray();
    }

    public static void main(String[] args) throws IOException {
        traceExample();
    }
}
반응형
Comments