Определить циклы в байт-коде Java

Я пытаюсь инструмент байт-код Java.

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

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

3 ответа

Решение

РЕДАКТИРОВАТЬ 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());
        }
    }
}

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

Единственный способ перейти назад в коде - использовать цикл. Итак, вы ищете 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.

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

Скриншот структуры байт-кода

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

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

L2 (173)
  GOTO L3

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

Я знаю, что это старый вопрос, однако был особый интерес к тому, как этого можно достичь с помощью библиотеки 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");
        }
    }

}

Вы можете дополнительно прикрепить к меткам информацию о номере строки, если она доступна, и отслеживать ее в посетителе метода, чтобы вывести, где начинаются и заканчиваются циклы обнаружения в источнике.

Другие вопросы по тегам