Java와 Bytecode, javaagent

kwak woojong·2023년 7월 25일

회사에서 솔루션 만들다가 제니퍼라는 타 회사 솔루션을 구경하게 됨.

와 우리 프로세스에 -javaagent:제니퍼.jar 만 줬는데 웹상에서 우리 메서드 호출시 TPS를 볼 수 있음. 몇 번 호출됐는지도 볼 수 있더라

이게 어떻게 가능할까 싶었음.

결국 javaagent를 통해 할 텐데 이게 뭔가 싶었다.

Java는 Jvm 위에서 돌아감.

.Java -> 컴파일 -> .class

JVM 위에서 돌아가는 코드는 .Java가 아니라 .Class임. 컴파일 된 Bytecode가 돌아간다.

이 Bytecode가 실행되기 전에 다른 애가 접근해서 바이트코드를 바꿀 수 있게, 또는 바이트코드 자체를 얻어갈 수 있게 해주는게 저 javaagent라고 보면 편하겠다.

public class Main {
    public static void main(String[] args) {

        int[] nums = {1,2,3,4};
        System.out.println("수정 당할 클래스 로그 : 실행전");

        for (int i = 0; i < 10; i++) {
            Test1 hihi = new Test1();
            int solution = hihi.solution(nums);
        }
        System.out.println("수정 당할 클래스 로그 : 실행후");

    }
}

우리가 모니터링 솔루션을 개발하는 회사의 직원이라고 하자.

상기와 같은 코드가 있다. 우린 저 solution이 실행될 때마다 얼마나 걸렸는지를 측정하고 싶음.

그럼 당연히 Spring.AOP 부터 생각날 건데 그건 메인 프로세스를 고쳐야 하니까 불가능하다.

저 코드는 우리가 작성할 수 있는 코드가 아니니까


원하는 로그는 저런 형식임. 수정 당항 클래스 로그는 지금 내가 테스트하고 있는 상기 코드고, javaagent 코드는 javaagent.jar로 만든 프로세스를 통해 바이트 변조로 끼워넣은 상태

그럼 결국 외부에서 바이트코드 변조를 해야 한다는거임.

javaagent가 이걸 해준다. 문제는 라이브러리 없이 javaagent로 Bytecode를 변조하려면 Bytecode에 대한 이해가 필요하다.

근본있는 전공자들이야 어셈블리어 배웠을 수 있지만, 나는 무근본이니까 이게 뭔가 싶었음.

public class BytecodeExample {
    public static void main(String[] args) {
        long a = 10L;
        long b = 11L;
        System.out.println("some String Text" + (a - b));
    }
}

상기 코드를 javac로 컴파일하고 javap -c {클래스명}으로 바이트코드를 보면

Compiled from "BytecodeExample.java"
public class BytecodeExample {
  public BytecodeExample();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public static void main(java.lang.String[]);
    Code:
       0: ldc2_w        #2                  // long 10l
       3: lstore_1
       4: ldc2_w        #4                  // long 11l
       7: lstore_3
       8: getstatic     #6                  // Field java/lang/System.out:Ljava/io/PrintStream;
      11: new           #7                  // class java/lang/StringBuilder
      14: dup
      15: invokespecial #8                  // Method java/lang/StringBuilder."<init>":()V
      18: ldc           #9                  // String some String Text
      20: invokevirtual #10                 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
      23: lload_1
      24: lload_3
      25: lsub
      26: invokevirtual #11                 // Method java/lang/StringBuilder.append:(J)Ljava/lang/StringBuilder;
      29: invokevirtual #12                 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
      32: invokevirtual #13                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      35: return
}

이렇게 된다.
main 이후로 보면
ldc2_w 로 10l을 만들고 lstore_1로 저장
ldc2_w 로 11l을 만들고 lstore_3으로 저장 하고 있다.

바닐라로 javaagent를 다룰려면 저치들을 알고 있어야 한다.

어찌저찌 코드를 짜본 결과

javaagent.jar에서 MethodVisit 클래스에서 다음과 같이 짤 수 있었따.

public class MyMethodVisitor extends LocalVariablesSorter {

    private int timeLocal;
    public MyMethodVisitor(int access, String desc, MethodVisitor mv) {
        super(Opcodes.ASM5, access, desc, mv);
    }

    @Override
    public void visitCode() {
        super.visitCode();
        mv.visitMethodInsn(Opcodes.INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", true);
        timeLocal = newLocal(Type.LONG_TYPE);
        mv.visitVarInsn(Opcodes.LSTORE, timeLocal);
    }

    @Override
    public void visitInsn(int opcode) {
        if (opcode >= Opcodes.IRETURN && opcode <= Opcodes.RETURN) {
            // Create a new StringBuilder
            mv.visitTypeInsn(Opcodes.NEW, "java/lang/StringBuilder");
            mv.visitInsn(Opcodes.DUP);
            mv.visitMethodInsn(Opcodes.INVOKESPECIAL, "java/lang/StringBuilder", "<init>", "()V", false);

            // log header
            mv.visitLdcInsn("javaagent 코드 : ");
            mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false);

            // Calculate the TPS value and append it to the StringBuilder
            mv.visitMethodInsn(Opcodes.INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false);
            mv.visitVarInsn(Opcodes.LLOAD, timeLocal);
            mv.visitInsn(Opcodes.LSUB);
            mv.visitMethodInsn(Opcodes.INVOKESTATIC, "java/lang/Long", "toString", "(J)Ljava/lang/String;", false);
            mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false);

            // Append the constant string "ms = " to the StringBuilder
            mv.visitLdcInsn("ms");
            mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false);

            // Convert StringBuilder to String
            mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/StringBuilder", "toString", "()Ljava/lang/String;", false);

            // Call PrintStream.println with the final String
            mv.visitFieldInsn(Opcodes.GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
            mv.visitInsn(Opcodes.SWAP); // Swap the order of arguments on the stack
            mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);

        }

        super.visitInsn(opcode);
    }

    @Override
    public void visitMaxs(int maxStack, int maxLocals) {
        super.visitMaxs(maxStack + 4, maxLocals);
    }

    @Override
    public void visitEnd() {
        super.visitEnd();
    }
}

킹 갓 GPT의 힘이다 이게 물론 예외가 계속 터지긴 했지만..

우선 눈에 띄는 코드는 Opcodes다. 이 친구가 뭐길래 계속 선언을 하냐면

Opcodes is a class in the ASM (Bytecode Analysis Framework) library, which is used for bytecode manipulation in Java. It is part of the org.objectweb.asm package. The Opcodes class contains a set of constants representing the various opcodes (bytecode instructions) used in Java bytecode.

Bytecode instructions are the low-level machine-readable instructions that the Java Virtual Machine (JVM) executes. Each bytecode instruction is represented by a numeric opcode, and these opcodes are defined in the Opcodes class.

Here are some common categories of opcodes:

Load and Store: Instructions for loading and storing values from/to local variables or fields.
Arithmetic and Logic: Instructions for performing arithmetic and logical operations.
Control Flow: Instructions for controlling the flow of execution (branching and looping).
Method Invocation: Instructions for invoking methods (both static and instance methods).
Object Creation: Instructions for creating new objects and arrays.
Type Conversion: Instructions for converting data between different types.
Exception Handling: Instructions for handling exceptions.
The Opcodes class provides mnemonic constants for each bytecode instruction, making it easier to work with bytecode during manipulation.

For example, some common opcodes and their corresponding constants in the Opcodes class are:

Opcodes.GETSTATIC: Representing the getstatic bytecode instruction.
Opcodes.IADD: Representing the iadd (integer addition) bytecode instruction.
Opcodes.INVOKEVIRTUAL: Representing the invokevirtual bytecode instruction for invoking instance methods.
Opcodes.NEW: Representing the new bytecode instruction for object creation.
When using the ASM library, you can use these mnemonic constants to specify the bytecode instructions you want to insert, modify, or remove in the MethodVisitor implementation.

For example, to add the iadd (integer addition) instruction, you can use mv.visitInsn(Opcodes.IADD) inside your MethodVisitor implementation.

Keep in mind that working with bytecode directly using ASM requires a good understanding of the Java Virtual Machine specification and bytecode format. It is a powerful technique but should be used with care, as incorrect manipulations can lead to runtime errors or unexpected behavior.

킹갓 GTP.

하튼 이런 친구다. ASM에서 Java로 명령질하는 코드로 보임.

ㅇㅋ 그건 ㅇㅋㅇㅋ

mv.visitMethodInsn(Opcodes.INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false);

mv.visitMethodInsn(Opcodes.INVOKESTATIC, "java/lang/Long", "toString", "(J)Ljava/lang/String;", false);

그럼 ()J랑 ()L은 뭐냐? (J)L은 또 뭐고?

저 인자는 String desc라고 함. 첨에 이게 뭔 소린가 했음.

()J는 Long type이고 ()L은 레퍼런스 (콜 바이 레프 할 때 그거)로 클래스 부를때
(J)L 은 리턴값은 레퍼런스 주소값이고 인자로는 Long이 들어간다는 소리다.

In bytecode notation, the strings ()J, ()V, and ([I)I represent method signatures. These signatures describe the method's parameter types and return type using specific codes.

()J: This method signature represents a method with no parameters (()) and a return type of long (J). In Java, long is a 64-bit integral data type.

()V: This method signature represents a method with no parameters (()) and a return type of void (V). In Java, void indicates that the method does not return a value.

([I)I: This method signature represents a method with a single parameter of an array of integers ([I) and a return type of int (I). In Java, int is a 32-bit integral data type.

Here's a breakdown of the codes used in the signatures:

(: Opening parenthesis, indicating the start of the parameter list.
): Closing parenthesis, indicating the end of the parameter list.
[: Opening square bracket, indicating an array type.
B: Byte type (8-bit signed integer).
C: Char type (16-bit Unicode character).
D: Double type (64-bit floating-point).
F: Float type (32-bit floating-point).
I: Int type (32-bit signed integer).
J: Long type (64-bit signed integer).
S: Short type (16-bit signed integer).
Z: Boolean type (true or false).
Lfully/qualified/ClassName;: Object reference type, where fully/qualified/ClassName represents the fully qualified class name of the object.
For example:

(Ljava/lang/String;)V: Method with a single parameter of type java.lang.String and no return value (void).
([Ljava/lang/Object;)Ljava/lang/Object;: Method with a single parameter of type array of java.lang.Object and a return value of type java.lang.Object.
These method signatures are used in Java bytecode to uniquely identify and describe methods in a class. They play a crucial role in method resolution during runtime and are essential for correct method invocation and linkage.

역시 킹갓 GPT

암튼 이 ByteCode를 보다보면 JVM 동작 이해에 좀 더 도움이 될 것 같다.

놀라운점은

public class BytecodeExample {
    public static void main(String[] args) {
        long a = 10L;
        long b = 11L;
        System.out.println("some String Text" + (a - b));
    }
}

상기 코드를 컴파일하면 알아서 StringBuilder를 불러서 계산하고 넣어준다...

profile
https://crazyleader.notion.site/Crazykwak-36c7ffc9d32e4e83b325da26ed8d1728?pvs=4<-- 포트폴리오

0개의 댓글