2

我目前正在尝试使我的自定义编译器允许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,虽然这里不需要,但已正确实现。)

4

1 回答 1

3

显然,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

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

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

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

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

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

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

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

  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常量。

对于前者,这里没有足够的信息,即我不知道被调用方法的签名,但是,由于验证者没有拒绝gotofrom 8to 17,因此可以安全地假设堆栈确实在之前保存了两个值分支。由于getstatic, ldc产生两个值,因此该static方法必须具有 avoid ()value (value)签名。这意味着getstatic在分支之前不使用第一条指令的值。

→阅读您的评论后,错误变得明显:第一getstatic条指令读取System.out了您想要在调用方法结束时使用的指令println,但是,当发生异常时,堆栈被刷新并且堆栈PrintWriter上没有异常处理程序尝试在PrintWriter需要调用的地方恢复并加入代码路径println。重要的是要理解异常处理程序总是从一个由单个元素组成的操作数堆栈开始,即异常。在异常发生之前您可能推送的任何值都不会保留。所以如果你想预取一个字段值(比如System.out) 在受保护的代码之前并使用它,无论是否发生异常,您都必须将其存储在局部变量中并在之后检索。

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

于 2015-04-09T08:51:00.337 回答