본문 바로가기

기술스택/ASM - [Bytecode]

[ASM]]MethodVisitor를 통해 특정 메서드 변조하기

목차

     

     

    나는 m의 메서드를 찾아, 여기 sysout을 통해 로깅을 붙이고 싶다..!

     

    바로 코드를 보자. (A)의 경우 정상적인 동작을 하고, (B)의 경우 잘못된 코드이다.

     

    public class WrappingMethodVisitor extends ClassVisitor {
        public WrappingMethodVisitor(ClassVisitor classVisitor) {
            super(Opcodes.ASM9, classVisitor);
        }
    
        //각 메서드 방문시 호출
        @Override
        public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
            MethodVisitor mv = cv.visitMethod(access, name, desc, signature, exceptions);
    
            // (A) 올바른 로직
            if (name.equals("m")) {
                // MethodVisitor를 래핑하는 Adapter를 생성하여 커스텀 로직 적용
                return new MethodVisitor(ASM9, mv) {
                    @Override
                    public void visitCode() {
                        super.visitCode(); 
                        // `System.out.println` 호출을 위한 바이트코드 삽입
                        mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
                        mv.visitLdcInsn("Method m is modified.");
                        mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
                    }
                };
            }
    
            // (B) 이렇게 할 경우 적용되지 않음
            if (name.equals("m")) {
                mv.visitCode(); 
                //ps : 이렇게 해도 이 로직은 작용하지 않는다.
    
                // `System.out.println` 호출을 위한 바이트코드 삽입
                mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
                mv.visitLdcInsn("Method m is modified.");
                mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
                return mv;
            }
            return mv; // 조건에 해당하지 않는 경우, 변경 없이 mv 반환
        }
    
        @Override
        public void visitEnd() {
            super.visitEnd();
        }
    
    }

     

    visitMethod 순회

     

    ClassVisitor의 visitMethod 메서드는 클래스의 각 메서드를 순회할 때 호출되며, 각 메서드에 대한 MethodVisitor 인스턴스를 제공한다.

     

     

    즉, 최외곽 MethodVisitor(visitMethod)의 경우 바이트코드 순회를 방문자 패턴을 통해 발생시킨다.

    이 경우 메서드로 분류된 Class 내부의 요소들을 전부 순회한다.

     

        //각 메서드 방문시 호출
        @Override
        public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
            MethodVisitor mv = cv.visitMethod(access, name, desc, signature, exceptions);//바이트코드
    		
            //(로직)
            
            return mv; //
        }

     

     

    이 부분을 보자. visitMethod를 통해 각 메서드의 바이트코드 값을 mv로 가져오고 있다.

    즉, 모든 메서드를 순회하며, Class 전체의 코드가 아닌 '특정 메서드 블록의 바이트코드'를 mv에 담았다.

     

    이 mv값을 수정하면 실제 코드가 변조되는 것이다.

    그러면 바로 코드를 넣으면 되는 것 아닐까?

    아니다. 이 경우 모든 메서드에 코드가 추가되게 된다. (모든 메서드를 순회하기에)

     

    그래서 if문으로 조건을 다는 것이다.

     

        //각 메서드 방문시 호출
        @Override
        public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
            MethodVisitor mv = cv.visitMethod(access, name, desc, signature, exceptions);
    		
            //m 메서드만 변조하고 싶다. 다른 메서드는 바로 (a)로 보내자.
            if(name.equals("m") {
            	(로직)
                return mv; //(b) : 로직에 의해 변경된 코드
            }
            
            return mv; //(a) 변경 없이 종료
        }

     

    이런 식으로 표현할 수 있을 것이다.

     

     

    MethodVisitor를 래핑하는 Adapter를 생성

    'm' 메서드를 만났을 때, 커스텀 로직을 수행하기 위해 새로운 MethodVisitor 인스턴스를 리턴하게 하면 된다.

    이 인스턴스는 'm' 메서드의 바이트코드를 조작하기 위한 "후크" 역할을 한다.

     

    여기서 (로직)에 속한 내용은 다음과 같다.

    mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
    mv.visitLdcInsn("Method m is modified.");
    mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);

     

    이 코드를 어떻게 담을 수 있을까? 정답은 '새로운 mv 변조값을 return하는 Adapter'를 생성하여 반환하면 된다.

    로직으로 확인하면 다음과 같다.

     

    new MethodVisitor(ASM9, mv) {
        @Override
        public void visitCode() {
            super.visitCode(); //메서드 방문의 시작을 ASM 시스템에 알리고, 필요한 초기화 작업을 수행
            // `System.out.println` 호출을 위한 바이트코드 삽입
            mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
            mv.visitLdcInsn("Method m is modified.");
            mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
        }
    };

     

    차분히 설명해보자.

     

    1. 이 로직은 new MethodVisitor를 통해 변조된 mv값을 리턴한다.

    = 즉, 변조된 바이트코드(내 코드가 추가된) mv를 리턴한다는 것이다.

     

    2. 이 메서드는 생성자를 통해 이전 체인(mv 메서드 블록)에서 이어진 mv값을 받는다.

     

     mv의 경우 애초에 Class의 전체 바이트코드가 아닌, 1번에서 순회하던 'MethodVisitor' 단위의 블록이다.

    쉽게 말하면 메서드 단위로 받았던 mv를 이 함수에서 그대로 받아온다는 것이다.  

    이 mv값에 Override visitCode()를 통해 변수 조작을 수행한다.

     

    3. 조작의 범위

    이 조작은 현재 순회 중인 메서드('m' 메서드)의 바이트코드에만 적용된다.

    이는 각 MethodVisitor 인스턴스가 특정 메서드의 바이트코드 블록에 대한 조작을 담당한다는 것을 의미한다.

     

    4. 조작 완료 후

    사실상 return mv(변조 값); 이므로 이 메서드 방문을 끝내고, 다음 체인으로 이동한다.

    후크 내부의 로직은 이제 사라진다. (여기서만 사용된 후크가 return하면서 끝났으므로)

     

     

    Adapter Class로 분리

    코드가 번잡해질 수 있으니, 저 new MethodVisitor를 Adapter Class로 분리해보자.

    훨씬 간단해진 것을 확인할 수 있다.

    package org.agent.util.asm.testcode.chap03;
    
    import org.objectweb.asm.*;
    
    import static org.objectweb.asm.Opcodes.*;
    
    
    public class StatelessTransformationsExample extends ClassVisitor {
        public StatelessTransformationsExample(ClassVisitor classVisitor) {
            super(Opcodes.ASM9, classVisitor);
            classVisitor.visitField(ACC_PUBLIC + ACC_STATIC, "timer", "J", null, null);
        }
    
    
        //각 메서드 방문시 호출
        @Override
        public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
            MethodVisitor mv = cv.visitMethod(access, name, desc, signature, exceptions);
            // MethodVisitor를 래핑하는 Adapter를 생성하여 커스텀 로직 적용
            if (name.equals("m")) {  
                return new AddTimerMethodAdapter(mv);
            }
            return mv; // 조건에 해당하지 않는 경우, 변경 없이 mv 반환
        }
    
        @Override
        public void visitEnd() {
            super.visitEnd();
        }
    
        class AddTimerMethodAdapter extends MethodVisitor {
            public AddTimerMethodAdapter(MethodVisitor mv) {
                super(ASM9, mv);
            }
    
            @Override
            public void visitCode() {
                super.visitCode(); 
                // `System.out.println` 호출을 위한 바이트코드 삽입
                mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
                mv.visitLdcInsn("Method m is modified.");
                mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
            }
        }
    }

     

    B는 왜 에러가 발생하나?

     

    B의 접근 방식에서는 visitCode와 같은 초기화 메서드를 명시적으로 호출하지 않고 직접적인 바이트코드 조작을 시도하는데, 이는 ASM의 내부 조작 순서와 초기화 과정을 무시한다.

    이로 인해 조작이 제대로 반영되지 않거나, 생성된 바이트코드가 JVM에서 실행될 때 오류를 발생시킬 수 있다.