jvm 使用ASM和Java代理插入堆栈Map帧时出现“类型不可分配给uninitializedThis”

kx1ctssn  于 2022-11-07  发布在  Java
关注(0)|答案(2)|浏览(135)

我想做的是记录Throwable被抛出方法的事件。**我写了下面的简单代码,没有特意使用COMPUTE_FRAME和COMPUTE_MAX来熟悉堆栈Map帧、操作数堆栈和局部变量的概念。**我只通过插装插入了三个堆栈Map帧:在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)

显然,这一定是我插入stackmap框架时的问题。但是我感到困惑:

  1. Current FrameStackmap Frame的确切含义和区别是什么?
    1.为什么在stackmap框架中的@9处有一个uninitializedThis?据我所知,一个对象在构造函数调用完成之前总是uninitializedThis,对吗?
    1.我认为我的插桩是正确的,因为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

顺便说一句,我的简化指令代码如下所示:
第一个

cetgtptt

cetgtptt1#

我们先收拾一下

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是不可达的。不可达的代码总是需要一个新的堆栈Map帧来描述它的初始状态,因为验证者无法猜测。
但是,当然,您可以省略这些不必要的代码,而不是为无法访问的代码提供一个框架。
简化后的代码如下所示

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

注意:由于我们不知道插桩代码包含什么类型的帧(例如,它可能会引入新的变量),我们不应该使用基于前一个帧定义堆栈状态的帧类型。上面的例子只是丢弃了所有变量,因为异常处理程序无论如何都不需要它们,这与每一种可能的堆栈状态都是兼容的--至少对于普通方法是这样。
上面的代码足以检测每一个普通的方法,但不能检测构造函数。创建一个覆盖整个构造函数的异常处理程序是不可能的,包括带有堆栈Map的super(…)。没有堆栈Map的旧类文件可以安装这样的异常处理程序,只要它不试图return或使用this。但是有了堆栈Map,不可能表达处理程序的初始状态:
来自JVMS第4.10.1.9节:

  • 但是如果<init>方法的调用抛出异常,则未初始化的对象可能会处于部分初始化状态,并且需要使其永久不可用。这由包含损坏对象的异常帧表示(本地的新值)和flagThisUninit标志(旧标志)。无法从带有flagThisUninit标志的明显初始化的对象获取正确初始化的对象,因此该对象永久不可用。*

问题是我们不能在栈Map中表示标志,栈Map的框架只包含类型,如果 UninitializedThis 存在,则假定标志flagThisUninit存在,这适合描述超构造函数调用之前的情况,当 UninitializedThis 不存在时,则假定标志flagThisUninit也不存在。其适合于描述在超级构造函数调用之后的情况。
但是当超级构造函数调用失败并出现异常时,堆栈状态如上所述,UninitializedThis 已经被local的新值替换,但标志flagThisUninit仍然存在。我们无法使用堆栈Map来描述这样的帧,因此,我们无法描述异常处理程序的初始帧。
所以,你不能用你的异常处理程序来覆盖超级构造函数调用。你只能为调用之前和之后的代码安装异常处理程序,并且由于不兼容的标志状态,你需要两个不同的处理程序。

8ehkhllq

8ehkhllq2#

也许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                   // {} | {}

在上面的代码片段中,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选项。

相关问题