ASM


  • Modérateurs

    ATTENTION: Ce tutoriel n'est pas fini et j'aurai besoin de retours sur celui-ci pour le continuer, merci

    Ce tutoriel est général mais peut être facilement utilisé pour Minecraft.

    Présentation de la librairie

    ASM est une librairie qui permet la manipulation de bytecode. Pour info, le bytecode est la version compilée du java (pour faire simple) et il est composé d'instructions simples telles que: ajout de deux nombres, multiplication de deux nombres, chargement d'une valeur en mémoire, etc.

    Vous pouvez télécharger ASM ici: https://search.maven.org/remotecontent?filepath=org/ow2/asm/asm/6.2.1/asm-6.2.1.jar (si vous voulez des versions plus récentes que la 6.2.1, cherchez sur Maven Central)

    Préparation du projet

    Créez un nouveau projet avec le nom que vous voulez (ce sera ASMTuto pour moi) puis ajoutez le fichier "asm-all-5.0.3.jar" à votre Build Path:
    Clic droit sur le project, Build Path, Configure Build Path…, onglet Libraries, appuyez sur le bouton "Add external jar" et choisissez ce fichier dans "/UnCertainDossierOùVousAvezTéléchargéASM/asm-5.0.3-bin/asm-5.0.3/lib/all/asm-all-5.0.3.jar"

    C'est tout ce qu'il y a à faire et vous avez ASM prêt!

    Créer une classe à partir de bytecode

    On commence par créer une instance de ClassWriter avec les options COMPUTE_MAXS et COMPUTRE_FRAMES qui permettent respectivement de laisser ASM gérer le nombre de variables locales et de frames.

    ​ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_MAXS | ClassWriter.COMPUTE_FRAMES);
    

    Ensuite, on utilise cette instance pour créer la base de la classe.

    Pour créer la classe TestASM suivante:

    ​public class TestASM {}
    

    Il suffit d'appeler la méthode visit de ClassWriter:

    ​classWriter.visit(classVersion, access, name, signature, superclass, interfaces);
    
    • classVersion: La version de la classe, nous utiliserons V1_8 pour Java 8 dans ce tutoriel.
    • access: public, abstract, final, protected, private, [default]
    • name: Le nom de la classe
    • La signature de la classe que l'on va laisser à null la plupart du temps.
    • superclass: La classe parente, "java/lang/Object" si il n'y en a aucune.
    • interfaces: Un tableau de String contenant les types internes des interfaces que la classe implémente.

    Et on utilise les bons arguments pour notre exemple:

    ​classWriter.visit(V1_8, ACC_PUBLIC, "TestASM", null, "java/lang/Object", new String[0]);
    

    Nous avons donc ce code ci maintenant:

    public class ASMMain extends ClassLoader implements Opcodes {
    
        public static void main(String[] args) throws IOException {
            String name = "TestASM";
            ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_MAXS | ClassWriter.COMPUTE_FRAMES);
            classWriter.visit(V1_8, ACC_PUBLIC, name, null, "java/lang/Object", new String[0]);
    
            if(new File(name + ".class").exists())
                new File(name + ".class").delete();
            FileOutputStream fos = new FileOutputStream(name+".class");
            fos.write(bytes);
            fos.close();
        }
    }
    

    Et si l'on ouvre le fichier TestASM.class avec JD-Gui par exemple:

    Et voilà!

    Créer une méthode à partir de bytecode

    MethodVisitor ClassWriter.visitMethod(int access, String name, String desc, String signature, String[] exceptions)
    
    • access: public, abstract, final, protected, private, [default]
    • name: Le nom de la méthode.
    • desc: La description de la méthode, on verra ça plus en détail juste après
    • signature: pareil que pour les classes
    • exceptions: Tableau contenant les types internes des exceptions renvoyées par cette méthode

    Nous allons créer cette méthode:

    ​public static void main(String[] args)
    {
            System.out.println("Bonjour tout le monde en ASM!");
    }
    

    Premièrement, on doit dire à ASM qu'on l'on va créer du code:

    ​classWriter.visitSource(name + ".class", null);
    

    Ensuite on crée la méthode avec visitMethod et on récupére l'instance de MethodVisitor retournée:

    ​MethodVisitor mv = classWriter.visitMethod(ACC_PUBLIC | ACC_STATIC, "main", Type.getMethodDescriptor(Type.VOID_TYPE, Type.getType(String[].class)), null, new String[0]);
    
    • ACC_PUBLIC | ACC_STATIC: la méthode sera publique et statique (l'opérateur bitwise | permet d'ajouter les différentes options)
    • "main": le nom de notre méthode
    • Type.getMethodDescriptor(Type.VOID_TYPE, Type.getType(String[].class)): Ceci est une méthode utilitaire d'ASM qui permet de créer rapidement la description d'une méthode, ici une méthode qui retourne void (d'où le VOID_TYPE) avec pour arguments une instance de String[] (d'où le Type.getType(String[].class))

    Ensuite on doit indiquer à ASM que l'on commence le code de la méthode:

    mv.visitCode();
    

    On ajoute un return directement pour ne pas laisser la méthode vide pour le moment:

    ​mv.visitInsn(RETURN);
    

    RETURN est un int qui provient d'Opcodes qui correspond à l'instruction

    ​return;
    

    Et on finit par prévenir ASM que l'on a fini:

    ​mv.visitEnd();
    

    On lance le programme et on ouvre la classe dans JD-GUI et on obtient:

    C'est un début!

    🤔 Pourquoi le paramètre String[] à un nom moche ?
    ❗  C'est un nom généré par JD-Gui quand il est incapable de trouver un nom correct, on va corriger ça!

    Les variables locales

    Les variables locales désignent toutes les variables disponibles uniquement au sein des méthodes. Ajoutez à cela les paramètres.
    Chaque variable locale a un index donné, de 0 à n. Elles disposent aussi d'un type et d'un nom.

    Pour créer une nouvelle variable locale, on utilise l'instruction

    void visitLocalVariable(String name, String desc, String signature, Label start, Label end, int index)
    
    • name: Le nom de la variable locale
    • desc: la description de la variable
    • signature: comme d'habitude, pas très important pour nous
    • start et end: Instructions pour lesquelles la variable locale est accessible, vides si elle est tout le monde accessible (par exemple les paramètres)
    • index: l'index de la variable locale, en commençant par 0

    Pour créer une variable String[] args, on remplace les valeurs ainsi:

    ​mv.visitLocalVariable("args", Type.getInternalName(String[].class), null, new Label(), new Label(), 0);
    
    • "args": le nom
    • Type.getInternalName(String[].class): Méthode utilitaire d'ASM pour récupérer le nom d'une classe directement
    • null: la signature
    • new Label() x2: début et fin, tous les deux vides pour signifier l'utilisation dans toute la méthode.
    • 0: l'index, ici on veut le premier argument: "args"

    Fil rouge:

    package fr.mff.asm;
    
    import java.io.*;
    
    import org.objectweb.asm.*;
    
    public class ASMMain extends ClassLoader implements Opcodes {
    
        public static void main(String[] args) throws IOException {
            String name = "TestASM";
            ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_MAXS | ClassWriter.COMPUTE_FRAMES);
            classWriter.visit(V1_8, ACC_PUBLIC | ACC_SUPER, name, null, "java/lang/Object", new String[0]);
    
            classWriter.visitSource(name + ".class", null);
            MethodVisitor mv = classWriter.visitMethod(ACC_PUBLIC | ACC_STATIC, "main", Type.getMethodDescriptor(Type.VOID_TYPE, Type.getType(String[].class)), null, new String[0]);
            mv.visitCode();
            mv.visitLocalVariable("args", Type.getDescriptor(String[].class), null, new Label(), new Label(), 0);
            mv.visitInsn(RETURN);
            mv.visitEnd();
    
            ASMMain instance = new ASMMain();
            byte[] bytes = classWriter.toByteArray();
            Class generatedClass = instance.defineClass(name, bytes, 0, bytes.length);
    
            if(new File(name + ".class").exists())
                new File(name + ".class").delete();
            FileOutputStream fos = new FileOutputStream(name + ".class");
            fos.write(bytes);
            fos.close();
        }
    }
    

    On lance pour voir:

    Ça marche!

    Petite explication du stack

    🤔 Le stack, qu'est-ce que c'est?
    ❗ Le stack est une grosse pile comportant des valeurs empilées les unes sur les autres avec le fait que l'on puisse que rajouter une valeur au dessus du stack ou en retirer une, sur le même principe de ces jeux pour enfants:
    0_1529786862247_HTB1tIIeGXXXXXa.XpXXq6xXFXXXe.jpg (crédit photo: aliexpress.com)

    🤔 Ouais, mais ça sert à quoi?
    ❗ Le stack est ce que la JVM utilise pour gérer les valeurs stockées en mémoire, voici une liste des principaux types que l'on peut retrouver:

    • arrayref: un tableau
    • null: la valeur null
    • value: une valeur quelconque, son type dépend de l'instruction
    • objectref: un objet
    • address: une adresse mémoire

    En ajoutant et en retirant des valeurs sur ce stack, on peut effectuer des calculs, des appels de méthodes, des sauts, etc.
    Il est important de noter qu'une fois qu'une valeur est utilisée dans le stack, il y en est retirée.

    Charger un primitif

    Pour charger un primitif (pour la JVM, c'est byte, int, float, double, long ou String) sur le stack, on utilise l'opcode LDC, représenter par la méthode visitLdcInsn(Object o)o représente l'objet à charger sur le stack.
    Exemples:

    mv.visitLdcInsn(1); // charge 1 int, voire byte sur le stack
    
    mv.visitLdcInsn(50.56); // charge 50.56 double sur le stack
    
    mv.visitLdcInsn(3.14f); // charge 3.14f float sur le stack
    
    mv.visitLdcInsn("Hello world"); // charge "Hello world" (String) sur le stack
    

    Et c'est tout!

    Sauvegarder dans des variables locales

    Pour sauvegarder dans une variable locale, on utilise les opcodes du type tSTOREt est le type de la variable locale:

    • L: long
    • D: double
    • F: float
    • I: int
    • A: le reste des objets

    Cet opcode est suivi par l'index de la variable à sauvegarder. Avec ASM, on utilise la méthode de MethodVisitor:

    public void visitVarInsn(int opcode, int index)
    
    • opcode: l'opcode à utiliser (exemple: ALOAD, ISTORE, etc.)
    • index: l'index de la variable à utiliser (exemple: 0, 1, 14, etc.)

    Pour démontrer ceci, nous allons créer une nouvelle variable locale, après

    mv.visitLocalVariable("args", Type.getDescriptor(String[].class), null, new Label(), new Label(), 0);
    

    Nous allons insérer ceci pour créer cette nouvelle variable locale:

    mv.visitLocalVariable("intExemple", Type.INT_TYPE.getDescriptor(), null, new Label(), new Label(), 1);
    

    Equivalent de:

    int intExemple;
    

    Et nous allons stocker 42 dedans: on charge 42 sur le stack puis on stocke la valeur à l'aide de ISTORE. Si vous avez suivi la partie précédente, pour charger 42, on va utiliser

    void visitLdcInsn(Object o)
    

    Ainsi, on insère ceci dans le code:

    mv.visitLdcInsn(42); // charge 42 sur le stack
    mv.visitVarInsn(ISTORE, 1); // prend la première valeur du stack et la sauvegarde dans la variable locale 1
    

    On lance et…

    🤔 Mais pourquoi c'est un byte?
    ❗ Ne t'inquiète pas, cela vient de mon décompileur (Fernflower) qui va choisir le type avec la taille la plus appropriée au contenu de la variable. JD-GUI, quant à lui, nous indique bien un int:

    Charger une variable locale sur le stack

    TODO

    TODO:
    Code couleur
    Ajouter des trucs





  • @'Gugu42':


  • Moddeurs confirmés Modérateurs

    Bravo xavpok un bon tuto en prévision;



  • @'Gugu42':

    Il me semble tu doit le finir nan ?


  • Administrateurs


  • Modérateurs

    Oh mon dieu, je l'ai oublié totalement ce tuto! =-O=-O
    Je vais essayer de le continuer ^^'

    Sent from my GT-I9000 using Tapatalk 2



  • Heureusement que je suis là xD


  • Modérateurs

    Oui merci ^^

    oublies de nouveau l'existance même de ce tuto

    Sent from my GT-I9000 using Tapatalk 2


  • Modérateurs

    Alors, bonne nouvelle!
    J'ai créé un langage tournant sur la JVM avec ASM, donc je vais avoir plein plein de contenu pour continuer le tuto!

    PS: Désolé robin-senpai pour le double post, ne me frappez pas :c

    Sent from my GT-I9000 using Tapatalk 2


  • Moddeurs confirmés Rédacteurs Administrateurs

    +24h entre deux posts = pas de problème.


  • Moddeurs confirmés

    Super tutoriel, encore d'actualité de nos jours !
    voici une petite coquille 5a044f8c-6ef4-451b-8785-4c234f37e4af-image.png
    et question a part, il y a moyen de faire de l'ASM mais uniquement au runtime ? C'est à dire que ça ne fait les modifications uniquement pendant l’exécution du programme, quand le programme est éteint, les modifications ne sont pas visible.


  • Moddeurs confirmés Rédacteurs Administrateurs

    J'ai corrigé le formatage du post, merci du signalement 😉


Log in to reply