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

时间:2018-11-09 19:58:49

标签: java java-bytecode-asm bytecode-manipulation jvm-bytecode

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中没有分支使其成为新节点。

1 个答案:

答案 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才能确定条件代码的开头。

因此,要标识所有基本块,您必须处理所有分支指令(即通过visitJumpInsnvisitLookupSwitchInsnvisitTableSwitchInsn)以及所有结束指令(即{{ 1}}和athrow的所有变体。此外,您需要处理所有return个调用。如果您需要一次性确定分支指令的潜在目标,我将使用visitTryCatchBlock而不是标签,因为对于51(Java 7)或更高版本的类文件版本,所有分支目标都必须使用框架。 / p>

顺便说一句,当您要注入的是这些序列的一个常量加载和调用一个静态方法(在可到达的位置)时,我将使用visitFrame而不是COMPUTE_MAXS作为当常规代码结构不变时,无需进行昂贵的重新计算。