识别Java字节码中的循环

问题描述 投票:17回答:4

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

我想识别java循环的进入和退出,但是我发现识别循环非常具有挑战性。我花了很多时间研究ASM开源反编译器(我一直认为自己必须始终解决此问题的人),但是,我的想法很短。

我正在扩充/扩展的工具正在使用ASM,因此理想地我想知道如何通过ASM在Java中检测不同循环结构的进入和退出的方式。但是,我也欢迎关于好的开源反编译器的建议,因为显然他们可以解决相同的问题。

java loops bytecode instrumentation
4个回答
21
投票

EDIT 4:一些背景/前言。

    在彼得的回答中,
  • 在代码中向后跳的唯一方法是通过循环。”并非完全正确。您可以来回跳动,而不意味着它是一个循环。一个简化的情况将是这样的:

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

    当然,这个示例非常人为,有点愚蠢。但是,对源代码到字节码编译器的行为进行假设可能会导致意外。正如Peter和我在各自的答案中所表明的那样,两个流行的编译器可以产生完全不同的输出(即使没有混淆)。几乎没有关系,因为在执行代码时,JIT编译器会很好地优化所有这些功能。话虽如此,在大多数情况下,向后跳将是循环从何处开始的合理指示。与其余的相比,找出循环的入口点是“容易”的部分。

  • 在考虑任何循环启动/退出工具之前,您应该研究什么是进入,退出和后继。尽管循环仅具有一个入口点,但它可能具有多个出口点和/或多个后继,通常是由break语句(有时带有标签),return语句和/或异常(明确捕获或未捕获)引起的。尽管您没有提供有关正在调查的工具类型的详细信息,但是当然值得考虑要在哪里插入代码(如果您要这样做)。通常,可能需要在每个exit语句之前或代替每个后继语句进行某些检测(在这种情况下,您必须移动原始语句)。


Soot是执行此操作的良好框架。它具有许多中间表示形式,使字节码分析更加方便(例如Jimple)。

您可以基于方法主体构建BlockGraph,例如ExceptionalBlockGraph。一旦将控制流图从节点分解成这样的框图,您就应该能够识别控制者(即,带有箭头的模块回到它们身上)。这将为您提供循环的起点。

您可能会在sections 4.3 to 4.7 of this dissertation中找到类似的内容。

编辑:

[与@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

由于在8到22处被零除而发生异常,因此3到12之间有一个循环(从10开始跳)。与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使用LoopNestTree,后者使用LoopFinder构建块列表。LoopFinder类将为您提供入口语句和出口语句。然后,您可以根据需要添加其他语句。 Jimple对此非常方便(它与字节码足够接近,但是级别略高一些,以免手动处理所有内容)。然后,如果需要,您可以输出修改后的ExceptionalBlockGraph文件。 (有关此信息,请参见Soot文档。)


6
投票

在代码中向后跳转的唯一方法是通过循环。因此,您正在寻找转到上一个字节代码指令的goto,if_icmplt等。一旦找到循环的结尾,然后又跳回到循环的起点。


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

Loop

此字节码在Loop中显示为

.class

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


0
投票

您实际上是逐字节构建类吗?那太疯狂了! ASM的首页链接到Eclipse的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; } 插件,我假设您正在使用它。如果单击那里的第一个图像,您会注意到该代码有一个while循环,并且您至少可以看到用于实现该循环的某些字节代码。供参考的是该屏幕截图:

<< img src =“ https://image.soinside.com/eyJ1cmwiOiAiaHR0cHM6Ly9pLnN0YWNrLmltZ3VyLmNvbS9DanhLVi5naWYifQ==” alt =“字节码轮廓截图”>“ >>

javap -c

类似循环的方法可以简单地实现为带有边界检查的GOTO。我在说这行:

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

我确定L3标记具有用于检查索引界限的代码,并决定是否继续使用JMP。如果您想一次循环检测一个字节代码,我认为这对您来说将是非常困难的。 ASM可以选择使用模板类作为检测的基础,您是否尝试过使用它?


0
投票

我知道这是一个老问题-但是,对于ASM库如何实现这一点特别感兴趣。牢记警告,其他答案针对与“ goto”语句相关的广义假设发出警告,有一种方法可以做到这一点。 (这假定应该检测给定方法中可以“循环”的任何代码组-通常这是一个实际的循环结构,但是提供了其他(很少见但存在的)示例,说明如何发生这种情况。)

© www.soinside.com 2019 - 2024. All rights reserved.