Ir para o conteúdo

Ejemplo: Class Transformer Avanzado

Ejemplo avanzado que muestra técnicas de transformación de bytecode con ASM.

Transformador Avanzado

package com.ejemplo.advanced;

import com.hypixel.hytale.plugin.early.ClassTransformer;
import org.objectweb.asm.*;
import org.objectweb.asm.commons.AdviceAdapter;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;

public class AdvancedTransformer implements ClassTransformer {

    @Override
    public int priority() {
        return 500; // Alta prioridad
    }

    @Override
    @Nullable
    public byte[] transform(@Nonnull String className,
                           @Nonnull String classPath,
                           @Nonnull byte[] bytecode) {
        if (className.equals("com.hypixel.hytale.server.core.entity.LivingEntity")) {
            return transformEntity(bytecode);
        }
        return null;
    }

    private byte[] transformEntity(byte[] bytecode) {
        ClassReader reader = new ClassReader(bytecode);
        ClassWriter writer = new ClassWriter(
            reader,
            ClassWriter.COMPUTE_FRAMES | ClassWriter.COMPUTE_MAXS
        );

        ClassVisitor visitor = new EntityTransformer(Opcodes.ASM9, writer);
        reader.accept(visitor, ClassReader.EXPAND_FRAMES);

        return writer.toByteArray();
    }

    /**
     * Transformer que:
     * 1. Agrega un campo customData
     * 2. Intercepta getMaxHealth() para duplicar la salud
     * 3. Agrega logging a métodos de daño
     */
    private static class EntityTransformer extends ClassVisitor {
        private boolean fieldAdded = false;

        EntityTransformer(int api, ClassVisitor cv) {
            super(api, cv);
        }

        @Override
        public FieldVisitor visitField(int access, String name,
                                       String descriptor,
                                       String signature, Object value) {
            if (name.equals("customPluginData")) {
                fieldAdded = true;
            }
            return super.visitField(access, name, descriptor, signature, value);
        }

        @Override
        public MethodVisitor visitMethod(int access, String name,
                                        String descriptor,
                                        String signature, String[] exceptions) {
            MethodVisitor mv = super.visitMethod(
                access, name, descriptor, signature, exceptions
            );

            // Interceptar getMaxHealth()
            if (name.equals("getMaxHealth") && descriptor.equals("()F")) {
                return new HealthMultiplierVisitor(api, mv);
            }

            // Interceptar applyDamage()
            if (name.equals("applyDamage")) {
                return new DamageLoggerVisitor(api, mv, access, name, descriptor);
            }

            return mv;
        }

        @Override
        public void visitEnd() {
            // Agregar campo si no existe
            if (!fieldAdded) {
                FieldVisitor fv = cv.visitField(
                    Opcodes.ACC_PRIVATE,
                    "customPluginData",
                    "Ljava/util/Map;",
                    "Ljava/util/Map<Ljava/lang/String;Ljava/lang/Object;>;",
                    null
                );
                fv.visitEnd();
                System.out.println("[Plugin] Campo customPluginData agregado");
            }
            super.visitEnd();
        }
    }

    /**
     * Duplica el valor de retorno de getMaxHealth()
     */
    private static class HealthMultiplierVisitor extends MethodVisitor {
        HealthMultiplierVisitor(int api, MethodVisitor mv) {
            super(api, mv);
        }

        @Override
        public void visitInsn(int opcode) {
            if (opcode == Opcodes.FRETURN) {
                // Antes del return, duplicar el valor (x2)
                mv.visitLdcInsn(2.0f);
                mv.visitInsn(Opcodes.FMUL);
            }
            super.visitInsn(opcode);
        }
    }

    /**
     * Agrega logging al inicio y final de applyDamage()
     */
    private static class DamageLoggerVisitor extends AdviceAdapter {
        DamageLoggerVisitor(int api, MethodVisitor mv,
                           int access, String name, String descriptor) {
            super(api, mv, access, name, descriptor);
        }

        @Override
        protected void onMethodEnter() {
            // System.out.println("Daño aplicándose...");
            mv.visitFieldInsn(Opcodes.GETSTATIC,
                "java/lang/System", "out", "Ljava/io/PrintStream;");
            mv.visitLdcInsn("Daño aplicándose...");
            mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL,
                "java/io/PrintStream", "println",
                "(Ljava/lang/String;)V", false);
        }

        @Override
        protected void onMethodExit(int opcode) {
            // System.out.println("Daño aplicado!");
            mv.visitFieldInsn(Opcodes.GETSTATIC,
                "java/lang/System", "out", "Ljava/io/PrintStream;");
            mv.visitLdcInsn("Daño aplicado!");
            mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL,
                "java/io/PrintStream", "println",
                "(Ljava/lang/String;)V", false);
        }
    }
}

Técnicas Demostradas

1. Agregar Campo a Clase

FieldVisitor fv = cv.visitField(
    Opcodes.ACC_PRIVATE,          // private
    "customPluginData",           // nombre
    "Ljava/util/Map;",           // tipo (Map)
    "Ljava/util/Map<...>;",      // firma genérica
    null                          // valor inicial
);

2. Modificar Valor de Retorno

if (opcode == Opcodes.FRETURN) {
    // Stack: [valor_original]
    mv.visitLdcInsn(2.0f);
    // Stack: [valor_original, 2.0]
    mv.visitInsn(Opcodes.FMUL);
    // Stack: [valor_original * 2.0]
}
// FRETURN consume el valor del stack

3. AdviceAdapter para Método Entry/Exit

class MyVisitor extends AdviceAdapter {
    @Override
    protected void onMethodEnter() {
        // Código al inicio del método
    }

    @Override
    protected void onMethodExit(int opcode) {
        // Código antes de cada return
    }
}

Técnicas Adicionales

Interceptar Llamadas a Métodos

@Override
public void visitMethodInsn(int opcode, String owner,
                           String name, String descriptor,
                           boolean isInterface) {
    // Interceptar llamadas a println
    if (owner.equals("java/io/PrintStream") && name.equals("println")) {
        // Hacer algo antes
        System.out.println("Interceptado println!");
    }
    super.visitMethodInsn(opcode, owner, name, descriptor, isInterface);
}

Modificar Constantes

@Override
public void visitLdcInsn(Object value) {
    // Cambiar strings específicos
    if (value instanceof String) {
        String str = (String) value;
        if (str.equals("ERROR")) {
            value = "MODIFICADO: " + str;
        }
    }
    super.visitLdcInsn(value);
}

Agregar Try-Catch

Label tryStart = new Label();
Label tryEnd = new Label();
Label catchStart = new Label();

mv.visitTryCatchBlock(tryStart, tryEnd, catchStart,
    "java/lang/Exception");

mv.visitLabel(tryStart);
// Código que puede lanzar excepción
mv.visitLabel(tryEnd);
mv.visitJumpInsn(Opcodes.GOTO, afterCatch);

mv.visitLabel(catchStart);
// Manejar excepción

Debugging

Imprimir Bytecode

import org.objectweb.asm.util.TraceClassVisitor;
import java.io.PrintWriter;

byte[] transformed = transform(bytecode);
ClassReader reader = new ClassReader(transformed);
TraceClassVisitor tracer = new TraceClassVisitor(new PrintWriter(System.out));
reader.accept(tracer, 0);

Verificar Bytecode

import org.objectweb.asm.util.CheckClassAdapter;

CheckClassAdapter.verify(new ClassReader(bytecode), false, new PrintWriter(System.err));

Ver También