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
来标识条件代码的开头,从而摆脱大部分这些报告的标签。
因此,要识别所有基本块,您必须处理所有分支指令,即通过 visitJumpInsn
、visitLookupSwitchInsn
和 visitTableSwitchInsn
,以及所有结束指令,即athrow
和 return
的所有变体。此外,您需要处理所有 visitTryCatchBlock
调用。如果您需要一次性识别分支指令的潜在目标,我会使用 visitFrame
而不是标签,因为对于 class 文件版本 51 (Java 7) 或更高。
顺便说一句,当你注入的只是这些加载常量和调用静态方法(在可到达的位置)的序列时,我会使用 COMPUTE_MAXS
而不是 COMPUTE_FRAMES
,因为当一般代码结构没有改变时,不需要昂贵的重新计算。
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
来标识条件代码的开头,从而摆脱大部分这些报告的标签。
因此,要识别所有基本块,您必须处理所有分支指令,即通过 visitJumpInsn
、visitLookupSwitchInsn
和 visitTableSwitchInsn
,以及所有结束指令,即athrow
和 return
的所有变体。此外,您需要处理所有 visitTryCatchBlock
调用。如果您需要一次性识别分支指令的潜在目标,我会使用 visitFrame
而不是标签,因为对于 class 文件版本 51 (Java 7) 或更高。
顺便说一句,当你注入的只是这些加载常量和调用静态方法(在可到达的位置)的序列时,我会使用 COMPUTE_MAXS
而不是 COMPUTE_FRAMES
,因为当一般代码结构没有改变时,不需要昂贵的重新计算。