본문 바로가기

기술스택/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에 명령어 추가, 제거 또는 변경 작업을 수행
    }