본문 바로가기

기술스택/ASM - [Bytecode]

[A Java bytecode engineering library] - [Core API] 2. Classes[1/2]

목차

    실습 파일 및 내용 요약

    https://csg1353.tistory.com/167

     

    [ASM Library Guildline]chap02 실습파일

    Logic package com.sch.testapm.test.controller; import com.sch.testapm.reference.chap2.ClassPrinter; import lombok.extern.slf4j.Slf4j; import org.objectweb.asm.ClassReader; import org.objectweb.asm.ClassWriter; import org.springframework.web.bind.annotation

    csg1353.tistory.com

     

     

     

    이 장에서는 컴파일된 자바 클래스의 멤버 제거, 인터페이스 및 컴포넌트 추가, 변환 체인, 도구 등에 대해 설명한다.

     

    1. 클래스 버전을 변환하는 방법은 다른 ClassVisitor 메소드에도 적용될 수 있으며, 필드나 메소드의 수정자나 이름을 변경할 수 있다.

    - RemoveDebugAdapter 클래스 어댑터는 외부 및 내부 클래스 정보와 클래스가 컴파일된 소스 파일 이름을 제거한다.

    이러한 정보는 디버깅 목적으로만 사용되기 때문에 제거해도 클래스는 여전히 완전히 동작한다.

     

    2. 또한, 클래스 멤버를 추가하는 방법도 설명된다.

    - AddFieldAdapter 클래스 어댑터는 존재하지 않는 필드를 클래스에 추가한다. 이는 visitEnd 메소드에서 추가된다.

    - 클래스 변환 체인은 ClassReader, 클래스 어댑터, ClassWriter로 구성될 수 있으며, 여러 클래스 어댑터를 연결하여 복잡한 변환을 수행할 수 있다.

     

    ASM은 클래스 생성기나 어댑터 개발에 유용한 여러 도구를 제공한다.

    이 도구들은 runtime에 필요하지 않지만, 내부 이름, 타입 설명자 및 메소드 설명자를 조작하는 데 유용하다.

     

    - Type 클래스 : 자바 타입을 나타내며, 타입 설명자나 Class 객체에서 구성될 수 있다.

    - TraceClassVisitor 클래스 : 클래스의 텍스트 표현을 생성하여 생성 또는 변환된 클래스가 기대에 부합하는지 확인하는 데 사용된다.

    - CheckClassAdapter 클래스 : 방문한 클래스의 메소드가 적절한 순서와 유효한 인수로 호출되는지 확인한다.

    - ASMifier 클래스 : TraceClassVisitor 도구에 대한 대안적 백엔드를 제공하며, TraceClassVisitor 클래스의 각 메소드에 대한 자바 코드를 출력한다. (이는 기존 클래스를 방문할 때 유용하다.)

     

    메소드 생성 :

    MethodVisitor 추상 클래스를 기반으로 하는 ASM API가 사용된다.

    이 클래스는 바이트코드 명령어 범주마다 하나의 메소드를 정의하며, 이 메소드들은 특정 순서로 호출되어야 한다. ClassVisitor와 MethodVisitor 클래스를 결합하여 완전한 클래스를 생성할 수 있다.

     

    메소드 변환 :

    메소드 어댑터를 사용하여 수행될 수 있으며, 이는 받은 메소드 호출을 수정하거나 새로운 지시문을 추가함으로써 변환을 수행한다.

    예를 들어, RemoveNopAdapter는 NOP 명령어를 제거하는 간단한 메소드 어댑터이다.

     

     

    2. 클래스

     

    이 장에서는 코어 ASM API를 사용하여 컴파일된 Java 클래스를 생성하고 변환하는 방법을 설명한다.

    컴파일된 클래스에 대한 소개로 시작하여 해당 ASM 인터페이스, 구성 요소 및 도구를 많은 예시와 함께 제시하고, 이를 통해 클래스를 생성하고 변환하는 방법을 소개한다. 메소드, 어노테이션 및 제네릭의 내용은 다음 장에서 설명된다.

     

    2.1. 구조

    2.1.1. 개요

     

    컴파일된 클래스의 전체 구조는 꽤 단순하다. 실제로, 기본적으로 컴파일된 애플리케이션과 달리, 컴파일된 클래스는 소스 코드에서의 구조적 정보와 거의 모든 심볼을 유지한다.

    실제로 컴파일된 클래스에는 다음이 포함된다:

     

    • 클래스의 수정자(예: public 또는 private), 이름, 슈퍼 클래스, 인터페이스 및 어노테이션을 설명하는 섹션.

    • 이 클래스에서 선언된 각 필드마다 하나의 섹션. 각 섹션은 필드의 수정자, 이름, 타입 및 어노테이션을 설명한다.

    • 이 클래스에서 선언된 각 메소드 및 생성자마다 하나의 섹션. 각 섹션은 메소드의 수정자, 이름, 반환 및 매개변수 타입, 어노테이션을 설명한다. 또한 Java 바이트코드 명령의 시퀀스 형태로 메소드의 컴파일된 코드를 포함한다.

     

    그러나 소스와 컴파일된 클래스 사이에는 몇 가지 차이점이 있다:

     

    • 컴파일된 클래스는 하나의 클래스만을 설명하는 반면, 소스 파일은 여러 클래스를 포함할 수 있다. 예를 들어, 하나의 내부 클래스를 가진 클래스를 설명하는 소스 파일은 메인 클래스와 내부 클래스를 위한 두 개의 클래스 파일로 컴파일된다. 그러나 메인 클래스 파일에는 내부 클래스에 대한 참조가 포함되며, 메소드 내부에서 정의된 내부 클래스는 해당 메소드에 대한 참조를 포함한다.

    • 컴파일된 클래스에는 주석이 포함되어 있지 않지만, 클래스, 필드, 메소드 및 코드 속성을 포함할 수 있으며, 이를 통해 이러한 요소에 추가 정보를 연결할 수 있다. Java 5에서 어노테이션이 도입된 이후로, 어노테이션은 동일한 목적으로 사용될 수 있으며, 속성은 대부분 불필요해졌다.

    • 컴파일된 클래스에는 패키지와 import 섹션이 포함되어 있지 않으므로, 모든 타입 이름은 완전한 이름으로 표시되어야 한다.

     

    또 다른 매우 중요한 구조적 차이점은 컴파일된 클래스에 상수 풀 섹션(constant pool section)이 포함되어 있다는 것이다. 이 풀은 클래스에 나타나는 모든 숫자, 문자열 및 타입 상수를 포함하는 배열이다. 이러한 상수들은 상수 풀 섹션에서 한 번만 정의되며, 클래스 파일의 다른 모든 섹션에서 인덱스로 참조된다. 다행히 ASM은 상수 풀과 관련된 모든 세부 사항을 숨기므로, 이에 대해 걱정할 필요가 없다.

     

    그림 2.1은 컴파일된 클래스의 전체 구조를 요약한다. 정확한 구조는 Java 가상 머신 사양, 섹션 4에서 설명된다.

     

     

    또한 컴파일된 클래스와 소스 클래스에서 Java 타입이 다르게 표현된다는 중요한 차이점이 있다. 다음 섹션에서는 컴파일된 클래스에서의 타입 표현에 대해 설명한다.

     

    2.1.2. 내부 이름(Internal names)

    많은 상황에서 타입은 클래스 또는 인터페이스 타입으로 제한된다.

    예를 들어, 클래스의 슈퍼 클래스, 클래스에서 구현한 인터페이스 또는 메소드에서 던진 예외는 기본 타입이나 배열 타입이 아니며, 반드시 클래스 또는 인터페이스 타입이다. 이러한 타입들은 컴파일된 클래스에서 내부 이름으로 표현된다. 클래스의 내부 이름은 단순히 이 클래스의 완전한 이름으로, 점(.)은 슬래시(/)로 대체된다.

     

    예를 들어, String의 내부 이름은 java/lang/String이다.

     

    2.1.3. 타입 설명자(Type descriptors)

    내부 이름은 클래스 또는 인터페이스 타입으로 제한된 타입에만 사용된다. 다른 모든 상황에서, 예를 들어 필드 타입과 같이, Java 타입은 컴파일된 클래스에서 타입 설명자로 표현된다(그림 2.2 참조).

     

     

    기본 타입의 설명자는 단일 문자이다:

    boolean은 Z, char은 C, byte는 B, short는 S, int는 I, float는 F, long은 J, double은 D이다.

     

    클래스 타입의 설명자는 이 클래스의 내부 이름으로, 앞에 L이 붙고 뒤에 세미콜론이 붙는다.

    예를 들어, String의 타입 설명자는 Ljava/lang/String;이다.

     

    마지막으로, 배열 타입의 설명자는 대괄호에 이어 배열 요소 타입의 설명자이다.

     

     

    2.1.4. 메소드 설명자( Method descriptors)

     

    메소드 설명자는 메소드의 매개변수 타입과 반환 타입을 설명하는 타입 설명자 목록으로, 단일 문자열로 표현된다.

    메소드 설명자는 왼쪽 괄호로 시작하여 각 형식 매개변수의 타입 설명자가 이어지고, 오른쪽 괄호와 메소드가 void를 반환하는 경우 V로, 아니면 반환 타입의 타입 설명자로 이어진다(메소드 설명자에는 메소드의 이름이나 인수 이름이 포함되지 않는다).

     

    타입 설명자의 작동 방식을 이해하면 메소드 설명자를 이해하는 것이 쉽다.

    예를 들어, (I)I는 int 타입의 인수를 하나 취하고 int를 반환하는 메소드를 설명한다. 그림 2.3은 여러 메소드 설명자 예시를 제공한다.

     

     

    2.2. 인터페이스와 컴포넌트 (Interfaces and components)

    개인 요약본

     

    ClassVisitor 클래스
    이 추상 클래스는 클래스 파일의 구조에 해당하는 메서드를 제공한다. 각 메서드는 클래스 파일의 특정 섹션을 방문하여 처리한다. 

    ClassVisitor는 visitMethod, visitField 등의 메서드를 통해 클래스 파일의 각 부분을 방문하고, 이러한 메서드들은 MethodVisitor, FieldVisitor와 같은 보조 방문자 클래스를 반환한다​​.

    ClassReader 클래스

    ClassReader는 컴파일된 클래스를 바이트 배열 형태로 읽어들이고, 이 배열을 분석하여 ClassVisitor 인터페이스에 정의된 visitXxx 메서드들을 호출한다. 이 과정을 통해 클래스 파일의 구조와 내용이 ClassVisitor를 구현한 클래스에 의해 처리된다​​.

    예를 들어, java.lang.Runnable 클래스를 파싱하려면 ClassReader 인스턴스를 생성하고 accept 메서드를 호출한다​​.

    ClassWriter 클래스

    ClassVisitor의 하위 클래스로, 바이너리 형태로 컴파일된 클래스를 직접 구축한다. ClassVisitor의 메서드들을 오버라이드하여 클래스의 새로운 버전을 구축할 때 사용된다​.

    toByteArray 메서드를 통해 생성된 클래스를 바이트 배열로 검색할 수 있다.

    예를 들어, 새로운 인터페이스를 생성하려면 ClassWriter 인스턴스를 생성하고 필요한 visitXxx 메서드들을 호출한다​​.

    클래스 변환

    ClassReader와 ClassWriter를 함께 사용하여 클래스를 변환할 수 있다.

    ClassReader로 클래스를 파싱하고 ClassWriter로 새로운 클래스를 구성한다.

    예를 들어, ChangeVersionAdapter라는 ClassVisitor 하위 클래스를 사용하여 클래스 버전을 변경할 수 있으며, 이 클래스는 visit 메서드를 오버라이드하여 클래스 버전을 변경한다다​​.

     

     

    2.2.1. Presentation

     

    컴파일된 클래스를 생성하고 변환하기 위한 ASM API는 ClassVisitor 추상 클래스를 기반으로 한다(그림 2.4 참조).

    이 클래스의 각 메서드는 클래스 파일 구조의 동일한 이름의 섹션에 해당한다(그림 2.1 참조). 간단한 섹션들은 내용을 설명하는 인자들을 가진 단일 메서드 호출로 방문되며, void를 반환한다. 내용의 길이와 복잡성이 임의적일 수 있는 섹션들은 보조 방문자 클래스를 반환하는 초기 메서드 호출로 방문된다. visitAnnotation, visitField, visitMethod 메서드의 경우 각각 AnnotationVisitor, FieldVisitor, MethodVisitor를 반환한다.

     

     

     

    이러한 보조 클래스들에 대해서도 동일한 원칙들이 재귀적으로 사용된다. 예를 들어, FieldVisitor 추상 클래스의 각 메서드는 동일한 이름의 클래스 파일 하위 구조에 해당한다(그림 2.5 참조).

     

     

     

     

    ClassVisitor 클래스의 메소드는 이 클래스의 Javadoc에 명시된 순서대로 호출되어야 한다:

     

     

    이것은 visit이 먼저 호출되어야 하며, 그 다음에 최대 한 번 visitSource가 호출되고, 그 다음에 최대 한 번 visitOuterClass가 호출되며, visitAnnotation 및 visitAttribute에 대한 어떤 숫자의 호출도 순서에 상관없이 이루어질 수 있고, visitInnerClass, visitField 및 visitMethod에 대한 어떤 숫자의 호출도 순서에 상관없이 이루어지며, 마지막으로 visitEnd에 대한 단일 호출로 종료된다는 것을 의미한다.

    ASM은 ClassVisitor API를 기반으로 하는 세 가지 코어 구성 요소를 제공하여 클래스를 생성하고 변환한다:

     

    • ClassReader 클래스는 컴파일된 클래스를 바이트 배열로 파싱하고, accept 메소드에 전달된 ClassVisitor 인스턴스에 해당하는 visitXxx 메소드를 호출한다. 이는 이벤트 생산자로 볼 수 있다.

    • ClassWriter 클래스는 바이너리 형태로 직접 컴파일된 클래스를 구축하는 ClassVisitor 추상 클래스의 하위 클래스이다. 출력으로 컴파일된 클래스를 포함하는 바이트 배열을 생성하며, toByteArray 메소드로 검색할 수 있다. 이는 이벤트 소비자로 볼 수 있다.

    • ClassVisitor 클래스는 받은 모든 메소드 호출을 다른 ClassVisitor 인스턴스에 위임한다. 이는 이벤트 필터로 볼 수 있다.

     

    다음 섹션에서는 이러한 구성 요소를 사용하여 클래스를 생성하고 변환하는 방법을 구체적인 예제를 통해 보여준다.

     

    2.2.2. 클래스 파싱

     

    기존 클래스를 파싱하는 데 필요한 유일한 구성 요소는 ClassReader 구성 요소이다. 예를 들어, javap 도구와 유사한 방식으로 클래스의 내용을 출력하려는 경우를 가정해 보자. 첫 번째 단계는 방문하는 클래스에 대한 정보를 출력하는 ClassVisitor 클래스의 서브클래스를 작성하는 것이다. 여기에 가능한 단순화된 구현이 있다:

     

    이 클래스는 ClassVisitor 클래스의 서브클래스로서, 클래스 파일을 분석하고 클래스에 대한 정보를 출력하는 기능을 한다. 이 예시는 javap 도구와 유사하게 클래스의 내용을 출력하는 방법을 보여주기 위해 만들어졌다.

     

    public class ClassPrinter extends ClassVisitor {
        public ClassPrinter() {
            super(ASM4);
        }
    
        public void visit(int version, int access, String name,
                          String signature, String superName, String[] interfaces) {
            System.out.println(name + " extends " + superName + " {");
        }
    
        public void visitSource(String source, String debug) {
        }
    
        public void visitOuterClass(String owner, String name, String desc) {
        }
    
        public AnnotationVisitor visitAnnotation(String desc, boolean visible) {
            return null;
        }
    
        public void visitAttribute(Attribute attr) {
        }
    
        public void visitInnerClass(String name, String outerName,
                                    String innerName, int access) {
        }
    
        public FieldVisitor visitField(int access, String name, String desc,
                                       String signature, Object value) {
            System.out.println(" " + desc + " " + name);
            return null;
        }
    
        public MethodVisitor visitMethod(int access, String name,
                                         String desc, String signature, String[] exceptions) {
            System.out.println(" " + name + desc);
            return null;
        }
    
        public void visitEnd() {
            System.out.println("}");
        }
    }

     

    두 번째 단계는 이 ClassPrinter를 ClassReader 구성 요소와 결합하는 것이므로, ClassReader에서 생성된 이벤트가 우리의 ClassPrinter에 의해 소비된다:

     

     

    Runnable 클래스를 파싱하기 위해 ClassReader를 생성하는 두 번째 줄이다. 마지막 줄에 호출된 accept 메소드는 Runnable 클래스의 바이트코드를 파싱하고 cp에 해당하는 ClassVisitor 메소드를 호출한다. 결과는 다음과 같은 출력이다:

     

     

    실제로 호출 결과 Runnable 인터페이스의 run 메서드를 파싱하였다.

     

    ClassReader 인스턴스를 구성하는 방법에는 여러 가지가 있다.

    읽어야 하는 클래스는 이름(name)으로, 위와 같이, 또는 값(value)으로, 바이트 배열 또는 InputStream으로 지정할 수 있다. ClassLoader의 getResourceAsStream 메서드를 사용하면 클래스의 내용을 읽을 입력 스트림을 얻을 수 있다:

     

    cl.getResourceAsStream(classname.replace(’.’, ’/’) + ".class");

     

    2.2.3. 클래스 생성

     

    클래스를 생성하는 데 필요한 유일한 구성 요소는 ClassWriter 구성 요소이다. 이를 설명하기 위해 다음 인터페이스를 고려해 보자:

     

     

    이는 ClassVisitor에 대한 여섯 번의 메소드 호출로 생성될 수 있다:

     

     

    [visit 부분] 

    - 첫 번째 줄은 실제로 클래스의 바이트 배열 표현을 구축할 ClassWriter 인스턴스를 생성한다(생성자 인수는 다음 장에서 설명된다).

    - visit 메소드 호출은 클래스 헤더를 정의한다.

    - V1_5 인수는 ASM Opcodes 인터페이스에서 정의된 모든 다른 ASM 상수와 마찬가지로 클래스 버전, Java 1.5를 명시한다.

    - ACC_XXX 상수는 Java 수정자에 해당하는 플래그이다. 여기에서는 클래스가 인터페이스이며, public이고 abstract(인스턴스화할 수 없기 때문에)임을 명시한다.

    - 다음 인수는 내부 형태로 클래스 이름을 명시한다(섹션 2.1.2 참조).  컴파일된 클래스에는 패키지 또는 import 섹션이 포함되어 있지 않으므로, 모든 클래스 이름은 완전히 지정되어야 한다.

    - 다음 인수는 제네릭에 해당한다(섹션 4.1 참조). 우리의 경우에는 인터페이스가 타입 변수로 매개변수화되지 않았기 때문에 null이다.

    - 다섯 번째 인수는 슈퍼 클래스로, 내부 형태로 지정된다(인터페이스 클래스는 암시적으로 Object에서 상속됨).

    - 마지막 인수는 확장되는 인터페이스의 내부 이름으로 지정된 배열이다.

     

    (이전 값과 동일)

    [visitField]

    다음 visitField 호출은 유사하며, 세 개의 인터페이스 필드를 정의하는 데 사용된다.

    첫 번째 인수는 Java 수정자에 해당하는 플래그 집합이다. 여기에서는 필드가 public, final 및 static임을 명시한다.

    두 번째 인수는 소스 코드에서 나타나는 필드의 이름이다.

    세 번째 인수는 필드의 타입으로, 타입 설명자 형태이다. 여기에서 필드는 int 필드이며, 설명자는 I이다. 네 번째 인수는 제네릭에 해당한다. 우리의 경우에는 필드 타입이 제네릭을 사용하지 않기 때문에 null이다.

    마지막 인수는 필드의 상수 값이다: 이 인수는 실제로 상수인 필드, 즉 final static 필드에만 사용되어야 한다. 다른 필드의 경우에는 null이어야 한다. 여기에서 어노테이션이 없으므로, 반환된 FieldVisitor의 visitEnd 메소드를 즉시 호출한다, 즉 visitAnnotation 또는 visitAttribute 메소드에 대한 어떠한 호출도 없이.

     

    (이전 값과 동일)

     

    [visitMethod / visitEnd]

    visitMethod 호출은 compareTo 메소드를 정의하는 데 사용된다.

    여기에서도 첫 번째 인수는 Java 수정자에 해당하는 플래그 집합이다.

    두 번째 인수는 소스 코드에서 나타나는 메소드 이름이다.

    세 번째 인수는 메소드의 설명자이다.

    네 번째 인수는 제네릭에 해당한다. 우리의 경우에는 메소드가 제네릭을 사용하지 않기 때문에 null이다.

    마지막 인수는 메소드에 의해 던져질 수 있는 예외의 배열로, 내부 이름으로 지정된다. 여기에서는 메소드가 예외를 선언하지 않기 때문에 null이다.

     

    visitMethod 메소드는 MethodVisitor를 반환한다(그림 3.4 참조), 이는 메소드의 어노테이션과 속성을 정의하는 데 사용될 수 있으며, 가장 중요하게는 메소드의 코드를 정의하는 데 사용된다. 여기에서는 어노테이션이 없고 메소드가 추상적이기 때문에, 반환된 MethodVisitor의 visitEnd 메소드를 즉시 호출한다.

     

    마지막으로 visitEnd에 대한 마지막 호출은 cw에게 클래스가 완료되었음을 알리는 데 사용되며, toByteArray 호출은 바이트 배열로 검색하는 데 사용된다.

     

    생성된 클래스 사용( Using generated classes)

     

    이전 바이트 배열은 향후 사용을 위해 Comparable.class 파일에 저장될 수 있다. 대안적으로, 클래스는 ClassLoader를 통해 동적으로 로드될 수 있다. 한 가지 방법은 defineClass 메소드가 public인 ClassLoader 하위 클래스를 정의하는 것이다:

     

    그런 다음 생성된 클래스는 직접 다음과 같이 로드될 수 있다:

    Class c = myClassLoader.defineClass("pkg.Comparable", b);

     

    생성된 클래스를 로드하는 또 다른 방법은, 요청된 클래스를 즉석에서 생성하기 위해 findClass 메소드가 재정의된 ClassLoader 하위 클래스를 정의하는 것이다:

     

     

     

    실제로 생성된 클래스를 사용하는 방법은 컨텍스트에 따라 다르며, ASM API의 범위를 벗어난다.

    컴파일러를 작성하는 경우, 클래스 생성 프로세스는 컴파일할 프로그램을 나타내는 추상 구문 트리에 의해 구동되며, 생성된 클래스는 디스크에 저장된다. 동적 프록시 클래스 생성기나 관점 직조기를 작성하는 경우에는 어떤 방식으로든 ClassLoader를 사용한다.

     

     

    Reference

    https://asm.ow2.io/asm4-guide.pdf

     

    ASM USER GUIDE

     

    Copyright c 2007, 2011 Eric Bruneton All rights reserved. Redistribution and use in source (LYX format) and compiled forms (LATEX, PDF, PostScript, HTML, RTF, etc), with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code (LYX format) must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in compiled form (converted to LATEX, PDF, PostScript, HTML, RTF, and other formats) must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 3. The name of the author may not be used to endorse or promote products derived from this documentation without specific prior written permission.

     

    THIS DOCUMENTATION IS PROVIDED BY THE AUTHOR “AS IS” AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS DOCUMENTATION, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

     

    Version 2.0, September 2011