通过ASM在字节码中添加try / catch块

时间:2014-05-06 10:16:23

标签: java java-bytecode-asm

我是ASM的新手,我想要一些与字节码转换相关的帮助。

问题:我想通过ASM在字节码中为整个方法添加try / catch块,并希望使用java -noverify选项运行该方法。我可以为整个方法添加try / catch块,但是当我尝试执行该方法时,我得到的是java.lang.VerifyError'。如果我使用java -noverify选项,那么它将运行。请帮帮我。

以下是详细信息。

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

我想转换上面的代码,如下所示,使用ASM字节码检测引入try / catch块。

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

下面的代码添加了try / catch块,但无法使用java -noverify选项执行代码。

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();
        }

    }
}

为了调试我使用了CheckClassAdapter,我遇到了以下验证问题。

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

我无法理解上述验证消息。

2 个答案:

答案 0 :(得分:5)

您需要遍历您的类并在此过程中使用修改后的MethodVisitor。如果将整个方法包装在try - catch构造中。您可以通过拦截调用块的开始和结束的回调来插入构造。这些方法是visitCodevisitEnd,你可以这样截取:

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();
  }
}

但是,请注意,此示例不包括在为Java 7 +生成字节代码时需要添加的堆栈映射帧。

请注意,此解决方案将在方法的异常表的开头注册一个显性处理程序,该异常表覆盖方法中已存在的所有其他try-catch-finally块!

注意:在较新版本的ASM中,处理程序的代码需要在方法visitMaxs(int,int)中编写:

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

这是因为标签和说明只能在visitMaxs之前访问,而visitMaxs只能在visitEnd之前访问,因此在visitEnd中生成代码会导致错误。

答案 1 :(得分:4)

上述异常与计算stackmap帧有关。 ASM提供了自己提供stackmap帧的机制。我们需要在ClassWriter构造函数中将参数标志用作COMPUTE_FRAMES。

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

public static final int COMPUTE_FRAMES 标记以从头开始自动计算方法的堆栈映射帧。如果设置了此标志,则忽略对MethodVisitor.visitFrame(int,int,java.lang.Object [],int,java.lang.Object [])方法的调用,并从中重新计算堆栈映射帧。方法字节码。 visitMaxs方法的参数也被忽略并从字节码重新计算。换句话说,computeFrames意味着computeMaxs。

来自ASM ClassWriter API。