본문 바로가기

기술스택/ASM - [Bytecode]

[ASM][TreeAPI]Tree API에서 주로 사용하는 클래스 및 메서드

개요

 

Tree API에서 주로 사용하는 클래스 및 메서드에는 여러 가지가 있다.

기본적으로 ClassNode, MethodNode, InsnList가 중요한 역할을 하지만, 이 외에도 여러 유용한 클래스와 인터페이스가 있다. 여기에는 다음과 같은 것들이 포함된다.

 

  • ClassNode : 클래스 노드. 트리 노드의 최상단 부모라고 생각하자.
  • MethodNode : 클래스의 메서드를 나타낸다. 메서드의 접근 지정자, 이름, 설명 등을 포함할 수 있다.
  • FieldNode: 클래스의 필드를 나타낸다. 필드의 접근 지정자, 이름, 설명, 초기값 등을 포함할 수 있다.
  • AnnotationNode: 어노테이션을 나타낸다. 어노테이션의 설명자와 값들을 포함한다.
  • VarInsnNode: 메서드의 지역 변수를 나타낸다. 변수의 이름, 설명자, 시작 및 종료 레이블, 인덱스 등의 정보를 포함한다.

이들은 ASM의 Tree API를 사용할 때 매우 중요한 구성 요소들이며, 클래스나 메서드를 분석하고 수정하는데 필수적인 역할을 한다.

 

예제 및 설명

MethodNode, InsnList 적용

 

MethodNode는 하나의 메서드 블록을 의미하고, InsnList는 지시 사항(즉, 코드 자체)의 List를 의미한다.
다음 구문을 통해 메서드와 지시 사항을 적용하는 코드를 살펴보자.

 

이해가 안 된다면 아래 포스팅을 참조할 것

https://csg1353.tistory.com/184

 

[ASM 바이트코드 작성] if문과 예외를 포함한 메서드 추가 예제(중요)

예제 코드 package org.agent.util.asm.testcode.chap03; import org.objectweb.asm.*; import static org.objectweb.asm.Opcodes.*; public class SetF extends ClassVisitor { public SetF(ClassVisitor classVisitor) { super(Opcodes.ASM9, classVisitor); } @Overrid

csg1353.tistory.com

 

        //기본 생성자 생성 후 적용
        MethodNode mn = new MethodNode(ACC_PUBLIC, "<init>", "()V", null, null); //메서드 노드
        InsnList il = new InsnList(); //지시사항
        il.add(new VarInsnNode(ALOAD, 0)); //생성자 스택 적재
        il.add(new MethodInsnNode(INVOKESPECIAL, "java/lang/Object", "<init>", "()V", false)); //init 호출
        il.add(new FieldInsnNode(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;"));
        il.add(new LdcInsnNode("생성자 생성됨."));
        il.add(new MethodInsnNode(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false));
        il.add(new InsnNode(RETURN));
        mn.instructions = il;   //메서드 노드에 지시사항 추가
        cn.methods.add(mn);     //클래스에 메서드 노드 추가


1. mn 인스턴스는 메서드 생성자를 의미하는 메서드 블록이다. 
이것을 고수준으로 추상화하면 public init(클래스이름) () {} 을 의미한다. 이는 생성자를 뜻한다.

 

2. il.add(new VarInsnNode(ALOAD, 0));

자기 자신(this)의 정보를 stack frame에 적재한다.

이 구문은 생성자의 첫 번째 지시사항으로, 'this' 참조(자기 자신의 객체 참조)를 스택에 푸시한다.

모든 인스턴스 메서드(생성자 포함) 호출 시, JVM은 'this' 참조를 메서드의 첫 번째 파라미터로 전달한다.

 

3. il.add(new MethodInsnNode(INVOKESPECIAL, "java/lang/Object", "<init>", "()V", false)); //init 호출

2번에 적재한 operand 스택을 소모하여, 자기 자신의 init(생성자)을 호출한다. 

 

이 부분은 자바에서 super() 호출과 동일하다. (일반적으로 생략하긴 하지만, 모든 생성자는 super를 통해 부모 클래스를 호출한다.)

 

여기서 INVOKESPECIAL은 특수 메서드 호출(생성자나 private 메서드)를 나타내며, java/lang/Object의 생성자를 호출한다. 즉, 현재 객체의 상위 클래스(Object 클래스) 생성자를 호출합니다.

 

4. 다음 세 줄은 출력 구문(System.out.println)을 의미한다.

il.add(new FieldInsnNode(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;"));
il.add(new LdcInsnNode("생성자 생성됨."));
il.add(new MethodInsnNode(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false));

 

5. il.add(new InsnNode(RETURN));

return 구문을 통해 il 지시문의 마지막 리턴 값을 호출한다.

 

6. mn.instructions = il;

이러한 지시사항의 List 타입을 Method 블록인 MethodNode에 적용한다.

 

7.cn.methods.add(mn);     

마지막으로 이 메서드 블록을 ClassNode cn에 추가한다.

 

최종 적용 코드는 다음과 같다.

 

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//

public class test1 {

    public test1() {
        System.out.println("생성자 생성됨.");
    }

}

 

FieldNode 를 추가하여 적용

Class 내부의 필드 파라미터는 다음과 같이 적용할 수 있다.

이전 로직에 포함해서 적용해보자.

      cn.version = V21;
        cn.access = ACC_PUBLIC;
        cn.name = className; //패키지 + 클래스명
        cn.superName = "java/lang/Object"; //객체 타입
//        cn.interfaces.add(""); //implements

        //기본 생성자 생성 후 적용
        MethodNode mn = new MethodNode(ACC_PUBLIC, "<init>", "()V", null, null); //메서드 노드
        InsnList il = new InsnList(); //지시사항
        il.add(new VarInsnNode(ALOAD, 0)); //생성자 스택 적재
        il.add(new MethodInsnNode(INVOKESPECIAL, "java/lang/Object", "<init>", "()V", false)); //init 호출
        il.add(new FieldInsnNode(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;"));
        il.add(new LdcInsnNode("생성자 생성됨."));
        il.add(new MethodInsnNode(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false));
        il.add(new InsnNode(RETURN));
        mn.instructions = il;   //메서드 노드에 지시사항 추가
        cn.methods.add(mn);     //클래스에 메서드 노드 추가

        //필드값
        cn.fields.add(new FieldNode(ACC_PUBLIC + ACC_FINAL + ACC_STATIC,
                "LESS", "I", null, -1));
        cn.fields.add(new FieldNode(ACC_PUBLIC + ACC_FINAL + ACC_STATIC,
                "EQUAL", "I", null, 0));
        cn.fields.add(new FieldNode(ACC_PUBLIC + ACC_FINAL + ACC_STATIC,
                "GREATER", "I", null, 1));
        cn.methods.add(new MethodNode(ACC_PUBLIC + ACC_ABSTRACT,
                "compareTo", "(Ljava/lang/Object;)I", null, null));

 

최종 결과는 다음과 같다.

 

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//

public class test1 {
    public static final int LESS = -1;
    public static final int EQUAL = 0;
    public static final int GREATER = 1;

    public test1() {
        System.out.println("생성자 생성됨.");
    }

    public abstract int compareTo(Object var1);
}

 

지역 변수 추가

public class BasicClassLocalValue {
    static private ClassNode cn = new ClassNode();

    public static ClassNode getClassNode(String className) {
        cn.version = V21;
        cn.access = ACC_PUBLIC;
        cn.name = className; //패키지 + 클래스명
        cn.superName = "java/lang/Object"; //객체 타입
//        cn.interfaces.add(""); //implements

        //기본 생성자 생성 후 적용
        MethodNode mn = new MethodNode(ACC_PUBLIC, "<init>", "()V", null, null); //메서드 노드
        InsnList il = new InsnList(); //지시사항
        il.add(new VarInsnNode(ALOAD, 0)); //생성자 스택 적재
        il.add(new MethodInsnNode(INVOKESPECIAL, "java/lang/Object", "<init>", "()V", false)); //init 호출
        il.add(new FieldInsnNode(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;"));
        il.add(new LdcInsnNode("생성자 생성됨. BasicClassLocalValue"));
        il.add(new MethodInsnNode(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false));
        il.add(new InsnNode(RETURN));
        mn.instructions = il;   //메서드 노드에 지시사항 추가
        cn.methods.add(mn);     //클래스에 메서드 노드 추가

        //특정 메서드의 지역 변수 넣기,
        //Static 정적 변수는 this 지역변수 0번 인덱스를 가지지 않는다. 이를 유의
        MethodNode mnLocalValue = new MethodNode(ACC_PUBLIC + ACC_STATIC, "addTwoNumbers", "()I", null, null);
        il = new InsnList();

        // 지역 변수 'a'에 5 할당
        il.add(new LdcInsnNode(5));
        il.add(new VarInsnNode(ISTORE, 1));

        // 지역 변수 'b'에 10 할당
        il.add(new LdcInsnNode(10));
        il.add(new VarInsnNode(ISTORE, 2));

        // 'a'와 'b' 불러와 더하기
        il.add(new VarInsnNode(ILOAD, 1));
        il.add(new VarInsnNode(ILOAD, 2));
        il.add(new InsnNode(IADD));

        // 결과 반환
        il.add(new InsnNode(IRETURN));

        mnLocalValue.instructions = il; //메서드 지시사항 추가
        cn.methods.add(mnLocalValue); //메서드 추가
        return cn;
    }
}

 

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//

public class testLocalClass {
    public testLocalClass() {
        System.out.println("생성자 생성됨. BasicClassLocalValue");
    }

    public static int addTwoNumbers() {
        byte var1 = 5;
        byte var2 = 10;
        return var1 + var2;
    }
}

클래스 변조의 경우

클래스 변조의 경우 ClassNode 내부 remove 메서드를 적용한다.

 

기본 개요는 다음과 같다.

premain에서 ClassFileTransformer 구현체를 통해 모든 클래스 빌드 시 바이트코드를 호출할 것이다.

@Slf4j
public class TreeAPITransformer implements ClassFileTransformer {


    @Override
    public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,
                            ProtectionDomain protectionDomain, byte[] classfileBuffer) {
        String containsName = "BasicExample"; //스캔할 클래스명

        if (className.contains(containsName)) {
            log.warn("[TRANSFORM] Find ClassName : {}", containsName);
            byte[] modifiedCode = init(classfileBuffer); //here to modify
            CodePrinter.printClass(modifiedCode, containsName); //print
            return modifiedCode;
        } else {
            return classfileBuffer;
        }

    }

	//ByteCode -> ClassReader가 읽음 -> ClassNode accept ->
    //ClassNode 변조(제거) -> ClassWriter accept -> 최종 결과물을 다시 바이트코드로 변환
    public byte[] init(byte[] bytes) {
        ClassReader classReader = new ClassReader(bytes);
        ClassNode cn = new ClassNode();
        classReader.accept(cn, 0); //바이트코드 제공
        ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES | ClassWriter.COMPUTE_MAXS);
        cn = BasicRemoveMethod.removeMethod(cn, "main", "()V"); //<< ClassNode testcode 적용점
        cn = BasicRemoveField.removeField(cn, "staticVal"); //<< ClassNode testcode 적용점
        cn.accept(cw); //변조된 classNode 바이트코드 제공
        return cw.toByteArray();
    }
}

 

메서드와 필드 제거 

public class BasicRemoveMethod {

    public static ClassNode removeMethod(ClassNode cn, String methodName, String methodDesc) {
        cn.methods.removeIf(methodNode -> methodNode.name.equals(methodName) && methodNode.desc.equals(methodDesc));
        return cn;
    }
}

 

public class BasicRemoveField {
    public static ClassNode removeField(ClassNode cn, String fieldName) {
        cn.fields.removeIf(fieldNode -> fieldNode.name.equals(fieldName));
        return cn;
    }
}

 

removeIf를 사용하여 각 노드의 하위 필드를 순회하며 동일 조건일 경우 제거하게 된다.

세부 로직(라이브러리 코드)는 다음과 같다.

    default boolean removeIf(Predicate<? super E> filter) {
        Objects.requireNonNull(filter);
        boolean removed = false;
        final Iterator<E> each = iterator();
        while (each.hasNext()) {
            if (filter.test(each.next())) {
                each.remove();
                removed = true;
            }
        }
        return removed;
    }

 

 

메서드와 필드 변조

변조의 경우는 제거와 유사하다.

기존의 부분을 제거하고, 제거된 부분을 다시 채워넣으면 될 것이다.

 

public void modifyMethodInstructions(MethodNode mn) {
    InsnList il = mn.instructions;
    // il에 명령어 추가, 제거 또는 변경 작업을 수행
}