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

noo*_*oop 4 java jvm bytecode stack-frame

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

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

给出以下字节码(来自 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
Run Code Online (Sandbox Code Playgroud)

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

此时看起来我需要完全忽略变量定义,并通过解释字节码来推断所有类型,运行 JVM 堆栈的模拟。

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

任何人都可以解释为什么我会看到这么奇怪的代码,除了编写自己的 JVM 解释器之外,我是否还有其他选择?

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

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

变量槽被视为另一个堆栈,尽管是通过 32 位字索引访问的,并且只要您使用匹配类型的加载和存储指令,总是可以用不同的临时变量覆盖其内容。

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

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

apa*_*gin 5

LocalVariableTable用于将源代码中的变量与方法字节码中的变量槽匹配。此可选属性主要用于调试器(打印变量的正确名称)。

正如您自己已经回答的那样,为了推断局部变量类型或表达式类型,您必须遍历字节码:从方法开始或从最近的堆栈映射。StackMapTable属性仅在合并点包含堆栈映射。