Java/Kotlin으로 작성된 JVM 바이트코드 실행 엔진

Java 바이트코드 실행 엔진

전통적인 Java 동적 디버깅은 소스 코드 수준에서만 가능하며, 소스 코드가 없거나 난독화된 Java 클래스 파일은 동적 디버깅을 할 수 없습니다.

Java 프로그램의 실행은 바이트코드를 실행하는 JVM 가상 머신을 기반으로 합니다. Kotlin을 사용하여 JVM 바이트코드 실행 엔진을 구축했으며, IntelliJ IDEA와 같은 최신 IDE를 사용하여 바이트코드 수준에서 Java 프로그램을 디버깅하고 실행 중 프로그램 동작을 관찰할 수 있습니다.

주의, 이 프로젝트는 JVM의 작동 원리를 학습하고 연구하며 악성 프로그램을 분석하기 위한 것입니다. 불법적인 목적으로 사용하는 것은 엄격히 금지됩니다.

사전 지식 기반

이 프로젝트를 사용하기 전에 다음 지식 기반을 갖추고 있는지 확인하세요.

  1. Java 클래스 파일의 형식 이해
  2. JVM의 각 바이트코드의 역할과 의미 이해

IDEA를 사용한 바이트코드 수준 디버깅

git clone https://github.com/vlinx-io/vlx-vmengine-jvm.git

IDEA로 이 프로젝트를 열고(JDK 17 필요) TestCases로 이동합니다.

TestCases에는 정적 메서드 실행과 인스턴스 메서드 실행을 위한 두 가지 테스트 케이스가 있으며, 각각 executeStaticMethodexecuteVirtualMethod로 명명됩니다.

해당 메서드에서 classPath, className, methodName, methodSignature 등의 정보를 입력합니다. 클래스 파일의 상세 정보는 ClassViewer를 사용하여 볼 수 있습니다.

직접 실행

다음 코드의 컴파일된 클래스 파일을 예로 들어보겠습니다.

public class Hello {

    public void hello() {
        System.out.println("hello");
    }

    public static void main(String[] args) {
        System.out.println("Hello World!");
    }
}

executeVirtualMethod를 실행하여 이 클래스의 hello 메서드를 실행합니다.

val classPath = "your-classpath"
val className = "Hello"
val methodName = "hello"
val methodSignature = "()V"
val args = listOf<Any?>()

val url = File(classPath).toURI().toURL()
val urls = arrayOf(url)
val loader = VlxClassLoader(urls)

val clazz = loader.loadClass(className)

val method = ClassUtils.getMethod(clazz, methodName, methodSignature, loader)
val instance = clazz.getDeclaredConstructor().newInstance()


val thread = VMThread(VMEngine.instance, loader)
thread.execute(instance, method!!, args, true, 0)

콘솔에서 다음과 같은 출력을 얻을 수 있습니다.

2023-05-21 17:51:10 [DEBUG] Execute method: public void Hello.hello()
2023-05-21 17:51:10 [DEBUG] Receiver: Hello@3daf7722
2023-05-21 17:51:10 [DEBUG] Args: []
2023-05-21 17:51:11 [DEBUG] LocalVars: [kotlin.Unit]
2023-05-21 17:51:11 [DEBUG] "L0: GETSTATIC"
2023-05-21 17:51:11 [DEBUG] "#7"
2023-05-21 17:51:11 [DEBUG] public static final java.io.PrintStream java.lang.System.out
2023-05-21 17:51:11 [DEBUG] "push" org.gradle.internal.io.LinePerThreadBufferingOutputStream@6aa3a905
2023-05-21 17:51:11 [DEBUG] "L3: LDC"
2023-05-21 17:51:11 [DEBUG] "#13"
2023-05-21 17:51:11 [DEBUG] "hello"
2023-05-21 17:51:11 [DEBUG] "push" "hello"
2023-05-21 17:51:11 [DEBUG] "L5: INVOKEVIRTUAL"
2023-05-21 17:51:11 [DEBUG] "#15"
2023-05-21 17:51:11 [DEBUG] "class java.io.PrintStream, NameAndType(name='println', type='(Ljava/lang/String;)V')"
2023-05-21 17:51:11 [DEBUG] public void java.io.PrintStream.println(java.lang.String)
2023-05-21 17:51:11 [DEBUG] "pop" "hello"
2023-05-21 17:51:11 [DEBUG] "pop" org.gradle.internal.io.LinePerThreadBufferingOutputStream@6aa3a905
2023-05-21 17:51:11 [DEBUG] 	Execute method: public void org.gradle.internal.io.LinePerThreadBufferingOutputStream.println(java.lang.String)
2023-05-21 17:51:11 [DEBUG] 	Receiver: org.gradle.internal.io.LinePerThreadBufferingOutputStream@6aa3a905
2023-05-21 17:51:11 [DEBUG] 	Args: [org.gradle.internal.io.LinePerThreadBufferingOutputStream@6aa3a905, hello]
2023-05-21 17:51:11 [ERROR] Can't parse class class org.gradle.internal.io.LinePerThreadBufferingOutputStream
hello
2023-05-21 17:51:11 [DEBUG] "L8: RETURN"

콘솔 출력은 이 메서드의 모든 바이트코드 명령, 명령 실행 중 스택의 변화, 각 바이트코드 명령의 결과를 표시합니다.

디버깅

바이트코드 명령을 디버깅해야 하는 경우, VMExecutorexecute() 메서드 전후에 중단점을 설정할 수 있습니다.

 fun execute() {
    while (true) {
        try {
            pc = sequence.index()
            val opcode = sequence.readUnsignedByte().toShort()
            if (execute(opcode)) {
                break
            }
        } catch (vme: VlxVmException) {
            Logger.FATAL(vme)
            exitProcess(1)
        } catch (t: Throwable) {
            handleException(t)
        }

    }
}

하위 메서드 바이트코드 디버깅

기본적으로 가상 엔진은 지정된 메서드의 바이트코드만 해석하고 실행합니다. 지정된 메서드 내에서 호출되는 하위 메서드는 여전히 JVM에서 실행되어 다중 계층 호출로 인한 상당한 성능 오버헤드를 방지합니다. 모든 메서드가 가상 엔진에 의해 해석되고 실행되기를 원하면, io.vlinx.vmengine.Options를 수정하고 handleSubMethod를 true로 변경하세요.