编辑 4:一点背景/序言。
-
“在代码中向后跳转的唯一方法是通过循环。”在彼得的回答中并不完全正确。你可以在没有它的情况下来回跳跃,这意味着它是一个循环。一个简化的案例是这样的:
0: goto 2
1: goto 3
2: goto 1
当然,这个特殊的例子是非常人为的,有点傻。但是,对源代码到字节码编译器的行为方式做出假设可能会导致意外。正如彼得和我在各自的答案中所表明的那样,两个流行的编译器可以产生相当不同的输出(即使没有混淆)。这很少有关系,因为当您执行代码时,JIT 编译器会很好地优化所有这些。
话虽如此,在绝大多数情况下,向后跳将是循环开始的合理指示。与其他相比,找出循环的入口点是“容易”的部分。
在考虑任何循环启动/退出检测之前,您应该查看什么是进入、退出和继任者的定义。虽然循环只有一个入口点,但它可能有多个出口点和/或多个后继点,通常由break 语句(有时带有标签)、return 语句和/或异常(是否明确捕获)引起。虽然您没有提供有关您正在调查的仪器类型的详细信息,但肯定值得考虑您想要在哪里插入代码(如果这是您想要做的)。通常,可能必须在每个退出语句之前或代替每个后继语句之前完成一些检测(在这种情况下,您必须移动原始语句)。
Soot 是一个很好的框架。它有许多中间表示,使字节码分析更方便(例如 Jimple)。
您可以基于您的方法主体构建BlockGraph,例如ExceptionalBlockGraph。一旦您将控制流图分解为这样的块图,从节点中,您应该能够识别支配者(即有箭头返回的块)。这将为您提供循环的开始。
您可能会在sections 4.3 to 4.7 of this dissertation 中找到类似的操作。
编辑:
在与@Peter 在 cmets 中讨论他的答案之后。说同样的例子:
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
在 3 和 12 之间有一个循环(在开始 10 时跳转)和另一个循环,因为在 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 表示,相当于字节码,但更容易分析和处理。
这是这个程序的输出:
主体:
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 文档。)