ASM文档说标签代表一个基本块,它是控制图中的一个节点。因此,我在此简单示例上测试了visitLabel
方法:
public static void main(String[] args) {
int x = 3, y = 4;
if (x < y) {
x++;
}
}
对于visitLabel
方法,我使用本机API setID(int id)
对其进行检测,其中id是递增的。在此示例中,CFG应该具有3个节点:一个在开始时,一个在if语句的每个分支中一个。因此,我预计setID
将在3个位置被调用。但是,它被调用了5次,并且有很多nop
指令。谁能为我解释为什么?
这是上面程序的检测字节代码。
public static void main(java.lang.String[]);
Code:
0: iconst_2
1: invokestatic #13 // Method setId:(I)V
4: iconst_3
5: istore_1
6: iconst_3
7: invokestatic #13 // Method setId:(I)V
10: iconst_4
11: istore_2
12: iconst_4
13: invokestatic #13 // Method setId:(I)V
16: iload_1
17: iload_2
18: if_icmpge 28
21: iconst_5
22: invokestatic #13 // Method setId:(I)V
25: iinc 1, 1
28: bipush 6
30: invokestatic #13 // Method setId:(I)V
33: return
34: nop
35: nop
36: nop
37: nop
38: athrow
我不明白的是为什么每个label
指令前都有一个istore
。 CFG中没有分支使其成为新节点。
答案 0 :(得分:1)
Label
的主要目的是表示字节码序列中的位置。由于分支目标需要这样做,因此可以使用它们来识别基本块。但您必须知道,当存在LineNumberTable
属性时,它们还用于报告行号;当存在LocalVariableTable
属性时,它们也用于报告局部变量作用域;以及用于较新的类文件,其类型注释记录在RuntimeVisibleTypeAnnotations
属性中。此外,标签可以标记异常处理程序的保护区域。对于从Java源代码生成的代码,此保护区与try
块匹配,因此它是基本块,但不需要保留其他字节码。
请参见
由于局部变量的范围可能跨越最后一条return
指令,因此有可能在最后一条指令之后遇到标签,这就是您所遇到的情况。您正在bipush 7, invokestatic #13
指令后注入return
,导致代码无法访问。
显然,您还使用COMPUTE_FRAMES
选项让ASM从头开始重新计算堆栈映射帧,但是由于未知的初始堆栈状态,因此无法为无法访问的代码计算帧。 ASM通过用nop
指令,然后是单个athrow
语句替换无法访问的代码来解决此问题。对于此序列,可以指定一个有效的初始堆栈帧,并且对执行没有影响(因为代码不可访问)。
如您所见,四个nop
指令加上一个athrow
指令跨越五个字节,这与被替换的bipush 7, invokestatic #13
序列的大小相同。
您可以通过在其ClassReader.SKIP_DEBUG
上指定accept
method来摆脱大多数报告的标签。然后,您的示例仅获得一个报告的标签,即与if
语句关联的分支目标。但是您必须处理visitJumpInsn
才能确定条件代码的开头。
因此,要标识所有基本块,您必须处理所有分支指令(即通过visitJumpInsn
,visitLookupSwitchInsn
和visitTableSwitchInsn
)以及所有结束指令(即{{ 1}}和athrow
的所有变体。此外,您需要处理所有return
个调用。如果您需要一次性确定分支指令的潜在目标,我将使用visitTryCatchBlock
而不是标签,因为对于51(Java 7)或更高版本的类文件版本,所有分支目标都必须使用框架。 / p>
顺便说一句,当您要注入的是这些序列的一个常量加载和调用一个静态方法(在可到达的位置)时,我将使用visitFrame
而不是COMPUTE_MAXS
作为当常规代码结构不变时,无需进行昂贵的重新计算。