提问者:小点点

注入try/catch块用于通过ASM序列化校验字节码


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

我想通过ASM为字节码中的每个局部变量添加带有try/catch块的打印函数。我发现以前关于添加try/catch块的问题是关于整个方法的。我对堆栈映射帧知之甚少,所以任何指针都将不胜感激。事先感谢。

我对每个对象的期望,例如一些对象:如果这个对象是可序列化的,则打印它的序列化表示,如果不是,则使用toString()打印:

try {
  ByteArrayOutputStream bos = new ByteArrayOutputStream();
  ObjectOutputStream oos = new ObjectOutputStream(bos);
  oos.writeObject(someObject);
  String serializedObject = Base64.getEncoder().encodeToString(bos.toByteArray());
  oos.close();
  System.out.println(serializedObject);
} catch (IOException ex) {
  System.out.println(someObject.toString());
}

由于我尝试为每个对象执行此操作,因此我在ControlodVisitor中覆盖visitVarInsn(),如下所示:

@Override
public void visitVarInsn(int opcode, int var) {
  super.visitVarInsn(opcode, var);
  switch (opcode) {
    case Opcodes.ASTORE:
      Label tryStart = new Label ();
      Label tryEnd = new Label ();
      Label catchStart = new Label ();
      Label catchEnd = new Label ();
      mv.visitTryCatchBlock(tryStart, tryEnd, catchStart, "java/io/IOException");

      mv.visitLabel(tryStart);
      // ==> ByteArrayOutputStream bos = new ByteArrayOutputStream();
      mv.visitTypeInsn(Opcodes.NEW, "java/io/ByteArrayOutputStream");
      mv.visitInsn(Opcodes.DUP);
      mv.visitMethodInsn(INVOKESPECIAL, "java/io/ByteArrayOutputStream", "<init>", "()V", false);
      mv.visitVarInsn(Opcodes.ASTORE, var + 1);
      // ==> ObjectOutputStream oos = new ObjectOutputStream(bos);
      mv.visitTypeInsn(Opcodes.NEW, "java/io/ObjectOutputStream");
      mv.visitInsn(Opcodes.DUP);
      mv.visitVarInsn(Opcodes.ALOAD, var + 1);
      mv.visitMethodInsn(INVOKESPECIAL, "java/io/ObjectOutputStream", "<init>", "(Ljava/io/OutputStream;)V", false);
      mv.visitVarInsn(Opcodes.ASTORE, var + 2);
      // ==> oos.writeObject(someObject);
      mv.visitVarInsn(Opcodes.ALOAD, var + 2);
      mv.visitVarInsn(Opcodes.ALOAD, var);
      mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/ObjectOutputStream", "writeObject", "(Ljava/lang/Object;)V", false);
      // ==> String serializedObject = Base64.getEncoder().encodeToString(bos.toByteArray());
      mv.visitMethodInsn(INVOKESTATIC, "java/util/Base64", "getEncoder", "()Ljava/util/Base64$Encoder;", false);
      mv.visitVarInsn(Opcodes.ALOAD, var + 1);
      mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/ByteArrayOutputStream", "toByteArray", "()[B", false);
      mv.visitMethodInsn(INVOKEVIRTUAL, "java/util/Base64$Encoder", "encodeToString", "([B)Ljava/lang/String;", false);
      mv.visitVarInsn(Opcodes.ASTORE, var + 3);
      // ==> oos.close();
      mv.visitVarInsn(Opcodes.ALOAD, var + 2);
      mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/ObjectOutputStream", "close", "()V", false);
      // ==> System.out.println
      mv.visitFieldInsn(Opcodes.GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
      mv.visitVarInsn(Opcodes.ALOAD, var + 3);
      mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);

      mv.visitLabel(tryEnd);
      mv.visitJumpInsn(Opcodes.GOTO, catchEnd);

      mv.visitLabel(catchStart);
      mv.visitVarInsn(ASTORE, var + 1); // store exception
      // ==> System.out.println
      mv.visitFieldInsn(Opcodes.GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
      mv.visitVarInsn(Opcodes.ALOAD, var);
      mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Object", "toString", "()Ljava/lang/String;", false);
      mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);

      mv.visitLabel(catchEnd);

      // not sure whether I should add this.
      mv.visitLocalVariable("e", "Ljava/io/IOException;", null, catchStart, catchEnd, var + 1);
      break;
    default: // do nothing
  }
}

但是当我测试时,我一直得到NotSerializableException——我以为我使用try-catch来捕获这个异常。

我不确定是否应该为try-catch块添加visitFrame(我也不知道怎么做)。

PS关于为每个局部变量记录日志的其他更好方法的任何指针也将受到高度赞赏!


共1个答案

匿名用户

你构建一个try-catch块的逻辑是正确的,除了你使用变量var 1var 3,这可能与原始代码的使用发生冲突。当我尝试你的代码来检测一个特别选择的示例,使得它没有这样的变量冲突时,它就起作用了。

您可以使用LocalVariablesSorter解决此类问题,但它需要调用newLocal来为您注入的代码声明变量,并且由于您的代码中没有此类调用,我假设您没有使用LocalVariablesSorter

通常,注入如此复杂的代码,甚至可能多次,不仅容易出错,还可能显着增加代码大小,直到超过方法的最大代码大小。

更可取的方法是将复杂的代码单独移动到方法中,甚至可以以预编译的形式交付,即使用普通Java源代码创建,并且只注入对该方法的调用。

所以,假设像这样的助手类

package mypackage;

import java.io.*;
import java.util.Base64;

public class MyUtil {
    public static void printSerializedWithToStringFallback(Object someObject) {
        try {
            ByteArrayOutputStream bos = new ByteArrayOutputStream();
            ObjectOutputStream oos = new ObjectOutputStream(bos);
            oos.writeObject(someObject);
            oos.close();
            System.out.println(Base64.getEncoder().encodeToString(bos.toByteArray()));
          } catch(IOException ex) {
            System.out.println(someObject.toString());
          }
    }
}

您可以像这样注入调用

@Override
public void visitVarInsn(int opcode, int var) {
    super.visitVarInsn(opcode, var);
    if(opcode == Opcodes.ASTORE) {
        super.visitVarInsn(Opcodes.ALOAD, var);
        super.visitMethodInsn(Opcodes.INVOKESTATIC, "mypackage/MyUtil",
            "printSerializedWithToStringFallback", "(Ljava/lang/Object;)V", false);
    }
}

注入这种调用不会引入任何分支,因此堆栈映射表不需要重新计算。甚至对堆栈的要求也不会改变。注入的代码不会引入新的局部变量,其最高堆栈大小,在aload之后,与astore之前的堆栈大小相同。所以这种简单的插桩不需要COMPUTE_FRAMES选项,甚至不需要COMPUTE_MAXS