ASM:visitLabel生成的标签和nop指令太多



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来标识条件代码的开始。

因此,要识别所有基本块,必须处理所有分支指令,即通过visitJumpInsnvisitLookupSwitchInsnvisitTableSwitchInsn,以及所有结束指令,即athrowreturn的所有变体。此外,您还需要处理所有visitTryCatchBlock调用。如果您需要在一次传递中识别分支指令的潜在目标,我会使用visitFrame而不是标签,因为对于51(Java 7(或更高版本的类文件,所有分支目标都必须使用帧。

顺便说一句,当你注入的只是这些加载常量和调用静态方法的序列(在可到达的位置(时,我会使用COMPUTE_MAXS而不是COMPUTE_FRAMES,因为当通用代码结构不变时,不需要昂贵的重新计算。

最新更新