我一直试图通过在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调用具有完全相同的堆栈,因此我决定在L35之前使用ASM添加ICONST_1; IFEQ L10
序列。
当我解散(再次与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
正如我所预料的那样。
此外,由于无法在StringBuilder#<init>
上呼叫Top
,此课程也无法通过验证。根据ASM手册,Top
指的是未初始化的值,但它似乎在代码中未初始化,无论是从跳转位置还是之前的代码。我不明白跳跃有什么问题。
我插入的跳转是否有问题导致该类无法计算帧?这可能是ASM的ClassWriter的一个错误吗?
答案 0 :(得分:3)
未初始化的实例很特殊。考虑到,当您dup
引用时,您已经对堆栈上的同一个实例进行了两次引用,您可能会执行更多的堆栈操作或将引用传递给局部变量,并从那里将其复制到其他变量或者再推一次。尽管如此,参考的目标应该在您以任何方式使用之前完全初始化一次。要验证这一点,必须跟踪对象的标识,以便所有对同一对象的这些引用将从未初始化转换为初始化你在它上面执行invokespecial <init>
。
Java编程语言不使用所有可能性,但是对于像。这样的合法代码
new Foo(new Foo(new Foo(), new Foo(b? new Foo(a): new Foo(b, c)))
,它不应该忽略哪个Foo
实例已经初始化,哪些不是,何时进行分支。
因此每个未初始化的实例堆栈帧条目都与创建它的new
指令相关联。传输或复制时,所有条目都保留引用(可以像remembering the byte code offset of the new
instruction一样简单处理)。只有在调用invokespecial <init>
之后,指向同一new
指令的所有引用都转向声明类的普通实例,并且随后可以与其他类型兼容的条目合并。
这意味着像你想要实现的分支是不可能的。相同类型的两个未初始化实例条目,但由不同的new
指令创建,是不兼容的。并且不兼容的类型合并到Top
条目,这基本上是一个不可用的条目。它甚至可以是正确的代码,如果你不尝试在分支目标上使用该条目,那么ASM在将它们合并到Top
而没有抱怨时没有做错任何事。
请注意,这也意味着不允许任何类型的循环导致堆栈帧具有由同一new
指令创建的多个未初始化实例。
答案 1 :(得分:0)
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
类型上进行未经检查的初始化,那么这将是有效的,但显然不应该允许您在Bar
上调用Foo
构造函数。