【问题标题】:Identify loops in java byte code识别 java 字节码中的循环
【发布时间】:2011-07-22 15:28:42
【问题描述】:

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

我想识别java循环的入口和出口,但我发现循环的识别非常具有挑战性。 我花了好几个小时研究 ASM开源反编译器(我认为它们必须一直解决这个问题),但是,我做不到。

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

【问题讨论】:

    标签: java loops bytecode instrumentation


    【解决方案1】:

    编辑 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 文档。)

    【讨论】:

    • 这确实让它看起来是一个相当大的挑战,但是反编译器必须已经解决了这个问题,至少在某种程度上,不是吗?
    • 嗯,Soot 实际上是一个反编译器,或者至少是一个框架来执行此操作(它甚至有一个反编译器前端,Dava)。这里有 Soot 的教程:sable.mcgill.ca/soot/tutorial/index.html。这还取决于您所追求的仪器类型。 Java 反编译并不总是完美的。 (无论如何,字节码分析通常并不容易。)
    • 当循环抛出异常时,检测后继者是否可行?
    • @biziclop:是的,有可能,例如通过编写一个新的指令块来捕获异常,执行检测,然后再次抛出异常(以同样的方式 try/@ 987654358@ 会工作)。这可能会导致重复代码(对于正常和例外情况):在这种情况下实际上有两个后继。
    • 公平点,尽管正确实施听起来确实很复杂。 (考虑到可能已经有一个可能突然结束的 finally 块,以及当场捕获的异常,而其他允许进一步传播的异常。)
    【解决方案2】:

    在代码中向后跳转的唯一方法是通过循环。因此,您正在寻找转到前一个字节码指令的 goto、if_icmplt 等。一旦你找到了循环的结束,它跳回的地方就是循环的开始。


    这是一个复杂的例子,来自布鲁诺建议的文档。

    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
    

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

    【讨论】:

    • 同意 + 请注意,有时会有一个字节码的前导码设置循环变量。
    • 字节码块不需要“线性”组织,因此知道何时“向后”并不总是很明显。您需要构建一个控制流图并对块进行分析。
    • 递归呢?如果您的定义中的兴趣是否也是一个循环?
    • @Bruno,编译器做的优化很少。这是由 JIT 完成的。这使得将字节码转换为 java 代码比您想象的要简单。
    • 恕我直言,递归 != 循环,它们在字节码中看起来完全不同。
    【解决方案3】:

    您实际上是在逐字节构建您的课程吗?这很狂野! ASM 的首页链接到 Eclipse 的 Bytecode Outline 插件,我假设您正在使用它。如果您单击那里的第一张图片,您会注意到代码有一个 while 循环,并且您至少可以看到一些用于实现该循环的字节码。供参考的是该屏幕截图:

    Direct link

    看起来循环只是简单地实现为带有边界检查的 GOTO。我说的是这一行:

    L2 (173)
      GOTO L3
    

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

    【讨论】:

    • 我不是逐字节构建,而是在检测现有代码,但想添加检测以通知分析器/可视化器循环正在进入和退出(支持嵌套/深度)。我查看了字节码并意识到我正在寻找带有一些保护条件的 goto。但是,我想知道 ASM 中是否存在现有模式或现有的反编译器源代码。我认为这是以前做过的事情,不想重新实现自己解析的那部分字节码(用于识别循环 X 的开始,循环 X 的结束)等。
    • @user858203 - 好吧,这更有意义。我在 ASM 文档中没有看到任何支持该确切场景的内容,您可能对开源反编译器有更好的运气。
    【解决方案4】:

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

    您需要做的主要事情是跟踪 ASM 在其所谓的“跳转指令”之前访问的“标签”(字节码中的位置) - 如果它跳转到的标签已经在相同方法的上下文中遇到,那么你就有可能循环代码。

    我在这里看到的答案与 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");
            }
        }
    
    }
    

    您还可以在标签可用时附加行号信息,并在方法访问者中对其进行跟踪,以输出检测循环在源中的开始和结束位置。

    【讨论】:

    • 需要注意的一点:如果您尝试检测循环以查找专门迭代的代码,您还需要找到使用 Java 流方法(如 map 和 filter)的位置 - 为此,您d 想查看方法访问者中的“调用动态”方法,以及“引导方法参数”的可能值
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2010-12-29
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2022-06-12
    • 2017-05-07
    • 1970-01-01
    相关资源
    最近更新 更多