2

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 中的新节点。

4

1 回答 1

1

a 的主要目的Label是表示字节码序列中的位置。由于这是分支目标所必需的,因此您可以使用它们来识别基本块。但是您必须注意,它们还用于报告行号、存在LineNumberTable属性时以及报告存在属性时的局部变量范围LocalVariableTable,以及对于较新的类文件,它们的类型注释记录在RuntimeVisibleTypeAnnotations属性中。此外,标签可以标记异常处理程序的受保护区域。对于从 Java 源代码生成的代码,此保护区与try块匹配,因此它是一个基本块,但不需要为其他字节码保留。

由于局部变量的范围可能跨越最后一条return指令,因此可能会在最后一条指令之后遇到标签,这就是您的情况。您bipush 7, invokestatic #13在指令之后注入 a return,导致代码无法访问。

显然,您还使用COMPUTE_FRAMES选项让 ASM 从头开始​​重新计算堆栈映射帧,但由于未知的初始堆栈状态,无法计算无法访问代码的帧。ASM 通过用指令替换无法访问的代码nop,后跟一条athrow语句来解决这个问题。对于这个序列,可以指定一个有效的初始堆栈帧,并且它对执行没有影响(因为代码无法访问)。

如您所见,四条nop指令加一条指令跨越五个字节,与替换序列athrow的大小相同。bipush 7, invokestatic #13

ClassReader.SKIP_DEBUG您可以通过指定它的accept方法来摆脱大多数这些报告的标签。然后,您只获得一个示例的报告标签,即与if语句关联的分支目标。但是您必须处理visitJumpInsn以识别条件代码的开始。

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

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

于 2018-11-12T11:56:15.843 回答