Ответ 1
EDIT 4: немного фона/преамбулы.
-
"Единственный способ перепрыгнуть назад в коде - через цикл". в Питере ответ не является строгим. Вы можете прыгать туда и обратно, не имея в виду петлю. Упрощенный случай будет примерно таким:
0: goto 2 1: goto 3 2: goto 1
Конечно, этот конкретный пример очень искусственный и немного глупый. Однако предположение о том, как будет работать компилятор от источника к байт-коду, может привести к неожиданностям. Как Питер и я показали в наших соответствующих ответах, два популярных компилятора могут производить довольно различный результат (даже без обфускации). Это редко имеет значение, потому что все это, как правило, оптимизируется JIT-компилятором при выполнении кода. При этом в подавляющем большинстве случаев прыжки назад будут разумным указанием на то, где начинается цикл. По сравнению с остальными, поиск точки входа цикла является "легкой" частью.
-
Прежде чем рассматривать любые инструменты запуска/выхода цикла, вы должны изучить определения того, что такое вход, выход и преемники. Хотя цикл имеет только одну точку входа, он может иметь несколько точек выхода и/или несколько преемников, обычно вызванных операторами
break
(иногда с метками), операторамиreturn
и/или исключениями (явно пойманными или нет). Хотя вы не указали подробные сведения о том, какие приборы вы изучаете, обязательно стоит подумать о том, где вы хотите вставить код (если это то, что вы хотите сделать). Как правило, некоторые инструменты могут потребоваться перед каждым оператором выхода или вместо каждого оператора-преемника (в этом случае вам придется переместить исходный оператор).
Soot является хорошей основой для этого. Он имеет ряд промежуточных представлений, которые делают анализ байт-кода более удобным (например, Jimple).
Вы можете создать BlockGraph на основе вашего тела метода, например ExceptionalBlockGraph., После того, как вы разделили граф потока управления на такой блок-граф, из узлов, вы должны иметь возможность идентифицировать доминанты (т.е. Блоки, у которых есть стрелка, возвращающаяся к ним). Это даст вам начало цикла.
Вы можете найти что-то подобное в разделе с 4.3 по 4.7 данной диссертации.
EDIT:
После обсуждения с @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:
Похоже, что новые классы/методы для анализа циклов были добавлены с прошлого времени, когда я смотрел на Саут, что делает его немного более удобным.
Вот полный пример.
Класс/метод для анализа (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;
}
Блоки:
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;
Loops:
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
предоставит вам инструкцию записи и инструкции exit. Затем вы можете добавить дополнительные заявления, если хотите. Jimple довольно удобен для этого (он достаточно близко к байт-коду, но имеет несколько более высокий уровень, чтобы не разобраться со всем вручную). Затем вы можете вывести ваш модифицированный файл .class
, если это необходимо. (Для этого см. Документацию по саже.)