ASM文档中说,标签表示一个基本块,它是控制图中的一个节点。所以我在这个简单的例子上测试了visitLabel
方法:
public static void main(String[] args) {
int x = 3, y = 4;
if (x < y) {
x++;
}
}
对于visitLabel
方法,我使用本机APIsetID(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
我不明白的是为什么每个istore
指令之前都有一个label
。没有分支使其成为CFG中的新节点。
Label
的主要用途是表示字节码序列中的位置。由于分支目标需要这样做,因此可以使用它们来识别基本块。但您必须注意,当存在LineNumberTable
属性时,它们也用于报告行号,当存在LocalVariableTable
属性时,用于报告局部变量作用域,对于较新的类文件,它们的类型注释记录在RuntimeVisibleTypeAnnotations
属性中。此外,标签可以标记异常处理程序的受保护区域。对于从Java源代码生成的代码,这个保护区与try
块匹配,因此它是一个基本块,但不需要为其他字节码保留。
参见
-
visitLocalVariable(java.lang.String name, java.lang.String descriptor, java.lang.String signature, Label start, Label end, int index)
-
visitLocalVariableAnnotation(int typeRef, TypePath typePath, Label[] start, Label[] end, int[] index, java.lang.String descriptor, boolean visible)
-
visitTryCatchBlock(Label start, Label end, Label handler, java.lang.String type)
由于局部变量的范围可能跨越最后一条return
指令,因此可能会在最后一条指令之后遇到标签,这就是您的情况。在return
指令之后注入bipush 7, invokestatic #13
,导致代码无法访问。
显然,您还使用COMPUTE_FRAMES
选项让ASM从头开始重新计算堆栈映射帧,但由于未知的初始堆栈状态,无法计算无法访问的代码的帧。ASM通过用nop
指令和单个athrow
语句替换无法访问的代码来解决此问题。对于这个序列,可以指定一个有效的初始堆栈帧,并且它对执行没有影响(因为代码是不可访问的(。
如您所见,四条nop
指令加上一条athrow
指令跨度为五个字节,与替换的bipush 7, invokestatic #13
序列的大小相同。
通过将ClassReader.SKIP_DEBUG
指定为其accept
方法,可以消除大多数报告的标签。然后,对于您的示例,您只得到一个报告的标签,即与if
语句关联的分支目标。但是您必须处理visitJumpInsn
来标识条件代码的开始。
因此,要识别所有基本块,必须处理所有分支指令,即通过visitJumpInsn
、visitLookupSwitchInsn
和visitTableSwitchInsn
,以及所有结束指令,即athrow
和return
的所有变体。此外,您还需要处理所有visitTryCatchBlock
调用。如果您需要在一次传递中识别分支指令的潜在目标,我会使用visitFrame
而不是标签,因为对于51(Java 7(或更高版本的类文件,所有分支目标都必须使用帧。
顺便说一句,当你注入的只是这些加载常量和调用静态方法的序列(在可到达的位置(时,我会使用COMPUTE_MAXS
而不是COMPUTE_FRAMES
,因为当通用代码结构不变时,不需要昂贵的重新计算。