具有输出值的ASM Try / Catch Block [英] ASM Try/Catch Block with an output value

查看:202
本文介绍了具有输出值的ASM Try / Catch Block的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我目前正在尝试让我的自定义编译器允许使用 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();

//检查块的变量是否实际使用
if(block.variable!= null)
{
//如果是,则注册一个新的局部变量for异常和
//存储它。
int localCount = writer.registerLocal();

writer.writeLabel(handlerLabel);
writer.writeVarInsn(Opcodes.ASTORE,localCount);
block.variable.index = localCount;
if(表达式&&!hasFinally)
{
block.action.writeExpression(writer);
}
else
{
block.action.writeStatement(writer);
}
writer.resetLocals(localCount);
}
//否则从堆栈弹出异常
else
{
writer.writeLabel(handlerLabel);
writer.writeInsn(Opcodes.POP);
if(表达式&&!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(尝试Integer.parseInt(10)catch(Throwable t)10)

我在课程加载时得到以下 VerifyError

  java.lang.VerifyError:分支目标17的不一致堆栈图帧
异常详细信息:
位置:
dyvil / test / Main。 main([Ljava / lang / String;] V @ 14:goto
原因:
当前帧的堆栈大小与stackmap不匹配。
当前帧:
bci:@ 14
flags:{}
locals:{'[Ljava / lang / String;'}
stack:{integer}
Stackmap框架:
bci:@ 17
flags:{}
locals:{'[Ljava / lang / String;'}
stack:{top,integer}
字节码:
0000000:b200 1412 16b8 001c a700 0957 100a a700
0000010:03b6 0024 b1
异常处理程序表:
bci [3,11] => handler:11
Stackmap表:
same_locals_1_stack_item_frame(@ 11,Object [#30])
full_frame(@ 17,{Object [#38]},{Top,Integer})

因为我不认为ASM在计算的堆栈帧时有问题尝试/ catch 具有输出值的块,我的检测代码有问题吗? (注意 ClassWriter.getCommonSuperclass 虽然在这里不需要,但是已经正确实现了。)

解决方案

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

  java.lang.VerifyError:分支目标处的堆栈映射帧不一致17 

有一个分支目标字节代码position 17

 异常详情: 
位置:
dyvil / test / Main.main([Ljava / lang / String;] V @ 14:转到

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

 原因:
当前帧的堆栈大小与stackmap不匹配。

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

 当前帧:
bci:@ 14
flags:{}
locals:{'[Ljava / lang / String;'}
stack:{integer}

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



< pre class =lang-none prettyprint-override> Stackmap框架:
bci:@ 17
flags:{}
locals:{'[Ljava / lang /字符串;'}
堆栈:{top,integer}

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

 字节码:
0000000:b200 1412 16b8 001c a700 0957 100a a700 $ b $ 00 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:返回

 异常处理程序表:
bci [3,11] =>处理程序:11

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



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



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



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


I am currently trying make my custom compiler allow using try/catch as an expression, i.e. leaving a value on the stack. The type checker and the backend already support this, but the problem seems to be ASM's COMPUTE_FRAMES. With the below code for instrumentation:

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

Compiling this code:

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

I get the following VerifyError upon class loading:

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

Since I don't think that ASM has a problem computing the stack frames for try/catch blocks with an output value, is there a problem with my instrumentation code? (Note that ClassWriter.getCommonSuperclass, although it is not needed here, is correctly implemented.)

解决方案

Obviously, ASM can calculate stackmap frames for correct code only as no stackmap can fix broken code. We can learn what went wrong when we analyze the exception.

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

there is a branch targeting byte code position 17.

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

the source of the branch is a goto instruction at position 14

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

quite self explaining. The only thing you have to consider that non-matching frames don’t necessarily indicate a wrong stackmap calculation; it might be that the bytecode itself is violates the constraints and the calculated stackmap just reflects that.

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

at 14, the source of the branch (the location of the goto instruction), the stack contains one int value.

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

at 17, the target of the branch, are two values on the stack.

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

well, the bytecode isn’t disassembled here, but you can’t say the exception message was too brief up to this point. Manual disassembling the bytecode yields:

 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

What we can see here is that there are two ways of reaching location 17, one is the ordinary execution of getstatic, ldc, invokestatic the other is the exception handler, starting at 11, performing pop bipush. We can deduce for the latter that it has indeed one int value on the stack as it pops the exception and pushes one int constant.

For the former, there is not enough information here, i.e. I don’t know the signature of the invoked method, however, since the verifier didn’t reject the goto from 8 to 17, it’s safe to assume that the stack indeed holds two values before the branch. Since getstatic, ldc produces two values, the static method must have either a void () or a value (value) signature. This implies that the value of the very first getstatic instruction is not used before the branch.

→After reading your comment, the error becomes apparent: that first getstatic instruction reads System.out which you want to use at the end of the method to invoke println, however, when an exception occurred, the stack is flushed and no PrintWriter is on the stack but the exception handler tries to recover and join the code path at the place where the PrintWriter is required for invoking println. It is important to understand that exception handlers always start with an operand stack consisting of a single element, the exception. None of the values you might have pushed before the exception occurred will persist. So if you want to prefetch a field value (like System.out) before the guarded code and use it regardless of whether an exception occurred, you have to store it in a local variable and retrieve afterwards.

It seems that ASM derived the stackmap frame for location @17 from the state before the first branch and when joining it with the frame of the state before the second branch, it only cared for the types but not the different depth, which is a pity as it’s an error that is easy to spot. But it’s only a missing feature (as COMPUTE_FRAMES is not specified to do error checking), not a bug.

这篇关于具有输出值的ASM Try / Catch Block的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆