JVM Java/Kotln
Moteur d'exécution de bytecode JAVA
Le débogage dynamique Java traditionnel ne peut se baser que sur le niveau du code source et ne peut être débogué dynamiquement sans code source ou fichiers de classes Java obscurcis.
L'exécution des programmes Java repose sur la machine virtuelle JVM, qui exécute le bytecode. Nous avons conçu un moteur d'exécution de bytecode JVM en Kotlin, ce qui nous permet de déboguer les programmes Java au niveau du bytecode à l'aide d'IDE modernes tels qu'IntelliJ IDEA afin d'observer leur comportement lors de l'exécution.
Attention ! Ce projet est exclusivement destiné à l’étude et à la recherche du principe de fonctionnement de la JVM ainsi qu’à l’analyse des programmes malveillants. Toute utilisation à des fins illégales est strictement interdite.
Connaissances préalables requises
Avant d'utiliser ce projet, veuillez vous assurer de posséder les connaissances de base suivantes.
- Comprendre le format des fichiers de classe Java
- Comprendre le rôle et la signification de chaque bytecode dans la JVM
Débogage au niveau du bytecode avec IDEA
git clone https://github.com/vlinx-io/vlx-vmengine-jvm.git
Ouvrez ce projet avec IDEA (nécessite JDK 17) et accédez à TestCases.
Le répertoire TestCases contient deux cas de test : l’un pour l’exécution des méthodes statiques et l’autre pour l’exécution des méthodes d’instance. executeStaticMethod et executeVirtualMethod.
Dans la méthode correspondante, renseignez les informations telles que : classPath, className, methodName, et methodSignatureLes informations détaillées du fichier de classe peuvent être consultées en utilisant ClassViewer.
Exécuter directement
En utilisant le fichier de classe compilé du code suivant comme exemple
public class Hello {
public void hello() {
System.out.println("hello");
}
public static void main(String[] args) {
System.out.println("Hello World!");
}
}
Exécuter executeVirtualMethod, exécutez la méthode hello de cette 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)
Vous pouvez obtenir le résultat suivant sur la 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"
La sortie de la console affiche toutes les instructions bytecode de cette méthode, les modifications apportées à la pile pendant l'exécution des instructions et les résultats de chaque instruction bytecode.
Débogage
Si vous devez déboguer des instructions de bytecode, vous pouvez définir des points d'arrêt avant et après la execute() méthode dans 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)
}
}
}
bytecode de la sous-méthode de débogage
Par défaut, le moteur virtuel interprète et exécute uniquement le bytecode de la méthode spécifiée. Les sous-méthodes appelées au sein de cette méthode s'exécutent toujours dans la JVM afin d'éviter une surcharge de performances importante due à la multiplication des appels. Si vous souhaitez que toutes les méthodes soient interprétées et exécutées par le moteur virtuel, veuillez modifier le code. io.vlinx.vmengine.Options et changer handleSubMethod à vrai.