Supporto per Java/Kotln in JVM
Motore di esecuzione bytecode JAVA
Il debug dinamico Java tradizionale può essere basato 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 il 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 è finalizzato esclusivamente allo studio e alla ricerca del principio di funzionamento della JVM e all'analisi di programmi dannosi. È severamente vietato utilizzarlo per scopi illegali.
Fondamenti di conoscenza prerequisiti
Prima di utilizzare questo progetto, assicurati di avere le seguenti conoscenze di base.
- Comprensione del formato dei file di classe Java
- Comprendere il ruolo e il significato di ogni bytecode in JVM
Debug 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 denominati executeStaticMethod E executeVirtualMethod.
Nel metodo corrispondente, compilare le informazioni come classPath, className, methodName, E methodSignatureLe informazioni dettagliate del file di classe possono essere visualizzate utilizzando Visualizzatore di classi.
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!");
}
}
Eseguire executeVirtualMethod, 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)
Sulla console è possibile ottenere il seguente output.
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 delle istruzioni e i risultati di ciascuna istruzione bytecode.
Debug
Se è necessario eseguire il debug delle istruzioni bytecode, è possibile impostare punti di interruzione prima e dopo execute() metodo in VMExecutor.
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)
}
}
}
Debug del bytecode del sottometodo
Per impostazione predefinita, il motore virtuale interpreta ed esegue solo il bytecode del metodo specificato. I sottometodi chiamati all'interno del metodo specificato continuano a essere eseguiti nella JVM per evitare un sovraccarico di prestazioni significativo dovuto a più livelli di chiamate. Se si desidera che tutti i metodi vengano interpretati ed eseguiti dal motore virtuale, modificare io.vlinx.vmengine.Options e cambiare handleSubMethod al vero.