具有输出值的ASM Try / Catch Block [英] ASM Try/Catch Block with an output value
问题描述
我目前正在尝试让我的自定义编译器允许使用 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屋!