ASM Try/Catch 具有输出值的块
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();
// 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)
我在 class 加载后得到以下 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 }
在14
,分支的源头(goto
指令的位置),堆栈包含一个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
常量。
对于前者,这里没有足够的信息,即我不知道调用方法的签名,但是,因为验证者没有拒绝来自 8
的 goto
到 17
,可以安全地假设堆栈确实在分支之前保存了两个值。由于 getstatic, ldc
产生两个值,因此 static
方法必须具有 void ()
或 value (value)
签名。这意味着第一个 getstatic
指令的值在分支之前没有被使用。
→阅读您的评论后,错误变得明显:第一个 getstatic
指令读取 System.out
,您想在方法末尾使用它来调用 println
,但是,当发生异常时,堆栈被刷新并且堆栈上没有 PrintWriter
但异常处理程序尝试恢复并在需要 PrintWriter
的地方加入代码路径以调用 println
.重要的是要了解异常处理程序 总是 以由单个元素(异常)组成的操作数堆栈开始。 None 在异常发生之前您可能已推送的值将保留。所以如果你想在保护代码之前预取一个字段值(如System.out
)并且无论是否发生异常都使用它,你必须将它存储在一个局部变量中并在之后检索。
似乎 ASM 从第一个分支之前的状态派生出位置 @17
的堆栈映射框架,当将它与第二个分支之前的状态框架连接时,它只关心类型而不关心类型深度不同,很遗憾,因为这是一个很容易发现的错误。但这只是一个缺失的功能(因为 COMPUTE_FRAMES
没有指定进行错误检查),而不是错误。
我目前正在尝试让我的自定义编译器允许使用 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)
我在 class 加载后得到以下 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 }
在14
,分支的源头(goto
指令的位置),堆栈包含一个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
常量。
对于前者,这里没有足够的信息,即我不知道调用方法的签名,但是,因为验证者没有拒绝来自 8
的 goto
到 17
,可以安全地假设堆栈确实在分支之前保存了两个值。由于 getstatic, ldc
产生两个值,因此 static
方法必须具有 void ()
或 value (value)
签名。这意味着第一个 getstatic
指令的值在分支之前没有被使用。
→阅读您的评论后,错误变得明显:第一个 getstatic
指令读取 System.out
,您想在方法末尾使用它来调用 println
,但是,当发生异常时,堆栈被刷新并且堆栈上没有 PrintWriter
但异常处理程序尝试恢复并在需要 PrintWriter
的地方加入代码路径以调用 println
.重要的是要了解异常处理程序 总是 以由单个元素(异常)组成的操作数堆栈开始。 None 在异常发生之前您可能已推送的值将保留。所以如果你想在保护代码之前预取一个字段值(如System.out
)并且无论是否发生异常都使用它,你必须将它存储在一个局部变量中并在之后检索。
似乎 ASM 从第一个分支之前的状态派生出位置 @17
的堆栈映射框架,当将它与第二个分支之前的状态框架连接时,它只关心类型而不关心类型深度不同,很遗憾,因为这是一个很容易发现的错误。但这只是一个缺失的功能(因为 COMPUTE_FRAMES
没有指定进行错误检查),而不是错误。