识别java字节代码中的循环

use*_*203 17 java instrumentation loops bytecode

我正在尝试设法java字节码.

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

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

Bru*_*uno 21

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

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

    0: goto 2
    1: goto 3
    2: goto 1
    
    Run Code Online (Sandbox Code Playgroud)

    当然,这个特殊的例子非常人为,有点傻.但是,假设源到字节码编译器将如何表现可能会导致意外.正如彼得和我在各自的答案中所表明的那样,两个流行的编译器可以产生相当不同的输出(即使没有混淆).它很少重要,因为当您执行代码时,所有这些都会被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;
}
Run Code Online (Sandbox Code Playgroud)

这一次,使用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
Run Code Online (Sandbox Code Playgroud)

有一个介于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);
    }
}
Run Code Online (Sandbox Code Playgroud)

在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
Run Code Online (Sandbox Code Playgroud)

在循环中执行任何操作之前,直接从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);
            }
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

当由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
Run Code Online (Sandbox Code Playgroud)

这是一个程序,使用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());
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

有关如何加载类的更多详细信息,请查看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;
    }
Run Code Online (Sandbox Code Playgroud)

块:

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;
Run Code Online (Sandbox Code Playgroud)

循环:

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
Run Code Online (Sandbox Code Playgroud)

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


Pet*_*rey 6

在代码中向后跳转的唯一方法是通过循环.所以你正在寻找一个转到前一个字节代码指令的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;
}
Run Code Online (Sandbox Code Playgroud)

其字节代码显示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
Run Code Online (Sandbox Code Playgroud)

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