ASM 中的 ClassWriter COMPUTE_FRAMES

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 调用具有完全相同的堆栈,因此我决定使用 ASM 在 L35 之前添加一个 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

如我所料。

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

我插入的跳转有什么问题导致 class 无法计算帧吗?这可能是 ASM 的 ClassWriter 的错误吗?

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.[=22= 上调用 Bar 构造函数]

未初始化的实例很特殊。考虑一下,当您 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 指令的引用都会变成声明 class 的普通实例,随后可以与其他类型兼容的条目合并。

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

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