Motore di esecuzione del bytecode JVM scritto in Java/Kotlin

Motore di esecuzione del bytecode JAVA

Il debug dinamico Java tradizionale può basarsi solo sul livello del codice sorgente e non può essere sottoposto a debug dinamico senza codice sorgente o file di classe Java offuscati.

L'esecuzione dei programmi Java si basa sulla macchina virtuale JVM, che esegue bytecode. Abbiamo costruito un motore di esecuzione del bytecode JVM utilizzando Kotlin, che ci consente di eseguire il debug dei programmi Java a livello di bytecode utilizzando IDE moderni come IntelliJ IDEA per osservare il comportamento del programma durante l'esecuzione.

Attenzione, questo progetto serve solo per studiare e ricercare il principio di funzionamento di JVM e analizzare programmi dannosi. È severamente vietato utilizzarlo per scopi illegali.

Conoscenza preliminare Fondazione

Prima di utilizzare questo progetto, assicurati di disporre delle seguenti conoscenze di base.

  1. Comprensione del formato dei file di classe Java
  2. Comprensione del ruolo e del significato di ogni bytecode in JVM

Debugging a livello di bytecode utilizzando IDEA

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

Apri questo progetto utilizzando IDEA (richiede JDK 17) e vai a TestCases

Ci sono due casi di test in TestCases, uno per l'esecuzione di metodi statici e uno per l'esecuzione di metodi di istanza, rispettivamente chiamatiexecuteStaticMethodeexecuteVirtualMethod.

Sul metodo corrispondente, compila le informazioni comeclassPath,className,methodNameemethodSignature. Le informazioni dettagliate del file di classe possono essere visualizzate utilizzandoClassViewer.

Esegui direttamente

Utilizzando il file di classe compilato del codice seguente come esempio

public class Hello {

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

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

EseguireexecuteVirtualMethod, esegui il metodo hello di questa classe.

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)

È possibile ottenere il seguente output sulla console.

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"

L'output della console visualizza tutte le istruzioni bytecode di questo metodo, le modifiche nello stack durante l'esecuzione dell'istruzione e i risultati di ciascuna istruzione bytecode.

Debugging

Se hai bisogno di eseguire il debug delle istruzioni bytecode, puoi impostare dei punti di interruzione prima e dopo ilexecute()metodo inVMExecutor.

 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 bytecode del sottometodo

Per impostazione predefinita, il motore virtuale interpreta ed esegue solo il bytecode del metodo specificato. I metodi secondari chiamati all'interno del metodo specificato vengono comunque eseguiti nella JVM per evitare un significativo sovraccarico delle prestazioni dovuto a più livelli di chiamate. Se vuoi che tutti i metodi vengano interpretati ed eseguiti dal motore virtuale, modificaliio.vlinx.vmengine.Optionse cambiahandleSubMethod a vero.