识别java字节代码中的循环

我正在尝试检测java字节码。

我想要识别java循环进入和退出 ,但我发现循环的识别非常具有挑战性。 我花了几个小时看ASM开源反编译器 (我认为他们必须一直解决这个问题)但是,我做得很短。

我正在扩充/扩展的工具是使用ASM,所以理想情况下我想知道如何通过ASM检测java中不同循环结构的进入和退出 。 但是,我也欢迎建议一个好的开源解编译器,因为很明显他们会解决同样的问题。

编辑4 :一点背景/序言。

  • 在代码中向后跳的唯一方法是通过一个循环。 ”在Peter的回答中并不完全正确。 你可以在没有它的情况下来回跳跃意味着它是一个循环。 一个简化的案例是这样的:

    0: goto 2 1: goto 3 2: goto 1 

    当然,这个特殊的例子非常人为,有点傻。 但是,假设源到字节码编译器将如何表现可能会导致意外。 正如彼得和我在各自的答案中所表明的那样,两个流行的编译器可以产生相当不同的输出(即使没有混淆)。 它很少重要,因为当您执行代码时,所有这些都会被JIT编译器很好地优化。 话虽如此,在绝大多数情况下,向后跳跃将是一个关于循环开始位置的合理指示。 与其余部分相比,找出循环的切入点是“简单”部分。

  • 在考虑任何循环启动/退出检测之前,您应该查看入口,出口和后继的定义。 虽然循环只有一个入口点,但它可能有多个出口点和/或多个后继者,通常由break语句(有时带有标签), return语句和/或exception(明确捕获或未捕获)引起。 虽然您没有提供有关您正在调查的仪器类型的详细信息,但是当然您还需要考虑插入代码的位置(如果这是您想要做的)。 通常,某些检测可能必须在每个退出语句之前完成,或者代替每个后继语句(在这种情况下,您必须移动原始语句)。


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 

有一个介于3和12之间的循环(从10开始跳转)和另一个循环,因为从8到22的除零发生exception。与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 = ; virtualinvoke $r1.(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 = ; virtualinvoke $r1.(i1); i1 = i1 + 1; Block 3: [preds: 1 2 ] [succs: 4 2 ] if i1 < 5 goto $r1 = ; 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 =  Found a loop with head: if i0 < 2 goto i1 = 0 

LoopNestTree使用LoopFinder ,它使用ExceptionalBlockGraph来构建块列表。 Loop类将为您提供entry语句和exit语句。 如果您愿意,您应该能够添加额外的声明。 Jimple非常方便(它与字节码足够接近,但有一个稍高的级别,以免手动处理所有内容)。 然后,您可以根据需要输出修改后的.class文件。 (有关此内容,请参阅Soot文档。)

在代码中向后跳转的唯一方法是通过循环。 所以你正在寻找转到前一个字节代码指令的goto,if_icmplt等。 一旦找到循环结束并且它跳回到的位置就是循环的开始。


这是一个复杂的例子,来自Bruno建议的文件。

 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 as中

 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 

你可以看到0到12之间有一个内部循环,0到15之间有一个try / catch块,0到22之间有一个外部循环。

你实际上是逐字节构建你的类吗? 多奇怪啊! ASM的首页链接到Eclipse的Bytecode Outline插件,我假设您正在使用它。 如果你点击那里的第一个图像,你会发现代码有一个while循环,你可以看到至少一些用于实现该循环的字节代码。 这里的参考是截图:

字典大纲截图

直接链接

看起来像循环只是作为带有边界检查的GOTO实现。 我在谈论这一行:

 L2 (173) GOTO L3 

我确定L3标记具有用于检查索引边界的代码,并且决定了JMP。 我想如果你想一次循环一个字节代码,这对你来说会很困难。 ASM可以选择使用模板类作为仪器的基础,您尝试过使用它吗?