프로젝트/APM prototype 개발

[ASM][Util]TraceClassVisitor/고수준 코드 출력하기[InteliJ]

블랑v 2024. 2. 23. 22:40

 

Java ASM 라이브러리의 TraceClassVisitor는 Java 바이트코드를 분석하고 조작하기 위한 도구 중 하나이다. ASM 라이브러리는 Java 클래스 파일을 직접 읽고 쓰며, 이를 통해 런타임에 클래스의 구조를 변경하거나 분석할 수 있는 기능을 제공한다. TraceClassVisitor는 ASM의 방문자(visitor) API 중 하나로, 클래스 파일의 내용을 분석하고 이해하기 쉬운 형태로 출력하는 데 사용된다.

1. 개념

TraceClassVisitor는 ASM 라이브러리에서 제공하는 클래스 방문자 중 하나로, 방문한 바이트코드 구조(클래스, 메소드, 필드 등)를 사람이 읽을 수 있는 형식으로 출력하는 기능을 한다. 이 클래스는 ClassVisitor 인터페이스를 구현하며, 방문자 패턴을 이용해 클래스의 구조를 순회한다.

 

https://asm.ow2.io/javadoc/org/objectweb/asm/util/TraceClassVisitor.html

 

TraceClassVisitor (ASM 9.6)

A ClassVisitor that prints the classes it visits with a Printer. This class visitor can be used in the middle of a class visitor chain to trace the class that is visited at a given point in this chain. This may be useful for debugging purposes. When used w

asm.ow2.io

 

A ClassVisitor that prints the classes it visits with a Printer. This class visitor can be used in the middle of a class visitor chain to trace the class that is visited at a given point in this chain. This may be useful for debugging purposes. 

 

클래스 방문자는 방문한 클래스를 프린터로 인쇄한다. 이는 클래스 방문자 체인 중간에 사용하여 이 체인의 특정 지점에 방문한 클래스를 추적할 수 있다. 이는 디버깅 목적으로 유용할 수 있을 것이다.

 

2. 사용 용도 

TraceClassVisitor의 주된 용도는 디버깅과 분석이다.

 

TraceClassVisitor는 클래스의 바이트코드를 방문하며, 방문한 구성 요소들(클래스, 메소드, 필드 등)을 사람이 읽기 쉬운 형태로 출력한다. 그러나 이것의 핵심 기능은 단순히 출력에 국한되지 않고 다음과 같은 역할을 한다.

 

  • 바이트코드 구조 시각화: 클래스 파일의 구조를 분석하여, 그 구조를 사람이 이해할 수 있는 형태로 변환하고 출력.
    이는 메소드 호출, 변수 사용, 제어 흐름 등의 바이트코드 레벨의 세부 사항을 포함할 수 있다.
  • 디버깅 지원: ASM을 사용하여 바이트코드를 조작할 때, TraceClassVisitor를 통해 조작 전후의 클래스 바이트코드를 비교함으로써 변경 사항을 명확하게 파악할 수 있다. 이는 코드 조작 시 의도치 않은 변경을 발견하고 수정하는 데 유용하다.
  • 조합성: TraceClassVisitor는 다른 ClassVisitor 구현체와 함께 연쇄적으로 사용될 수 있다. 예를 들어, 바이트코드를 조작하는 ClassVisitor 구현체 뒤에 TraceClassVisitor를 배치함으로써, 조작의 결과를 직접 확인할 수 있다.

Tree API와의 호환성

TraceClassVisitor는 Core API의 일부이며, 기본적으로 Tree API와는 직접적으로 호환되지 않는다.

 

그러나 Tree API를 사용한 후 결과를 Core API 구조로 변환하여 TraceClassVisitor를 통해 출력할 수는 있다.

 

1. Tree API 조작

2. 조작된 결과를 ClassNode 객체 저장

3. 이를accept로 ClassVisitor 체인으로 내보냄

4. 그 과정에서 TraceClassVisitor를 사용하여 결과를 확인

 

의 방식으로 사용할 수 있다.

 

3. 사용 예시

아래는 TraceClassVisitor를 사용하여 클래스 파일의 바이트코드를 출력하는 간단한 예시이다.

 

package org.agent;

import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.util.TraceClassVisitor;
import org.objectweb.asm.tree.*;
import static org.objectweb.asm.Opcodes.*;

import java.io.PrintWriter;


public class Main {

    public static void main(String[] args) {
        //ClassNode
        ClassNode cn = new ClassNode();
        cn.version = V21; //java 21
        cn.access = ACC_PRIVATE;
        cn.name = "Class0223";
        cn.superName = "java/lang/Object"; //객체 타입
        
        //Method
        MethodNode mn = new MethodNode(ACC_PUBLIC, "method", "()I", null, null);
        InsnList il = new InsnList();
        il.add(new InsnNode(ICONST_5));
        il.add(new InsnNode(IRETURN));
        mn.instructions.add(il);
        cn.methods.add(mn);
        cn.fields.add(new FieldNode(ACC_PRIVATE, "field", "F", null, -1F));

        //Accept
        ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES | ClassWriter.COMPUTE_MAXS);
        TraceClassVisitor tcv = new TraceClassVisitor(cw, new PrintWriter(System.out));
        cn.accept(tcv);

        System.out.println(cw.toByteArray());
    }
}

 

 

 

직접 Printer 만들기

하지만 이 출력값은 Low data에 가깝다. 실제 만들어지는 고수준의 언어를 확인하고 싶어, 새로운 printer를 생성하였다.

변조되는 byte[]값을 실제 고수준의 언어로 번역하고, 이를 특정 폴더에 로그 파일로 저장한다.

 

package org.agent.util;

import lombok.extern.slf4j.Slf4j;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;

@Slf4j
public class CodePrinter {

    /**
     * 바이트코드 조작 후 Class 파일 출력 로직
     */
    public static void printClass(byte[] bytecodes, String testCode) {
        try {
            DateTimeFormatter formattedDate = DateTimeFormatter.ofPattern("yyyy-MM-dd_HH-mm-ss");
            String fileName = LocalDateTime.now().format(formattedDate) + "[" + testCode + "]" + ".class";
            String directoryPath = System.getProperty("user.dir") + File.separator + "byteLog"; // 디렉토리 경로
            String filePath = directoryPath + File.separator + fileName; // 최종 파일 경로

            File directory = new File(directoryPath);
            if (!directory.exists()) {
                directory.mkdirs(); // 디렉토리가 없으면 생성
            }

            FileOutputStream out = new FileOutputStream(filePath); // 파일 출력 스트림 생성

            out.write(bytecodes);
            out.close();
            log.warn("[CODE_PRINTER_DONE] byteLog print done. : " + testCode);
        } catch (IOException e) {
            log.warn("[CODE_PRINTER ERR] : {}", e.getMessage());
        }
    }
}

 

 

실제 고수준의 바이트코드로 저장되는 모습.