Определить циклы в байт-коде Java - PullRequest
17 голосов
/ 22 июля 2011

Я пытаюсь обработать Java-байт-код.

Я хочу распознать вход и выход из цикла Java , но я обнаружил, что идентификация циклов довольно сложна.Я потратил много часов на просмотр декомпиляторов с открытым исходным кодом ASM и (которые, как мне казалось, должны решить эту проблему все время), однако я потерпел неудачу.

Инструмент, который я расширяю / расширяю, использует ASM, поэтому в идеале Я хотел бы знать, как оборудовать вход и выход различных конструкций цикла в Java через ASM .Однако я также приветствовал бы рекомендацию о хорошем декомпиляторе с открытым исходным кодом, поскольку очевидно, что они решили бы ту же проблему.

Ответы [ 3 ]

21 голосов
/ 22 июля 2011

РЕДАКТИРОВАТЬ 4 : немного фона / преамбулы.

  • " Единственный способ вернуться назад в коде - через цикл."в ответе Петра не совсем верно.Вы можете прыгать туда-сюда без этого, означая, что это петля.Упрощенный случай будет выглядеть примерно так:

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

    Конечно, этот конкретный пример очень искусственный и немного глупый.Однако предположения о том, как будет вести себя компилятор исходного кода, могут привести к неожиданностям.Как мы с Питером показали в наших соответствующих ответах, два популярных компилятора могут выдавать довольно разные результаты (даже без запутывания).Это редко имеет значение, потому что все это имеет тенденцию довольно хорошо оптимизироваться компилятором JIT при выполнении кода.Это, как говорится, в подавляющем большинстве случаев, прыжок назад будет разумным указанием того, где начинается цикл.По сравнению с остальными, выяснение точки входа в цикл является «легкой» частью.

  • Прежде чем рассматривать любые инструменты запуска / выхода из цикла, вы должны посмотреть определения того, что входВыход и преемники.Хотя цикл будет иметь только одну точку входа, он может иметь несколько точек выхода и / или несколько преемников, обычно вызванных операторами break (иногда с метками), операторами return и / или исключениями (явно перехваченными или нет).Несмотря на то, что вы не предоставили подробных сведений о типе инструментария, который вы исследуете, безусловно, стоит подумать, куда вы хотите вставить код (если это то, что вы хотите сделать).Как правило, некоторые инструментарии, возможно, придется выполнять перед каждым оператором выхода или вместо каждого оператора-преемника (в этом случае вам придется переместить исходный оператор).


Сажа - хорошая основа для этого.Он имеет ряд промежуточных представлений, которые делают анализ байт-кода более удобным (например, 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. В отличие от результата компилятора 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());
        }
    }
}

Проверьте документацию по саже для более подробной информации о том, как загружать классы. 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;
    }

Blocks:

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, если это необходимо. (См. Документацию по саже для этого.)

6 голосов
/ 22 июля 2011

Единственный способ перейти назад в коде - через цикл. Итак, вы ищете 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, блок try / catch между 0 и 15 и внешний цикл между 0 и 22.

0 голосов
/ 22 июля 2011

Вы на самом деле строите свой класс за байтом? Это довольно дикий! Главная страница ASM ссылается на плагин Outline Bytecode Outline, который, как я полагаю, вы используете. Если вы нажмете на первое изображение там, вы заметите, что в коде есть цикл while, и вы можете увидеть хотя бы часть байтового кода, использованного для реализации этого цикла. Для справки вот этот скриншот:

Bytecode Outline Screenshot

Прямая ссылка

Похоже, что циклы просто реализованы как GOTO с проверкой границ. Я говорю об этой строке:

L2 (173)
  GOTO L3

Я уверен, что у маркера L3 есть код для проверки привязки индекса, и он решил выбрать JMP. Я думаю, что это будет довольно сложно для вас, если вы хотите, чтобы инструмент зацикливался по одному байту кода за раз. ASM имеет возможность использовать шаблонный класс в качестве основы для ваших инструментов, вы пробовали использовать его?

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...