【问题标题】:"Type is not assignable to uninitializedThis" when inserting stackmap frame using ASM and Javaagent使用 ASM 和 Javaagent 插入堆栈图帧时,“类型不可分配给 uninitializedThis”
【发布时间】:2023-04-04 04:51:01
【问题描述】:

我想要做的是记录一个 Throwable 被抛出方法的事件。 我写了以下简单的代码,并没有刻意使用 COMPUTE_FRAME 和 COMPUTE_MAX 来熟悉堆栈映射帧、操作数堆栈和局部变量的概念。我只通过插桩插入三个堆栈映射帧:后tryEnd 标签,catchStart 标签之后,catchEnd 之后(在我的班级 MyMethodVisitor 中,代码是 sho)。

当我在joda-time的测试过程中尝试我的javaagent时,它崩溃并显示以下消息:

[ERROR] There was an error in the forked process
[ERROR] Stack map does not match the one at exception handler 9
[ERROR] Exception Details:
[ERROR]   Location:
[ERROR]     org/joda/time/TestAllPackages.<init>(Ljava/lang/String;)V @9: ldc
[ERROR]   Reason:
[ERROR]     Type 'org/joda/time/TestAllPackages' (current frame, locals[0]) is not assignable to uninitializedThis (stack map, locals[0])
[ERROR]   Current Frame:
[ERROR]     bci: @2
[ERROR]     flags: { flagThisUninit }
[ERROR]     locals: { 'org/joda/time/TestAllPackages', 'java/lang/String' }
[ERROR]     stack: { 'java/lang/Throwable' }
[ERROR]   Stackmap Frame:
[ERROR]     bci: @9
[ERROR]     flags: { flagThisUninit }
[ERROR]     locals: { uninitializedThis, 'java/lang/String' }
[ERROR]     stack: { 'java/lang/Throwable' }
[ERROR]   Bytecode:
[ERROR]     0x0000000: 2a2b b700 01b1 a700 0912 57b8 005c bfb1
[ERROR]     0x0000010:                                        
[ERROR]   Exception Handler Table:
[ERROR]     bci [0, 6] => handler: 9
[ERROR]   Stackmap Table:
[ERROR]     same_frame(@6)
[ERROR]     same_locals_1_stack_item_frame(@9,Object[#85])
[ERROR]     same_frame(@15)

显然,这一定是我插入堆栈图帧时的问题。但我很困惑:

  1. Current FrameStackmap Frame的具体含义和区别是什么?
  2. 为什么@9 处的堆栈图帧中有一个uninitializedThis?据我了解,在构造函数调用完成之前,对象始终为 uninitializedThis,对吗?
  3. 我认为我的检测是正确的,因为org/joda/time/TestAllPackagesthis 的类型。如何避免org/joda/time/TestAllPackagesuninitializedThis不一致?

当我查看字节码时,它看起来像:

public org.joda.time.TestAllPackages(java.lang.String);
  descriptor: (Ljava/lang/String;)V
  flags: ACC_PUBLIC
  Code:
    stack=7, locals=2, args_size=2
       0: aload_0
       1: aload_1
       2: invokespecial #1                  // Method junit/framework/TestCase."<init>":(Ljava/lang/String;)V
       5: return
       6: goto          15
       9: ldc           #87                 // String org/joda/time/TestAllPackages#<init>#(Ljava/lang/String;)V
      11: invokestatic  #92                 // Method MyRecorder.exception_caught:(Ljava/lang/String;)V
      14: athrow
      15: return
    Exception table:
       from    to  target type
           0     6     9   Class java/lang/Throwable
    StackMapTable: number_of_entries = 3
      frame_type = 6 /* same */
      frame_type = 66 /* same_locals_1_stack_item */
        stack = [ class java/lang/Throwable ]
      frame_type = 5 /* same */
    LineNumberTable:
      line 31: 0
      line 32: 5

顺便说一句,我的简化检测代码如下:

public class PreMain {
    public static void premain(String args, Instrumentation inst){
        inst.addTransformer(new MyTransformer());
    }
}
public class MyTransformer implements ClassFileTransformer {
    @Override
    public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,
                            ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
        byte[] result = classfileBuffer;

        try{
            if (className == null || shouldExcludeClass(className)) return result;
            ClassReader cr = new ClassReader(classfileBuffer);
            // I don't use COMPUTE_FRAME and COMPUTE_MAX deliberately
            ClassWriter cw = new ClassWriter(cr, 0);
            ClassVisitor cv = new MyClassVistor(cw, className, loader);
            cr.accept(cv, 0);
            result = cw.toByteArray();
        } catch (Throwable t){
            t.printStackTrace();
        }

        return result;
    }
}
public class MyClassVistor extends ClassVisitor {
    ...
    @Override
    public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
        MethodVisitor mv = cv.visitMethod(access, name, desc, signature, exceptions);
        if (!isNative && !isEnum && !isAbstract && !"<clinit>".equals(name)){
            mv = new MyMethodVisitor(mv, name, access, desc, slashClassName, isStatic, isPublic);
        }
        return mv;
    }
}
public class MyMethodVisitor extends MethodVisitor {

    private Label tryStart = new Label();
    private Label tryEnd = new Label();
    private Label catchStart = new Label();
    private Label catchEnd = new Label();

    public void visitCode() {
        mv.visitCode();
        mv.visitTryCatchBlock(tryStart, tryEnd, catchStart, "java/lang/Throwable");
        mv.visitLabel(tryStart);
    }

    @Override
    public void visitEnd() {
        mv.visitLabel(tryEnd);
        mv.visitFrame(F_SAME, 0, null, 0, null); /* This line takes me more than 6 hours to figure out. Why this line can't be omitted? */
        mv.visitJumpInsn(GOTO, catchEnd);

        mv.visitLabel(catchStart);
        // exception caught
        mv.visitFrame(F_SAME1, 0, null, 1, new Object[] {"java/lang/Throwable"}); /* add stackmap frame after jump target */
        mv.visitLdcInsn(this.selfMethodId);
        mv.visitMethodInsn(INVOKESTATIC, MyRecorder.SLASH_CLASS_NAME, MyRecorder.EXCEPTION_CAUGHT,
                "(Ljava/lang/String;)V", false);
        mv.visitInsn(ATHROW);

        mv.visitLabel(catchEnd);
        mv.visitFrame(F_SAME, 0, null, 0, null); /* add stackmap frame after jump target */
        // Make up a return statement
        switch (Type.getReturnType(selfDesc).getSort()){
            case BYTE:
            case CHAR:
            case SHORT:
            case BOOLEAN:
            case INT:
                mv.visitLdcInsn(0);
                mv.visitInsn(IRETURN);
                break;
            case LONG:
                mv.visitLdcInsn(0L);
                mv.visitInsn(LRETURN);
                break;
            case FLOAT:
                mv.visitLdcInsn(0f);
                mv.visitInsn(FRETURN);
                break;
            case DOUBLE:
                mv.visitLdcInsn(0.0);
                mv.visitInsn(DRETURN);
                break;
            case OBJECT:
                mv.visitInsn(ACONST_NULL);
                mv.visitInsn(ARETURN);
                break;
            case VOID:
                mv.visitInsn(RETURN);
                break;
        }
        super.visitEnd();
    }

    @Override
    public void visitMaxs(int maxStack, int maxLocals) {
        // +5 because other logic need more space on operand stack
        super.visitMaxs(maxStack + 5, maxLocals);
    }
}

【问题讨论】:

    标签: java jvm instrumentation java-bytecode-asm javaagents


    【解决方案1】:

    也许org.objectweb.asm.tree.analysis.Analyzer 类的analyze 方法会给我们一点启示:

    if (newControlFlowExceptionEdge(insnIndex, tryCatchBlock)) {
        Frame<V> handler = newFrame(oldFrame);
        handler.clearStack(); // clear the stack
        handler.push(interpreter.newExceptionValue(tryCatchBlock, handler, catchType)); // push the exception
        merge(insnList.indexOf(tryCatchBlock.handler), handler, subroutine); // merge two frames
    }
    

    try 块中的每条指令都会做以下两件事:

    • 首先,清空堆栈并将预期的异常压入堆栈
    • 然后,尝试将当前帧catch块开始处的帧合并

    那么,我们来模拟一下指令的执行:

    <init>:(Ljava/lang/String;)V
                                   // {uninitialized_this, String} | {}
    0000: aload_0                  // {uninitialized_this, String} | {uninitialized_this}   ──────── compatible ────────┐
    0001: aload_1                  // {uninitialized_this, String} | {uninitialized_this, String} ─── compatible ──┐    │
    0002: invokespecial   #8       // {this, String} | {} ──────── incompatible ────────┐                          │    │
    0005: return                   // {} | {}                                           │                          │    │
                                   // {uninitialized_this, String} | {Throwable} ───────┴──────────────────────────┴────┘
    0006: ldc             #11      // {uninitialized_this, String} | {Throwable, String}
    0008: invokestatic    #16      // {uninitialized_this, String} | {Throwable}
    0011: athrow                   // {} | {}
                                   // {uninitialized_this, String} | {}
    0012: return                   // {} | {}
    

    在上面的sn-p中,0002处的locals[0]this;但是,0006 处的 locals[0]uninitialized_this。这两个值是不相容的。 Current Frame特定位置的实际帧Stackmap Frame另一个特定位置的预期帧

    恕我直言,我们不应该捕获 super() 方法。

    一些小事:

    • MyMethodVisitor.visitEnd() 中的代码应该放在visitMax() 方法中。这是因为visitCode() 方法标志着方法体的开始,visitMax() 标志着方法体的结束,visitEnd() 标志着结束整个方法
    • mv.visitTryCatchBlock() 应该放在visitMax() 方法中。如果我们将mv.visitTryCatchBlock() 放在visitCode() 中,它将使所有其他try-catch 子句无效。
    • goto 指令之前已经有一个return。以下两行代码可能是多余的:
    mv.visitFrame(F_SAME, 0, null, 0, null); /* This line takes me more than 6 hours to figure out. Why this line can't be omitted? */
    mv.visitJumpInsn(GOTO, catchEnd);
    

    最后,为避免不一致,建议使用COMPUTE_FRAME选项。

    【讨论】:

    • 如果我们只看类型,将thisuninitialized_this 合并到top 是可能的。不幸的是,这些框架也有不兼容的 标志,即使指定 COMPUTE_FRAME 也无法修复。一般来说,我不同意最后一句话。如果开发人员试图理解框架,这是一件好事。而且 ASM 的 COMPUTE_FRAME 选项很昂贵,并且有 unavoidable limitations
    • @Holger 我看过很多与 java-bytecode-asm 相关的问题,我真的非常非常喜欢你的回答。
    • @Instein topthe Verification Type System 的根。所以一切都可以分配给top,这基本上是一个不可用的条目。
    • @Instein 这与调用访问方法的顺序有关。当您将visitTryCatchBlock 放入visitCode 时,它是第一个,在任何其他visitTryCatchBlock 调用原始代码之前。由于它是第一个 then 并且匹配整个代码并捕获所有 throwable,因此在发生异常时它将始终具有优先权。同样,visitEnd() visitMaxs 之后被调用,但所有指令(和异常处理程序)都应该在super.visitMaxs(…) 调用之前被访问。
    • @Instein 好吧,毕竟它必须有一些规则来决定。它被指定为here, within §2.10:“方法的异常处理程序搜索匹配项的顺序很重要。 [...] 在运行时,当抛出异常时,Java 虚拟机按照它们出现在类文件中相应异常处理程序表中的顺序从该表的开头开始搜索当前方法的异常处理程序。
    【解决方案2】:

    我们先清理

    mv.visitFrame(F_SAME, 0, null, 0, null); /* This line takes me more than 6 hours to figure out. Why this line can't be omitted? */
    mv.visitJumpInsn(GOTO, catchEnd);
    

    您正在为整个方法创建一个异常处理程序,将处理程序附加到原始代码之后。假设原始代码是有效的,它必须以…returnathrowgoto指令结尾,因为代码不允许“脱落”代码末尾。

    因此,您在此处附加的代码,处理程序上的 goto 到新生成的返回指令是无法访问的。无法访问的代码总是需要一个新的堆栈映射框架来描述其初始状态,因为验证者无法猜测。

    但是,当然,您可以省略这些不必要的代码,而不是为无法访问的代码提供框架。

    所以简化的代码看起来像

    public class MyMethodVisitor extends MethodVisitor {
        private final Label tryStart = new Label();
        private final Label tryEndCatchStart = new Label();
    
        …
    
        @Override
        public void visitCode() {
            mv.visitCode();
            mv.visitLabel(tryStart);
        }
    
        @Override
        public void visitMaxs(int maxStack, int maxLocals) {
            mv.visitTryCatchBlock(
                tryStart, tryEndCatchStart, tryEndCatchStart, "java/lang/Throwable");
    
            mv.visitLabel(tryEndCatchStart);
            mv.visitFrame(F_FULL, 0, null, 1, new Object[] {"java/lang/Throwable"});
            mv.visitLdcInsn(this.selfMethodId);
            mv.visitMethodInsn(INVOKESTATIC, MyRecorder.SLASH_CLASS_NAME,
                MyRecorder.EXCEPTION_CAUGHT, "(Ljava/lang/String;)V", false);
            mv.visitInsn(ATHROW);
            // the exception handler needs two stack entries, the throwable and a string
            super.visitMaxs(Math.max(2, maxStack), maxLocals);
        }
    }
    

    注意:因为我们不知道检测代码包含什么样的帧(例如,它可能会引入新变量),所以我们不应该使用基于前一帧定义堆栈状态的帧类型。上面的示例只是删除了所有变量,因为异常处理程序无论如何都不需要它们,这与所有可能的堆栈状态兼容——至少对于普通方法而言。

    上面的代码足以检测每个普通方法,但不能检测构造函数。不可能创建一个覆盖整个构造函数的异常处理程序,包括带有堆栈映射的super(…)。没有堆栈映射的旧类文件可以安装这样的异常处理程序,只要它不尝试return 或使用this。但是使用堆栈映射,无法表达处理程序的初始状态:

    来自JVMS §4.10.1.9

    但是如果调用&lt;init&gt; 方法引发异常,则未初始化的对象可能会处于部分初始化状态,并且需要永久不可用。这由包含损坏对象(本地的新值)和flagThisUninit 标志(旧标志)的异常帧表示。没有办法从带有flagThisUninit 标志的明显初始化对象到正确初始化的对象,因此该对象永久不可用。

    问题是我们不能在堆栈映射中表达标志。堆栈映射的框架仅包含类型,如果存在 UninitializedThis,则假定存在标志 flagThisUninit,这适合描述超级构造函数调用之前的情况。当UninitializedThis不存在时,标志flagThisUninit也被认为不存在,适合描述超级构造函数调用后的情况。

    但是当超级构造函数调用失败并出现异常时,堆栈状态如上所述,UninitializedThis 已经被本地的新值替换,但标志flagThisUninit 仍然存在。我们无法使用堆栈映射来描述这样的帧,因此,我们无法描述异常处理程序的初始帧。


    所以,你不能用你的异常处理程序来覆盖超级构造函数调用。您只能在调用前后为代码安装异常处理程序,并且由于标志状态不兼容,您需要两个不同的处理程序。

    【讨论】:

    • 感谢您的详细解释!我很感激!我不明白的一件事是mv.visitFrame(F_FULL, 0, null, 1, new Object[] {"java/lang/Throwable"});。 “简单地删除所有变量”是什么意思?从API,它说 F_FULL 帧需要包含完整的帧数据。但是这里你好像把locals里的所有变量都删了?
    • 没错。我指定了一个不包含变量的完整框架。由于异常处理程序不使用任何变量,因此无需猜测存在哪些变量。一般来说,frames 不需要描述前面代码中已经存在的内容,而是后续代码中将使用的内容。当然,这必须是兼容的,这就是基于堆栈映射的验证的全部内容。这与COMPUTE_FRAME 方法不同,因为 ASM 将尝试根据先前的状态计算状态。 This answer 显示,结果会有多么不同。
    猜你喜欢
    • 1970-01-01
    • 2014-10-26
    • 2014-10-11
    • 1970-01-01
    • 1970-01-01
    • 2023-01-18
    • 2012-06-26
    • 2020-04-02
    • 2014-07-22
    相关资源
    最近更新 更多