I. Qu'est-ce qu'un décompilateur▲
I-A. Petit rappel théorique▲
Contrairement à des langages tels que le C ou le C++, qui sont des langages compilés, Java fait partie d'une autre famille qu'on appelle langages semi-compilés.
C'est-à-dire que le compilateur Java ne produira pas du code binaire, qui aurait pu être compris directement par le système d'exploitation, mais un code intermédiaire communément appelé bytecode.
La conséquence de cela est qu'un programme Java n'est pas « autoexécutable », et qu'il a besoin d'une machine virtuelle Java, dont le rôle est d'interpréter ce bytecode et d'exécuter les appels systèmes correspondants.
Ce bytecode est stocké dans les fichiers .class que génère le compilateur Java.
I-B. À quoi ressemble du bytecode ?▲
Afin de mieux comprendre, nous allons compiler la classe com.developpez.hikage.bytecode.ByteCode, et visualiser le fichier .class grâce à un outil.
Dans ce screenshot, on peut voir différentes sections, telles que Fields, Constants, Methods, Interfaces…
Étudions un peu le bytecode de la méthode maMethode :
0 ldc #23 <Visitez Developpez.com>
2 astore_1
3 iconst_0
4 istore_2
5 goto 18 (+13)
8 getstatic #25 <java/lang/System.out>
11 aload_1
12 invokevirtual #31<java/io/PrintStream.println>
15 iinc 2 by 1
18 iload_2
19 bipush 10
21 if_icmplt 8 (-13)
24 return
Ce qui donne comme suite d'exécution :
- ligne 0 : chargement de la constante 23 (une chaine « Visitez Developpez.com ») sur la pile ;
- ligne 2 : stockage de la variable de la pile (donc la chaine précédente) dans la première variable locale ;
- ligne 3 : chargement de la valeur entière 0 sur la pile ;
- ligne 4 : stockage de la variable de la pile (donc la valeur 0) dans la deuxième variable locale ;
- ligne 5 : saut à la ligne 18 ;
- ligne 18 : chargement d'une valeur entière provenant de la variable 2 vers la pile ;
- ligne 19 : chargement de la valeur 10 sur la pile (valeur sur la pile : 10 suivi de la valeur de la variable 2) ;
- ligne 21 : si la dernière valeur de la pile est plus petite que l'avant-dernière valeur de la pile, alors saut en ligne 8 ;
- ligne 8 : chargement du champ « out » de la classe java.lang.System ;
- ligne 11 : chargement de la valeur de la variable 1 (« Visitez Developpez.com ») sur la pile ;
- ligne 12 : appel de la méthode println de la classe java.io.PrintStream (le champ out de ligne 8) ;
- ligne 15 : incrémentation de la variable 2 par 1 ;
- ligne 24 : fin de la méthode.
En français, on pourrait dire que la logique de cette méthode est de boucler sur un compteur allant de 0 à 10, et d'appeler à chaque itération la méthode println(String texte) de l'objet System.out.
Pourriez-vous tenter de réécrire cette méthode en Java ?
Aviez-vous trouvé ? ;-)
I-C. Décompilateur▲
Dès que l'on connait les opérations bytecode, ainsi que le langage Java, et que l'on est doté d'une bonne logique, il devient très simple de traduire du bytecode en code source Java.
Et bien c'est exactement ce que fait un décompilateur.
De plus, hormis la logique, d'autres informations sur la classe sont stockées dans le fichier .class : le nom des champs, le nom des méthodes, les exceptions qu'une méthode est susceptible de lancer, etc.
Grâce à toutes ces informations, le code source peut être restitué exactement !!
Enfin presque. En effet, les commentaires ne sont pas enregistrés lors de la compilation, et sont réellement perdus. Cela dit, le code est parfois plus clair ainsi.
Un seul souci apparait lors de debug. En effet, du fait de la disparition des commentaires, les points d'arrêt risquent d'être mal placés.
Par exemple si vous mettez un point d'arrêt à la ligne 5 dans un code décompilé, il est tout à fait possible que cette ligne correspondait au départ à une ligne de commentaire, et donc le point d'arrêt ne sera jamais atteint.
Fâcheux ?
Non, car les informations sur l'emplacement d'une ligne de code sont aussi stockées dans le fichier .class, et donc certains décompilateurs (dont JAD) permettent de recréer le fichier source avec la même structure que le fichier original, en remplaçant les lignes de commentaires par des lignes blanches.
Il faut cependant avouer que le code restitué n'est plus « totalement » le même avec Java 5 dans le cas particulier des génériques.
En effet, les génériques de Java sont vérifiés durant la phase de compilation (et généreront une erreur si un type ne correspond pas à celui attendu), mais sont ensuite traduits en Object. Autrement dit une List<String> sera une simple List dans le bytecode compilé, il devient donc impossible à JAD de rendre la notion de génériques.