使用 ASM 监视 Java 中的对象创建时出现“VerifyError:期望在堆栈上找到对象/数组”?

Posted

技术标签:

【中文标题】使用 ASM 监视 Java 中的对象创建时出现“VerifyError:期望在堆栈上找到对象/数组”?【英文标题】:"VerifyError: Expecting to find object/array on stack" when using ASM to monitor object creation in Java? 【发布时间】:2020-10-10 11:34:42 【问题描述】:

我想做的是监控对象的创建并为该对象记录一个唯一的 ID。所以我使用 ASM 来监控“NEW”指令。在我的方法访问者适配器中:

public void visitTypeInsn(int opcode, String desc)
    mv.visitTypeInsn(opcode, desc);
    if (opcode == NEW)
        mv.visitInsn(DUP);
        mv.visitMethodInsn(INVOKESTATIC, "org/.../.../MyRecorder", "object_new",
                "(Ljava/lang/Object;)V", false);
    

MyRecorder.java:

public static void object_new(Object ref)
    log("object_new !");
    log("MyRecorder: " + ref);
    log("ref.getClass().getName(): " + ref.getClass().getName());

但是,这些代码导致java.lang.VerifyError: (...) Expecting to find object/array on stack。我不知道为什么这行不通。如果这种方式不对,如何监控对象的创建?


其实我也试过监控object.<init>,而不是监控NEW指令。但是,使用以下代码,它会抛出 java.lang.VerifyError: (...) Unable to pop operand off an empty stack:

public void visitMethodInsn(int opc, String owner, String name, String desc, boolean isInterface) 
    ...
    mv.visitMethodInsn(opc, owner, name, desc, isInterface);
    if (opc == INVOKESPECIAL && name.equals("<init>")) 
        mv.visitInsn(DUP);
        mv.visitMethodInsn(INVOKESTATIC, "org/myekstazi/agent/PurityRecorder", "object_new",
                "(Ljava/lang/Object;)V", false);
    

【问题讨论】:

生成的字节码是什么样的?您可以在已编译的类上使用javap 来解决这个问题。我猜你可能在NEW 操作码之后干预得太早了,需要等到INVOKESPECIAL 被调用。 @nrainer 我不知道生成的字节码是什么样的,因为我在加载类时使用 java 代理来转换类,所以我没有将它们转储到磁盘。我认为您的意思是在对象初始化后插入这些指令?我也尝试过,但遗憾的是它也无法顺利运行。 问题是你不能用一个未初始化的对象做很多事情。生成的字节码看起来像 new C, dup, invokestatic ...PurityRecorder.object_new, dup, invokespecial C.&lt;init&gt; @JohannesKuhn 感谢您的评论!你能解释更多关于“不能对未初始化的对象做很多事情”吗?我认为字节码与您描述的完全一样。但我不明白为什么它会抛出 VerifyError,因为它看起来是有效的。 docs.oracle.com/javase/specs/jvms/se14/html/… 搜索未初始化的。 【参考方案1】:

对象实例化分为两条指令。这会使对象实例化变得困难。这实际上是我的多个项目中最大的问题。简单的表达式new Integer(0) 可能会编译为以下代码。

new java/lang/Integer                       ═╤═
dup                                         ═╪═══╤═
iconst_0                                     │   │  ═╤═
invokespecial java/lang/Integer.<init>(I)V   │  ═╧═══╧═
                                             ↓

invokespecial 初始化之前,无法使用使用new 创建的引用。因此,您不能将您的仪器直接放在new 之后。在上面的示例中,您可以将检测放在invokespecial 后面。但是,您需要将其与对超级构造函数的调用区分开来。 super(0) 之类的语句可能会编译为以下代码。

aload_0                                     ═╤═
iconst_0                                     │  ═╤═
invokespecial super class.<init>(I)V      ═╧═══╧═

这两个示例可能涵盖了调用&lt;init&gt; 的两种最常见的情况。如果您不对new Integer(0) 创建的值做任何事情,编译器也可能会决定跳过dup 指令。我不确定他们是否真的这样做。不管怎样,第一个例子会变成下面的代码。

new java/lang/Integer                       ═╤═
iconst_0                                     │  ═╤═
invokespecial java/lang/Integer.<init>(I)V  ═╧═══╧═

这仍然可以通过在new 后面注入dup 和在invokespecial 后面注入invokestatic 来处理。

无论如何,如果您想处理更奇特的情况,字节码还可以包含dup_x1dup_x2 之类的指令。 GeneratorAdapter.box(Type) 实际上会生成这样的代码。一些“惰性”字节码操纵器也可能导致new 指令在使用pop 删除实例之前从未初始化实例。如您所见,使用字节码中的对象实例化可能需要大量工作。但是,根据您的情况,您可能不需要支持所有这些情况。很遗憾,我没有经验来估计您需要支持哪些案例。


这是一个不处理dup_x1 和类似指令的示例。因此,它不适用于GeneratorAdapter.box(Type) 生成的代码。除此之外,它似乎在我的测试中运行良好。它使用AnalyzerAdapter 来获取有关操作数堆栈的一些信息。

public final class InstrumentationMethodVisitor extends AnalyzerAdapter 
  public InstrumentationMethodVisitor(
      String owner, int access, String name, String descriptor,
      MethodVisitor methodVisitor) 
    super(ASM8, owner, access, name, descriptor, methodVisitor);
  

  @Override
  public void visitTypeInsn(int opcode, String type) 
    super.visitTypeInsn(opcode, type);
    if (opcode == NEW && stack != null) 
      mv.visitInsn(DUP);
    
  

  @Override
  public void visitInsn(int opcode) 
    if (opcode == POP && stack != null && stack.get(stack.size() - 1) instanceof Label) 
      mv.visitInsn(POP);
    
    super.visitInsn(opcode);
  

  @Override
  public void visitMethodInsn(int opcode, String owner, String name, String descriptor, boolean isInterface) 
    if (opcode == INVOKESPECIAL && name.equals("<init>")) 
      boolean hasRelatedNew = stack != null && stack.get(stack.size() - getAmountOfArgumentSlots(descriptor) - 1) instanceof Label;

      super.visitMethodInsn(opcode, owner, name, descriptor, isInterface);

      if (hasRelatedNew) 
        mv.visitMethodInsn(
            INVOKESTATIC,
            "org/myekstazi/agent/PurityRecorder",
            "object_new",
            "(Ljava/lang/Object;)V",
            false);
      
    
    else 
      super.visitMethodInsn(opcode, owner, name, descriptor, isInterface);
    
  

  private static int getAmountOfArgumentSlots(String descriptor) 
    int slots = 0;
    for (Type type : Type.getArgumentTypes(descriptor)) 
      slots += type.getSize();
    
    return slots;
  

【讨论】:

以上是关于使用 ASM 监视 Java 中的对象创建时出现“VerifyError:期望在堆栈上找到对象/数组”?的主要内容,如果未能解决你的问题,请参考以下文章

尝试创建 OkHttpClient 对象时出现 kotlin/TypeCastException

java.lang.UnsatisfiedLinkError 在 teiid jboss 翻译器开发中从 FesApi 创建 EpcDocument 对象时出现异常

使用对象 java.util.Date 时出现错误的星期几 [重复]

使用 StorageLibraryChangeTracker 监视文件夹时出现 0x80080222 错误

解析为对象而不是数组时出现Json错误:JSONObject中的getJSONObject(java.lang.String)无法应用于(int)

使用 initWithRelationship:attributes 创建 RKConnectionDescription 时出现异常: