Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Adding try/catch block in bytecode through ASM

I am new to ASM and I want some help related to bytecode transformation.

Question: I would like to add try/catch block for the entire method in bytecode through ASM and want to run the method with out using java -noverify option. I can able to add try/catch block for the entire method but when I tried to execute the method I am getting 'java.lang.VerifyError'. If I use java -noverify option then it will run. Please help me.

Below are the details.

public class Example {
    public static void hello() {
        System.out.println("Hello world");
    }
}

I want to transform the above code as below introducing try/catch blocks, with ASM bytecode instrumentation.

public class Example {
  public static void hello() {
       try
       {
          System.out.println("Hello world");
       } catch(Exception ex) {
         ex.printStackTrace();
       }
    }
}

Below code add try/catch block but fails to execute the code with out java -noverify option.

public class InstrumentExample {

    /**
     * Our custom method modifier method visitor class. It delegate all calls to
     * the super class. Do our logic of adding try/catch block
     * 
     */
    public static class ModifierMethodWriter extends MethodVisitor {

        // methodName to make sure adding try catch block for the specific
        // method.
        private String methodName;

        // below label variables are for adding try/catch blocks in instrumented
        // code.
        private Label lTryBlockStart;
        private Label lTryBlockEnd;
        private Label lCatchBlockStart;
        private Label lCatchBlockEnd;

        /**
         * constructor for accepting methodVisitor object and methodName
         * 
         * @param api: the ASM API version implemented by this visitor
         * @param mv: MethodVisitor obj
         * @param methodName : methodName to make sure adding try catch block for the specific method.
         */
        public ModifierMethodWriter(int api, MethodVisitor mv, String methodName) {
            super(api, mv);
            this.methodName = methodName;
        }

        // We want to add try/catch block for the entire code in the method
        // so adding the try/catch when the method is started visiting the code.
        @Override
        public void visitCode() {
            super.visitCode();

            // adding try/catch block only if the method is hello()
            if (methodName.equals("hello")) {
                lTryBlockStart = new Label();
                lTryBlockEnd = new Label();
                lCatchBlockStart = new Label();
                lCatchBlockEnd = new Label();

                // set up try-catch block for RuntimeException
                visitTryCatchBlock(lTryBlockStart, lTryBlockEnd,
                        lCatchBlockStart, "java/lang/Exception");

                // started the try block
                visitLabel(lTryBlockStart);
            }

        }

        @Override
        public void visitMaxs(int maxStack, int maxLocals) {

            // closing the try block and opening the catch block if the method
            // is hello()
            if (methodName.equals("hello")) {
                // closing the try block
                visitLabel(lTryBlockEnd);

                // when here, no exception was thrown, so skip exception handler
                visitJumpInsn(GOTO, lCatchBlockEnd);

                // exception handler starts here, with RuntimeException stored
                // on stack
                visitLabel(lCatchBlockStart);

                // store the RuntimeException in local variable
                visitVarInsn(ASTORE, 2);

                // here we could for example do e.printStackTrace()
                visitVarInsn(ALOAD, 2); // load it
                visitMethodInsn(INVOKEVIRTUAL, "java/lang/Exception",
                        "printStackTrace", "()V", false);

                // exception handler ends here:
                visitLabel(lCatchBlockEnd);
            }

            super.visitMaxs(maxStack, maxLocals);
        }

    }

    /**
     * Our class modifier class visitor. It delegate all calls to the super
     * class Only makes sure that it returns our MethodVisitor for every method
     * 
     */
    public static class ModifierClassWriter extends ClassVisitor {
        private int api;

        public ModifierClassWriter(int api, ClassWriter cv) {
            super(api, cv);
            this.api = api;
        }

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

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

            // Our custom MethodWriter
            ModifierMethodWriter mvw = new ModifierMethodWriter(api, mv, name);
            return mvw;
        }

    }

    public static void main(String[] args) throws IOException {

        DataOutputStream dout = null;
        try {
            // loading the class
            InputStream in = InstrumentExample.class
                    .getResourceAsStream("Example.class");
            ClassReader classReader = new ClassReader(in);
            ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS);

            // Wrap the ClassWriter with our custom ClassVisitor
            ModifierClassWriter mcw = new ModifierClassWriter(ASM4, cw);
            ClassVisitor cv = new CheckClassAdapter(mcw);

            classReader.accept(cv, 0);

            byte[] byteArray = cw.toByteArray();
            dout = new DataOutputStream(new FileOutputStream(new File("Example.class")));
            dout.write(byteArray);

        } catch (Exception ex) {
            ex.printStackTrace();
        } finally {
            if (dout != null)
                dout.close();
        }

    }
}

For debugging I have used CheckClassAdapter and I got below verification problem.

Message:org.objectweb.asm.tree.analysis.AnalyzerException: Execution can fall off end of the code
    at org.objectweb.asm.tree.analysis.Analyzer.findSubroutine(Unknown Source)
    at org.objectweb.asm.tree.analysis.Analyzer.findSubroutine(Unknown Source)
    at org.objectweb.asm.tree.analysis.Analyzer.analyze(Unknown Source)
    at org.objectweb.asm.util.CheckClassAdapter.verify(Unknown Source)
    at org.objectweb.asm.util.CheckClassAdapter.verify(Unknown Source)
    at com.mfr.instrumentation.selenium.work.InstrumentExample.main(InstrumentExample.java:166)
hello()V
00000 ?      :    L0
00001 ?      :     GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
00002 ?      :     LDC "Hello world"
00003 ?      :     INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V
00004 ?      :     RETURN
00005 ?      :    L1
00006 ?      :     GOTO L2
00007 ?      :    L3
00008 ?      :     ASTORE 2
00009 ?      :     ALOAD 2
00010 ?      :     INVOKEVIRTUAL java/lang/Exception.printStackTrace ()V
00011 ?      :    L2
     TRYCATCHBLOCK L0 L1 L3 java/lang/Exception

I failed to understand the above verification message.

like image 535
Farooq Rahman Avatar asked May 06 '14 10:05

Farooq Rahman


2 Answers

You need to traverse your class and use a modified MethodVisitor in the process. If you wrap your entire method in a try-catch construct. You can do so by inserting the construct by intercepting the callbacks for the start and the end of the call block. These methods are visitCode and visitEnd which you could intercept like this:

class MyMethodVisitor extends MethodVisitor {
  // constructor omitted

 private final Label start = new Label(), 
                     end = new Label(), 
                     handler = new Label();

  @Override
  public void visitCode() {
    super.visitCode();
    visitTryCatchBlock(start, 
        end, 
        handler, 
        "java/lang/Exception");
    visitLabel(start);
  }

  @Override
  public void visitEnd() {
    visitJumpInsn(GOTO, end); 
    visitLabel(handler);
    visitMethodInsn(INVOKEVIRTUAL, 
        "java/lang/RuntimeException", 
        "printStackTrace", 
        "()V");
    visitInsn(RETURN);
    visitLabel(lCatchBlockEnd);
    super.visitEnd();
  }
}

However, note that this example does not include stack map frames which you need to add if you produce byte code for Java 7+.

Do however note that this solution will register a dominant handler at the beginning of your method's exception table which override all other try-catch-finally blocks in your method that are already present!

Note: In newer versions of ASM, the code for the handler would need to be written in the method visitMaxs(int, int) instead:

@Override
public void visitMaxs(int maxStack, int maxLocals) {
    // visit the corresponding instructions
    super.visitMaxs(maxStack, maxLocals);
}

This is because labels and instructions can only be visited before visitMaxs, and visitMaxs before visitEnd, therefore producing code in visitEnd will result in an error.

like image 166
Rafael Winterhalter Avatar answered Oct 11 '22 01:10

Rafael Winterhalter


The above exception is related to computing stackmap frames. ASM has provided mechanism to provide stackmap frames itself. We need to use the parameter flag as COMPUTE_FRAMES in ClassWriter constructor.

Ex: ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES);

public static final int COMPUTE_FRAMES Flag to automatically compute the stack map frames of methods from scratch. If this flag is set, then the calls to the MethodVisitor.visitFrame(int, int, java.lang.Object[], int, java.lang.Object[]) method are ignored, and the stack map frames are recomputed from the methods bytecode. The arguments of the visitMaxs method are also ignored and recomputed from the bytecode. In other words, computeFrames implies computeMaxs.

from ASM ClassWriter API.

like image 45
Farooq Rahman Avatar answered Oct 11 '22 02:10

Farooq Rahman