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 位字索引访问的,并且只要您使用匹配类型的加载和存储指令,总是可以用不同的临时变量覆盖其内容。
堆栈帧节点包含从变量帧的开头分配到最后一个变量的变量列表,该变量将在后续代码中加载而不先存储。无论采用何种执行路径到达其标签,该分配映射都应该是相同的。它们还包含操作数堆栈的类似映射。它们的内容可以指定为相对于前一个堆栈帧节点的增量。
仅存在于线性代码序列中的变量只会出现在堆栈帧节点中,如果在更高的槽地址分配了更长生命周期的变量。
LocalVariableTable用于将源代码中的变量与方法字节码中的变量槽匹配。此可选属性主要用于调试器(打印变量的正确名称)。
正如您自己已经回答的那样,为了推断局部变量类型或表达式类型,您必须遍历字节码:从方法开始或从最近的堆栈映射。StackMapTable属性仅在合并点包含堆栈映射。
| 归档时间: |
|
| 查看次数: |
760 次 |
| 最近记录: |