![](/img/trans.png)
[英]ASM skips Classes if COMPUTE_FRAMES is set in ClassWriter
[英]ClassWriter COMPUTE_FRAMES in ASM
我一直试图通过玩转 ASM 中的跳转来了解堆栈映射框架在 Java 中的工作原理。 我创建了一个简单的方法来尝试一些东西:(用 Krakatau 拆卸):
L0: ldc 'hello'
L2: astore_1
L3: getstatic Field java/lang/System out Ljava/io/PrintStream;
L6: new java/lang/StringBuilder
L9: dup
L10: invokespecial Method java/lang/StringBuilder <init> ()V
L13: ldc 'concat1'
L15: invokevirtual Method java/lang/StringBuilder append (Ljava/lang/String;)Ljava/lang/StringBuilder;
L18: aload_1
L19: invokevirtual Method java/lang/StringBuilder append (Ljava/lang/String;)Ljava/lang/StringBuilder;
L22: invokevirtual Method java/lang/StringBuilder toString ()Ljava/lang/String;
L25: invokevirtual Method java/io/PrintStream println (Ljava/lang/String;)V
L28: getstatic Field java/lang/System out Ljava/io/PrintStream;
L31: new java/lang/StringBuilder
L34: dup
L35: invokespecial Method java/lang/StringBuilder <init> ()V
L38: ldc 'concat2'
L40: invokevirtual Method java/lang/StringBuilder append (Ljava/lang/String;)Ljava/lang/StringBuilder;
L43: aload_1
L44: invokevirtual Method java/lang/StringBuilder append (Ljava/lang/String;)Ljava/lang/StringBuilder;
L47: invokevirtual Method java/lang/StringBuilder toString ()Ljava/lang/String;
L50: invokevirtual Method java/io/PrintStream println (Ljava/lang/String;)V
L53: return
它所做的只是创建一个StringBuilder
来连接一些带有变量的字符串。
由于 L35 处的 invokespecial 调用与 L10 处的 invokespecial 调用具有完全相同的堆栈,因此我决定添加一个ICONST_1; IFEQ L10
ICONST_1; IFEQ L10
序列就在带有 ASM 的 L35 之前。
当我拆解时(再次用 Krakatau),我发现结果很奇怪。 ASM 已计算出 L10 处的堆栈帧为:
.stack full
locals Object [Ljava/lang/String; Object java/lang/String
stack Object java/io/PrintStream Top Top
.end stack
代替
stack Object java/io/PrintStream Object java/lang/StringBuilder Object java/lang/StringBuilder
正如我所料。
此外,这个类也不会通过验证,因为不能在Top
上调用StringBuilder#<init>
。 根据ASM手册, Top
指的是一个未初始化的值,但在代码中似乎没有未初始化,无论是跳转位置还是之前的代码。 我不明白跳跃有什么问题。
我插入的跳转是否有问题,以某种方式使类无法计算帧? 这可能是 ASM 的 ClassWriter 的错误吗?
未初始化的实例是特殊的。 考虑到,当您dup
引用时,您已经在堆栈上有两个对同一实例的引用,您可能会执行更多堆栈操作或将引用转移到局部变量,然后从那里将其复制到其他变量或再次推送. 尽管如此,在您以任何方式使用它之前,引用的目标应该被初始化一次。 为了验证这一点,必须跟踪对象的身份,以便在对同一对象执行invokespecial <init>
时,所有这些对同一对象的引用将从未初始化变为已初始化。
Java 编程语言并没有使用所有的可能性,但是对于像这样的合法代码
new Foo(new Foo(new Foo(), new Foo(b? new Foo(a): new Foo(b, c)))
它不应该了,履带哪些Foo
实例已初始化,并且没有,当分支成立。
因此,每个未初始化的实例堆栈帧条目都与创建它的new
指令相关联。 所有条目在传输或复制时都保留引用(可以像记住new
指令的字节码偏移一样轻松处理)。 只有在invokespecial <init>
调用了invokespecial <init>
之后,指向同一条new
指令的所有引用才转向声明类的普通实例,并且随后可以与其他类型兼容的条目合并。
这意味着像您试图实现的分支是不可能的。 相同类型但由不同new
指令创建的两个未初始化实例条目是不兼容的。 并且不兼容的类型被合并到一个Top
条目,这基本上是一个不可用的条目。 它甚至可能是正确的代码,如果您不尝试在分支目标上使用该条目,那么 ASM 在将它们合并到Top
时不会做任何错误而不会抱怨。
请注意,这也意味着任何可能导致堆栈帧具有多个由同一条new
指令创建的未初始化实例的循环都是不允许的。
new java/lang/StringBuilder
不会创建有效的StringBuilder
而是一个统一的对象,该对象在堆栈映射帧中与TOP
一起捐赠。 这个值在对象构造过程中加入跳转指令时使用,例如:
new Foo(a ? b : c);
这被翻译成几个 goto 语句。
在对象上调用构造函数时,该对象首先被视为StringBuilder
,即invokespecial Method java/lang/StringBuilder <init> ()V
。 JVM 不支持在不同位置初始化此对象,因为验证器只能查看TOP
类型,该类型不能反映所需类型的实际阴影,即未初始化的StringBuilder
。 您可能会争辩说 JVM 应该支持这一点,但这将需要更大的数组来包含堆栈映射帧以反映类型和初始化状态,这可能无法证明这种能力甚至不被 Java 语言使用。
为了说明这一点,请考虑以下情况:
new Foo
dup
.stack full
locals
stack Top Top
.end stack
invokespecial Bar <init> ()V
如果 JVM 允许对TOP
类型进行未经检查的初始化,这将是有效的,但显然不应允许您在Foo
上调用Bar
构造函数。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.