具有输出值的 ASM 尝试/捕获块



>我目前正在尝试让我的自定义编译器允许使用 try/catch 作为表达式,即在堆栈上保留一个值。类型检查器和后端已经支持此功能,但问题似乎是ASM的COMPUTE_FRAMES。使用以下代码进行检测:

private void write(MethodWriter writer, boolean expression)
{
    org.objectweb.asm.Label tryStart = new org.objectweb.asm.Label();
    org.objectweb.asm.Label tryEnd = new org.objectweb.asm.Label();
    org.objectweb.asm.Label endLabel = new org.objectweb.asm.Label();
    boolean hasFinally = this.finallyBlock != null;
    writer.writeLabel(tryStart);
    if (this.action != null)
    {
        if (expression && !hasFinally)
        {
            this.action.writeExpression(writer);
        }
        else
        {
            this.action.writeStatement(writer);
        }
        writer.writeJumpInsn(Opcodes.GOTO, endLabel);
    }
    writer.writeLabel(tryEnd);
    for (int i = 0; i < this.catchBlockCount; i++)
    {
        CatchBlock block = this.catchBlocks[i];
        org.objectweb.asm.Label handlerLabel = new org.objectweb.asm.Label();
        // Check if the block's variable is actually used
        if (block.variable != null)
        {
            // If yes register a new local variable for the exception and
            // store it.
            int localCount = writer.registerLocal();
            writer.writeLabel(handlerLabel);
            writer.writeVarInsn(Opcodes.ASTORE, localCount);
            block.variable.index = localCount;
            if (expression && !hasFinally)
            {
                block.action.writeExpression(writer);
            }
            else
            {
                block.action.writeStatement(writer);
            }
            writer.resetLocals(localCount);
        }
        // Otherwise pop the exception from the stack
        else
        {
            writer.writeLabel(handlerLabel);
            writer.writeInsn(Opcodes.POP);
            if (expression && !hasFinally)
            {
                block.action.writeExpression(writer);
            }
            else
            {
                block.action.writeStatement(writer);
            }
        }
        writer.writeTryCatchBlock(tryStart, tryEnd, handlerLabel, block.type.getInternalName());
        writer.writeJumpInsn(Opcodes.GOTO, endLabel);
    }
    if (hasFinally)
    {
        org.objectweb.asm.Label finallyLabel = new org.objectweb.asm.Label();
        writer.writeLabel(finallyLabel);
        writer.writeInsn(Opcodes.POP);
        writer.writeLabel(endLabel);
        if (expression)
        {
            this.finallyBlock.writeExpression(writer);
        }
        else
        {
            this.finallyBlock.writeStatement(writer);
        }
        writer.writeFinallyBlock(tryStart, tryEnd, finallyLabel);
    }
    else
    {
        writer.writeLabel(endLabel);
    }
}

编译此代码:

System.out.println(try Integer.parseInt("10") catch (Throwable t) 10)

我在类加载时收到以下VerifyError

java.lang.VerifyError: Inconsistent stackmap frames at branch target 17
Exception Details:
  Location:
    dyvil/test/Main.main([Ljava/lang/String;)V @14: goto
  Reason:
    Current frame's stack size doesn't match stackmap.
  Current Frame:
    bci: @14
    flags: { }
    locals: { '[Ljava/lang/String;' }
    stack: { integer }
  Stackmap Frame:
    bci: @17
    flags: { }
    locals: { '[Ljava/lang/String;' }
    stack: { top, integer }
  Bytecode:
    0000000: b200 1412 16b8 001c a700 0957 100a a700
    0000010: 03b6 0024 b1                           
  Exception Handler Table:
    bci [3, 11] => handler: 11
  Stackmap Table:
    same_locals_1_stack_item_frame(@11,Object[#30])
    full_frame(@17,{Object[#38]},{Top,Integer})

由于我认为 ASM 在计算具有输出值的try/catch块的堆栈帧时没有问题,因此我的检测代码是否有问题?(请注意,ClassWriter.getCommonSuperclass 虽然这里不需要,但已正确实现。

显然,ASM 只能计算正确代码的堆栈映射帧,因为没有堆栈映射可以修复损坏的代码。当我们分析异常时,我们可以了解出了什么问题。

java.lang.VerifyError: Inconsistent stackmap frames at branch target 17

有一个分支针对字节码位置17

Exception Details:
  Location:
    dyvil/test/Main.main([Ljava/lang/String;)V @14: goto

分支的来源是位置 14 处的goto指令

  Reason:
    Current frame's stack size doesn't match stackmap.

相当不言自明。您唯一需要考虑的是,不匹配的帧并不一定表示错误的堆栈图计算;可能是字节码本身违反了约束,而计算的堆栈映射只是反映了这一点。

  Current Frame:
    bci: @14
    flags: { }
    locals: { '[Ljava/lang/String;' }
    stack: { integer }

在分支的源(goto指令的位置(14,堆栈包含一个int值。

  Stackmap Frame:
    bci: @17
    flags: { }
    locals: { '[Ljava/lang/String;' }
    stack: { top, integer }

17 时,分支的目标,是堆栈上的两个值。

  Bytecode:
    0000000: b200 1412 16b8 001c a700 0957 100a a700
    0000010: 03b6 0024 b1                           

好吧,字节码在这里没有反汇编,但你不能说到目前为止异常消息太简短了。手动反汇编字节码会产生:

 0: getstatic     0x0014
 3: ldc           0x16
 5: invokestatic  0x001c
 8: goto          +9 (=>17)
11: pop
12: bipush        #10
14: goto          +3 (=>17)
17: invokevirtual 0x0024
20: return

 

  Exception Handler Table:
    bci [3, 11] => handler: 11

我们在这里可以看到的是,到达位置17有两种方式,一种是普通执行getstatic, ldc, invokestatic另一种是异常处理程序,从11开始,执行pop bipush。对于后者,我们可以推断出它在堆栈上确实有一个int值,因为它会弹出异常并推送一个常量int

对于前者,这里没有足够的信息,即我不知道调用方法的签名,但是,由于验证者没有拒绝从817 goto,因此可以安全地假设堆栈确实在分支之前拥有两个值。由于getstatic, ldc生成两个值,因此 static 方法必须具有void ()value (value)签名。这意味着第一个getstatic指令的值不会在分支之前使用。

→阅读您的注释后,错误变得明显:第一个getstatic指令读取System.out您希望在方法末尾用于调用println,但是,当发生异常时,堆栈将被刷新并且堆栈上没有PrintWriter,但异常处理程序尝试恢复并加入调用println需要PrintWriter的位置的代码路径。请务必了解,异常处理程序始终以由单个元素(异常(组成的操作数堆栈开头。在异常发生之前可能已推送的任何值都不会保留。因此,如果要在受保护的代码之前预取字段值(如System.out(,并在是否发生异常时使用它,则必须将其存储在局部变量中并在之后检索。

似乎ASM从第一个分支之前的状态派生了位置@17的堆栈图帧,当将其与第二个分支之前的状态帧连接时,它只关心类型而不关心不同的深度,这很遗憾,因为这是一个很容易发现的错误。但它只是一个缺失的功能(因为COMPUTE_FRAMES没有指定进行错误检查(,而不是错误。

相关内容

  • 没有找到相关文章

最新更新