【问题标题】:Injecting try/catch block for serializable check in bytecode through ASM通过 ASM 注入 try/catch 块以在字节码中进行可序列化检查
【发布时间】:2026-02-01 11:45:01
【问题描述】:

我是 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 -- 任何关于其他更好的方法来记录每个局部变量的指针也将不胜感激!

【问题讨论】:

  • 你注射太多了。您可能会覆盖原始代码使用的变量(除非您使用LocalVariableSorter)并且为每个存储操作注入此代码块会使代码大小爆炸。创建一个专门的方法,封装你的第一个代码sn-p的操作,可以正常编译,只需注入该辅助方法的调用。
  • 此外,请注意,要确保所有序列化数据都已刷新到ByteArrayOutputStream,您必须在close ObjectOutputStream 之前 调用toByteArray()。但是为什么你认为base64编码的序列化数据比toString()输出更好呢?
  • @Holger 生成序列化数据的原因是我想将这些数据反序列化回对象并进行比较。
  • @Holger Inject an invocation -- 你的意思是在MethodVisitor之外进行吗?
  • 不,只是在某处创建一个方法,比如 public static void printSerializedWithToStringFallback(Object someObject) 在类 mypackage.MyUtil 中包含所有代码,然后,而不是所有复杂的代码,你只是在做 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); } 注入一个调用该方法传递要打印的对象。

标签: java bytecode java-bytecode-asm bytecode-manipulation jvm-bytecode


【解决方案1】:

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

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

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

更可取的方法是将复杂代码单独移动到方法中,甚至可以以预编译形式交付,即使用普通 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

【讨论】:

  • 我试过你的方法来记录局部变量。在大多数情况下,它工作得很好,但在某些情况下,它得到了VerifyError: Bad local variable type,原因是Type top (current frame, locals[14]) is not assignable to reference type for ALOAD。我以为每个ASTORE 都可以有一个ALOAD,但有时它会因not assignable 而失败。对此有任何想法吗?
  • 只有一个角盒。当您有相当旧的字节码(针对 Java 6 或更早版本)时,您可能会遇到具有奇怪不对称性的 jsr/ret 对。 jsr 会将 returnaddress 推送到堆栈,但 ret 期望 returnaddress 在局部变量中。为了支持这一点,astore 可以弹出这样的 returnaddress 并将其存储到局部变量中。但是aload 无法读取返回地址。否则,我不知道存储后立即读取会失败的情况。但是看到完整的VerifyError 会很有帮助。通常,它包含用于分析的整个字节码。