Mit Java/Kotlin geschriebene JVM-Bytecode-Ausführungsengine

JAVA-Bytecode-Ausführungsengine

Traditionelles dynamisches Java-Debugging kann nur auf Quellcode-Ebene basieren und kann nicht ohne Quellcode oder bei obfuskierten Java-Klassendateien dynamisch debuggt werden.

Die Ausführung von Java-Programmen basiert auf der virtuellen JVM-Maschine, die Bytecode ausführt. Wir haben eine JVM-Bytecode-Ausführungsengine mit Kotlin konstruiert, die es uns ermöglicht, Java-Programme auf Bytecode-Ebene mit modernen IDEs wie IntelliJ IDEA zu debuggen, um das Programmverhalten während der Ausführung zu beobachten.

Achtung, dieses Projekt dient nur zum Studium und zur Erforschung des Funktionsprinzips der JVM und zur Analyse bösartiger Programme. Es ist strengstens verboten, es für illegale Zwecke zu verwenden.

Vorausgesetztes Grundwissen

Bevor Sie dieses Projekt verwenden, stellen Sie bitte sicher, dass Sie über die folgenden Grundkenntnisse verfügen.

  1. Verständnis des Formats von Java-Klassendateien
  2. Verständnis der Rolle und Bedeutung jedes Bytecodes in der JVM

Debugging auf Bytecode-Ebene mit IDEA

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

Öffnen Sie dieses Projekt mit IDEA (erfordert JDK 17) und navigieren Sie zu TestCases

Es gibt zwei Testfälle in TestCases, einen zum Ausführen statischer Methoden und einen zum Ausführen von Instanzmethoden, nämlich executeStaticMethod und executeVirtualMethod.

Bei der entsprechenden Methode tragen Sie die Informationen wie classPath, className, methodName und methodSignature ein. Die detaillierten Informationen der Klassendatei können mit ClassViewer angezeigt werden.

Direkt ausführen

Als Beispiel verwenden wir die kompilierte Klassendatei des folgenden Codes

public class Hello {

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

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

Führen Sie executeVirtualMethod aus, um die hello-Methode dieser Klasse auszuführen.

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)

Sie erhalten die folgende Ausgabe auf der Konsole.

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"

Die Konsolenausgabe zeigt alle Bytecode-Anweisungen dieser Methode, die Änderungen im Stack während der Befehlsausführung und die Ergebnisse jeder Bytecode-Anweisung.

Debugging

Wenn Sie Bytecode-Anweisungen debuggen müssen, können Sie Breakpoints vor und nach der execute()-Methode in VMExecutor setzen.

 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)
        }

    }
}

Debugging von Untermethoden-Bytecode

Standardmäßig interpretiert und führt die virtuelle Engine nur den Bytecode der angegebenen Methode aus. Die innerhalb der angegebenen Methode aufgerufenen Untermethoden werden weiterhin in der JVM ausgeführt, um erhebliche Leistungseinbußen durch mehrschichtige Aufrufe zu vermeiden. Wenn Sie möchten, dass alle Methoden von der virtuellen Engine interpretiert und ausgeführt werden, ändern Sie bitte io.vlinx.vmengine.Options und setzen Sie handleSubMethod auf true.