Search code examples
javajava-bytecode-asm

Replacing full method through ASM


I am trying to write a script that replaces each method body with a basic throw new exception() line. I am in the starting steps of learning ASM so any pointers on where to look would be appreciated.

What I have done so far:

package methodtester;

import java.io.DataOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.MethodVisitor;
import static org.objectweb.asm.Opcodes.ACC_INTERFACE;
import static org.objectweb.asm.Opcodes.ASM4;
import static org.objectweb.asm.Opcodes.ATHROW;
import static org.objectweb.asm.Opcodes.DUP;
import static org.objectweb.asm.Opcodes.INVOKESPECIAL;
import static org.objectweb.asm.Opcodes.NEW;

public class MethodTransformer {
    public static void main(String[] args) throws IOException {
        InputStream in = MethodTester.class.getResourceAsStream("/methodtester/testingMethod.class");

        ClassReader classReader = new ClassReader(in);

        ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_FRAMES);

        ExceptionThrower exceptionThrower = new ExceptionThrower(classWriter);

        classReader.accept(exceptionThrower, 0);

        File outputDir = new File("build/classes/methodtester/");

        outputDir.mkdirs();

        DataOutputStream dataOutputStream =
            new DataOutputStream(
                new FileOutputStream(
                    new File(outputDir,"testingMethod-postASM.class")));

        dataOutputStream.write(classWriter.toByteArray());
    }

    public static class ExceptionThrower extends ClassVisitor {
        private String _className;
        private boolean _isInterface;

        public ExceptionThrower(ClassVisitor classVisitor) {
            super(ASM4, classVisitor);
        }

        @Override
        public void visit(
            int version, int access, String name, String signature,
            String superName, String[] interfaces) {

            cv.visit(version, access, name, signature, superName, interfaces);

            _className = name;

            _isInterface = (access & ACC_INTERFACE) != 0;
        }

        @Override
        public MethodVisitor visitMethod(
            int access, String name, String desc, String signature,
            String[] exceptions) {

            MethodVisitor mv = cv.visitMethod(access, name, desc, signature,
                exceptions);

            if (!_isInterface && mv != null && !name.equals("<init>")) {
                ExceptionThrowerMethod exceptionThrowerMethod =
                    new ExceptionThrowerMethod(mv);

                return exceptionThrowerMethod;
            }

            return mv;
        }

        public static class ExceptionThrowerMethod extends MethodVisitor {
            public ExceptionThrowerMethod(MethodVisitor methodVisitor) {
                super(ASM4, methodVisitor);
            }

            @Override
            public void visitCode() {
                mv.visitCode();
                mv.visitTypeInsn(NEW, "java/io/IOException");
                mv.visitInsn(DUP);
                mv.visitMethodInsn(INVOKESPECIAL, "java/io/IOException", "<init>", "()V", false);
                mv.visitInsn(ATHROW);
                mv.visitMaxs(2, 0);
                mv.visitEnd();
            }
        }
    }
}

So far I am able to insert the throw new IOException() instructions into the beginning of the method, but this does not run at runtime as I am given this error:

Exception in thread "main" java.lang.ClassFormatError: Invalid start_pc 8 in LocalVariableTable in class file methodtester/testingMethod
    at java.lang.ClassLoader.defineClass1(Native Method)
    at java.lang.ClassLoader.defineClass(ClassLoader.java:763)
    at java.security.SecureClassLoader.defineClass(SecureClassLoader.java:142)
    at java.net.URLClassLoader.defineClass(URLClassLoader.java:467)
    at java.net.URLClassLoader.access$100(URLClassLoader.java:73)
    at java.net.URLClassLoader$1.run(URLClassLoader.java:368)
    at java.net.URLClassLoader$1.run(URLClassLoader.java:362)
    at java.security.AccessController.doPrivileged(Native Method)
    at java.net.URLClassLoader.findClass(URLClassLoader.java:361)
    at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
    at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:331)
    at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
    at methodtester.MethodTester.main(MethodTester.java:27)

Looking at javap -c-v I get:

 public void testing() throws java.io.IOException;
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=1, args_size=1
         0: new           #14                 // class java/io/IOException
         3: dup
         4: invokespecial #15                 // Method java/io/IOException."<init>":()V
         7: athrow
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            8       0     0  this   Lmethodtester/testingMethod;
      LineNumberTable:
        line 24: 8
        line 25: 8
    Exceptions:
      throws java.io.IOException
}

Looking at the classes with ASMifier I see that the method is:

mv = cw.visitMethod(ACC_PUBLIC, "testing", "()V", null, null);
mv.visitCode();
mv.visitTypeInsn(NEW, "java/io/IOException");
mv.visitInsn(DUP);
mv.visitMethodInsn(INVOKESPECIAL, "java/io/IOException", "<init>", "()V", false);
mv.visitInsn(ATHROW);
mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
mv.visitLdcInsn("testing method");
mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
mv.visitInsn(RETURN);
mv.visitMaxs(2, 1);
mv.visitEnd();
}

Looks like I need to change the LocalVariableTable to get it to start at 0 with length 0.

Thanks!

Edit: updated using COMPUTE_FRAMES and updated the new error code


Solution

  • You are passing the original MethodVisitor, you received from the ClassWriter, to the super constructor of your custom MethodVisitor. This implies, that every visit… call, you didn’t override, will be delegated to that MethodVisitor, replicating the entire original code.

    Of course, first, you don’t want to replicate the original code, second, receiving contradicting visit… calls on the writer after your invocations of visitMaxs and visitEnd does not help creating valid code.

    When you want to entirely replace a method, you should not exhibit the target writer’s MethodVisitor to the base class, but use it solely for your own code generation:

    public static class ExceptionThrowerMethod extends MethodVisitor {
        private final MethodVisitor target;
    
        public ExceptionThrowerMethod(MethodVisitor methodVisitor) {
            super(ASM4, null);
            this.target=methodVisitor;
        }
    
        @Override
        public void visitCode() {
            target.visitCode();
            target.visitTypeInsn(NEW, "java/io/IOException");
            target.visitInsn(DUP);
            target.visitMethodInsn(INVOKESPECIAL,"java/io/IOException","<init>","()V",false);
            target.visitInsn(ATHROW);
            target.visitMaxs(2, 0);
            target.visitEnd();
        }
    }
    

    By they way, you can simplify your resource access by letting the Class resolve the resource locally, e.g. testingMethod.class.getResourceAsStream("testingMethod.class").