通过 ASM 注入 try/catch 块以在字节码中进行可序列化检查

Injecting try/catch block for serializable check in bytecode through ASM

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

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

我对每个对象的期望,例如someObject:如果这个对象是可序列化的,打印它的序列化表示,如果不是,使用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());
}

因为我试图对每个对象都这样做,所以我在 MethodVisitor 中覆盖了 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 -- 任何有关为每个局部变量进行日志记录的其他更好方法的任何指示也将受到高度赞赏!

您构建 try-catch 块的逻辑是正确的,除了您使用的变量 var + 1var + 3 可能与原始代码的使用冲突。当我尝试使用您的代码来检测专门选择的示例以使其不存在此类变量冲突时,它可以正常工作。

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

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

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

所以,假设一个助手class喜欢

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

注入这个调用不会引入任何分支,所以栈映射table不需要重新计算。甚至对堆栈的要求也没有改变。注入的代码没有引入新的局部变量,它在 aload 之后的最大堆栈大小与 astore 之前的堆栈大小相同。所以这个简单的仪器不需要 COMPUTE_FRAMES option and not even COMPUTE_MAXS.