본문 바로가기

기술스택/ASM - [Bytecode]

[A Java bytecode engineering library] - [Core API] 3. Method[3/3]

목차

    3.3. 도구들 (tools)

    org.objectweb.asm.commons 패키지에는 자신만의 어댑터를 정의하는데 유용할 수 있는 몇 가지 사전 정의된 메소드 어댑터들이 포함되어 있다.

    이 섹션에서는 그 중 세 가지를 소개하고,

    이들이 어떻게 섹션 3.2.4의 AddTimerAdapter 예제와 함께 사용될 수 있는지 보여준다.

     

    또한, 이전 장에서 본 도구들이 메소드 생성이나 변환을 용이하게 하는 방법도 보여준다.

     

    3.3.1. 기본 도구들

    섹션 2.3에서 소개된 도구들은 메소드에도 사용될 수 있다.

     

    타입(Type)

    많은 바이트코드 명령어들, 예를 들어 xLOAD, xADD 또는 xRETURN은 적용되는 타입에 따라 다르다.

    Type 클래스는 이러한 명령어들에 대해, 주어진 타입에 해당하는 opcode를 얻기 위해 사용될 수 있는 getOpcode 메소드를 제공한다. 이 메소드는 int 타입에 대한 opcode를 매개변수로 받고, 호출된 타입에 대한 opcode를 반환한다.

    예를 들어, t가 Type.FLOAT_TYPE과 같을 경우 t.getOpcode(IMUL)은 FMUL을 반환한다.

     

     TraceClassVisitor

    이 클래스는 이전 장에서 이미 소개되었으며, 방문하는 클래스의 텍스트 표현을 출력한다. 여기에는 이 장에서 사용된 형식과 매우 유사한 형태로 메소드의 텍스트 표현도 포함된다. 따라서, 변환 체인의 어느 지점에서든 생성되거나 변환된 메소드의 내용을 추적하는 데 사용될 수 있다. 예를 들어:

     

     

    이는 정적 블록 static { ... }을 어떻게 생성하는지, 즉 <clinit> 메소드(클래스 초기화를 위한)로 보여준다.

    만약 체인의 어느 지점에서 단일 메소드의 내용을 추적하고 싶다면, TraceClassVisitor 대신 TraceMethodVisitor를 사용할 수 있다(이 경우 백엔드를 명시적으로 지정해야 한다; 여기서는 Textifier를 사용한다):

     

    public MethodVisitor visitMethod(int access, String name,
                                     String desc, String signature, String[] exceptions) {
        MethodVisitor mv = cv.visitMethod(access, name, desc, signature,
                exceptions);
        if (debug && mv != null && ...){ // if this method must be traced
            Printer p = new Textifier(ASM4) {
                @Override
                public void visitMethodEnd() {
                    print(aPrintWriter); // print it after it has been visited
                }
            };
            mv = new TraceMethodVisitor(mv, p);
        }
        return new MyMethodAdapter(mv);
    }

     

    이 코드는 MyMethodAdapter에 의해 변환된 후의 메소드를 출력한다.

     

    CheckClassAdapter

    이 클래스도 이전 장에서 이미 소개되었으며, ClassVisitor 메소드가 적절한 순서로 호출되고 유효한 인수와 함께 사용되는지 확인한다. 그리고 MethodVisitor 메소드에 대해서도 같은 역할을 한다.

     

    따라서, 변환 체인의 어느 지점에서든 MethodVisitor API가 올바르게 사용되고 있는지 확인하는 데 사용될 수 있다. TraceMethodVisitor처럼, CheckMethodAdapter 클래스를 사용하여 단일 메소드를 확인하는 대신 전체 클래스를 확인할 수 있다:

     

    public MethodVisitor visitMethod(int access, String name,
                                     String desc, String signature, String[] exceptions) {
        MethodVisitor mv = cv.visitMethod(access, name, desc, signature,
                exceptions);
        if (debug && mv != null && ...){ // if this method must be checked
            mv = new CheckMethodAdapter(mv);
        }
        return new MyMethodAdapter(mv);
    }

     

    이 코드는 MyMethodAdapter가 MethodVisitor API를 올바르게 사용하고 있는지 확인한다.

    그러나 이 어댑터는 바이트코드가 정확한지 확인하지 않는다. 예를 들어, ISTORE 1 ALOAD 1이 유효하지 않다는 것을 감지하지 못한다. 사실, 이러한 종류의 오류는 CheckMethodAdapter의 다른 생성자를 사용하고, visitMaxs에서 유효한 maxStack 및 maxLocals 인수를 제공하는 경우 감지될 수 있다. (java docs을 참조).

     

    ASMifier

    이 클래스도 이전 장에서 이미 소개되었으며, 메소드의 내용과 함께 작동한다.

    ASM을 사용하여 컴파일된 코드를 어떻게 생성하는지 알고 싶다면, 해당 소스 코드를 Java로 작성하고, javac로 컴파일한 다음, ASMifier를 사용하여 이 클래스를 방문하면 된다. 그러면 소스 코드에 해당하는 바이트코드를 생성하기 위한 ASM 코드를 얻을 수 있다.

     

    3.3.2. AnalyzerAdapter

    이 메소드 어댑터는 visitFrame에서 방문한 프레임을 기반으로 각 명령어 앞에서 스택 맵 프레임을 계산한다. 

    실제로, 섹션 3.1.5에서 설명한 바와 같이, visitFrame은 메소드 내의 특정 명령어 앞에서만 호출되어 공간을 절약하기 위함이며, "다른 프레임은 이들로부터 쉽고 빠르게 추론될 수 있다". 

     

    이 어댑터는 바로 이 작업을 수행한다. 물론 이것은 Java 6 이상으로 컴파일되었거나(COMPUTE_FRAMES 옵션을 사용하는 ASM 어댑터로 Java 6으로 업그레이드된 경우) 사전 계산된 스택 맵 프레임을 포함하는 클래스에서만 작동한다.


    AddTimerAdapter 예제의 경우, 이 어댑터는 RETURN 명령어 바로 앞에서 피연산자 스택의 크기를 얻는 데 사용될 수 있으며, 이를 통해 visitMaxs에서 maxStack의 최적 변환 값을 계산할 수 있다(실제로는 COMPUTE_MAXS를 사용하는 것이 훨씬 효율적이기 때문에 이 방법은 권장되지 않는다):

     

    class AddTimerMethodAdapter2 extends AnalyzerAdapter {
        private int maxStack;
        public AddTimerMethodAdapter2(String owner, int access,
                                      String name, String desc, MethodVisitor mv) {
            super(ASM4, owner, access, name, desc, mv);
        }
        @Override public void visitCode() {
            super.visitCode();
            mv.visitFieldInsn(GETSTATIC, owner, "timer", "J");
            mv.visitMethodInsn(INVOKESTATIC, "java/lang/System",
                    "currentTimeMillis", "()J");
            mv.visitInsn(LSUB);
            mv.visitFieldInsn(PUTSTATIC, owner, "timer", "J");
            maxStack = 4;
        }
        @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");
                maxStack = Math.max(maxStack, stack.size() + 4);
            }
            super.visitInsn(opcode);
        }
        @Override public void visitMaxs(int maxStack, int maxLocals) {
            super.visitMaxs(Math.max(this.maxStack, maxStack), maxLocals);
        }
    }

     

    stack 필드는 AnalyzerAdapter 클래스에 정의되어 있으며, 피연산자 스택에 있는 타입들을 포함한다. 보다 정확히는, visitXxx Insn을 방문하고, 오버라이드된 메소드가 호출되기 전에, 이 리스트는 이 명령어 바로 앞의 피연산자 스택의 상태를 포함한다. 오버라이드된 메소드들은 스택 필드가 올바르게 업데이트되도록 호출되어야 한다(따라서 원본 코드에서 super 대신 mv를 사용하는 것).

     

    대안적으로, super 클래스의 메소드를 호출함으로써 새로운 명령어들을 삽입할 수 있다: 이 효과는 이러한 명령어들에 대한 프레임이 AnalyzerAdapter에 의해 계산될 것임을 의미한다. 또한, 이 어댑터는 계산된 프레임을 기반으로 visitMaxs의 인수들을 업데이트하므로, 우리는 스스로 업데이트할 필요가 없다:

     

    class AddTimerMethodAdapter3 extends AnalyzerAdapter {
        public AddTimerMethodAdapter3(String owner, int access,
                                      String name, String desc, MethodVisitor mv) {
            super(ASM4, owner, access, name, desc, mv);
        }
        @Override public void visitCode() {
            super.visitCode();
            super.visitFieldInsn(GETSTATIC, owner, "timer", "J");
            super.visitMethodInsn(INVOKESTATIC, "java/lang/System",
                    "currentTimeMillis", "()J");
            super.visitInsn(LSUB);
            super.visitFieldInsn(PUTSTATIC, owner, "timer", "J");
        }
        @Override public void visitInsn(int opcode) {
            if ((opcode >= IRETURN && opcode <= RETURN) || opcode == ATHROW) {
                super.visitFieldInsn(GETSTATIC, owner, "timer", "J");
                super.visitMethodInsn(INVOKESTATIC, "java/lang/System",
                        "currentTimeMillis", "()J");
                super.visitInsn(LADD);
                super.visitFieldInsn(PUTSTATIC, owner, "timer", "J");
            }
            super.visitInsn(opcode);
        }
    }
    

     

    3.3.3. LocalVariablesSorter

    이 메소드 어댑터는 메소드에서 사용된 지역 변수들을 이 메소드에서 나타나는 순서대로 재번호한다.

    예를 들어, 두 개의 매개변수가 있는 메소드에서, 인덱스가 3 이상인 첫 번째 지역 변수(첫 세 개의 지역 변수는 this와 두 개의 메소드 매개변수에 해당하며 변경될 수 없다)를 읽거나 쓰는 경우, 그 변수에는 인덱스 3이 할당되고, 두 번째 변수에는 인덱스 4가 할당되는 식이다.

     

    이 어댑터는 메소드에 새로운 지역 변수를 삽입하는 데 유용하다.

    이 어댑터 없이는 새로운 지역 변수를 기존 변수들 뒤에 추가해야 하지만, 불행히도 그들의 수는 메소드의 끝에서야 알려진다, visitMaxs에서. 이 어댑터를 어떻게 사용할 수 있는지 보여주기 위해, AddTimerAdapter를 구현하기 위해 지역 변수를 사용하고 싶다고 가정해보자:

     

    public class C {
        public static long timer;
        public void m() throws Exception {
            long t = System.currentTimeMillis();
            Thread.sleep(100);
            timer += System.currentTimeMillis() - t;
        }
    }
    
    

     

    이 작업은 LocalVariablesSorter를 확장하고, 이 클래스에서 정의된 newLocal 메소드를 사용함으로써 쉽게 수행될 수 있다:

    class AddTimerMethodAdapter4 extends LocalVariablesSorter {
        private int time;
        public AddTimerMethodAdapter4(int access, String desc,
                                      MethodVisitor mv) {
            super(ASM4, access, desc, mv);
        }
        @Override public void visitCode() {
            super.visitCode();
            mv.visitMethodInsn(INVOKESTATIC, "java/lang/System",
                    "currentTimeMillis", "()J");
            time = newLocal(Type.LONG_TYPE);
            mv.visitVarInsn(LSTORE, time);
        }
        @Override public void visitInsn(int opcode) {
            if ((opcode >= IRETURN && opcode <= RETURN) || opcode == ATHROW) {
                mv.visitMethodInsn(INVOKESTATIC, "java/lang/System",
                        "currentTimeMillis", "()J");
                mv.visitVarInsn(LLOAD, time);
                mv.visitInsn(LSUB);
                mv.visitFieldInsn(GETSTATIC, owner, "timer", "J");
                mv.visitInsn(LADD);
                mv.visitFieldInsn(PUTSTATIC, owner, "timer", "J");
            }
            super.visitInsn(opcode);
        }
        @Override public void visitMaxs(int maxStack, int maxLocals) {
            super.visitMaxs(maxStack + 4, maxLocals);
        }
    }
    

     

    지역 변수가 재번호될 때 원본 메소드와 관련된 프레임이 유효하지 않게 되고, 새로운 지역 변수가 삽입될 때 더더욱 그렇다.

    다행히 이 프레임들을 처음부터 다시 계산할 필요는 없다: 실제로 프레임을 추가하거나 제거할 필요는 없으며, 원본 프레임의 지역 변수 내용을 재정렬하기만 하면 변환된 메소드의 프레임을 얻을 수 있다.

     

    LocalVariablesSorter는 이를 자동으로 처리한다. 메소드 어댑터에 대해 증분 스택 맵 프레임 업데이트가 필요한 경우, 이 클래스의 소스에서 영감을 얻을 수 있다. 위에서 볼 수 있듯이, 지역 변수를 사용하는 것은 이 클래스의 원본 버전에서 가진 maxStack에 대한 최악의 경우 값을 해결하지 않는다. AnalyzerAdapter를 사용하여 그 문제를 해결하고 싶다면, LocalVariablesSorter와 함께, 상속 대신 대리(delegation)을 통해 이 어댑터들을 사용해야 한다(다중 상속이 불가능하기 때문에):

     

    class AddTimerMethodAdapter5 extends MethodVisitor {
        public LocalVariablesSorter lvs;
        public AnalyzerAdapter aa;
        private int time;
        private int maxStack;
        public AddTimerMethodAdapter5(MethodVisitor mv) {
            super(ASM4, mv);
        }
        @Override public void visitCode() {
            mv.visitCode();
            mv.visitMethodInsn(INVOKESTATIC, "java/lang/System",
                    "currentTimeMillis", "()J");
            time = lvs.newLocal(Type.LONG_TYPE);
            mv.visitVarInsn(LSTORE, time);
            maxStack = 4;
        }
        @Override public void visitInsn(int opcode) {
            if ((opcode >= IRETURN && opcode <= RETURN) || opcode == ATHROW) {
                mv.visitMethodInsn(INVOKESTATIC, "java/lang/System",
                        "currentTimeMillis", "()J");
                mv.visitVarInsn(LLOAD, time);
                mv.visitInsn(LSUB);
                mv.visitFieldInsn(GETSTATIC, owner, "timer", "J");
                mv.visitInsn(LADD);
                mv.visitFieldInsn(PUTSTATIC, owner, "timer", "J");
                maxStack = Math.max(aa.stack.size() + 4, maxStack);
            }
            mv.visitInsn(opcode);
        }
        @Override public void visitMaxs(int maxStack, int maxLocals) {
            mv.visitMaxs(Math.max(this.maxStack, maxStack), maxLocals);
        }
    }

     

    이 어댑터를 사용하기 위해서는 LocalVariablesSorter를 AnalyzerAdapter에 연결해야 하며, 그 자체가 당신의 어댑터에 연결된다:

     

    1. 첫 번째 어댑터는 지역 변수를 정렬하고 프레임을 그에 따라 업데이트할 것이며,

    2. 분석 어댑터는 이전 어댑터에서 수행된 재번호를 고려하여 중간 프레임을 계산할 것이고,

    3. 당신의 어댑터는 이러한 재번호된 중간 프레임에 접근할 수 있을 것이다.

    이 체인은 visitMethod에서 다음과 같이 구성될 수 있다:

     

    mv =cv.visitMethod(access,name,desc,signature,exceptions);
    if(!isInterface &&mv !=null&&!name.equals("<init>")) {
            AddTimerMethodAdapter5 at = new AddTimerMethodAdapter5(mv);
            at.aa = new AnalyzerAdapter(owner, access, name, desc, at);
            at.lvs = new LocalVariablesSorter(access, desc, at.aa);
            return at.lvs; 
       }

     

    3.3.4. AdviceAdapter

     

    이 메소드 어댑터는 추상 클래스로, 메소드의 시작 부분과 모든 RETURN 또는 ATHROW 명령어 바로 전에 코드를 삽입하는 데 사용될 수 있다.

     

    주요 장점은 생성자에도 작동한다는 것으로, 코드는 생성자의 시작 부분에 바로 삽입되어서는 안 되지만, 슈퍼 생성자 호출 이후에 삽입되어야 한다. 실제로, 이 어댑터의 대부분의 코드는 이 슈퍼 생성자 호출을 감지하는 데 전념한다.

     

    섹션 3.2.4의 AddTimerAdapter 클래스를 주의 깊게 살펴보면, 이 문제 때문에 AddTimerMethodAdapter가 생성자에 사용되지 않는 것을 볼 수 있다. AdviceAdapter에서 상속함으로써 이 메소드 어댑터는 생성자에서도 작동하도록 개선될 수 있다(AdviceAdapter가 LocalVariablesSorter에서 상속되므로, 지역 변수를 쉽게 사용할 수도 있다):

     

    class AddTimerMethodAdapter6 extends AdviceAdapter {
        public AddTimerMethodAdapter6(int access, String name, String desc,
                                      MethodVisitor mv) {
            super(ASM4, mv, access, name, 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(maxStack + 4, maxLocals);
        }
    }