我正在尝试检测Java字节码。
我想要识别Java循环的入口和出口,但我发现识别循环非常具有挑战性。我花费了好几个小时研究ASM和开源反编译器(我认为他们必须经常解决这个问题),但我没有找到答案。
我正在使用ASM增强/扩展工具,因此最好的情况是我想知道如何通过ASM检测Java中不同循环结构的入口和出口。但是,我也希望推荐一个好的开源反编译器,因为他们显然已经解决了相同的问题。
我正在尝试检测Java字节码。
我想要识别Java循环的入口和出口,但我发现识别循环非常具有挑战性。我花费了好几个小时研究ASM和开源反编译器(我认为他们必须经常解决这个问题),但我没有找到答案。
我正在使用ASM增强/扩展工具,因此最好的情况是我想知道如何通过ASM检测Java中不同循环结构的入口和出口。但是,我也希望推荐一个好的开源反编译器,因为他们显然已经解决了相同的问题。
编辑4:一些背景信息/序言。
Peter的回答中“唯一倒退到代码中的方式是通过循环”并不完全正确。你可以来回跳转,而这并不意味着它是一个循环。一个简单的例子可能是这样的:
0: goto 2
1: goto 3
2: goto 1
当然,这个例子非常人为且有点愚蠢。但是,对源代码到字节码编译器的行为进行假设可能会导致意外情况。正如 Peter 和我在各自的答案中所示,两种流行的编译器可以产生相当不同的输出(即使没有混淆)。这很少有关系,因为当您执行代码时,所有这些都往往被 JIT 编译器很好地优化了。
话虽如此,在绝大多数情况下,向后跳转将是确定循环开始位置的一个合理指标。与其他部分相比,找到循环的入口点是“容易”的部分。
在考虑任何循环启动/退出工具之前,您应该查看入口、出口和后继项的定义。尽管循环只有一个入口点,但它可能有多个出口点和/或多个后继项,通常是由于 break
语句(有时带有标签)、return
语句和/或异常(明确捕获或未捕获)引起的。虽然您没有给出有关您正在调查的工具类型的详细信息,但考虑插入代码的位置(如果这是您想做的事情)肯定是值得考虑的。通常,某些工具可能需要在每个退出语句之前或者代替每个后继语句进行一些插装(在这种情况下,您将不得不移动原始语句)。
Soot 是一个很好的框架来做这个事情。它有许多中间表示使字节码分析更方便(例如 Jimple)。
您可以基于方法体构建一个 BlockGraph,例如一个 ExceptionalBlockGraph。一旦你将控制流图分解成这样的块图形式,从节点中,您应该能够识别支配者(即有箭头返回到它们的块)。这将给出循环开始的位置。
您可能会在 本论文的第 4.3 至 4.7 节中找到类似的东西。
编辑:
根据与 @Peter 在评论中的讨论。谈论同一个例子:
public int foo(int i, int j) {
while (true) {
try {
while (i < j)
i = j++ / i;
} catch (RuntimeException re) {
i = 10;
continue;
}
break;
}
return j;
}
这次使用Eclipse编译器编译(没有具体选项:只是从IDE内部自动编译)。
这段代码没有被混淆(除了代码本身就糟糕之外,但那是另一回事)。
以下是结果(来自javap -c
):
public int foo(int, int);
Code:
0: goto 10
3: iload_2
4: iinc 2, 1
7: iload_1
8: idiv
9: istore_1
10: iload_1
11: iload_2
12: if_icmplt 3
15: goto 25
18: astore_3
19: bipush 10
21: istore_1
22: goto 10
25: iload_2
26: ireturn
Exception table:
from to target type
0 15 18 Class java/lang/RuntimeException
在开始于10的情况下,3到12之间存在一个循环,以及由于8到22的除数为零而发生异常所导致的另一个循环。与javac
编译器的结果不同,其中人们可能会猜测在0到22之间有一个外部循环和0到12之间的内部循环,但这里的嵌套不太明显。
编辑2:
为了举例说明可能遇到的问题,这里是一个相对简单的循环:
public void foo2() {
for (int i = 0; i < 5; i++) {
System.out.println(i);
}
}
在Eclipse中进行(正常)编译后,javap -c
的输出如下:
public void foo2();
Code:
0: iconst_0
1: istore_1
2: goto 15
5: getstatic #25; //Field java/lang/System.out:Ljava/io/PrintStream;
8: iload_1
9: invokevirtual #31; //Method java/io/PrintStream.println:(I)V
12: iinc 1, 1
15: iload_1
16: iconst_5
17: if_icmplt 5
20: return
在循环内执行任何操作之前,您直接从2跳转到15。块15到17是循环的头部(即“入口点”)。有时,头块可能包含更多指令,特别是如果退出条件涉及更多评估或者如果它是一个 do {} while()
循环。
循环的“入口”和“出口”的概念并不总是反映您作为Java源代码编写时的合理写法(包括您可以将 for
循环重写为 while
循环的事实,例如)。使用 break
还可能导致多个退出点。
顺便说一下,“块”是指一个字节码序列,您无法在其中间跳转,并且只能从第一行(可能不是从上一行开始,也可能从其他地方跳转)进入,从最后一行(不一定是到下一行,它也可以跳转到其他地方)退出。
编辑3:
自上次我查看Soot以来,似乎已经添加了新的分析循环的类/方法,这使得它更加方便。
以下是完整示例。
要分析的类/方法( TestLoop.foo()
)
public class TestLoop {
public void foo() {
for (int j = 0; j < 2; j++) {
for (int i = 0; i < 5; i++) {
System.out.println(i);
}
}
}
}
使用 Eclipse 编译器编译时,会产生以下字节码(javap -c
):
public void foo();
Code:
0: iconst_0
1: istore_1
2: goto 28
5: iconst_0
6: istore_2
7: goto 20
10: getstatic #25; //Field java/lang/System.out:Ljava/io/PrintStream;
13: iload_2
14: invokevirtual #31; //Method java/io/PrintStream.println:(I)V
17: iinc 2, 1
20: iload_2
21: iconst_5
22: if_icmplt 10
25: iinc 1, 1
28: iload_1
29: iconst_2
30: if_icmplt 5
33: return
这里有一个程序,使用Soot加载类(假设它在类路径上),并显示其块和循环:
import soot.Body;
import soot.Scene;
import soot.SootClass;
import soot.SootMethod;
import soot.jimple.toolkits.annotation.logic.Loop;
import soot.toolkits.graph.Block;
import soot.toolkits.graph.BlockGraph;
import soot.toolkits.graph.ExceptionalBlockGraph;
import soot.toolkits.graph.LoopNestTree;
public class DisplayLoops {
public static void main(String[] args) throws Exception {
SootClass sootClass = Scene.v().loadClassAndSupport("TestLoop");
sootClass.setApplicationClass();
Body body = null;
for (SootMethod method : sootClass.getMethods()) {
if (method.getName().equals("foo")) {
if (method.isConcrete()) {
body = method.retrieveActiveBody();
break;
}
}
}
System.out.println("**** Body ****");
System.out.println(body);
System.out.println();
System.out.println("**** Blocks ****");
BlockGraph blockGraph = new ExceptionalBlockGraph(body);
for (Block block : blockGraph.getBlocks()) {
System.out.println(block);
}
System.out.println();
System.out.println("**** Loops ****");
LoopNestTree loopNestTree = new LoopNestTree(body);
for (Loop loop : loopNestTree) {
System.out.println("Found a loop with head: " + loop.getHead());
}
}
}
请查阅Soot文档以获取有关如何加载类的更多详细信息。 Body
是循环体的模型,即由字节码生成的所有语句。这使用中间Jimple表示形式,它等效于字节码,但更易于分析和处理。
以下是此程序的输出:
Body:
public void foo()
{
TestLoop r0;
int i0, i1;
java.io.PrintStream $r1;
r0 := @this: TestLoop;
i0 = 0;
goto label3;
label0:
i1 = 0;
goto label2;
label1:
$r1 = <java.lang.System: java.io.PrintStream out>;
virtualinvoke $r1.<java.io.PrintStream: void println(int)>(i1);
i1 = i1 + 1;
label2:
if i1 < 5 goto label1;
i0 = i0 + 1;
label3:
if i0 < 2 goto label0;
return;
}
块:
Block 0:
[preds: ] [succs: 5 ]
r0 := @this: TestLoop;
i0 = 0;
goto [?= (branch)];
Block 1:
[preds: 5 ] [succs: 3 ]
i1 = 0;
goto [?= (branch)];
Block 2:
[preds: 3 ] [succs: 3 ]
$r1 = <java.lang.System: java.io.PrintStream out>;
virtualinvoke $r1.<java.io.PrintStream: void println(int)>(i1);
i1 = i1 + 1;
Block 3:
[preds: 1 2 ] [succs: 4 2 ]
if i1 < 5 goto $r1 = <java.lang.System: java.io.PrintStream out>;
Block 4:
[preds: 3 ] [succs: 5 ]
i0 = i0 + 1;
Block 5:
[preds: 0 4 ] [succs: 6 1 ]
if i0 < 2 goto i1 = 0;
Block 6:
[preds: 5 ] [succs: ]
return;
循环:
Found a loop with head: if i1 < 5 goto $r1 = <java.lang.System: java.io.PrintStream out>
Found a loop with head: if i0 < 2 goto i1 = 0
LoopNestTree
使用LoopFinder
,后者使用ExceptionalBlockGraph
来构建块列表。 Loop
类将为您提供入口语句和退出语句。然后,如果需要,您应该能够添加额外的语句。Jimple对此非常方便(它足够接近字节码,但具有稍高的级别,因此不必手动处理所有内容)。然后,如果需要,可以输出修改后的.class
文件。 (请参阅Soot文档。)public int foo(int i, int j) {
while (true) {
try {
while (i < j)
i = j++ / i;
} catch (RuntimeException re) {
i = 10;
continue;
}
break;
}
return j;
}
这段代码的字节码在 javap -c
中显示为
public int foo(int, int);
Code:
0: iload_1
1: iload_2
2: if_icmpge 15
5: iload_2
6: iinc 2, 1
9: iload_1
10: idiv
11: istore_1
12: goto 0
15: goto 25
18: astore_3
19: bipush 10
21: istore_1
22: goto 0
25: iload_2
26: ireturn
Exception table:
from to target type
0 15 18 Class java/lang/RuntimeException
你是真的在逐字节地构建你的类吗?这太疯狂了!ASM 的首页链接到 Eclipse 的 Bytecode Outline 插件,我想你正在使用它。如果你点击那里的第一张图片,你会注意到代码有一个 while 循环,并且你可以看到至少一些用于实现该循环的字节码。以下是该截图:
看起来循环是通过带有边界检查的GOTO实现的。我指的是这一行:
L2 (173)
GOTO L3
我相信L3标记器有检查索引边界的代码,并决定是否跳转。如果你想一次一个字节码地检测循环,我认为这对你来说会非常困难。汇编语言确实有使用模板类作为仪器基础的选项,你试过使用它吗?
我知道这是一个老问题 - 然而,对于如何使用ASM库实现这一点存在特定的兴趣,这可能对未来的访问者有用。请注意,其他答案给出了警告,要避免与“goto”语句相关的概括性假设,但是有一种方法可以做到这一点。(这假设在给定方法中任何代码组合都应该被检测到“循环” - 通常这是一个实际的循环结构,但其他(罕见但存在)例子已经提供了如何发生这种情况的说明。)
你需要做的主要事情是跟踪ASM在“跳转指令”之前访问的“标签”(字节码中的位置) - 如果它跳转到的标签已经在同一方法的上下文中遇到过,那么你就有了潜在的循环代码。
我在这里看到的一个显着差异是,它读取了一个简单文件的“循环”跳转命令,其操作码不是“goto” - 这可能只是自此提问以来Java编译中的更改,但值得注意。
ASM的基本示例代码如下(通过checkForLoops
方法输入):
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.Label;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
public void checkForLoops(Path classFile) {
LoopClassVisitor classVisitor = new LoopClassVisitor();
try (InputStream inputStream = Files.newInputStream(classFile)) {
ClassReader cr = new ClassReader(inputStream);
cr.accept(classVisitor, 0);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
public class LoopClassVisitor extends ClassVisitor {
public LoopClassVisitor() {
super(Opcodes.ASM7);
}
@Override
public MethodVisitor visitMethod(int access, String name, String descriptor, String signature,
String[] exceptions) {
return new LoopMethodVisitor();
}
}
public class LoopMethodVisitor extends MethodVisitor {
private List<Label> visitedLabels;
public LoopMethodVisitor() {
super(Opcodes.ASM7);
visitedLabels = new ArrayList<>();
}
@Override
public void visitLineNumber(final int line, final Label start) {
System.out.println("lnLabel: " + start.toString());
visitedLabels.add(start);
}
@Override
public void visitLabel(final Label label) {
System.out.println("vLabel: " + label.toString());
visitedLabels.add(label);
}
@Override
public void visitJumpInsn(final int opcode, final Label label) {
System.out.println("Label: " + label.toString());
if (visitedLabels.contains(label)) {
System.out.println("Op: " + opcode + ", GOTO to previous command - possible looped execution");
}
}
}
如果有可用的行号信息,您还可以将其附加到标签上,并在方法访问器中跟踪它,以输出源代码中检测循环开始和结束的位置。
after
、after throwing
或after returning
的切入点(AspectJ语法)来选择何时使用该方面。无论如何,这是织入器系统地完成的(包括更传统的切入点,例如call
)。通过块(和通过Jimple)进行推理也使得实现变得更容易一些。 - Bruno