4

我一直在尝试通过玩 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使用 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

正如我所料。

此外,这个类也不会通过验证,因为无法调用StringBuilder#<init>Top根据 ASM 手册,Top指的是一个未初始化的值,但从跳转位置和之前的代码来看,它似乎并没有在代码中未初始化。我不明白跳跃有什么问题。

我插入的跳转是否有问题,以某种方式使该类无法计算帧?这可能是 ASM 的 ClassWriter 的错误吗?

4

2 回答 2

7

未初始化的实例是特殊的。考虑一下,当您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>它被调用之后,所有指向同一new指令的引用才会转向声明类的普通实例,并且可以随后与其他类型兼容的条目合并。

这意味着像您试图实现的分支是不可能的。相同类型但由不同指令创建的两个未初始化实例条目是不兼容的。new并且不兼容的类型被合并到一个Top条目中,这基本上是一个不可用的条目。如果您不尝试在分支目标处使用该条目,它甚至可能是正确的代码,因此 ASM 在合并它们时不会做任何错误Top而不会抱怨。

new请注意,这也意味着不允许任何可能导致堆栈帧具有多个由同一指令创建的未初始化实例的循环。

于 2016-12-19T16:48:18.267 回答
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但显然不应允许您BarFoo.

于 2016-12-19T07:36:22.950 回答