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

ASM: visitLabel generates too many labels and nop instructions

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

我不明白的是为什么每个istore指令前都有一个label。没有分支使其成为CFG中的新节点。

Label 的主要目的是表示字节码序列中的位置。由于这是分支目标所必需的,因此您可以使用它们来识别基本块。但是你必须知道,当 LineNumberTable attribute is present and for reporting local variable scopes when a LocalVariableTable attribute is present as well as, for newer class files, their type annotations recorded in a RuntimeVisibleTypeAnnotations 属性时,它们也用于报告行号。此外,标签可以标记异常处理程序的受保护区域。对于从 Java 源代码生成的代码,此保护区与 try 块匹配,因此它是一个基本块,但不需要为其他字节码保留。

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

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

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

您可以通过指定 ClassReader.SKIP_DEBUG to its accept method. Then, you get only one reported label for your example, the branch target associated with the if statement. But you have to handle the visitJumpInsn 来标识条件代码的开头,从而摆脱大部分这些报告的标签。

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

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