为什么Java编译器会生成奇怪的局部变量和堆栈映射框架,以及如何使用它们可靠地确定变量类型?

不:

我正在ASM框架的帮助下创建Java字节码检测工具,并且需要确定并可能更改方法的局部变量的类型。我很快遇到了一个简单的情况,变量和堆栈映射节点看起来有些奇怪,并且没有为我提供有关所使用变量的足够信息:

public static void test() {
    List l = new ArrayList();
    for (Object i : l) {
        int a = (int)i;
    }
}

给出以下字节码(来自Idea):

public static test()V
   L0
    LINENUMBER 42 L0
    NEW java/util/ArrayList
    DUP
    INVOKESPECIAL java/util/ArrayList.<init> ()V
    ASTORE 0
   L1
    LINENUMBER 43 L1
    ALOAD 0
    INVOKEINTERFACE java/util/List.iterator ()Ljava/util/Iterator;
    ASTORE 1
   L2
   FRAME APPEND [java/util/List java/util/Iterator]
    ALOAD 1
    INVOKEINTERFACE java/util/Iterator.hasNext ()Z
    IFEQ L3
    ALOAD 1
    INVOKEINTERFACE java/util/Iterator.next ()Ljava/lang/Object;
    ASTORE 2
   L4
    LINENUMBER 44 L4
    ALOAD 2
    CHECKCAST java/lang/Integer
    INVOKEVIRTUAL java/lang/Integer.intValue ()I
    ISTORE 3
   L5
    LINENUMBER 45 L5
    GOTO L2
   L3
    LINENUMBER 46 L3
   FRAME CHOP 1
    RETURN
   L6
    LOCALVARIABLE i Ljava/lang/Object; L4 L5 2
    LOCALVARIABLE l Ljava/util/List; L1 L6 0
    MAXSTACK = 2
    MAXLOCALS = 4

可以看到,所有4个显式和隐式定义的var都占用1个插槽,保留了4个插槽,但只有2个插槽以奇怪的顺序(地址2在地址0之前)定义,并且它们之间有一个“洞”。列表迭代器随后使用ASTORE 1写入此“空洞”,而无需先声明此变量的类型。仅在出现此操作后,堆栈映射框架才出现,但我不清楚为什么只将2个变量放入其中,因为后来使用了2个以上的变量。后来,在ISTORE 3中,将int再次写入变量插槽,而没有任何声明。

在这一点上,我似乎需要完全忽略变量定义,并通过解释字节码,运行JVM堆栈模拟来推断所有类型。

尝试使用ASM EXPAND_FRAME选项,但是它没有用,仅将单帧节点的类型更改为F_NEW,其余的仍与以前一样。

谁能解释为什么我会看到这样一个奇怪的代码,并且除了编写自己的JVM解释器之外,还有其他选择吗?

结论,基于所有答案(如果我错了,请再次纠正我):

变量定义仅用于将源变量名称/类型与在特定代码行访问的特定变量插槽匹配,这些变量插槽显然被JVM类验证程序忽略并在代码执行期间被忽略。可以不存在或与实际字节码不匹配。

可变插槽被视为另一个堆栈,尽管可以通过32位字索引访问,但只要使用匹配类型的装入和存储指令,始终可以用不同的临时性覆盖其内容。

堆栈帧节点包含从变量帧的开始到最后一个变量分配的变量列表,该变量将在后续代码中加载而不先存储。无论采用何种执行路径到达其标签,该分配图都应相同。它们还包含针对操作数堆栈的相似映射。它们的内容可以指定为相对于先前堆栈帧节点的增量。

如果在较高的插槽地址分配了具有较长生命周期的变量,则仅存在于线性代码序列中的变量将仅出现在堆栈帧节点中。

霍尔格:

简短的答案是,如果您想知道每个代码位置处的堆栈框架元素的类型,则确实需要编写某种解释器,尽管大部分工作已经完成,但仍不足以还原源级别的局部变量类型,根本没有通用的解决方案。

就像在其他答案中所说的那样,诸如此类LocalVariableTable属性确实旨在帮助恢复局部变量的正式声明,例如在调试时,但是仅覆盖了源代码中存在的变量(好吧,这实际上是编译器的决定),而不是强制性的。也不能保证它是正确的,例如,字节码转换工具可能在不更新这些调试属性的情况下更改了代码,但是JVM不在您调试时不在乎。

正如在其他答案中所说的那样,该StackMapTable属性仅用于帮助字节码验证,而不用于提供正式声明。它将在分支合并点告知堆栈帧状态,以进行验证

因此,对于没有分支的线性代码序列,局部变量和操作数堆栈条目的类型仅由推断确定,但这些推断类型根本无法保证与正式声明的类型匹配。

为了说明这个问题,以下无分支代码序列产生相同的字节码:

CharSequence cs;
cs = "hello";
cs = CharBuffer.allocate(20);
{
    String s = "hello";
}
{
    CharBuffer cb = CharBuffer.allocate(20);
}

由编译器决定将局部变量的插槽重新用于具有相异作用域的变量,但是所有相关的编译器都会这样做。

对于验证,仅正确性的问题,所以存储类型的值时X到一个局部变量槽,随后通过读取它并访问成员Y.someMember,则X必须分配给Y,而不管局部变量的声明的类型是否实际上是Z,的超类型X而是的子类型Y

在没有调试属性的情况下,您可能会倾向于分析随后的使用以猜测实际类型(我想,这是大多数反编译器所做的事情),例如以下代码

CharSequence cs;
cs = "hello";
cs.charAt(0);
cs = CharBuffer.allocate(20);
cs.charAt(0);

包含两条invokeinterface CharSequence.charAt指令,指示变量的实际类型可能是CharSequence而不是StringCharBuffer,但是字节码仍与之相同,例如

{
    String s = "hello";
    ((CharSequence)s).charAt(0);
}
{
    CharBuffer cb = CharBuffer.allocate(20);
    ((CharSequence)cb).charAt(0);
}

因为这些类型强制转换只会影响后续方法的调用,而不会自行生成字节码指令,因为它们正在扩大强制类型转换。

因此,不可能以线性顺序从字节码中精确地恢复源级别变量的声明类型,并且堆栈映射框架条目也无济于事。它们的目的是帮助验证后续代码(可以通过不同的代码路径到达)的正确性,为此,它不需要声明所有现有元素。它只需要声明合并点之前存在的元素,并在合并点之后实际使用它们。但是,这取决于编译器是否存在验证程序实际不需要的条目(以及哪些条目)。

本文收集自互联网,转载请注明来源。

如有侵权,请联系 [email protected] 删除。

编辑于
0

我来说两句

0 条评论
登录 后参与评论

相关文章