diff --git a/src/org/jetbrains/java/decompiler/main/rels/MethodProcessorRunnable.java b/src/org/jetbrains/java/decompiler/main/rels/MethodProcessorRunnable.java index 32cdbc3..637260c 100644 --- a/src/org/jetbrains/java/decompiler/main/rels/MethodProcessorRunnable.java +++ b/src/org/jetbrains/java/decompiler/main/rels/MethodProcessorRunnable.java @@ -107,6 +107,10 @@ public class MethodProcessorRunnable implements Runnable { if (ExceptionDeobfuscator.hasObfuscatedExceptions(graph)) { DecompilerContext.getLogger().writeMessage("Heavily obfuscated exception ranges found!", IFernflowerLogger.Severity.WARN); + if(!ExceptionDeobfuscator.handleMultipleEntryExceptionRanges(graph)) { + DecompilerContext.getLogger().writeMessage("Found multiple entry exception ranges which could not be splitted", IFernflowerLogger.Severity.WARN); + } + ExceptionDeobfuscator.insertDummyExceptionHandlerBlocks(graph, cl.getBytecodeVersion()); } RootStatement root = DomHelper.parseGraph(graph); diff --git a/src/org/jetbrains/java/decompiler/modules/decompiler/deobfuscator/ExceptionDeobfuscator.java b/src/org/jetbrains/java/decompiler/modules/decompiler/deobfuscator/ExceptionDeobfuscator.java index 676f448..7f7c9aa 100644 --- a/src/org/jetbrains/java/decompiler/modules/decompiler/deobfuscator/ExceptionDeobfuscator.java +++ b/src/org/jetbrains/java/decompiler/modules/decompiler/deobfuscator/ExceptionDeobfuscator.java @@ -8,6 +8,8 @@ import org.jetbrains.java.decompiler.code.SimpleInstructionSequence; import org.jetbrains.java.decompiler.code.cfg.BasicBlock; import org.jetbrains.java.decompiler.code.cfg.ControlFlowGraph; import org.jetbrains.java.decompiler.code.cfg.ExceptionRangeCFG; +import org.jetbrains.java.decompiler.main.DecompilerContext; +import org.jetbrains.java.decompiler.main.extern.IFernflowerLogger; import org.jetbrains.java.decompiler.modules.decompiler.decompose.GenericDominatorEngine; import org.jetbrains.java.decompiler.modules.decompiler.decompose.IGraph; import org.jetbrains.java.decompiler.modules.decompiler.decompose.IGraphNode; @@ -233,7 +235,7 @@ public class ExceptionDeobfuscator { if (rangeList.contains(handler)) { // TODO: better removing strategy - List lstRemBlocks = getReachableBlocksRestricted(range, engine); + List lstRemBlocks = getReachableBlocksRestricted(range.getHandler(), range, engine); if (lstRemBlocks.size() < rangeList.size() || rangeList.size() == 1) { for (BasicBlock block : lstRemBlocks) { @@ -249,22 +251,21 @@ public class ExceptionDeobfuscator { } } - private static List getReachableBlocksRestricted(ExceptionRangeCFG range, GenericDominatorEngine engine) { + private static List getReachableBlocksRestricted(BasicBlock start, ExceptionRangeCFG range, GenericDominatorEngine engine) { List lstRes = new ArrayList<>(); LinkedList stack = new LinkedList<>(); Set setVisited = new HashSet<>(); - BasicBlock handler = range.getHandler(); - stack.addFirst(handler); + stack.addFirst(start); while (!stack.isEmpty()) { BasicBlock block = stack.removeFirst(); setVisited.add(block); - if (range.getProtectedRange().contains(block) && engine.isDominator(block, handler)) { + if (range.getProtectedRange().contains(block) && engine.isDominator(block, start)) { lstRes.add(block); List lstSuccs = new ArrayList<>(block.getSuccs()); @@ -308,4 +309,139 @@ public class ExceptionDeobfuscator { return false; } + + public static boolean handleMultipleEntryExceptionRanges(ControlFlowGraph graph) { + + GenericDominatorEngine engine = new GenericDominatorEngine(new IGraph() { + public List getReversePostOrderList() { + return graph.getReversePostOrder(); + } + + public Set getRoots() { + return new HashSet<>(Collections.singletonList(graph.getFirst())); + } + }); + + engine.initialize(); + + boolean found = false; + + while(true) { + found = false; + boolean splitted = false; + + for(ExceptionRangeCFG range : graph.getExceptions()) { + Set setEntries = getRangeEntries(range); + + if(setEntries.size() > 1) { // multiple-entry protected range + found = true; + + if(splitExceptionRange(range, setEntries, graph, engine)) { + splitted = true; + break; + } + } + } + + if(!splitted) { + break; + } + } + + return !found; + } + + private static Set getRangeEntries(ExceptionRangeCFG range) { + + Set setEntries = new HashSet<>(); + Set setRange= new HashSet<>(range.getProtectedRange()); + + for(BasicBlock block : range.getProtectedRange()) { + Set setPreds = new HashSet<>(block.getPreds()); + setPreds.removeAll(setRange); + + if (!setPreds.isEmpty()) { + setEntries.add(block); + } + } + + return setEntries; + } + + private static boolean splitExceptionRange(ExceptionRangeCFG range, Set setEntries, ControlFlowGraph graph, GenericDominatorEngine engine) { + + for(BasicBlock entry : setEntries) { + List lstSubrangeBlocks = getReachableBlocksRestricted(entry, range, engine); + if(!lstSubrangeBlocks.isEmpty() && lstSubrangeBlocks.size() < range.getProtectedRange().size()) { + // add new range + ExceptionRangeCFG subRange = new ExceptionRangeCFG(lstSubrangeBlocks, range.getHandler(), range.getExceptionTypes()); + graph.getExceptions().add(subRange); + // shrink the original range + range.getProtectedRange().removeAll(lstSubrangeBlocks); + return true; + } else { + // should not happen + DecompilerContext.getLogger().writeMessage("Inconsistency found while splitting protected range", IFernflowerLogger.Severity.WARN); + } + } + + return false; + } + + public static void insertDummyExceptionHandlerBlocks(ControlFlowGraph graph, int bytecode_version) { + + Map> mapRanges = new HashMap<>(); + for (ExceptionRangeCFG range : graph.getExceptions()) { + mapRanges.computeIfAbsent(range.getHandler(), k -> new HashSet<>()).add(range); + } + + for (Entry> ent : mapRanges.entrySet()) { + BasicBlock handler = ent.getKey(); + Set ranges = ent.getValue(); + + if(ranges.size() == 1) { + continue; + } + + for(ExceptionRangeCFG range : ranges) { + + // add some dummy instructions to prevent optimizing away the empty block + SimpleInstructionSequence seq = new SimpleInstructionSequence(); + seq.addInstruction(Instruction.create(CodeConstants.opc_bipush, false, CodeConstants.GROUP_GENERAL, bytecode_version, new int[]{0}), -1); + seq.addInstruction(Instruction.create(CodeConstants.opc_pop, false, CodeConstants.GROUP_GENERAL, bytecode_version, null), -1); + + BasicBlock dummyBlock = new BasicBlock(++graph.last_id); + dummyBlock.setSeq(seq); + + graph.getBlocks().addWithKey(dummyBlock, dummyBlock.id); + + // only exception predecessors from this range considered + List lstPredExceptions = new ArrayList<>(handler.getPredExceptions()); + lstPredExceptions.retainAll(range.getProtectedRange()); + + // replace predecessors + for (BasicBlock pred : lstPredExceptions) { + pred.replaceSuccessor(handler, dummyBlock); + } + + // replace handler + range.setHandler(dummyBlock); + // add common exception edges + Set commonHandlers = new HashSet<>(handler.getSuccExceptions()); + for(BasicBlock pred : lstPredExceptions) { + commonHandlers.retainAll(pred.getSuccExceptions()); + } + // TODO: more sanity checks? + for(BasicBlock commonHandler : commonHandlers) { + ExceptionRangeCFG commonRange = graph.getExceptionRange(commonHandler, handler); + + dummyBlock.addSuccessorException(commonHandler); + commonRange.getProtectedRange().add(dummyBlock); + } + + dummyBlock.addSuccessor(handler); + } + } + } + } \ No newline at end of file diff --git a/test/org/jetbrains/java/decompiler/SingleClassesTest.java b/test/org/jetbrains/java/decompiler/SingleClassesTest.java index f2b28b9..a84907e 100644 --- a/test/org/jetbrains/java/decompiler/SingleClassesTest.java +++ b/test/org/jetbrains/java/decompiler/SingleClassesTest.java @@ -113,6 +113,7 @@ public class SingleClassesTest { @Test public void testPrivateEmptyConstructor() { doTest("pkg/TestPrivateEmptyConstructor"); } @Test public void testSynchronizedUnprotected() { doTest("pkg/TestSynchronizedUnprotected"); } @Test public void testInterfaceSuper() { doTest("pkg/TestInterfaceSuper"); } + @Test public void testFieldSingleAccess() { doTest("pkg/TestFieldSingleAccess"); } // TODO: fix all below //@Test public void testPackageInfo() { doTest("pkg/package-info"); } @@ -125,6 +126,7 @@ public class SingleClassesTest { @Test public void testGroovyTrait() { doTest("pkg/TestGroovyTrait"); } @Test public void testPrivateClasses() { doTest("pkg/PrivateClasses"); } @Test public void testSuspendLambda() { doTest("pkg/TestSuspendLambdaKt"); } + @Test public void testNamedSuspendFun2Kt() { doTest("pkg/TestNamedSuspendFun2Kt"); } private void doTest(String testFile, String... companionFiles) { ConsoleDecompiler decompiler = fixture.getDecompiler(); diff --git a/testData/classes/pkg/TestNamedSuspendFun2Kt$foo2$1.class b/testData/classes/pkg/TestNamedSuspendFun2Kt$foo2$1.class new file mode 100644 index 0000000..af894aa Binary files /dev/null and b/testData/classes/pkg/TestNamedSuspendFun2Kt$foo2$1.class differ diff --git a/testData/classes/pkg/TestNamedSuspendFun2Kt.class b/testData/classes/pkg/TestNamedSuspendFun2Kt.class new file mode 100644 index 0000000..83f3b75 Binary files /dev/null and b/testData/classes/pkg/TestNamedSuspendFun2Kt.class differ diff --git a/testData/results/TestNamedSuspendFun2Kt.dec b/testData/results/TestNamedSuspendFun2Kt.dec new file mode 100644 index 0000000..8c4d7c5 --- /dev/null +++ b/testData/results/TestNamedSuspendFun2Kt.dec @@ -0,0 +1,301 @@ +import kotlin.Metadata; +import kotlin.SuccessOrFailure.Failure; +import kotlin.coroutines.Continuation; +import kotlin.coroutines.intrinsics.IntrinsicsKt; +import kotlin.coroutines.jvm.internal.ContinuationImpl; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +@Metadata( + mv = {1, 1, 11}, + bv = {1, 0, 2}, + k = 2, + xi = 2, + d1 = {"\u0000\n\n\u0000\n\u0002\u0010\b\n\u0002\b\u0002\u001a\u0011\u0010\u0000\u001a\u00020\u0001H\u0086@ø\u0001\u0000¢\u0006\u0002\u0010\u0002\u001a\u0011\u0010\u0003\u001a\u00020\u0001H\u0086@ø\u0001\u0000¢\u0006\u0002\u0010\u0002\u0082\u0002\u0004\n\u0002\b\u0019"}, + d2 = {"bar", "", "(Lkotlin/coroutines/Continuation;)Ljava/lang/Object;", "foo2"} +) +public final class TestNamedSuspendFun2Kt { + @Nullable + public static final Object foo2(@NotNull Continuation var0) { + @Metadata( + mv = {1, 1, 11}, + bv = {1, 0, 2}, + k = 3, + xi = 2, + d1 = {"\u0000\u0010\n\u0000\n\u0002\u0010\u0000\n\u0000\n\u0002\u0018\u0002\n\u0002\u0010\b\u0010\u0000\u001a\u0004\u0018\u00010\u00012\f\u0010\u0002\u001a\b\u0012\u0004\u0012\u00020\u00040\u0003H\u0086@ø\u0001\u0000"}, + d2 = {"foo2", "", "continuation", "Lkotlin/coroutines/Continuation;", ""} + ) + final class NamelessClass_1 extends ContinuationImpl { + int label; + int I$0; + Object L$0; + + @Nullable + public final Object invokeSuspend(@NotNull Object result) { + this.data = result; + this.label |= -2147483648; + return TestNamedSuspendFun2Kt.foo2(this); + } + + NamelessClass_1(Continuation var1) { + super(var1); + } + } + + NamelessClass_1 var3; + label463: { + if (var0 instanceof NamelessClass_1) { + var3 = (NamelessClass_1)var0; + if ((var3.getLabel() & -2147483648) != 0) { + var3.setLabel(var3.getLabel() - -2147483648); + break label463; + } + } + + var3 = new NamelessClass_1(var0); + } + + Object var4; + int x; + label491: { + Throwable var1; + Throwable var10000; + label472: { + Object var2 = var3.data; + var4 = IntrinsicsKt.getCOROUTINE_SUSPENDED();// 2 + boolean var10001; + Object var22; + switch(var3.label) { + case 0: + if (var2 instanceof Failure) { + throw ((Failure)var2).exception; + } + break; + case 1: + try { + if (var2 instanceof Failure) { + throw ((Failure)var2).exception; + } + + var22 = var2; + } catch (Throwable var19) { + var10000 = var19; + var10001 = false; + break label472; + } + + try { + x = ((Number)var22).intValue();// 6 + if (x == 0) { + break label491; + } + } catch (Throwable var17) { + var10000 = var17; + var10001 = false; + break label472; + } + + var3.label = 3; + if (bar(var3) == var4) { + return var4; + } + break; + case 2: + x = var3.I$0; + if (var2 instanceof Failure) { + throw ((Failure)var2).exception; + } + + return 1;// 11 + case 3: + if (var2 instanceof Failure) { + throw ((Failure)var2).exception; + } + break; + case 4: + var1 = (Throwable)var3.L$0; + if (var2 instanceof Failure) { + throw ((Failure)var2).exception; + } + + throw var1;// 9 + default: + throw new IllegalStateException("call to 'resume' before 'invoke' with coroutine"); + } + + do { + try { + var3.label = 1;// 5 + var22 = bar(var3); + } catch (Throwable var18) { + var10000 = var18; + var10001 = false; + break label472; + } + + if (var22 == var4) { + return var4; + } + + try { + x = ((Number)var22).intValue(); + if (x == 0) { + break label491; + } + } catch (Throwable var20) { + var10000 = var20; + var10001 = false; + break label472; + } + + var3.label = 3; + } while(bar(var3) != var4); + + return var4; + } + + var1 = var10000; + var3.L$0 = var1; + var3.label = 4; + if (bar(var3) == var4) {// 8 + return var4; + } + + throw var1; + } + + var3.I$0 = x; + var3.label = 2; + if (bar(var3) == var4) { + return var4; + } else { + return 1; + } + } + + @Nullable + public static final Object bar(@NotNull Continuation var0) { + return 0;// 14 + } +} + +class 'TestNamedSuspendFun2Kt$foo2$1' { + method 'invokeSuspend (Ljava/lang/Object;)Ljava/lang/Object;' { + 2 34 + a 35 + d 35 + 11 36 + 14 36 + } + + method ' (Lkotlin/coroutines/Continuation;)V' { + 2 40 + 5 41 + } +} + +class 'TestNamedSuspendFun2Kt' { + method 'foo2 (Lkotlin/coroutines/Continuation;)Ljava/lang/Object;' { + 1 46 + 4 46 + 8 47 + b 47 + d 48 + 10 48 + 12 48 + 13 48 + 18 49 + 1b 49 + 1d 49 + 1e 49 + 21 50 + 2c 54 + 2e 63 + 31 63 + 32 64 + 35 64 + 38 67 + 3b 67 + 5e 69 + 61 69 + 64 70 + 67 70 + 6a 70 + 6f 127 + 70 127 + 73 128 + 79 135 + 7e 136 + 81 75 + 84 75 + 87 76 + 8a 76 + 8d 76 + 90 87 + 93 87 + 96 87 + 98 88 + 9e 166 + a2 167 + a3 167 + a6 168 + ac 168 + b1 169 + b3 103 + b6 103 + b9 104 + bc 104 + bf 105 + c2 105 + c5 105 + c9 108 + ce 97 + cf 97 + d2 98 + d8 98 + dd 99 + e0 110 + e3 110 + e6 111 + e9 111 + ec 111 + f3 156 + f7 157 + fb 158 + fc 158 + ff 159 + 105 159 + 10a 160 + 10c 115 + 10f 115 + 112 115 + 115 116 + 118 116 + 11b 117 + 11e 117 + 121 117 + 126 120 + 12a 108 + 12b 108 + 133 122 + 138 122 + } + + method 'bar (Lkotlin/coroutines/Continuation;)Ljava/lang/Object;' { + 0 177 + 1 177 + 4 177 + } +} + +Lines mapping: +2 <-> 65 +5 <-> 128 +6 <-> 88 +8 <-> 160 +9 <-> 121 +11 <-> 109 +14 <-> 178 +Not mapped: +3 +4 diff --git a/testData/src/pkg/TestNamedSuspendFun2.kt b/testData/src/pkg/TestNamedSuspendFun2.kt new file mode 100644 index 0000000..670a91f --- /dev/null +++ b/testData/src/pkg/TestNamedSuspendFun2.kt @@ -0,0 +1,14 @@ + +suspend fun foo2(): Int { + while (true) { + try { + val x = bar() + if (x == 0) break + } finally { + bar() + } + } + return 1 +} + +suspend fun bar(): Int = 0