From d01a280ddf13f6830055e7df7ae15feb592b2c1d Mon Sep 17 00:00:00 2001 From: Skylot Date: Mon, 18 Mar 2013 21:05:28 +0400 Subject: [PATCH] Add source files and samples --- src/main/java/jadx/Consts.java | 13 + src/main/java/jadx/JadxArgs.java | 187 +++++ src/main/java/jadx/Main.java | 143 ++++ src/main/java/jadx/ProcessClass.java | 37 + src/main/java/jadx/codegen/AnnotationGen.java | 195 +++++ src/main/java/jadx/codegen/ClassGen.java | 310 ++++++++ src/main/java/jadx/codegen/CodeGen.java | 35 + src/main/java/jadx/codegen/CodeWriter.java | 181 +++++ src/main/java/jadx/codegen/InsnGen.java | 533 +++++++++++++ src/main/java/jadx/codegen/MethodGen.java | 305 ++++++++ src/main/java/jadx/codegen/RegionGen.java | 241 ++++++ src/main/java/jadx/codegen/TypeGen.java | 128 ++++ src/main/java/jadx/deobf/NameMapper.java | 70 ++ .../java/jadx/dex/attributes/AttrNode.java | 14 + .../jadx/dex/attributes/AttributeFlag.java | 19 + .../jadx/dex/attributes/AttributeType.java | 47 ++ .../jadx/dex/attributes/AttributesList.java | 153 ++++ .../jadx/dex/attributes/BlockRegState.java | 55 ++ .../dex/attributes/DeclareVariableAttr.java | 42 + .../jadx/dex/attributes/EnumClassAttr.java | 78 ++ .../jadx/dex/attributes/ForceReturnAttr.java | 28 + .../java/jadx/dex/attributes/IAttribute.java | 7 + .../jadx/dex/attributes/IAttributeNode.java | 7 + .../jadx/dex/attributes/JadxErrorAttr.java | 38 + .../jadx/dex/attributes/JumpAttribute.java | 50 ++ .../java/jadx/dex/attributes/LoopAttr.java | 59 ++ .../attributes/annotations/Annotation.java | 44 ++ .../annotations/AnnotationsList.java | 45 ++ .../annotations/MethodParameters.java | 32 + src/main/java/jadx/dex/info/AccessInfo.java | 156 ++++ src/main/java/jadx/dex/info/ClassInfo.java | 163 ++++ src/main/java/jadx/dex/info/FieldInfo.java | 47 ++ src/main/java/jadx/dex/info/LocalVarInfo.java | 49 ++ src/main/java/jadx/dex/info/MethodInfo.java | 85 ++ .../java/jadx/dex/instructions/ArithNode.java | 74 ++ .../java/jadx/dex/instructions/ArithOp.java | 31 + .../jadx/dex/instructions/FillArrayOp.java | 44 ++ .../java/jadx/dex/instructions/GotoNode.java | 28 + .../java/jadx/dex/instructions/IfNode.java | 78 ++ src/main/java/jadx/dex/instructions/IfOp.java | 42 + .../jadx/dex/instructions/IndexInsnNode.java | 24 + .../jadx/dex/instructions/InsnDecoder.java | 725 ++++++++++++++++++ .../java/jadx/dex/instructions/InsnType.java | 55 ++ .../jadx/dex/instructions/InvokeNode.java | 57 ++ .../jadx/dex/instructions/InvokeType.java | 9 + .../jadx/dex/instructions/SwitchNode.java | 52 ++ .../jadx/dex/instructions/args/ArgType.java | 299 ++++++++ .../jadx/dex/instructions/args/InsnArg.java | 75 ++ .../dex/instructions/args/InsnWrapArg.java | 34 + .../dex/instructions/args/LiteralArg.java | 35 + .../dex/instructions/args/PrimitiveType.java | 50 ++ .../dex/instructions/args/RegisterArg.java | 102 +++ .../jadx/dex/instructions/args/Typed.java | 50 ++ .../jadx/dex/instructions/args/TypedVar.java | 82 ++ .../instructions/mods/ConstructorInsn.java | 75 ++ .../dex/instructions/mods/TernaryInsn.java | 31 + src/main/java/jadx/dex/nodes/BlockNode.java | 173 +++++ src/main/java/jadx/dex/nodes/ClassNode.java | 258 +++++++ src/main/java/jadx/dex/nodes/DexNode.java | 119 +++ src/main/java/jadx/dex/nodes/FieldNode.java | 22 + src/main/java/jadx/dex/nodes/IBlock.java | 8 + src/main/java/jadx/dex/nodes/IContainer.java | 11 + src/main/java/jadx/dex/nodes/ILoadable.java | 19 + src/main/java/jadx/dex/nodes/IRegion.java | 11 + .../java/jadx/dex/nodes/InsnContainer.java | 20 + src/main/java/jadx/dex/nodes/InsnNode.java | 162 ++++ src/main/java/jadx/dex/nodes/MethodNode.java | 403 ++++++++++ src/main/java/jadx/dex/nodes/RootNode.java | 103 +++ .../dex/nodes/parser/AnnotationsParser.java | 103 +++ .../dex/nodes/parser/DebugInfoParser.java | 157 ++++ .../jadx/dex/nodes/parser/EncValueParser.java | 87 +++ .../jadx/dex/nodes/parser/FieldValueAttr.java | 27 + .../dex/nodes/parser/StaticValuesParser.java | 27 + .../java/jadx/dex/regions/AbstractRegion.java | 19 + src/main/java/jadx/dex/regions/IfRegion.java | 62 ++ .../java/jadx/dex/regions/LoopRegion.java | 122 +++ src/main/java/jadx/dex/regions/Region.java | 38 + .../java/jadx/dex/regions/SwitchRegion.java | 64 ++ .../jadx/dex/regions/SynchronizedRegion.java | 37 + .../java/jadx/dex/trycatch/CatchAttr.java | 28 + .../jadx/dex/trycatch/ExcHandlerAttr.java | 33 + .../jadx/dex/trycatch/ExceptionHandler.java | 95 +++ .../jadx/dex/trycatch/SplitterBlockAttr.java | 29 + .../java/jadx/dex/trycatch/TryCatchBlock.java | 131 ++++ .../jadx/dex/visitors/AbstractVisitor.java | 18 + .../jadx/dex/visitors/BlockMakerVisitor.java | 393 ++++++++++ .../dex/visitors/BlockProcessingHelper.java | 127 +++ .../java/jadx/dex/visitors/ClassCheck.java | 30 + .../java/jadx/dex/visitors/CodeShrinker.java | 199 +++++ .../dex/visitors/ConstInlinerVisitor.java | 167 ++++ .../jadx/dex/visitors/DepthTraverser.java | 33 + .../jadx/dex/visitors/DotGraphVisitor.java | 183 +++++ .../java/jadx/dex/visitors/EnumVisitor.java | 144 ++++ .../dex/visitors/FallbackModeVisitor.java | 40 + .../jadx/dex/visitors/IDexTreeVisitor.java | 26 + .../jadx/dex/visitors/InstructionRemover.java | 101 +++ .../java/jadx/dex/visitors/ModVisitor.java | 235 ++++++ .../regions/AbstractRegionVisitor.java | 21 + .../dex/visitors/regions/CheckRegions.java | 60 ++ .../regions/DepthRegionTraverser.java | 32 + .../dex/visitors/regions/FinishRegions.java | 41 + .../dex/visitors/regions/IRegionVisitor.java | 15 + .../visitors/regions/MarkTryCatchRegions.java | 144 ++++ .../visitors/regions/PostRegionVisitor.java | 59 ++ .../visitors/regions/ProcessVariables.java | 188 +++++ .../dex/visitors/regions/RegionMaker.java | 476 ++++++++++++ .../visitors/regions/RegionMakerVisitor.java | 31 + .../dex/visitors/regions/RegionStack.java | 92 +++ .../visitors/regions/TracedRegionVisitor.java | 31 + .../typeresolver/FinishTypeResolver.java | 42 + .../visitors/typeresolver/TypeResolver.java | 98 +++ .../typeresolver/finish/CheckTypeVisitor.java | 34 + .../typeresolver/finish/PostTypeResolver.java | 72 ++ .../finish/SelectTypeVisitor.java | 27 + src/main/java/jadx/utils/BlockUtils.java | 226 ++++++ src/main/java/jadx/utils/ErrorsCounter.java | 95 +++ src/main/java/jadx/utils/InsnUtils.java | 46 ++ src/main/java/jadx/utils/RegionUtils.java | 164 ++++ src/main/java/jadx/utils/StringUtils.java | 61 ++ src/main/java/jadx/utils/Utils.java | 69 ++ .../utils/exceptions/CodegenException.java | 34 + .../utils/exceptions/DecodeException.java | 25 + .../jadx/utils/exceptions/JadxException.java | 27 + .../exceptions/JadxRuntimeException.java | 11 + src/main/java/jadx/utils/files/InputFile.java | 85 ++ src/main/java/jadx/utils/files/JavaToDex.java | 58 ++ src/main/resources/logback.xml | 13 + .../java/jadx/samples/AbstractTest.java | 18 + src/samples/java/jadx/samples/RunTests.java | 114 +++ .../java/jadx/samples/TestAnnotations.java | 102 +++ src/samples/java/jadx/samples/TestArrays.java | 34 + src/samples/java/jadx/samples/TestCF.java | 183 +++++ src/samples/java/jadx/samples/TestCF2.java | 102 +++ src/samples/java/jadx/samples/TestCF3.java | 114 +++ .../java/jadx/samples/TestDeadCode.java | 15 + src/samples/java/jadx/samples/TestEnum.java | 128 ++++ src/samples/java/jadx/samples/TestFields.java | 25 + src/samples/java/jadx/samples/TestInner.java | 76 ++ src/samples/java/jadx/samples/TestInvoke.java | 50 ++ .../jadx/samples/TestStringProcessing.java | 15 + src/samples/java/jadx/samples/TestSwitch.java | 90 +++ .../java/jadx/samples/TestTryCatch.java | 156 ++++ .../java/jadx/samples/TestTypeResolver.java | 54 ++ src/test/java/jadx/tests/StringUtilsTest.java | 31 + src/test/java/jadx/tests/TypeMergeTest.java | 76 ++ 145 files changed, 13472 insertions(+) create mode 100644 src/main/java/jadx/Consts.java create mode 100644 src/main/java/jadx/JadxArgs.java create mode 100644 src/main/java/jadx/Main.java create mode 100644 src/main/java/jadx/ProcessClass.java create mode 100644 src/main/java/jadx/codegen/AnnotationGen.java create mode 100644 src/main/java/jadx/codegen/ClassGen.java create mode 100644 src/main/java/jadx/codegen/CodeGen.java create mode 100644 src/main/java/jadx/codegen/CodeWriter.java create mode 100644 src/main/java/jadx/codegen/InsnGen.java create mode 100644 src/main/java/jadx/codegen/MethodGen.java create mode 100644 src/main/java/jadx/codegen/RegionGen.java create mode 100644 src/main/java/jadx/codegen/TypeGen.java create mode 100644 src/main/java/jadx/deobf/NameMapper.java create mode 100644 src/main/java/jadx/dex/attributes/AttrNode.java create mode 100644 src/main/java/jadx/dex/attributes/AttributeFlag.java create mode 100644 src/main/java/jadx/dex/attributes/AttributeType.java create mode 100644 src/main/java/jadx/dex/attributes/AttributesList.java create mode 100644 src/main/java/jadx/dex/attributes/BlockRegState.java create mode 100644 src/main/java/jadx/dex/attributes/DeclareVariableAttr.java create mode 100644 src/main/java/jadx/dex/attributes/EnumClassAttr.java create mode 100644 src/main/java/jadx/dex/attributes/ForceReturnAttr.java create mode 100644 src/main/java/jadx/dex/attributes/IAttribute.java create mode 100644 src/main/java/jadx/dex/attributes/IAttributeNode.java create mode 100644 src/main/java/jadx/dex/attributes/JadxErrorAttr.java create mode 100644 src/main/java/jadx/dex/attributes/JumpAttribute.java create mode 100644 src/main/java/jadx/dex/attributes/LoopAttr.java create mode 100644 src/main/java/jadx/dex/attributes/annotations/Annotation.java create mode 100644 src/main/java/jadx/dex/attributes/annotations/AnnotationsList.java create mode 100644 src/main/java/jadx/dex/attributes/annotations/MethodParameters.java create mode 100644 src/main/java/jadx/dex/info/AccessInfo.java create mode 100644 src/main/java/jadx/dex/info/ClassInfo.java create mode 100644 src/main/java/jadx/dex/info/FieldInfo.java create mode 100644 src/main/java/jadx/dex/info/LocalVarInfo.java create mode 100644 src/main/java/jadx/dex/info/MethodInfo.java create mode 100644 src/main/java/jadx/dex/instructions/ArithNode.java create mode 100644 src/main/java/jadx/dex/instructions/ArithOp.java create mode 100644 src/main/java/jadx/dex/instructions/FillArrayOp.java create mode 100644 src/main/java/jadx/dex/instructions/GotoNode.java create mode 100644 src/main/java/jadx/dex/instructions/IfNode.java create mode 100644 src/main/java/jadx/dex/instructions/IfOp.java create mode 100644 src/main/java/jadx/dex/instructions/IndexInsnNode.java create mode 100644 src/main/java/jadx/dex/instructions/InsnDecoder.java create mode 100644 src/main/java/jadx/dex/instructions/InsnType.java create mode 100644 src/main/java/jadx/dex/instructions/InvokeNode.java create mode 100644 src/main/java/jadx/dex/instructions/InvokeType.java create mode 100644 src/main/java/jadx/dex/instructions/SwitchNode.java create mode 100644 src/main/java/jadx/dex/instructions/args/ArgType.java create mode 100644 src/main/java/jadx/dex/instructions/args/InsnArg.java create mode 100644 src/main/java/jadx/dex/instructions/args/InsnWrapArg.java create mode 100644 src/main/java/jadx/dex/instructions/args/LiteralArg.java create mode 100644 src/main/java/jadx/dex/instructions/args/PrimitiveType.java create mode 100644 src/main/java/jadx/dex/instructions/args/RegisterArg.java create mode 100644 src/main/java/jadx/dex/instructions/args/Typed.java create mode 100644 src/main/java/jadx/dex/instructions/args/TypedVar.java create mode 100644 src/main/java/jadx/dex/instructions/mods/ConstructorInsn.java create mode 100644 src/main/java/jadx/dex/instructions/mods/TernaryInsn.java create mode 100644 src/main/java/jadx/dex/nodes/BlockNode.java create mode 100644 src/main/java/jadx/dex/nodes/ClassNode.java create mode 100644 src/main/java/jadx/dex/nodes/DexNode.java create mode 100644 src/main/java/jadx/dex/nodes/FieldNode.java create mode 100644 src/main/java/jadx/dex/nodes/IBlock.java create mode 100644 src/main/java/jadx/dex/nodes/IContainer.java create mode 100644 src/main/java/jadx/dex/nodes/ILoadable.java create mode 100644 src/main/java/jadx/dex/nodes/IRegion.java create mode 100644 src/main/java/jadx/dex/nodes/InsnContainer.java create mode 100644 src/main/java/jadx/dex/nodes/InsnNode.java create mode 100644 src/main/java/jadx/dex/nodes/MethodNode.java create mode 100644 src/main/java/jadx/dex/nodes/RootNode.java create mode 100644 src/main/java/jadx/dex/nodes/parser/AnnotationsParser.java create mode 100644 src/main/java/jadx/dex/nodes/parser/DebugInfoParser.java create mode 100644 src/main/java/jadx/dex/nodes/parser/EncValueParser.java create mode 100644 src/main/java/jadx/dex/nodes/parser/FieldValueAttr.java create mode 100644 src/main/java/jadx/dex/nodes/parser/StaticValuesParser.java create mode 100644 src/main/java/jadx/dex/regions/AbstractRegion.java create mode 100644 src/main/java/jadx/dex/regions/IfRegion.java create mode 100644 src/main/java/jadx/dex/regions/LoopRegion.java create mode 100644 src/main/java/jadx/dex/regions/Region.java create mode 100644 src/main/java/jadx/dex/regions/SwitchRegion.java create mode 100644 src/main/java/jadx/dex/regions/SynchronizedRegion.java create mode 100644 src/main/java/jadx/dex/trycatch/CatchAttr.java create mode 100644 src/main/java/jadx/dex/trycatch/ExcHandlerAttr.java create mode 100644 src/main/java/jadx/dex/trycatch/ExceptionHandler.java create mode 100644 src/main/java/jadx/dex/trycatch/SplitterBlockAttr.java create mode 100644 src/main/java/jadx/dex/trycatch/TryCatchBlock.java create mode 100644 src/main/java/jadx/dex/visitors/AbstractVisitor.java create mode 100644 src/main/java/jadx/dex/visitors/BlockMakerVisitor.java create mode 100644 src/main/java/jadx/dex/visitors/BlockProcessingHelper.java create mode 100644 src/main/java/jadx/dex/visitors/ClassCheck.java create mode 100644 src/main/java/jadx/dex/visitors/CodeShrinker.java create mode 100644 src/main/java/jadx/dex/visitors/ConstInlinerVisitor.java create mode 100644 src/main/java/jadx/dex/visitors/DepthTraverser.java create mode 100644 src/main/java/jadx/dex/visitors/DotGraphVisitor.java create mode 100644 src/main/java/jadx/dex/visitors/EnumVisitor.java create mode 100644 src/main/java/jadx/dex/visitors/FallbackModeVisitor.java create mode 100644 src/main/java/jadx/dex/visitors/IDexTreeVisitor.java create mode 100644 src/main/java/jadx/dex/visitors/InstructionRemover.java create mode 100644 src/main/java/jadx/dex/visitors/ModVisitor.java create mode 100644 src/main/java/jadx/dex/visitors/regions/AbstractRegionVisitor.java create mode 100644 src/main/java/jadx/dex/visitors/regions/CheckRegions.java create mode 100644 src/main/java/jadx/dex/visitors/regions/DepthRegionTraverser.java create mode 100644 src/main/java/jadx/dex/visitors/regions/FinishRegions.java create mode 100644 src/main/java/jadx/dex/visitors/regions/IRegionVisitor.java create mode 100644 src/main/java/jadx/dex/visitors/regions/MarkTryCatchRegions.java create mode 100644 src/main/java/jadx/dex/visitors/regions/PostRegionVisitor.java create mode 100644 src/main/java/jadx/dex/visitors/regions/ProcessVariables.java create mode 100644 src/main/java/jadx/dex/visitors/regions/RegionMaker.java create mode 100644 src/main/java/jadx/dex/visitors/regions/RegionMakerVisitor.java create mode 100644 src/main/java/jadx/dex/visitors/regions/RegionStack.java create mode 100644 src/main/java/jadx/dex/visitors/regions/TracedRegionVisitor.java create mode 100644 src/main/java/jadx/dex/visitors/typeresolver/FinishTypeResolver.java create mode 100644 src/main/java/jadx/dex/visitors/typeresolver/TypeResolver.java create mode 100644 src/main/java/jadx/dex/visitors/typeresolver/finish/CheckTypeVisitor.java create mode 100644 src/main/java/jadx/dex/visitors/typeresolver/finish/PostTypeResolver.java create mode 100644 src/main/java/jadx/dex/visitors/typeresolver/finish/SelectTypeVisitor.java create mode 100644 src/main/java/jadx/utils/BlockUtils.java create mode 100644 src/main/java/jadx/utils/ErrorsCounter.java create mode 100644 src/main/java/jadx/utils/InsnUtils.java create mode 100644 src/main/java/jadx/utils/RegionUtils.java create mode 100644 src/main/java/jadx/utils/StringUtils.java create mode 100644 src/main/java/jadx/utils/Utils.java create mode 100644 src/main/java/jadx/utils/exceptions/CodegenException.java create mode 100644 src/main/java/jadx/utils/exceptions/DecodeException.java create mode 100644 src/main/java/jadx/utils/exceptions/JadxException.java create mode 100644 src/main/java/jadx/utils/exceptions/JadxRuntimeException.java create mode 100644 src/main/java/jadx/utils/files/InputFile.java create mode 100644 src/main/java/jadx/utils/files/JavaToDex.java create mode 100644 src/main/resources/logback.xml create mode 100644 src/samples/java/jadx/samples/AbstractTest.java create mode 100644 src/samples/java/jadx/samples/RunTests.java create mode 100644 src/samples/java/jadx/samples/TestAnnotations.java create mode 100644 src/samples/java/jadx/samples/TestArrays.java create mode 100644 src/samples/java/jadx/samples/TestCF.java create mode 100644 src/samples/java/jadx/samples/TestCF2.java create mode 100644 src/samples/java/jadx/samples/TestCF3.java create mode 100644 src/samples/java/jadx/samples/TestDeadCode.java create mode 100644 src/samples/java/jadx/samples/TestEnum.java create mode 100644 src/samples/java/jadx/samples/TestFields.java create mode 100644 src/samples/java/jadx/samples/TestInner.java create mode 100644 src/samples/java/jadx/samples/TestInvoke.java create mode 100644 src/samples/java/jadx/samples/TestStringProcessing.java create mode 100644 src/samples/java/jadx/samples/TestSwitch.java create mode 100644 src/samples/java/jadx/samples/TestTryCatch.java create mode 100644 src/samples/java/jadx/samples/TestTypeResolver.java create mode 100644 src/test/java/jadx/tests/StringUtilsTest.java create mode 100644 src/test/java/jadx/tests/TypeMergeTest.java diff --git a/src/main/java/jadx/Consts.java b/src/main/java/jadx/Consts.java new file mode 100644 index 000000000..821ca24a0 --- /dev/null +++ b/src/main/java/jadx/Consts.java @@ -0,0 +1,13 @@ +package jadx; + +public class Consts { + public static final String JADX_VERSION = "dev"; + + public static final boolean DEBUG = false; + + public static final String CLASS_OBJECT = "java.lang.Object"; + public static final String CLASS_STRING = "java.lang.String"; + public static final String CLASS_CLASS = "java.lang.Class"; + public static final String CLASS_THROWABLE = "java.lang.Throwable"; + public static final String CLASS_ENUM = "java.lang.Enum"; +} diff --git a/src/main/java/jadx/JadxArgs.java b/src/main/java/jadx/JadxArgs.java new file mode 100644 index 000000000..69ff93319 --- /dev/null +++ b/src/main/java/jadx/JadxArgs.java @@ -0,0 +1,187 @@ +package jadx; + +import jadx.utils.exceptions.JadxException; +import jadx.utils.files.InputFile; + +import java.io.File; +import java.io.IOException; +import java.io.PrintStream; +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.List; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.beust.jcommander.JCommander; +import com.beust.jcommander.Parameter; +import com.beust.jcommander.ParameterDescription; +import com.beust.jcommander.ParameterException; + +public class JadxArgs { + private final static Logger LOG = LoggerFactory.getLogger(JadxArgs.class); + + @Parameter(description = " (.dex, .apk, .jar or .class)", required = true) + protected List files; + + @Parameter(names = { "-d", "--output-dir" }, description = "output directory") + protected String outDirName; + + @Parameter(names = { "-j", "--threads-count" }, description = "processing threads count") + protected int threadsCount = Runtime.getRuntime().availableProcessors(); + + @Parameter(names = { "-f", "--fallback" }, description = "make simple dump (using goto instead of 'if', 'for', etc)", help = true) + protected boolean fallbackMode = false; + + @Parameter(names = { "--not-obfuscated" }, description = "set this flag if code not obfuscated") + protected boolean notObfuscated = false; + + @Parameter(names = { "--cfg" }, description = "save methods control flow graph") + protected boolean cfgOutput = false; + + @Parameter(names = { "--raw-cfg" }, description = "save methods control flow graph (use raw instructions)") + protected boolean rawCfgOutput = false; + + @Parameter(names = { "-v", "--verbose" }, description = "verbose output") + protected boolean verbose = false; + + @Parameter(names = { "-h", "--help" }, description = "print this help", help = true) + protected boolean printHelp = false; + + private final List input = new ArrayList(); + private File outputDir; + + public void parse(String[] args) throws JadxException { + try { + new JCommander(this, args); + processArgs(); + } catch (ParameterException e) { + System.out.println("Arguments parse error: " + e.getMessage()); + System.out.println(); + printHelp = true; + } + } + + private void processArgs() throws JadxException { + if (printHelp) + return; + + if (files == null || files.isEmpty()) + throw new JadxException("Please specify at least one input file"); + + for (String fileName : files) { + File file = new File(fileName); + if (!file.exists()) + throw new JadxException("File not found: " + file); + + try { + input.add(new InputFile(file)); + } catch (IOException e) { + throw new JadxException("File processing error: " + file, e); + } + } + + if (input.isEmpty()) + throw new JadxException("No files with correct extension (must be '.dex', '.class' or '.jar')"); + + if (threadsCount <= 0) + throw new JadxException("Threads count must be positive"); + + if (outDirName == null) { + File file = new File(files.get(0)); + String name = file.getName(); + int pos = name.lastIndexOf('.'); + if (pos != -1) + outDirName = name.substring(0, pos); + else + outDirName = name + "-jadx-out"; + + LOG.info("output directory: " + outDirName); + } + + outputDir = new File(outDirName); + if (!outputDir.exists() && !outputDir.mkdirs()) + throw new JadxException("Can't create directory " + outputDir); + if (!outputDir.isDirectory()) + throw new JadxException("Output file exists as file " + outputDir); + } + + public void printUsage() { + JCommander jc = new JCommander(this); + // print usage in not sorted fields order (by default its sorted by description) + PrintStream out = System.out; + out.println("jadx - dex to java decompiler, version: '" + Consts.JADX_VERSION + "'"); + out.println(); + out.println("usage: jadx [options] " + jc.getMainParameterDescription()); + out.println("options:"); + + List params = jc.getParameters(); + + int maxNamesLen = 0; + for (ParameterDescription p : params) { + int len = p.getNames().length(); + if (len > maxNamesLen) + maxNamesLen = len; + } + + Field[] fields = this.getClass().getDeclaredFields(); + for (Field f : fields) { + for (ParameterDescription p : params) { + if (f.getName().equals(p.getParameterized().getName())) { + StringBuilder opt = new StringBuilder(); + opt.append(' ').append(p.getNames()); + addSpaces(opt, maxNamesLen - opt.length() + 2); + opt.append("- ").append(p.getDescription()); + if (p.getParameter().required()) + opt.append(" [required]"); + out.println(opt.toString()); + break; + } + } + } + out.println("Example:"); + out.println(" jadx -d out classes.dex"); + } + + private static void addSpaces(StringBuilder str, int count) { + for (int i = 0; i < count; i++) + str.append(' '); + } + + public File getOutDir() { + return outputDir; + } + + public int getThreadsCount() { + return threadsCount; + } + + public boolean isCFGOutput() { + return cfgOutput; + } + + public boolean isRawCFGOutput() { + return rawCfgOutput; + } + + public List getInput() { + return input; + } + + public boolean isFallbackMode() { + return fallbackMode; + } + + public boolean isNotObfuscated() { + return notObfuscated; + } + + public boolean isVerbose() { + return verbose; + } + + public boolean isPrintHelp() { + return printHelp; + } + +} diff --git a/src/main/java/jadx/Main.java b/src/main/java/jadx/Main.java new file mode 100644 index 000000000..5d4d1aed3 --- /dev/null +++ b/src/main/java/jadx/Main.java @@ -0,0 +1,143 @@ +package jadx; + +import jadx.codegen.CodeGen; +import jadx.dex.info.ClassInfo; +import jadx.dex.nodes.ClassNode; +import jadx.dex.nodes.RootNode; +import jadx.dex.visitors.BlockMakerVisitor; +import jadx.dex.visitors.ClassCheck; +import jadx.dex.visitors.CodeShrinker; +import jadx.dex.visitors.ConstInlinerVisitor; +import jadx.dex.visitors.DotGraphVisitor; +import jadx.dex.visitors.EnumVisitor; +import jadx.dex.visitors.FallbackModeVisitor; +import jadx.dex.visitors.IDexTreeVisitor; +import jadx.dex.visitors.ModVisitor; +import jadx.dex.visitors.regions.CheckRegions; +import jadx.dex.visitors.regions.PostRegionVisitor; +import jadx.dex.visitors.regions.ProcessVariables; +import jadx.dex.visitors.regions.RegionMakerVisitor; +import jadx.dex.visitors.typeresolver.FinishTypeResolver; +import jadx.dex.visitors.typeresolver.TypeResolver; +import jadx.utils.ErrorsCounter; +import jadx.utils.exceptions.JadxException; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class Main { + private static final Logger LOG = LoggerFactory.getLogger(Main.class); + + static { + if (Consts.DEBUG) + LOG.info("debug enabled"); + if (Main.class.desiredAssertionStatus()) + LOG.info("assertions enabled"); + } + + public static int run(JadxArgs args) { + int errorCount; + try { + RootNode root = new RootNode(args); + LOG.info("loading ..."); + root.load(); + LOG.info("processing ..."); + root.init(); + + int threadsCount = args.getThreadsCount(); + LOG.debug("processing threads count: {}", threadsCount); + + List passes = getPassesList(args); + if (threadsCount == 1) { + for (ClassNode cls : root.getClasses()) { + ProcessClass job = new ProcessClass(cls, passes); + job.run(); + } + } else { + ExecutorService executor = Executors.newFixedThreadPool(threadsCount); + for (ClassNode cls : root.getClasses()) { + ProcessClass job = new ProcessClass(cls, passes); + executor.execute(job); + } + executor.shutdown(); + executor.awaitTermination(100, TimeUnit.DAYS); + } + } catch (Throwable e) { + LOG.error("jadx error:", e); + } finally { + errorCount = ErrorsCounter.getErrorCount(); + if (errorCount != 0) + ErrorsCounter.printReport(); + + // clear resources if we use jadx as a library + ClassInfo.clearCache(); + ErrorsCounter.reset(); + } + LOG.info("done"); + return errorCount; + } + + private static List getPassesList(JadxArgs args) { + List passes = new ArrayList(); + if (args.isFallbackMode()) { + passes.add(new FallbackModeVisitor()); + } else { + passes.add(new BlockMakerVisitor()); + + passes.add(new TypeResolver()); + passes.add(new ConstInlinerVisitor()); + passes.add(new FinishTypeResolver()); + + passes.add(new ClassCheck()); + + if (args.isRawCFGOutput()) + passes.add(new DotGraphVisitor(args.getOutDir(), false, true)); + + passes.add(new ModVisitor()); + passes.add(new EnumVisitor()); + + if (args.isCFGOutput()) + passes.add(new DotGraphVisitor(args.getOutDir(), false)); + + passes.add(new RegionMakerVisitor()); + passes.add(new PostRegionVisitor()); + + passes.add(new CodeShrinker()); + passes.add(new ProcessVariables()); + passes.add(new CheckRegions()); + if (args.isCFGOutput()) + passes.add(new DotGraphVisitor(args.getOutDir(), true)); + } + passes.add(new CodeGen(args)); + return passes; + } + + public static void main(String[] args) { + JadxArgs jadxArgs = new JadxArgs(); + try { + jadxArgs.parse(args); + if (jadxArgs.isPrintHelp()) { + jadxArgs.printUsage(); + System.exit(0); + } + } catch (JadxException e) { + LOG.error("Error: " + e.getMessage()); + System.exit(1); + } + + if (jadxArgs.isVerbose()) { + ch.qos.logback.classic.Logger rootLogger = + (ch.qos.logback.classic.Logger) LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME); + rootLogger.setLevel(ch.qos.logback.classic.Level.DEBUG); + } + + int result = run(jadxArgs); + System.exit(result); + } +} diff --git a/src/main/java/jadx/ProcessClass.java b/src/main/java/jadx/ProcessClass.java new file mode 100644 index 000000000..313290cdd --- /dev/null +++ b/src/main/java/jadx/ProcessClass.java @@ -0,0 +1,37 @@ +package jadx; + +import jadx.dex.nodes.ClassNode; +import jadx.dex.visitors.DepthTraverser; +import jadx.dex.visitors.IDexTreeVisitor; +import jadx.utils.exceptions.DecodeException; + +import java.util.List; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +class ProcessClass implements Runnable { + private final static Logger LOG = LoggerFactory.getLogger(ProcessClass.class); + + private final ClassNode cls; + private final List passes; + + ProcessClass(ClassNode cls, List passes) { + this.cls = cls; + this.passes = passes; + } + + @Override + public void run() { + try { + cls.load(); + for (IDexTreeVisitor visitor : passes) { + DepthTraverser.visit(visitor, cls); + } + } catch (DecodeException e) { + LOG.error("Decode exception: " + cls, e); + } finally { + cls.unload(); + } + } +} diff --git a/src/main/java/jadx/codegen/AnnotationGen.java b/src/main/java/jadx/codegen/AnnotationGen.java new file mode 100644 index 000000000..3bb8d1eb8 --- /dev/null +++ b/src/main/java/jadx/codegen/AnnotationGen.java @@ -0,0 +1,195 @@ +package jadx.codegen; + +import jadx.Consts; +import jadx.dex.attributes.AttributeType; +import jadx.dex.attributes.IAttributeNode; +import jadx.dex.attributes.annotations.Annotation; +import jadx.dex.attributes.annotations.AnnotationsList; +import jadx.dex.attributes.annotations.MethodParameters; +import jadx.dex.info.FieldInfo; +import jadx.dex.instructions.args.ArgType; +import jadx.dex.nodes.ClassNode; +import jadx.dex.nodes.FieldNode; +import jadx.dex.nodes.MethodNode; +import jadx.utils.StringUtils; +import jadx.utils.Utils; +import jadx.utils.exceptions.JadxRuntimeException; + +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; + +public class AnnotationGen { + + private final ClassNode cls; + private final ClassGen classGen; + + public AnnotationGen(ClassNode cls, ClassGen classGen) { + this.cls = cls; + this.classGen = classGen; + } + + public void addForClass(CodeWriter code) { + add(cls, code); + } + + public void addForMethod(CodeWriter code, MethodNode mth) { + add(mth, code); + } + + public void addForField(CodeWriter code, FieldNode field) { + add(field, code); + } + + public void addForParameter(CodeWriter code, MethodParameters paramsAnnotations, int n) { + AnnotationsList aList = paramsAnnotations.getParamList().get(n); + if (aList == null || aList.size() == 0) + return; + + for (Annotation a : aList.getAll()) { + code.add(formatAnnotation(a)); + code.add(' '); + } + } + + private void add(IAttributeNode node, CodeWriter code) { + AnnotationsList aList = (AnnotationsList) node.getAttributes().get(AttributeType.ANNOTATION_LIST); + if (aList == null || aList.size() == 0) + return; + + for (Annotation a : aList.getAll()) { + String aCls = a.getAnnotationClass(); + if (aCls.startsWith("dalvik.annotation.")) { + // skip + if (aCls.equals("dalvik.annotation.Signature")) + code.startLine("// signature: " + + Utils.mergeSignature((List) a.getValues().get("value"))); + else if (Consts.DEBUG) + code.startLine("// " + a); + } else { + code.startLine(); + code.add(formatAnnotation(a)); + } + } + } + + private CodeWriter formatAnnotation(Annotation a) { + CodeWriter code = new CodeWriter(); + code.add('@'); + code.add(classGen.useClass(a.getType())); + Map vl = a.getValues(); + if (vl.size() != 0) { + code.add('('); + if (vl.size() == 1 && vl.containsKey("value")) { + code.add(encValueToString(vl.get("value"))); + } else { + for (Iterator> it = vl.entrySet().iterator(); it.hasNext();) { + Entry e = it.next(); + code.add(e.getKey()); + code.add(" = "); + code.add(encValueToString(e.getValue())); + if (it.hasNext()) + code.add(", "); + } + } + code.add(')'); + } + return code; + } + + @SuppressWarnings("unchecked") + public void addThrows(MethodNode mth, CodeWriter code) { + AnnotationsList anList = (AnnotationsList) mth.getAttributes().get(AttributeType.ANNOTATION_LIST); + if (anList == null || anList.size() == 0) + return; + + Annotation an = anList.get("dalvik.annotation.Throws"); + if (an != null) { + Object exs = an.getValues().get("value"); + code.add(" throws "); + for (Iterator it = ((List) exs).iterator(); it.hasNext();) { + ArgType ex = it.next(); + code.add(TypeGen.translate(classGen, ex)); + if (it.hasNext()) + code.add(", "); + } + } + } + + public Object getAnnotationDefaultValue(String name) { + AnnotationsList anList = (AnnotationsList) cls.getAttributes().get(AttributeType.ANNOTATION_LIST); + if (anList == null || anList.size() == 0) + return null; + + Annotation an = anList.get("dalvik.annotation.AnnotationDefault"); + if (an != null) { + Annotation defAnnotation = (Annotation) an.getValues().get("value"); + return defAnnotation.getValues().get(name); + } + return null; + } + + // TODO: refactor this boilerplate code + @SuppressWarnings("unchecked") + public String encValueToString(Object val) { + if (val == null) + return "null"; + + if (val instanceof String) + return StringUtils.unescapeString((String) val); + if (val instanceof Integer) + return TypeGen.formatInteger((Integer) val); + if (val instanceof Character) + return StringUtils.unescapeChar((Character) val); + if (val instanceof Boolean) + return Boolean.TRUE.equals(val) ? "true" : "false"; + if (val instanceof Float) + return TypeGen.formatFloat((Float) val); + if (val instanceof Double) + return TypeGen.formatDouble((Double) val); + if (val instanceof Long) + return TypeGen.formatLong((Long) val); + if (val instanceof Short) + return TypeGen.formatShort((Short) val); + if (val instanceof Byte) + return TypeGen.formatByte((Byte) val); + + if (val instanceof ArgType) + return TypeGen.translate(classGen, (ArgType) val) + ".class"; + + if (val instanceof FieldInfo) { + // must be a static field + FieldInfo field = (FieldInfo) val; + // FIXME: !!code from InsnGen.sfield + String thisClass = cls.getFullName(); + if (field.getDeclClass().getFullName().equals(thisClass)) { + return field.getName(); + } else { + return classGen.useClass(field.getDeclClass()) + '.' + field.getName(); + } + } + + if (val instanceof List) { + StringBuilder str = new StringBuilder(); + str.append('{'); + List list = (List) val; + for (Iterator it = list.iterator(); it.hasNext();) { + Object obj = it.next(); + str.append(encValueToString(obj)); + if (it.hasNext()) + str.append(", "); + } + str.append('}'); + return str.toString(); + } + + if (val instanceof Annotation) { + return formatAnnotation((Annotation) val).toString(); + } + + // TODO: also can be method values + + throw new JadxRuntimeException("Can't decode value: " + val + " (" + val.getClass() + ")"); + } +} diff --git a/src/main/java/jadx/codegen/ClassGen.java b/src/main/java/jadx/codegen/ClassGen.java new file mode 100644 index 000000000..dd36974e4 --- /dev/null +++ b/src/main/java/jadx/codegen/ClassGen.java @@ -0,0 +1,310 @@ +package jadx.codegen; + +import jadx.Consts; +import jadx.dex.attributes.AttributeFlag; +import jadx.dex.attributes.AttributeType; +import jadx.dex.attributes.EnumClassAttr; +import jadx.dex.attributes.EnumClassAttr.EnumField; +import jadx.dex.info.AccessInfo; +import jadx.dex.info.ClassInfo; +import jadx.dex.instructions.args.ArgType; +import jadx.dex.instructions.args.InsnArg; +import jadx.dex.nodes.ClassNode; +import jadx.dex.nodes.FieldNode; +import jadx.dex.nodes.MethodNode; +import jadx.dex.nodes.parser.FieldValueAttr; +import jadx.utils.ErrorsCounter; +import jadx.utils.Utils; +import jadx.utils.exceptions.CodegenException; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Set; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.android.dx.rop.code.AccessFlags; + +public class ClassGen { + private final static Logger LOG = LoggerFactory.getLogger(ClassGen.class); + + private final ClassNode cls; + private final ClassGen parentGen; + private final AnnotationGen annotationGen; + private final boolean fallback; + + private final Set imports = new HashSet(); + + public ClassGen(ClassNode cls, ClassGen parentClsGen, boolean fallback) { + this.cls = cls; + this.parentGen = parentClsGen; + this.fallback = fallback; + + this.annotationGen = new AnnotationGen(cls, this); + } + + public ClassNode getClassNode() { + return cls; + } + + public CodeWriter makeClass() throws CodegenException { + CodeWriter clsBody = new CodeWriter(); + addClassCode(clsBody); + + CodeWriter clsCode = new CodeWriter(); + + if (!"".equals(cls.getPackage())) { + clsCode.add("package ").add(cls.getPackage()).add(";"); + clsCode.endl(); + } + + if (imports.size() != 0) { + List sortImports = new ArrayList(); + for (ClassInfo ic : imports) + sortImports.add(ic.getFullName()); + Collections.sort(sortImports); + + for (String imp : sortImports) { + clsCode.startLine("import ").add(imp).add(";"); + } + clsCode.endl(); + + sortImports.clear(); + imports.clear(); + } + + clsCode.add(clsBody); + return clsCode; + } + + public void addClassCode(CodeWriter code) throws CodegenException { + if (cls.getAttributes().contains(AttributeFlag.DONT_GENERATE)) + return; + + makeClassDeclaration(code); + makeClassBody(code); + code.endl(); + } + + public void makeClassDeclaration(CodeWriter clsCode) { + AccessInfo af = cls.getAccessFlags(); + if (af.isInterface()) { + af = af.remove(AccessFlags.ACC_ABSTRACT); + } else if (af.isEnum()) { + af = af.remove(AccessFlags.ACC_FINAL).remove(AccessFlags.ACC_ABSTRACT); + } + + annotationGen.addForClass(clsCode); + clsCode.startLine(af.makeString()); + if (af.isInterface()) { + if (af.isAnnotation()) + clsCode.add('@'); + clsCode.add("interface "); + } else if (af.isEnum()) { + clsCode.add("enum "); + } else { + clsCode.add("class "); + } + clsCode.add(cls.getShortName()); + ClassInfo sup = cls.getSuperClass(); + + if (sup != null + && !sup.getFullName().equals(Consts.CLASS_OBJECT) + && !sup.getFullName().equals(Consts.CLASS_ENUM)) { + clsCode.add(" extends ").add(useClass(sup)); + } + + if (cls.getInterfaces().size() > 0 && !af.isAnnotation()) { + if (cls.getAccessFlags().isInterface()) + clsCode.add(" extends "); + else + clsCode.add(" implements "); + + for (Iterator it = cls.getInterfaces().iterator(); it.hasNext();) { + ClassInfo interf = it.next(); + clsCode.add(useClass(interf)); + if (it.hasNext()) + clsCode.add(", "); + } + } + } + + public void makeClassBody(CodeWriter clsCode) throws CodegenException { + clsCode.add(" {"); + CodeWriter mthsCode = makeMethods(clsCode, cls.getMethods()); + clsCode.add(makeFields(clsCode, cls, cls.getFields())); + + // insert inner classes code + if (cls.getInnerClasses().size() != 0) { + clsCode.add(makeInnerClasses(cls, clsCode.getIndent())); + } + clsCode.add(mthsCode); + clsCode.startLine("}"); + } + + private CodeWriter makeInnerClasses(ClassNode cls2, int indent) throws CodegenException { + CodeWriter innerClsCode = new CodeWriter(indent + 1); + for (ClassNode inCls : cls.getInnerClasses()) { + if (inCls.isAnonymous()) + continue; + + ClassGen inClGen = new ClassGen(inCls, parentGen == null ? this : parentGen, fallback); + inClGen.addClassCode(innerClsCode); + imports.addAll(inClGen.getImports()); + } + return innerClsCode; + } + + private CodeWriter makeMethods(CodeWriter clsCode, List mthList) throws CodegenException { + CodeWriter code = new CodeWriter(clsCode.getIndent() + 1); + for (Iterator it = mthList.iterator(); it.hasNext();) { + MethodNode mth = it.next(); + try { + if (mth.getAccessFlags().isAbstract() || mth.getAccessFlags().isNative()) { + MethodGen mthGen = new MethodGen(this, mth); + mthGen.addDefinition(code); + if (cls.getAccessFlags().isAnnotation()) { + Object def = annotationGen.getAnnotationDefaultValue(mth.getName()); + if (def != null) { + String v = annotationGen.encValueToString(def); + code.add(" default ").add(v); + } + } + code.add(";"); + } else { + if (mth.isNoCode()) + continue; + + MethodGen mthGen = new MethodGen(this, mth); + mthGen.addDefinition(code); + code.add(" {"); + code.add(mthGen.makeInstructions(code.getIndent())); + code.startLine("}"); + } + } catch (Throwable e) { + String msg = ErrorsCounter.methodError(mth, "Method generation error", e); + code.startLine("/* " + msg + CodeWriter.NL + Utils.getStackTrace(e) + "*/"); + } + + if (it.hasNext()) + code.endl(); + } + return code; + } + + private CodeWriter makeFields(CodeWriter clsCode, ClassNode cls, List fields) throws CodegenException { + CodeWriter code = new CodeWriter(clsCode.getIndent() + 1); + + EnumClassAttr enumFields = (EnumClassAttr) cls.getAttributes().get(AttributeType.ENUM_CLASS); + if (enumFields != null) { + MethodGen mthGen = new MethodGen(this, enumFields.getStaticMethod()); + InsnGen igen = new InsnGen(mthGen, enumFields.getStaticMethod(), false); + + for (Iterator it = enumFields.getFields().iterator(); it.hasNext();) { + EnumField f = it.next(); + code.startLine(f.getName()); + if (f.getArgs().size() != 0) { + code.add('('); + for (Iterator aIt = f.getArgs().iterator(); aIt.hasNext();) { + InsnArg arg = aIt.next(); + code.add(igen.arg(arg)); + if (aIt.hasNext()) + code.add(", "); + } + code.add(')'); + } + if (f.getCls() != null) { + new ClassGen(f.getCls(), this, fallback).makeClassBody(code); + } + if (it.hasNext()) + code.add(','); + } + if (enumFields.getFields().isEmpty()) + code.startLine(); + + code.add(';'); + code.endl(); + } + + for (FieldNode f : fields) { + annotationGen.addForField(code, f); + code.startLine(f.getAccessFlags().makeString()); + code.add(TypeGen.translate(this, f.getType())); + code.add(" "); + code.add(f.getName()); + FieldValueAttr fv = (FieldValueAttr) f.getAttributes().get(AttributeType.FIELD_VALUE); + if (fv != null) { + code.add(" = "); + if (fv.getValue() == null) { + code.add(TypeGen.literalToString(0, f.getType())); + } else { + code.add(annotationGen.encValueToString(fv.getValue())); + } + } + code.add(";"); + } + if (fields.size() != 0) + code.endl(); + return code; + } + + public String useClass(ArgType clsType) { + return useClass(ClassInfo.fromType(cls.dex(), clsType)); + } + + public String useClass(ClassInfo classInfo) { + if (parentGen != null) + return parentGen.useClass(classInfo); + + String clsStr = classInfo.getFullName(); + if (fallback) + return clsStr; + + String shortName = classInfo.getShortName(); + + if (classInfo.getPackage().equals("java.lang") && classInfo.getParentClass() == null) { + return shortName; + } else { + // don't add import if this class inner for current class + if (isInner(classInfo, cls.getClassInfo())) + return shortName; + + for (ClassInfo cls : imports) { + if (!cls.equals(classInfo)) { + if (cls.getShortName().equals(shortName)) + return clsStr; + } + } + imports.add(classInfo); + return shortName; + } + } + + private boolean isInner(ClassInfo inner, ClassInfo parent) { + if (inner.isInner()) { + ClassInfo p = inner.getParentClass(); + return p.equals(parent) || isInner(p, parent); + } + return false; + } + + public Set getImports() { + return imports; + } + + public ClassGen getParentGen() { + return parentGen; + } + + public AnnotationGen getAnnotationGen() { + return annotationGen; + } + + public boolean isFallbackMode() { + return fallback; + } +} diff --git a/src/main/java/jadx/codegen/CodeGen.java b/src/main/java/jadx/codegen/CodeGen.java new file mode 100644 index 000000000..17ceebc19 --- /dev/null +++ b/src/main/java/jadx/codegen/CodeGen.java @@ -0,0 +1,35 @@ +package jadx.codegen; + +import jadx.JadxArgs; +import jadx.dex.nodes.ClassNode; +import jadx.dex.visitors.AbstractVisitor; +import jadx.utils.exceptions.CodegenException; + +import java.io.File; + +public class CodeGen extends AbstractVisitor { + + private final File dir; + private final JadxArgs args; + + public CodeGen(JadxArgs args) { + this.args = args; + this.dir = args.getOutDir(); + } + + @Override + public boolean visit(ClassNode cls) throws CodegenException { + ClassGen clsGen = new ClassGen(cls, null, isFallbackMode()); + CodeWriter clsCode = clsGen.makeClass(); + String fileName = cls.getClassInfo().getFullPath() + ".java"; + if (isFallbackMode()) + fileName += ".jadx"; + clsCode.save(dir, fileName); + return false; + } + + public boolean isFallbackMode() { + return args.isFallbackMode(); + } + +} diff --git a/src/main/java/jadx/codegen/CodeWriter.java b/src/main/java/jadx/codegen/CodeWriter.java new file mode 100644 index 000000000..3fc715853 --- /dev/null +++ b/src/main/java/jadx/codegen/CodeWriter.java @@ -0,0 +1,181 @@ +package jadx.codegen; + +import jadx.utils.exceptions.JadxRuntimeException; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.PrintWriter; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class CodeWriter { + private final static Logger LOG = LoggerFactory.getLogger(CodeWriter.class); + private static final int MAX_FILENAME_LENGTH = 128; + + public final static String NL = System.getProperty("line.separator"); + public final static String INDENT = "\t"; + + private StringBuilder buf = new StringBuilder(); + private String indentStr; + private int indent; + + public CodeWriter() { + this.indent = 0; + this.indentStr = ""; + } + + public CodeWriter(int indent) { + this.indent = indent; + updateIndent(); + } + + public CodeWriter startLine(String str) { + buf.append(NL); + buf.append(indentStr); + buf.append(str); + return this; + } + + public CodeWriter startLine(int ind, String str) { + buf.append(NL); + buf.append(indentStr); + for (int i = 0; i < ind; i++) + buf.append(INDENT); + buf.append(str); + return this; + } + + public CodeWriter startLine() { + buf.append(NL); + buf.append(indentStr); + return this; + } + + public CodeWriter add(String str) { + buf.append(str); + return this; + } + + public CodeWriter add(char c) { + buf.append(c); + return this; + } + + public CodeWriter add(CodeWriter mthsCode) { + buf.append(mthsCode.toString()); + return this; + } + + public CodeWriter endl() { + buf.append(NL); + return this; + } + + private static final String[] indentCache = new String[] { + "", + INDENT, + INDENT + INDENT, + INDENT + INDENT + INDENT, + INDENT + INDENT + INDENT + INDENT, + INDENT + INDENT + INDENT + INDENT + INDENT, + }; + + private void updateIndent() { + if (indent < 6) { + this.indentStr = indentCache[indent]; + } else { + StringBuilder s = new StringBuilder(indent * INDENT.length()); + for (int i = 0; i < indent; i++) { + s.append(INDENT); + } + this.indentStr = s.toString(); + } + } + + public int getIndent() { + return indent; + } + + public void incIndent() { + incIndent(1); + } + + public void decIndent() { + decIndent(1); + } + + public void incIndent(int c) { + this.indent += c; + updateIndent(); + } + + public void decIndent(int c) { + this.indent -= c; + if (this.indent < 0) { + LOG.warn("Indent < 0"); + this.indent = 0; + } + updateIndent(); + } + + private static String removeFirstEmptyLine(String str) { + if (str.startsWith(NL)) { + return str.substring(NL.length()); + } else { + return str; + } + } + + @Override + public String toString() { + return buf.toString(); + } + + public void save(File dir, String subDir, String fileName) { + save(dir, new File(subDir, fileName).getPath()); + } + + public void save(File dir, String fileName) { + save(new File(dir, fileName)); + } + + public void save(File file) { + String name = file.getName(); + if (name.length() > MAX_FILENAME_LENGTH) { + int dotIndex = name.indexOf('.'); + int cutAt = MAX_FILENAME_LENGTH - name.length() + dotIndex - 1; + if (cutAt <= 0) + name = name.substring(0, MAX_FILENAME_LENGTH - 1); + else + name = name.substring(0, cutAt) + name.substring(dotIndex); + file = new File(file.getParentFile(), name); + } + + PrintWriter out = null; + try { + makeDirsForFile(file); + out = new PrintWriter(file); + String code = buf.toString(); + code = removeFirstEmptyLine(code); + out.print(code); + } catch (FileNotFoundException e) { + LOG.error("Save file error", e); + } finally { + if (out != null) + out.close(); + buf = null; + } + } + + private void makeDirsForFile(File file) { + File dir = file.getParentFile(); + if (!dir.exists()) { + // if directory already created in other thread mkdirs will return false, + // so check dir existence again + if (!dir.mkdirs() && !dir.exists()) + throw new JadxRuntimeException("Can't create directory " + dir); + } + } + +} diff --git a/src/main/java/jadx/codegen/InsnGen.java b/src/main/java/jadx/codegen/InsnGen.java new file mode 100644 index 000000000..97b564914 --- /dev/null +++ b/src/main/java/jadx/codegen/InsnGen.java @@ -0,0 +1,533 @@ +package jadx.codegen; + +import jadx.dex.attributes.AttributeType; +import jadx.dex.info.ClassInfo; +import jadx.dex.info.FieldInfo; +import jadx.dex.info.MethodInfo; +import jadx.dex.instructions.ArithNode; +import jadx.dex.instructions.ArithOp; +import jadx.dex.instructions.FillArrayOp; +import jadx.dex.instructions.GotoNode; +import jadx.dex.instructions.IfNode; +import jadx.dex.instructions.IndexInsnNode; +import jadx.dex.instructions.InvokeNode; +import jadx.dex.instructions.InvokeType; +import jadx.dex.instructions.SwitchNode; +import jadx.dex.instructions.args.ArgType; +import jadx.dex.instructions.args.InsnArg; +import jadx.dex.instructions.args.InsnWrapArg; +import jadx.dex.instructions.args.LiteralArg; +import jadx.dex.instructions.args.RegisterArg; +import jadx.dex.instructions.mods.ConstructorInsn; +import jadx.dex.nodes.ClassNode; +import jadx.dex.nodes.FieldNode; +import jadx.dex.nodes.InsnNode; +import jadx.dex.nodes.MethodNode; +import jadx.dex.nodes.RootNode; +import jadx.utils.StringUtils; +import jadx.utils.exceptions.CodegenException; + +import java.util.EnumSet; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class InsnGen { + private final static Logger LOG = LoggerFactory.getLogger(InsnGen.class); + + protected final MethodGen mgen; + protected final MethodNode mth; + protected final RootNode root; + private final boolean fallback; + + public enum InsnGenState { + SKIP, + + NO_SEMICOLON, + NO_RESULT, + + BODY_ONLY, + + INC_INDENT, + DEC_INDENT, + } + + public InsnGen(MethodGen mgen, MethodNode mth, boolean fallback) { + this.mgen = mgen; + this.mth = mth; + this.root = mth.dex().root(); + this.fallback = fallback; + } + + private boolean isFallback() { + return fallback; + } + + public String arg(InsnNode insn, int arg) throws CodegenException { + return arg(insn.getArg(arg)); + } + + public String arg(InsnArg arg) throws CodegenException { + if (arg.isRegister()) { + return mgen.makeArgName((RegisterArg) arg); + } else if (arg.isLiteral()) { + return lit((LiteralArg) arg); + } else { + CodeWriter code = new CodeWriter(); + makeInsn(((InsnWrapArg) arg).getWrapInsn(), code, true); + return code.toString(); + } + } + + public String assignVar(InsnNode insn) { + // return mgen.assignArg(arg); + try { + RegisterArg arg = insn.getResult(); + if (insn.getAttributes().contains(AttributeType.DECLARE_VARIABLE)) { + return declareVar(arg); + } else { + return arg(arg); + } + } catch (CodegenException e) { + LOG.error("Assign var codegen error", e); + } + return ""; + } + + public String declareVar(RegisterArg arg) throws CodegenException { + return TypeGen.translate(mgen.getClassGen(), arg.getType()) + " " + arg(arg); + } + + private String lit(LiteralArg arg) { + return TypeGen.literalToString(arg.getLiteral(), arg.getType()); + } + + private String ifield(IndexInsnNode insn, int reg) throws CodegenException { + FieldInfo field = (FieldInfo) insn.getIndex(); + return arg(insn.getArg(reg)) + '.' + field.getName(); + } + + private String sfield(IndexInsnNode insn) { + FieldInfo field = (FieldInfo) insn.getIndex(); + String thisClass = mth.getParentClass().getFullName(); + if (field.getDeclClass().getFullName().equals(thisClass)) { + return field.getName(); + } else { + return useClass(field.getDeclClass()) + '.' + field.getName(); + } + } + + private void fieldPut(IndexInsnNode insn) { + FieldInfo field = (FieldInfo) insn.getIndex(); + String thisClass = mth.getParentClass().getFullName(); + if (field.getDeclClass().getFullName().equals(thisClass)) { + // if we generate this field - don't init if its final and used + FieldNode fn = mth.getParentClass().searchField(field); + if (fn != null && fn.getAccessFlags().isFinal()) + fn.getAttributes().remove(AttributeType.FIELD_VALUE); + } + } + + public String useClass(ClassInfo cls) { + return mgen.getClassGen().useClass(cls); + } + + private String useType(ArgType type) { + if (type.isObject()) + return mgen.getClassGen().useClass(type); + else + return translate(type); + } + + private String translate(ArgType type) { + return TypeGen.translate(mgen.getClassGen(), type); + } + + public void makeInsn(InsnNode insn, CodeWriter code) throws CodegenException { + makeInsn(insn, code, false); + } + + private void makeInsn(InsnNode insn, CodeWriter code, boolean bodyOnly) throws CodegenException { + try { + // code.startLine("/* " + insn + "*/"); + EnumSet state = EnumSet.noneOf(InsnGenState.class); + if (bodyOnly) { + state.add(InsnGenState.BODY_ONLY); + makeInsnBody(code, insn, state); + } else { + CodeWriter body = new CodeWriter(code.getIndent()); + makeInsnBody(body, insn, state); + if (state.contains(InsnGenState.SKIP)) + return; + + if (state.contains(InsnGenState.DEC_INDENT)) + code.decIndent(); + if (insn.getResult() != null && !state.contains(InsnGenState.NO_RESULT)) + code.startLine(assignVar(insn)).add(" = "); + else + code.startLine(); + + code.add(body); + + if (!state.contains(InsnGenState.NO_SEMICOLON)) + code.add(';'); + if (state.contains(InsnGenState.INC_INDENT)) + code.incIndent(); + } + } catch (Throwable th) { + throw new CodegenException(mth, "Error generate insn: " + insn, th); + } + } + + private void makeInsnBody(CodeWriter code, InsnNode insn, EnumSet state) throws CodegenException { + switch (insn.getType()) { + case CONST: + if (insn.getArgsCount() == 0) { + // const in 'index' - string or class + Object ind = ((IndexInsnNode) insn).getIndex(); + if (ind instanceof String) + code.add(StringUtils.unescapeString(ind.toString())); + else if (ind instanceof ArgType) + code.add(useType((ArgType) ind)).add(".class"); + } else { + LiteralArg arg = (LiteralArg) insn.getArg(0); + code.add(lit(arg)); + } + break; + + case MOVE: + code.add(arg(insn.getArg(0))); + break; + + case CHECK_CAST: + case CAST: + code.add("(("); + code.add(translate(((ArgType) ((IndexInsnNode) insn).getIndex()))); + code.add(") ("); + code.add(arg(insn.getArg(0))); + code.add("))"); + break; + + case ARITH: + makeArith((ArithNode) insn, code, state); + break; + + case NEG: + String base = "-" + arg(insn.getArg(0)); + if (state.contains(InsnGenState.BODY_ONLY)) + code.add('(').add(base).add(')'); + else + code.add(base); + break; + + case RETURN: + if (insn.getArgsCount() != 0) + code.add("return " + arg(insn.getArg(0))); + else + code.add("return"); + break; + + case BREAK: + code.add("break"); + break; + + case CONTINUE: + code.add("continue"); + break; + + case THROW: + code.add("throw " + arg(insn.getArg(0))); + break; + + case CMP_L: + case CMP_G: + code.add(String.format("(%1$s > %2$s ? 1 : (%1$s == %2$s ? 0 : -1))", arg(insn, 0), arg(insn, 1))); + break; + + case INSTANCE_OF: + code.add("(").add(arg(insn, 0)).add(" instanceof ") + .add(useType((ArgType) ((IndexInsnNode) insn).getIndex())).add(")"); + break; + + case CONSTRUCTOR: + makeConstructor((ConstructorInsn) insn, code, state); + break; + + case INVOKE: + makeInvoke((InvokeNode) insn, code); + break; + + case NEW_ARRAY: { + ArgType arrayType = insn.getResult().getType(); + int dim = arrayType.getArrayDimension(); + code.add("new ").add(useType(arrayType.getArrayRootElement())).add('[').add(arg(insn, 0)).add(']'); + for (int i = 0; i < dim - 1; i++) + code.add("[]"); + break; + } + + case ARRAY_LENGTH: + code.add(arg(insn, 0)).add(".length"); + break; + + case FILL_ARRAY: + fillArray((FillArrayOp) insn, code); + break; + + case FILLED_NEW_ARRAY: + filledNewArray(insn, code); + break; + + case AGET: + code.add(arg(insn.getArg(0))).add('[').add(arg(insn.getArg(1))).add(']'); + break; + + case APUT: + code.add(arg(insn, 0)).add('[').add(arg(insn, 1)).add("] = ").add(arg(insn, 2)); + break; + + case IGET: + code.add(ifield((IndexInsnNode) insn, 0)); + break; + case IPUT: + code.add(ifield((IndexInsnNode) insn, 1) + " = " + arg(insn.getArg(0))); + break; + + case SGET: + code.add(sfield((IndexInsnNode) insn)); + break; + case SPUT: + IndexInsnNode node = (IndexInsnNode) insn; + fieldPut(node); + code.add(sfield(node) + " = " + arg(node.getArg(0))); + break; + + case MONITOR_ENTER: + if (isFallback()) { + code.add("monitor-enter(").add(arg(insn.getArg(0))).add(")"); + } else { + state.add(InsnGenState.SKIP); + } + break; + + case MONITOR_EXIT: + if (isFallback()) { + code.add("monitor-exit(").add(arg(insn.getArg(0))).add(")"); + } else { + state.add(InsnGenState.SKIP); + } + break; + + case MOVE_EXCEPTION: + if (isFallback()) { + code.add("move-exception"); + } else { + // don't have body + if (state.contains(InsnGenState.BODY_ONLY)) + code.add(arg(insn.getResult())); + else + state.add(InsnGenState.SKIP); + } + break; + + case TERNARY: + break; + + /* fallback mode instructions */ + case NOP: + state.add(InsnGenState.SKIP); + break; + + case IF: + assert isFallback(); + IfNode ifInsn = (IfNode) insn; + String cond = arg(insn.getArg(0)) + " " + ifInsn.getOp().getSymbol() + " " + + (ifInsn.isZeroCmp() ? "0" : arg(insn.getArg(1))); + code.add("if (").add(cond).add(") goto ").add(MethodGen.getLabelName(ifInsn.getTarget())); + break; + + case GOTO: + assert isFallback(); + code.add("goto ").add(MethodGen.getLabelName(((GotoNode) insn).getTarget())); + break; + + case SWITCH: + assert isFallback(); + SwitchNode sw = (SwitchNode) insn; + code.add("switch(").add(arg(insn, 0)).add(") {"); + code.incIndent(); + for (int i = 0; i < sw.getCasesCount(); i++) { + code.startLine("case " + sw.getKeys()[i] + + ": goto " + MethodGen.getLabelName(sw.getTargets()[i]) + ";"); + } + code.startLine("default: goto " + MethodGen.getLabelName(sw.getDefaultCaseOffset()) + ";"); + code.decIndent(); + code.startLine("}"); + state.add(InsnGenState.NO_SEMICOLON); + break; + + case NEW_INSTANCE: + // only fallback - make new instance in constructor invoke + assert isFallback(); + code.add("new " + insn.getResult().getType()); + break; + + default: + throw new CodegenException(mth, "Unknown instruction: " + insn.getType()); + } + } + + private void filledNewArray(InsnNode insn, CodeWriter code) throws CodegenException { + int c = insn.getArgsCount(); + code.add("new ").add(useType(insn.getResult().getType())); + code.add("{"); + for (int i = 0; i < c; i++) { + code.add(arg(insn, i)); + if (i + 1 < c) + code.add(", "); + } + code.add("}"); + } + + private void fillArray(FillArrayOp insn, CodeWriter code) throws CodegenException { + ArgType elType = insn.getResult().getType().getArrayElement(); + if (elType.getPrimitiveType() == null) { + elType = elType.selectFirst(); + } + StringBuilder str = new StringBuilder(); + switch (elType.getPrimitiveType()) { + case BOOLEAN: + case BYTE: + byte[] array = (byte[]) insn.getData(); + for (byte b : array) { + str.append(TypeGen.literalToString(b, elType)); + str.append(", "); + } + break; + case SHORT: + case CHAR: + short[] sarray = (short[]) insn.getData(); + for (short b : sarray) { + str.append(TypeGen.literalToString(b, elType)); + str.append(", "); + } + break; + case INT: + case FLOAT: + int[] iarray = (int[]) insn.getData(); + for (int b : iarray) { + str.append(TypeGen.literalToString(b, elType)); + str.append(", "); + } + break; + case LONG: + case DOUBLE: + long[] larray = (long[]) insn.getData(); + for (long b : larray) { + str.append(TypeGen.literalToString(b, elType)); + str.append(", "); + } + break; + + default: + throw new CodegenException(mth, "Unknown type: " + elType); + } + int len = str.length(); + str.delete(len - 2, len); + code.add("new ").add(translate(elType)).add("[] { ").add(str.toString()).add(" }"); + } + + private void makeConstructor(ConstructorInsn insn, CodeWriter code, EnumSet state) + throws CodegenException { + ClassNode cls = root.resolveClass(insn.getClassType()); + if (cls != null && cls.isAnonymous()) { + // anonymous class construction + ClassInfo parent; + if (cls.getSuperClass() != null + && !cls.getSuperClass().getFullName().equals("java.lang.Object")) + parent = cls.getSuperClass(); + else + parent = cls.getInterfaces().get(0); + + code.add("new ").add(useClass(parent)).add("()"); + code.incIndent(2); + new ClassGen(cls, mgen.getClassGen().getParentGen(), fallback).makeClassBody(code); + code.decIndent(2); + } else if (insn.isSuper()) { + code.add("super"); + addArgs(code, insn, 0); + } else if (insn.isThis()) { + code.add("this"); + addArgs(code, insn, 0); + } else if (insn.isSelf()) { + // skip + state.add(InsnGenState.SKIP); + } else { + code.add("new ").add(useClass(insn.getClassType())); + addArgs(code, insn, 0); + } + } + + private void makeInvoke(InvokeNode insn, CodeWriter code) throws CodegenException { + MethodInfo callMth = insn.getCallMth(); + + int k = 0; + InvokeType type = insn.getInvokeType(); + switch (type) { + case DIRECT: + case VIRTUAL: + case INTERFACE: + code.add(arg(insn.getArg(0))).add('.'); + k++; + break; + + case SUPER: + // use 'super' instead 'this' in 0 arg + code.add("super").add('.'); + k++; + break; + + case STATIC: + ClassInfo insnCls = mth.getParentClass().getClassInfo(); + if (!insnCls.equals(callMth.getDeclClass())) + code.add(useClass(callMth.getDeclClass())).add('.'); + break; + } + code.add(callMth.getName()); + addArgs(code, insn, k); + } + + private void addArgs(CodeWriter code, InsnNode insn, int k) throws CodegenException { + code.add('('); + for (int i = k; i < insn.getArgsCount(); i++) { + code.add(arg(insn.getArg(i))); + if (i < insn.getArgsCount() - 1) + code.add(", "); + } + code.add(")"); + } + + private void makeArith(ArithNode insn, CodeWriter code, EnumSet state) throws CodegenException { + ArithOp op = insn.getOp(); + String v1 = arg(insn.getArg(0)); + + if (op == ArithOp.INC || op == ArithOp.DEC) { + code.add(v1 + op.getSymbol()); + } else { + String res = arg(insn.getResult()); + String v2 = arg(insn.getArg(1)); + if (res.equals(v1) && !state.contains(InsnGenState.BODY_ONLY)) { + code.add(assignVar(insn) + " " + op.getSymbol() + "= " + v2); + state.add(InsnGenState.NO_RESULT); + } else { + if (state.contains(InsnGenState.BODY_ONLY)) + // wrap insn in brackets for save correct operation order + // TODO don't wrap first insn in wrapped stack + code.add("(" + v1 + " " + op.getSymbol() + " " + v2 + ")"); + else + code.add(v1 + " " + op.getSymbol() + " " + v2); + } + } + } + +} diff --git a/src/main/java/jadx/codegen/MethodGen.java b/src/main/java/jadx/codegen/MethodGen.java new file mode 100644 index 000000000..bdd0d66a0 --- /dev/null +++ b/src/main/java/jadx/codegen/MethodGen.java @@ -0,0 +1,305 @@ +package jadx.codegen; + +import jadx.Consts; +import jadx.dex.attributes.AttributeFlag; +import jadx.dex.attributes.AttributeType; +import jadx.dex.attributes.AttributesList; +import jadx.dex.attributes.JadxErrorAttr; +import jadx.dex.attributes.annotations.MethodParameters; +import jadx.dex.info.AccessInfo; +import jadx.dex.instructions.args.ArgType; +import jadx.dex.instructions.args.RegisterArg; +import jadx.dex.nodes.InsnNode; +import jadx.dex.nodes.MethodNode; +import jadx.dex.trycatch.CatchAttr; +import jadx.dex.visitors.DepthTraverser; +import jadx.dex.visitors.FallbackModeVisitor; +import jadx.utils.ErrorsCounter; +import jadx.utils.InsnUtils; +import jadx.utils.Utils; +import jadx.utils.exceptions.CodegenException; +import jadx.utils.exceptions.DecodeException; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.android.dx.rop.code.AccessFlags; + +public class MethodGen { + private final static Logger LOG = LoggerFactory.getLogger(MethodGen.class); + + private final MethodNode mth; + private final Set mthArgsDecls; + private final Map varDecls = new HashMap(); + private final ClassGen classGen; + private final boolean fallback; + private final AnnotationGen annotationGen; + + public MethodGen(ClassGen classGen, MethodNode mth) { + this.mth = mth; + this.classGen = classGen; + this.fallback = classGen.isFallbackMode(); + this.annotationGen = classGen.getAnnotationGen(); + + List args = mth.getArguments(true); + mthArgsDecls = new HashSet(args.size()); + for (RegisterArg arg : args) { + mthArgsDecls.add(makeArgName(arg)); + } + } + + public ClassGen getClassGen() { + return classGen; + } + + public void addDefinition(CodeWriter code) { + if (mth.getMethodInfo().isClassInit()) { + code.startLine("static"); + } else { + if (mth.getAttributes().contains(AttributeFlag.INCONSISTENT_CODE)) { + code.startLine("// FIXME: Jadx generate inconsistent code"); + // ErrorsCounter.methodError(mth, "Inconsistent code"); + } + + annotationGen.addForMethod(code, mth); + + AccessInfo ai = mth.getAccessFlags(); + // don't add 'abstract' to methods in interface + if (mth.getParentClass().getAccessFlags().isInterface()) { + ai = ai.remove(AccessFlags.ACC_ABSTRACT); + } + + code.startLine(ai.makeString()); + if (mth.getAccessFlags().isConstructor()) { + code.add(classGen.getClassNode().getShortName()); // constructor + } else { + code.add(TypeGen.translate(classGen, mth.getMethodInfo().getReturnType())); + code.add(" "); + code.add(mth.getName()); + } + code.add("("); + + mth.resetArgsTypes(); + List args = mth.getArguments(false); + if (mth.getMethodInfo().isConstructor() + && mth.getParentClass().getAttributes().contains(AttributeType.ENUM_CLASS)) { + if (args.size() == 2) + args.clear(); + else if (args.size() > 2) + args = args.subList(2, args.size()); + else + LOG.warn(ErrorsCounter.formatErrorMsg(mth, + "Incorrect number of args for enum constructor: " + args.size() + + " (expected >= 2)")); + } + code.add(makeArguments(args)); + code.add(")"); + + annotationGen.addThrows(mth, code); + } + } + + public CodeWriter makeArguments(List args) { + CodeWriter argsCode = new CodeWriter(); + + MethodParameters paramsAnnotation = + (MethodParameters) mth.getAttributes().get(AttributeType.ANNOTATION_MTH_PARAMETERS); + + int i = 0; + for (Iterator it = args.iterator(); it.hasNext();) { + RegisterArg arg = it.next(); + + // add argument annotation + if (paramsAnnotation != null) + annotationGen.addForParameter(argsCode, paramsAnnotation, i); + + if (!it.hasNext() && mth.getAccessFlags().isVarArgs()) { + // change last array argument to varargs + ArgType type = arg.getType(); + if (type.isArray()) { + ArgType elType = type.getArrayElement(); + argsCode.add(TypeGen.translate(classGen, elType)); + argsCode.add(" ..."); + } else { + LOG.warn(ErrorsCounter.formatErrorMsg(mth, "Last argument in varargs method not array")); + argsCode.add(TypeGen.translate(classGen, arg.getType())); + } + } else { + argsCode.add(TypeGen.translate(classGen, arg.getType())); + } + argsCode.add(" "); + argsCode.add(makeArgName(arg)); + + i++; + if (it.hasNext()) + argsCode.add(", "); + } + return argsCode; + } + + /** + * Make variable name for register, + * Name contains register number and + * variable type or name (if debug info available) + */ + public String makeArgName(RegisterArg arg) { + String name = arg.getTypedVar().getName(); + String base = "r" + arg.getRegNum(); + if (fallback) { + if (name != null) + return base + "_" + name; + else + return base; + } else { + if (name != null) { + if (name.equals("this")) + return name; + else if (Consts.DEBUG) + return name + "_" + base; + else + return name; + } else { + return base + "_" + Utils.escape(TypeGen.translate(classGen, arg.getType())); + } + } + } + + /** + * Put variable declaration and return variable name (used for assignments) + * + * @param arg + * register variable + * @return variable name + */ + public String assignArg(RegisterArg arg) { + String name = makeArgName(arg); + if (!mthArgsDecls.contains(name)) + varDecls.put(name, arg.getType()); + return name; + } + + private void makeInitCode(CodeWriter code) throws CodegenException { + InsnGen igen = new InsnGen(this, mth, fallback); + // generate super call + if (mth.getSuperCall() != null) + igen.makeInsn(mth.getSuperCall(), code); + } + + public void makeVariablesDeclaration(CodeWriter code) { + for (Entry var : varDecls.entrySet()) { + code.startLine(TypeGen.translate(classGen, var.getValue())); + code.add(" "); + code.add(var.getKey()); + code.add(";"); + } + if (!varDecls.isEmpty()) + code.endl(); + } + + public CodeWriter makeInstructions(int mthIndent) throws CodegenException { + CodeWriter code = new CodeWriter(mthIndent + 1); + + if (mth.getAttributes().contains(AttributeType.JADX_ERROR)) { + code.startLine("throw new UnsupportedOperationException(\"Method not decompiled: "); + code.add(mth.toString()); + code.add("\");"); + + JadxErrorAttr err = (JadxErrorAttr) mth.getAttributes().get(AttributeType.JADX_ERROR); + code.startLine("// FIXME: Jadx error processing method"); + Throwable cause = err.getCause(); + if (cause != null) { + code.endl().add("/*"); + code.startLine("Message: ").add(cause.getMessage()); + code.startLine("Error: ").add(Utils.getStackTrace(cause)); + code.add("*/"); + } + + // load original instructions + try { + mth.load(); + DepthTraverser.visit(new FallbackModeVisitor(), mth); + } catch (DecodeException e) { + // ignore + return code; + } + + code.startLine("/*"); + makeFullMethodDump(code, mth); + code.startLine("*/"); + } else { + if (mth.getRegion() != null) { + CodeWriter insns = new CodeWriter(mthIndent + 1); + (new RegionGen(this, mth)).makeRegion(insns, mth.getRegion()); + + makeInitCode(code); + makeVariablesDeclaration(code); + code.add(insns); + } else { + makeFallbackMethod(code, mth); + } + } + return code; + } + + private void makeFullMethodDump(CodeWriter code, MethodNode mth) { + getFallbackMethodGen(mth).addDefinition(code); + code.add(" {"); + code.incIndent(); + + makeFallbackMethod(code, mth); + + code.decIndent(); + code.startLine("}"); + } + + private void makeFallbackMethod(CodeWriter code, MethodNode mth) { + if (!mth.getAccessFlags().isStatic()) { + code.startLine(getFallbackMethodGen(mth).makeArgName(mth.getThisArg())).add(" = this;"); + } + makeFallbackInsns(code, mth, mth.getInstructions(), true); + } + + public static void makeFallbackInsns(CodeWriter code, MethodNode mth, List insns, boolean addLabels) { + InsnGen insnGen = new InsnGen(getFallbackMethodGen(mth), mth, true); + for (InsnNode insn : insns) { + AttributesList attrs = insn.getAttributes(); + if (addLabels) { + if (attrs.contains(AttributeType.JUMP) + || attrs.contains(AttributeType.EXC_HANDLER)) { + code.decIndent(); + code.startLine(getLabelName(insn.getOffset()) + ":"); + code.incIndent(); + } + } + try { + insnGen.makeInsn(insn, code); + } catch (CodegenException e) { + code.startLine("// error: " + insn); + } + CatchAttr _catch = (CatchAttr) attrs.get(AttributeType.CATCH_BLOCK); + if (_catch != null) + code.add("\t // " + _catch); + } + } + + /** + * Return fallback variant of method codegen + */ + private static MethodGen getFallbackMethodGen(MethodNode mth) { + ClassGen clsGen = new ClassGen(mth.getParentClass(), null, true); + return new MethodGen(clsGen, mth); + } + + public static String getLabelName(int offset) { + return "L_" + InsnUtils.formatOffset(offset); + } + +} diff --git a/src/main/java/jadx/codegen/RegionGen.java b/src/main/java/jadx/codegen/RegionGen.java new file mode 100644 index 000000000..5cbdd4b18 --- /dev/null +++ b/src/main/java/jadx/codegen/RegionGen.java @@ -0,0 +1,241 @@ +package jadx.codegen; + +import jadx.dex.attributes.AttributeFlag; +import jadx.dex.attributes.AttributeType; +import jadx.dex.attributes.DeclareVariableAttr; +import jadx.dex.attributes.ForceReturnAttr; +import jadx.dex.attributes.IAttribute; +import jadx.dex.instructions.IfNode; +import jadx.dex.instructions.IfOp; +import jadx.dex.instructions.SwitchNode; +import jadx.dex.instructions.args.ArgType; +import jadx.dex.instructions.args.InsnArg; +import jadx.dex.instructions.args.PrimitiveType; +import jadx.dex.instructions.args.RegisterArg; +import jadx.dex.nodes.IBlock; +import jadx.dex.nodes.IContainer; +import jadx.dex.nodes.IRegion; +import jadx.dex.nodes.InsnNode; +import jadx.dex.nodes.MethodNode; +import jadx.dex.regions.IfRegion; +import jadx.dex.regions.LoopRegion; +import jadx.dex.regions.Region; +import jadx.dex.regions.SwitchRegion; +import jadx.dex.regions.SynchronizedRegion; +import jadx.dex.trycatch.CatchAttr; +import jadx.dex.trycatch.ExceptionHandler; +import jadx.dex.trycatch.TryCatchBlock; +import jadx.utils.ErrorsCounter; +import jadx.utils.RegionUtils; +import jadx.utils.exceptions.CodegenException; + +import java.util.List; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class RegionGen extends InsnGen { + private final static Logger LOG = LoggerFactory.getLogger(RegionGen.class); + + public RegionGen(MethodGen mgen, MethodNode mth) { + super(mgen, mth, false); + } + + public void makeRegion(CodeWriter code, IContainer cont) throws CodegenException { + assert cont != null; + + if (cont instanceof IBlock) { + makeSimpleBlock((IBlock) cont, code); + } else if (cont instanceof IRegion) { + declareVars(code, cont); + if (cont instanceof Region) { + Region r = (Region) cont; + CatchAttr tc = (CatchAttr) r.getAttributes().get(AttributeType.CATCH_BLOCK); + if (tc != null) { + makeTryCatch(cont, tc.getTryBlock(), code); + } else { + for (IContainer c : r.getSubBlocks()) + makeRegion(code, c); + } + } else if (cont instanceof IfRegion) { + makeIf((IfRegion) cont, code); + } else if (cont instanceof SwitchRegion) { + makeSwitch((SwitchRegion) cont, code); + } else if (cont instanceof LoopRegion) { + makeLoop((LoopRegion) cont, code); + } else if (cont instanceof SynchronizedRegion) { + makeSynchronizedRegion((SynchronizedRegion) cont, code); + } + } else { + throw new CodegenException("Not processed container: " + cont.toString()); + } + } + + private void declareVars(CodeWriter code, IContainer cont) throws CodegenException { + DeclareVariableAttr declVars = + (DeclareVariableAttr) cont.getAttributes().get(AttributeType.DECLARE_VARIABLE); + if (declVars != null) { + for (RegisterArg v : declVars.getVars()) { + code.startLine(declareVar(v)).add(';'); + } + } + } + + public void makeRegionIndent(CodeWriter code, IContainer region) throws CodegenException { + code.incIndent(); + makeRegion(code, region); + code.decIndent(); + } + + private void makeSimpleBlock(IBlock block, CodeWriter code) throws CodegenException { + for (InsnNode insn : block.getInstructions()) { + makeInsn(insn, code); + } + if (block.getAttributes().contains(AttributeFlag.BREAK)) { + code.startLine("break;"); + } else { + IAttribute attr; + if ((attr = block.getAttributes().get(AttributeType.FORCE_RETURN)) != null) { + ForceReturnAttr retAttr = (ForceReturnAttr) attr; + makeInsn(retAttr.getReturnInsn(), code); + } + } + } + + private void makeIf(IfRegion region, CodeWriter code) throws CodegenException { + IfNode insn = region.getIfInsn(); + code.startLine("if ").add(makeCondition(insn)).add(" {"); + makeRegionIndent(code, region.getThenRegion()); + code.startLine("}"); + + IContainer els = region.getElseRegion(); + if (els != null && RegionUtils.notEmpty(els)) { + code.add(" else {"); + code.incIndent(); + makeRegion(code, els); + code.decIndent(); + code.startLine("}"); + } + } + + private CodeWriter makeLoop(LoopRegion region, CodeWriter code) throws CodegenException { + if (region.getConditionBlock() == null) { + // infinite loop + code.startLine("while (true) {"); + makeRegionIndent(code, region.getBody()); + code.startLine("}"); + return code; + } + + IfNode insn = region.getIfInsn(); + if (!region.isConditionAtEnd()) { + code.startLine("while ").add(makeCondition(insn)).add(" {"); + makeRegionIndent(code, region.getBody()); + code.startLine("}"); + } else { + code.startLine("do {"); + makeRegionIndent(code, region.getBody()); + code.startLine("} while ").add(makeCondition(insn)).add(";"); + } + return code; + } + + private void makeSynchronizedRegion(SynchronizedRegion cont, CodeWriter code) throws CodegenException { + code.startLine("synchronized(").add(arg(cont.getArg())).add(") {"); + makeRegionIndent(code, cont.getRegion()); + code.startLine("}"); + } + + private String makeCondition(IfNode insn) throws CodegenException { + String second; + IfOp op = insn.getOp(); + if (insn.isZeroCmp()) { + ArgType type = insn.getArg(0).getType(); + if (type.getPrimitiveType() == PrimitiveType.BOOLEAN) { + if (op == IfOp.EQ) { + // == false + return "(!" + arg(insn.getArg(0)) + ")"; + } else if (op == IfOp.NE) { + // == true + return "(" + arg(insn.getArg(0)) + ")"; + } + LOG.warn(ErrorsCounter.formatErrorMsg(mth, "Unsupported boolean condition " + op.getSymbol())); + } + second = arg(InsnArg.lit(0, type)); + } else { + second = arg(insn.getArg(1)); + } + return "(" + arg(insn.getArg(0)) + " " + + op.getSymbol() + " " + + second + ")"; + } + + private CodeWriter makeSwitch(SwitchRegion sw, CodeWriter code) throws CodegenException { + SwitchNode insn = (SwitchNode) sw.getHeader().getInstructions().get(0); + InsnArg arg = insn.getArg(0); + code.startLine("switch(").add(arg(arg)).add(") {"); + code.incIndent(); + + int size = sw.getKeys().size(); + for (int i = 0; i < size; i++) { + List keys = sw.getKeys().get(i); + IContainer c = sw.getCases().get(i); + for (Integer k : keys) { + code.startLine("case ") + .add(TypeGen.literalToString(k, arg.getType())) + .add(":"); + } + makeRegionIndent(code, c); + if (RegionUtils.hasExitEdge(c)) + code.startLine(1, "break;"); + } + if (sw.getDefaultCase() != null) { + code.startLine("default:"); + makeRegionIndent(code, sw.getDefaultCase()); + if (RegionUtils.hasExitEdge(sw.getDefaultCase())) + code.startLine(1, "break;"); + } + code.decIndent(); + code.startLine("}"); + return code; + } + + private void makeTryCatch(IContainer region, TryCatchBlock tryCatchBlock, CodeWriter code) + throws CodegenException { + code.startLine("try {"); + region.getAttributes().remove(AttributeType.CATCH_BLOCK); + makeRegionIndent(code, region); + ExceptionHandler allHandler = null; + for (ExceptionHandler handler : tryCatchBlock.getHandlers()) { + if (!handler.isCatchAll()) { + makeCatchBlock(code, handler); + } else { + if (allHandler != null) + LOG.warn("Several 'all' handlers in try/catch block in " + mth); + allHandler = handler; + } + } + if (allHandler != null) { + makeCatchBlock(code, allHandler); + } + if (tryCatchBlock.getFinalBlock() != null) { + code.startLine("} finally {"); + makeRegionIndent(code, tryCatchBlock.getFinalBlock()); + } + code.startLine("}"); + } + + private void makeCatchBlock(CodeWriter code, ExceptionHandler handler) + throws CodegenException { + IContainer region = handler.getHandlerRegion(); + if (region != null /* && RegionUtils.notEmpty(region) */) { + code.startLine("} catch ("); + code.add(handler.isCatchAll() ? "Throwable" : useClass(handler.getCatchType())); + code.add(' '); + code.add(arg(handler.getArg())); + code.add(") {"); + makeRegionIndent(code, region); + } + } + +} diff --git a/src/main/java/jadx/codegen/TypeGen.java b/src/main/java/jadx/codegen/TypeGen.java new file mode 100644 index 000000000..b427bb346 --- /dev/null +++ b/src/main/java/jadx/codegen/TypeGen.java @@ -0,0 +1,128 @@ +package jadx.codegen; + +import jadx.dex.instructions.args.ArgType; +import jadx.dex.instructions.args.PrimitiveType; +import jadx.utils.StringUtils; +import jadx.utils.Utils; +import jadx.utils.exceptions.JadxRuntimeException; + +public class TypeGen { + + public static String translate(ClassGen clsGen, ArgType type) { + final PrimitiveType stype = type.getPrimitiveType(); + if (stype == null) + return type.toString(); + + if (stype == PrimitiveType.OBJECT) { + return clsGen.useClass(type); + } + if (stype == PrimitiveType.ARRAY) { + return translate(clsGen, type.getArrayElement()) + "[]"; + } + return stype.getLongName(); + } + + public static String shortString(ArgType type) { + final PrimitiveType stype = type.getPrimitiveType(); + if (stype == null) + return type.toString(); + + if (stype == PrimitiveType.OBJECT) { + return "L"; + } + if (stype == PrimitiveType.ARRAY) { + return shortString(type.getArrayElement()) + "A"; + } + return stype.getLongName(); + } + + public static String signature(ArgType type) { + final PrimitiveType stype = type.getPrimitiveType(); + if (stype == PrimitiveType.OBJECT) { + return Utils.makeQualifiedObjectName(type.getObject()); + } + if (stype == PrimitiveType.ARRAY) { + return '[' + signature(type.getArrayElement()); + } + return stype.getShortName(); + } + + /** + * Convert literal value to string according to value type + * + * @throws JadxRuntimeException + * for incorrect type or literal value + */ + public static String literalToString(long lit, ArgType type) { + if (type == null || !type.isTypeKnown()) { + String n = Long.toString(lit); + if (Math.abs(lit) > 100) + n += "; // 0x" + Long.toHexString(lit) + + " float:" + Float.intBitsToFloat((int) lit) + + " double:" + Double.longBitsToDouble(lit); + return n; + } + + switch (type.getPrimitiveType()) { + case BOOLEAN: + return lit == 0 ? "false" : "true"; + case CHAR: + return StringUtils.unescapeChar((char) lit); + case BYTE: + return formatByte((byte) lit); + case SHORT: + return formatShort((short) lit); + case INT: + return formatInteger((int) lit); + case LONG: + return formatLong(lit); + case FLOAT: + return formatFloat(Float.intBitsToFloat((int) lit)); + case DOUBLE: + return formatDouble(Double.longBitsToDouble(lit)); + + case OBJECT: + case ARRAY: + if (lit != 0) + throw new JadxRuntimeException("Wrong object literal: " + type + " = " + lit); + return "null"; + + default: + throw new JadxRuntimeException("Unknown type in literalToString: " + type); + } + } + + public static String formatShort(short s) { + return "(short) " + wrapNegNum(s < 0, Short.toString(s)); + } + + public static String formatByte(byte b) { + return "(byte) " + wrapNegNum(b < 0, Byte.toString(b)); + } + + public static String formatInteger(int i) { + return wrapNegNum(i < 0, Integer.toString(i)); + } + + public static String formatDouble(double d) { + return wrapNegNum(d < 0, Double.toString(d) + "d"); + } + + public static String formatFloat(float f) { + return wrapNegNum(f < 0, Float.toString(f) + "f"); + } + + public static String formatLong(long lit) { + String l = Long.toString(lit); + if (lit == Long.MIN_VALUE || Math.abs(lit) >= Integer.MAX_VALUE) + l += "L"; + return wrapNegNum(lit < 0, l); + } + + private static String wrapNegNum(boolean lz, String str) { + if (lz) + return "(" + str + ")"; + else + return str; + } +} diff --git a/src/main/java/jadx/deobf/NameMapper.java b/src/main/java/jadx/deobf/NameMapper.java new file mode 100644 index 000000000..365dd7074 --- /dev/null +++ b/src/main/java/jadx/deobf/NameMapper.java @@ -0,0 +1,70 @@ +package jadx.deobf; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; + +public class NameMapper { + + private static final Set reservedNames = new HashSet( + Arrays.asList(new String[] { + "abstract", + "assert", + "boolean", + "break", + "byte", + "case", + "catch", + "char", + "class", + "const", + "continue", + "default", + "do", + "double ", + "else", + "enum", + "extends", + "false", + "final", + "finally", + "float", + "for", + "goto", + "if", + "implements", + "import", + "instanceof", + "int", + "interface", + "long", + "native", + "new", + "null", + "package", + "private", + "protected", + "public", + "return", + "short", + "static", + "strictfp", + "super", + "switch", + "synchronized", + "this", + "throw", + "throws", + "transient", + "true", + "try", + "void", + "volatile", + "while", + })); + + public static boolean isReserved(String str) { + return reservedNames.contains(str); + } + +} diff --git a/src/main/java/jadx/dex/attributes/AttrNode.java b/src/main/java/jadx/dex/attributes/AttrNode.java new file mode 100644 index 000000000..27e03600c --- /dev/null +++ b/src/main/java/jadx/dex/attributes/AttrNode.java @@ -0,0 +1,14 @@ +package jadx.dex.attributes; + +public abstract class AttrNode implements IAttributeNode { + + private AttributesList attributesList; + + @Override + public AttributesList getAttributes() { + if (attributesList == null) + attributesList = new AttributesList(); + return attributesList; + } + +} diff --git a/src/main/java/jadx/dex/attributes/AttributeFlag.java b/src/main/java/jadx/dex/attributes/AttributeFlag.java new file mode 100644 index 000000000..a41a6f67c --- /dev/null +++ b/src/main/java/jadx/dex/attributes/AttributeFlag.java @@ -0,0 +1,19 @@ +package jadx.dex.attributes; + +public enum AttributeFlag { + TRY_ENTER, + TRY_LEAVE, + + LOOP_START, + LOOP_END, + + SYNTHETIC, + + BREAK, + RETURN, // block contains only return instruction + + DONT_SHRINK, + DONT_GENERATE, + + INCONSISTENT_CODE, // warning about incorrect decompilation +} diff --git a/src/main/java/jadx/dex/attributes/AttributeType.java b/src/main/java/jadx/dex/attributes/AttributeType.java new file mode 100644 index 000000000..815aa4f8a --- /dev/null +++ b/src/main/java/jadx/dex/attributes/AttributeType.java @@ -0,0 +1,47 @@ +package jadx.dex.attributes; + +public enum AttributeType { + // TODO? add attribute target (insn, block, method, field, class) + + // instructions + JUMP(false), + + // blocks + LOOP(false), + + CATCH_BLOCK(false), + EXC_HANDLER(true), + + SPLITTER_BLOCK(true), + + FORCE_RETURN(true), + + // fields + FIELD_VALUE(true), + + // methods + JADX_ERROR(true), + + // classes + ENUM_CLASS(true), + + // any + ANNOTATION_LIST(true), + ANNOTATION_MTH_PARAMETERS(true), + + DECLARE_VARIABLE(true); + + private final boolean uniq; + + private AttributeType(boolean isUniq) { + this.uniq = isUniq; + } + + public boolean isUniq() { + return uniq; + } + + public boolean notUniq() { + return !uniq; + } +} diff --git a/src/main/java/jadx/dex/attributes/AttributesList.java b/src/main/java/jadx/dex/attributes/AttributesList.java new file mode 100644 index 000000000..dcb6cdcfc --- /dev/null +++ b/src/main/java/jadx/dex/attributes/AttributesList.java @@ -0,0 +1,153 @@ +package jadx.dex.attributes; + +import jadx.utils.Utils; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.EnumMap; +import java.util.EnumSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; + +public class AttributesList { + + private final Set flags; + private final Map uniqAttr; + private final List attributes; + private final int[] attrCount; + + public AttributesList() { + flags = EnumSet.noneOf(AttributeFlag.class); + uniqAttr = new EnumMap(AttributeType.class); + attributes = new ArrayList(1); + attrCount = new int[AttributeType.values().length]; + } + + public void add(IAttribute attr) { + if (attr.getType().isUniq()) + uniqAttr.put(attr.getType(), attr); + else + addMultiAttribute(attr); + } + + public void add(AttributeFlag flag) { + flags.add(flag); + } + + public boolean contains(AttributeFlag flag) { + return flags.contains(flag); + } + + public void remove(AttributeFlag flag) { + flags.remove(flag); + } + + private void addMultiAttribute(IAttribute attr) { + attributes.add(attr); + attrCount[attr.getType().ordinal()]++; + } + + private int getCountInternal(AttributeType type) { + return attrCount[type.ordinal()]; + } + + public void addAll(AttributesList otherList) { + flags.addAll(otherList.flags); + uniqAttr.putAll(otherList.uniqAttr); + for (IAttribute attr : otherList.attributes) + addMultiAttribute(attr); + } + + public boolean contains(AttributeType type) { + if (type.isUniq()) + return uniqAttr.containsKey(type); + else + return getCountInternal(type) != 0; + } + + public IAttribute get(AttributeType type) { + if (type.isUniq()) { + return uniqAttr.get(type); + } else { + int count = getCountInternal(type); + if (count != 0) { + for (IAttribute attr : attributes) + if (attr.getType() == type) + return attr; + } + return null; + } + } + + public int getCount(AttributeType type) { + if (type.isUniq()) { + return 0; + } else { + return getCountInternal(type); + } + } + + public List getAll(AttributeType type) { + assert type.notUniq(); + + int count = getCountInternal(type); + if (count == 0) { + return Collections.emptyList(); + } else { + List attrs = new ArrayList(count); + for (IAttribute attr : attributes) { + if (attr.getType() == type) + attrs.add(attr); + } + return attrs; + } + } + + public void remove(AttributeType type) { + if (type.isUniq()) { + uniqAttr.remove(type); + } else { + for (Iterator it = attributes.iterator(); it.hasNext();) { + IAttribute attr = it.next(); + if (attr.getType() == type) + it.remove(); + } + attrCount[type.ordinal()] = 0; + } + } + + public void clear() { + flags.clear(); + uniqAttr.clear(); + attributes.clear(); + Arrays.fill(attrCount, 0); + } + + public List getAttributeStrings() { + int size = flags.size() + uniqAttr.size() + attributes.size(); + if (size == 0) + return Collections.emptyList(); + + List list = new ArrayList(size); + for (AttributeFlag a : flags) + list.add(a.toString()); + for (IAttribute a : uniqAttr.values()) + list.add(a.toString()); + for (IAttribute a : attributes) + list.add(a.toString()); + return list; + } + + @Override + public String toString() { + List list = getAttributeStrings(); + if (list.isEmpty()) + return ""; + + return "A:{" + Utils.listToString(list) + "}"; + } + +} diff --git a/src/main/java/jadx/dex/attributes/BlockRegState.java b/src/main/java/jadx/dex/attributes/BlockRegState.java new file mode 100644 index 000000000..61c147137 --- /dev/null +++ b/src/main/java/jadx/dex/attributes/BlockRegState.java @@ -0,0 +1,55 @@ +package jadx.dex.attributes; + +import jadx.dex.instructions.args.RegisterArg; +import jadx.dex.instructions.args.TypedVar; +import jadx.dex.nodes.MethodNode; + +public class BlockRegState { + + private final RegisterArg[] regs; + + public BlockRegState(MethodNode mth) { + this.regs = new RegisterArg[mth.getRegsCount()]; + for (int i = 0; i < regs.length; i++) { + regs[i] = new RegisterArg(i); + } + } + + public BlockRegState(BlockRegState state) { + this.regs = new RegisterArg[state.regs.length]; + System.arraycopy(state.regs, 0, regs, 0, state.regs.length); + } + + public void assignReg(RegisterArg arg) { + int rn = arg.getRegNum(); + regs[rn] = new RegisterArg(rn, arg.getType()); + use(arg); + } + + public void use(RegisterArg arg) { + TypedVar regType = regs[arg.getRegNum()].getTypedVar(); + if (regType == null) { + regType = new TypedVar(arg.getType()); + regs[arg.getRegNum()].setTypedVar(regType); + } + arg.replace(regType); + regType.getUseList().add(arg); + } + + public RegisterArg getRegister(int r) { + return regs[r]; + } + + @Override + public String toString() { + StringBuilder str = new StringBuilder(); + for (RegisterArg reg : regs) { + if (reg.getTypedVar() != null) { + if (str.length() != 0) + str.append(", "); + str.append(reg.toString()); + } + } + return str.toString(); + } +} diff --git a/src/main/java/jadx/dex/attributes/DeclareVariableAttr.java b/src/main/java/jadx/dex/attributes/DeclareVariableAttr.java new file mode 100644 index 000000000..0f26fb2cb --- /dev/null +++ b/src/main/java/jadx/dex/attributes/DeclareVariableAttr.java @@ -0,0 +1,42 @@ +package jadx.dex.attributes; + +import jadx.dex.instructions.args.RegisterArg; +import jadx.utils.Utils; + +import java.util.List; + +public class DeclareVariableAttr implements IAttribute { + + private final List vars; + + public DeclareVariableAttr() { + this.vars = null; // for instruction use result + } + + public DeclareVariableAttr(List vars) { + this.vars = vars; // for regions + } + + public List getVars() { + return vars; + } + + public void addVar(RegisterArg arg) { + int i; + if ((i = vars.indexOf(arg)) != -1) { + if (vars.get(i).getType().equals(arg.getType())) + return; + } + vars.add(arg); + } + + @Override + public AttributeType getType() { + return AttributeType.DECLARE_VARIABLE; + } + + @Override + public String toString() { + return "DECL_VAR: " + Utils.listToString(vars); + } +} diff --git a/src/main/java/jadx/dex/attributes/EnumClassAttr.java b/src/main/java/jadx/dex/attributes/EnumClassAttr.java new file mode 100644 index 000000000..b5cf0224d --- /dev/null +++ b/src/main/java/jadx/dex/attributes/EnumClassAttr.java @@ -0,0 +1,78 @@ +package jadx.dex.attributes; + +import jadx.dex.instructions.args.InsnArg; +import jadx.dex.nodes.ClassNode; +import jadx.dex.nodes.MethodNode; +import jadx.utils.Utils; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public class EnumClassAttr implements IAttribute { + + public static class EnumField { + private final String name; + private final List args; + private ClassNode cls; + + public EnumField(String name, int argsCount) { + this.name = name; + if (argsCount != 0) + this.args = new ArrayList(argsCount); + else + this.args = Collections.emptyList(); + } + + public String getName() { + return name; + } + + public List getArgs() { + return args; + } + + public ClassNode getCls() { + return cls; + } + + public void setCls(ClassNode cls) { + this.cls = cls; + } + + @Override + public String toString() { + return name + "(" + Utils.listToString(args) + ") " + cls; + } + } + + private final List fields; + private MethodNode staticMethod; + + public EnumClassAttr(int fieldsCount) { + this.fields = new ArrayList(fieldsCount); + } + + public List getFields() { + return fields; + } + + public MethodNode getStaticMethod() { + return staticMethod; + } + + public void setStaticMethod(MethodNode staticMethod) { + this.staticMethod = staticMethod; + } + + @Override + public AttributeType getType() { + return AttributeType.ENUM_CLASS; + } + + @Override + public String toString() { + return "Enum fields: " + fields; + } + +} diff --git a/src/main/java/jadx/dex/attributes/ForceReturnAttr.java b/src/main/java/jadx/dex/attributes/ForceReturnAttr.java new file mode 100644 index 000000000..516aa3396 --- /dev/null +++ b/src/main/java/jadx/dex/attributes/ForceReturnAttr.java @@ -0,0 +1,28 @@ +package jadx.dex.attributes; + +import jadx.dex.nodes.InsnNode; +import jadx.utils.Utils; + +public class ForceReturnAttr implements IAttribute { + + private final InsnNode returnInsn; + + public ForceReturnAttr(InsnNode retInsn) { + this.returnInsn = retInsn; + } + + public InsnNode getReturnInsn() { + return returnInsn; + } + + @Override + public AttributeType getType() { + return AttributeType.FORCE_RETURN; + } + + @Override + public String toString() { + return "FORCE_RETURN " + Utils.listToString(returnInsn.getArguments()); + } + +} diff --git a/src/main/java/jadx/dex/attributes/IAttribute.java b/src/main/java/jadx/dex/attributes/IAttribute.java new file mode 100644 index 000000000..c70aede31 --- /dev/null +++ b/src/main/java/jadx/dex/attributes/IAttribute.java @@ -0,0 +1,7 @@ +package jadx.dex.attributes; + +public interface IAttribute { + + AttributeType getType(); + +} diff --git a/src/main/java/jadx/dex/attributes/IAttributeNode.java b/src/main/java/jadx/dex/attributes/IAttributeNode.java new file mode 100644 index 000000000..a04f4116b --- /dev/null +++ b/src/main/java/jadx/dex/attributes/IAttributeNode.java @@ -0,0 +1,7 @@ +package jadx.dex.attributes; + +public interface IAttributeNode { + + AttributesList getAttributes(); + +} diff --git a/src/main/java/jadx/dex/attributes/JadxErrorAttr.java b/src/main/java/jadx/dex/attributes/JadxErrorAttr.java new file mode 100644 index 000000000..222aee99e --- /dev/null +++ b/src/main/java/jadx/dex/attributes/JadxErrorAttr.java @@ -0,0 +1,38 @@ +package jadx.dex.attributes; + +import jadx.utils.Utils; + +public class JadxErrorAttr implements IAttribute { + + private final Throwable cause; + + public JadxErrorAttr(Throwable cause) { + this.cause = cause; + } + + public Throwable getCause() { + return cause; + } + + @Override + public AttributeType getType() { + return AttributeType.JADX_ERROR; + } + + @Override + public String toString() { + StringBuilder str = new StringBuilder(); + str.append("JadxError: "); + if (cause == null) { + str.append("null"); + } else { + str.append(cause.getClass().toString()); + str.append(":"); + str.append(cause.getMessage()); + str.append("\n"); + str.append(Utils.getStackTrace(cause)); + } + return str.toString(); + } + +} diff --git a/src/main/java/jadx/dex/attributes/JumpAttribute.java b/src/main/java/jadx/dex/attributes/JumpAttribute.java new file mode 100644 index 000000000..147988bfe --- /dev/null +++ b/src/main/java/jadx/dex/attributes/JumpAttribute.java @@ -0,0 +1,50 @@ +package jadx.dex.attributes; + +import jadx.utils.InsnUtils; + +public class JumpAttribute implements IAttribute { + + private final int src; + private final int dest; + + public JumpAttribute(int src, int dest) { + this.src = src; + this.dest = dest; + } + + @Override + public AttributeType getType() { + return AttributeType.JUMP; + } + + public int getSrc() { + return src; + } + + public int getDest() { + return dest; + } + + @Override + public String toString() { + return "JUMP: " + InsnUtils.formatOffset(src) + " -> " + InsnUtils.formatOffset(dest); + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + dest; + result = prime * result + src; + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (obj == null) return false; + if (getClass() != obj.getClass()) return false; + JumpAttribute other = (JumpAttribute) obj; + return dest == other.dest && src == other.src; + } +} diff --git a/src/main/java/jadx/dex/attributes/LoopAttr.java b/src/main/java/jadx/dex/attributes/LoopAttr.java new file mode 100644 index 000000000..515f92930 --- /dev/null +++ b/src/main/java/jadx/dex/attributes/LoopAttr.java @@ -0,0 +1,59 @@ +package jadx.dex.attributes; + +import jadx.dex.nodes.BlockNode; +import jadx.utils.BlockUtils; + +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + +public class LoopAttr implements IAttribute { + + private final BlockNode start; + private final BlockNode end; + private final Set loopBlocks; + + public LoopAttr(BlockNode start, BlockNode end) { + this.start = start; + this.end = end; + this.loopBlocks = Collections.unmodifiableSet(BlockUtils.getAllPathsBlocks(start, end)); + } + + public BlockNode getStart() { + return start; + } + + public BlockNode getEnd() { + return end; + } + + @Override + public AttributeType getType() { + return AttributeType.LOOP; + } + + public Set getLoopBlocks() { + return loopBlocks; + } + + /** + * Return block nodes with exit edges from loop
+ * Exit nodes belongs to loop (contains in {@code loopBlocks}) + */ + public Set getExitNodes() { + Set nodes = new HashSet(); + Set inloop = getLoopBlocks(); + for (BlockNode block : inloop) { + // exit: successor node not from this loop, (don't change to getCleanSuccessors) + for (BlockNode s : block.getSuccessors()) + if (!inloop.contains(s) && !s.getAttributes().contains(AttributeType.EXC_HANDLER)) + nodes.add(block); + } + return nodes; + } + + @Override + public String toString() { + return "LOOP: " + start + "->" + end; + } +} diff --git a/src/main/java/jadx/dex/attributes/annotations/Annotation.java b/src/main/java/jadx/dex/attributes/annotations/Annotation.java new file mode 100644 index 000000000..69a9ebdfd --- /dev/null +++ b/src/main/java/jadx/dex/attributes/annotations/Annotation.java @@ -0,0 +1,44 @@ +package jadx.dex.attributes.annotations; + +import jadx.dex.instructions.args.ArgType; + +import java.util.Map; + +public class Annotation { + + public static enum Visibility { + BUILD, RUNTIME, SYSTEM + } + + private final Visibility visibility; + private final ArgType atype; + private final Map values; + + public Annotation(Visibility visibility, ArgType type, Map values) { + this.visibility = visibility; + this.atype = type; + this.values = values; + } + + public Visibility getVisibility() { + return visibility; + } + + public ArgType getType() { + return atype; + } + + public String getAnnotationClass() { + return atype.getObject(); + } + + public Map getValues() { + return values; + } + + @Override + public String toString() { + return "Annotation[" + visibility + ", " + atype + ", " + values + "]"; + } + +} diff --git a/src/main/java/jadx/dex/attributes/annotations/AnnotationsList.java b/src/main/java/jadx/dex/attributes/annotations/AnnotationsList.java new file mode 100644 index 000000000..70113be27 --- /dev/null +++ b/src/main/java/jadx/dex/attributes/annotations/AnnotationsList.java @@ -0,0 +1,45 @@ +package jadx.dex.attributes.annotations; + +import jadx.dex.attributes.AttributeType; +import jadx.dex.attributes.IAttribute; +import jadx.utils.Utils; + +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class AnnotationsList implements IAttribute { + + private final Map map; + + public AnnotationsList(List anList) { + map = new HashMap(anList.size()); + for (Annotation a : anList) { + map.put(a.getAnnotationClass(), a); + } + } + + public Annotation get(String className) { + return map.get(className); + } + + public Collection getAll() { + return map.values(); + } + + public int size() { + return map.size(); + } + + @Override + public AttributeType getType() { + return AttributeType.ANNOTATION_LIST; + } + + @Override + public String toString() { + return Utils.listToString(map.values()); + } + +} diff --git a/src/main/java/jadx/dex/attributes/annotations/MethodParameters.java b/src/main/java/jadx/dex/attributes/annotations/MethodParameters.java new file mode 100644 index 000000000..514de8770 --- /dev/null +++ b/src/main/java/jadx/dex/attributes/annotations/MethodParameters.java @@ -0,0 +1,32 @@ +package jadx.dex.attributes.annotations; + +import jadx.dex.attributes.AttributeType; +import jadx.dex.attributes.IAttribute; +import jadx.utils.Utils; + +import java.util.ArrayList; +import java.util.List; + +public class MethodParameters implements IAttribute { + + private final List paramList; + + public MethodParameters(int paramCount) { + paramList = new ArrayList(paramCount); + } + + public List getParamList() { + return paramList; + } + + @Override + public AttributeType getType() { + return AttributeType.ANNOTATION_MTH_PARAMETERS; + } + + @Override + public String toString() { + return Utils.listToString(paramList); + } + +} diff --git a/src/main/java/jadx/dex/info/AccessInfo.java b/src/main/java/jadx/dex/info/AccessInfo.java new file mode 100644 index 000000000..9a71f0558 --- /dev/null +++ b/src/main/java/jadx/dex/info/AccessInfo.java @@ -0,0 +1,156 @@ +package jadx.dex.info; + +import jadx.Consts; + +import com.android.dx.rop.code.AccessFlags; + +public class AccessInfo { + + private final int accFlags; + + public static enum AFType { + CLASS, FIELD, METHOD + } + + private final AFType type; + + public AccessInfo(int accessFlags, AFType type) { + this.accFlags = accessFlags; + this.type = type; + } + + public boolean containsFlag(int flag) { + return (accFlags & flag) != 0; + } + + public AccessInfo remove(int flag) { + if (containsFlag(flag)) + return new AccessInfo(accFlags - flag, type); + else + return this; + } + + public AccessInfo getVisibility() { + int f = (accFlags & AccessFlags.ACC_PUBLIC) + | (accFlags & AccessFlags.ACC_PROTECTED) + | (accFlags & AccessFlags.ACC_PRIVATE); + return new AccessInfo(f, type); + } + + public boolean isAbstract() { + return (accFlags & AccessFlags.ACC_ABSTRACT) != 0; + } + + public boolean isInterface() { + return (accFlags & AccessFlags.ACC_INTERFACE) != 0; + } + + public boolean isAnnotation() { + return (accFlags & AccessFlags.ACC_ANNOTATION) != 0; + } + + public boolean isNative() { + return (accFlags & AccessFlags.ACC_NATIVE) != 0; + } + + public boolean isStatic() { + return (accFlags & AccessFlags.ACC_STATIC) != 0; + } + + public boolean isFinal() { + return (accFlags & AccessFlags.ACC_FINAL) != 0; + } + + public boolean isConstructor() { + return (accFlags & AccessFlags.ACC_CONSTRUCTOR) != 0; + } + + public boolean isEnum() { + return (accFlags & AccessFlags.ACC_ENUM) != 0; + } + + public boolean isSynthetic() { + return (accFlags & AccessFlags.ACC_SYNTHETIC) != 0; + } + + public boolean isBridge() { + return (accFlags & AccessFlags.ACC_BRIDGE) != 0; + } + + public boolean isVarArgs() { + return (accFlags & AccessFlags.ACC_VARARGS) != 0; + } + + public int getFlags() { + return accFlags; + } + + public String makeString() { + StringBuilder code = new StringBuilder(); + if ((accFlags & AccessFlags.ACC_PUBLIC) != 0) + code.append("public "); + + if ((accFlags & AccessFlags.ACC_PRIVATE) != 0) + code.append("private "); + + if ((accFlags & AccessFlags.ACC_PROTECTED) != 0) + code.append("protected "); + + if (isStatic()) + code.append("static "); + + if (isFinal()) + code.append("final "); + + if (isAbstract()) + code.append("abstract "); + + if (isNative()) + code.append("native "); + + switch (type) { + case METHOD: + if ((accFlags & AccessFlags.ACC_SYNCHRONIZED) != 0) + code.append("synchronized "); + + if ((accFlags & AccessFlags.ACC_DECLARED_SYNCHRONIZED) != 0) + code.append("synchronized "); + + if (isBridge()) + code.append("/* bridge */ "); + + if (Consts.DEBUG) { + if (isVarArgs()) + code.append("/* varargs */ "); + } + break; + + case FIELD: + if ((accFlags & AccessFlags.ACC_VOLATILE) != 0) + code.append("volatile "); + + if ((accFlags & AccessFlags.ACC_TRANSIENT) != 0) + code.append("transient "); + break; + + case CLASS: + if ((accFlags & AccessFlags.ACC_STRICT) != 0) + code.append("strict "); + + if (Consts.DEBUG) { + if ((accFlags & AccessFlags.ACC_SUPER) != 0) + code.append("/* super */ "); + + if ((accFlags & AccessFlags.ACC_ENUM) != 0) + code.append("/* enum */ "); + } + break; + } + + if (isSynthetic()) + code.append("/* synthetic */ "); + + return code.toString(); + } + +} diff --git a/src/main/java/jadx/dex/info/ClassInfo.java b/src/main/java/jadx/dex/info/ClassInfo.java new file mode 100644 index 000000000..cb7652e2b --- /dev/null +++ b/src/main/java/jadx/dex/info/ClassInfo.java @@ -0,0 +1,163 @@ +package jadx.dex.info; + +import jadx.deobf.NameMapper; +import jadx.dex.instructions.args.ArgType; +import jadx.dex.nodes.DexNode; + +import java.io.File; +import java.util.HashMap; +import java.util.Map; + +public final class ClassInfo { + + private static final Map CLASSINFO_CACHE = new HashMap(); + private static final String DEFAULT_PACKAGE_NAME = "defpackage"; + + private final String clsName; + private final String clsPackage; + private final ArgType type; + private final String fullName; + + private final ClassInfo parentClass; // not equals null if this is inner class + + public static ClassInfo fromDex(DexNode dex, int clsIndex) { + if (clsIndex == DexNode.NO_INDEX) + return null; + + ArgType type = dex.getType(clsIndex); + if (type.isArray()) + type = ArgType.OBJECT; + + return fromType(dex, type); + } + + public static ClassInfo fromName(DexNode dex, String clsName) { + return fromType(dex, ArgType.object(clsName)); + } + + public static ClassInfo fromType(DexNode dex, ArgType type) { + ClassInfo cls = CLASSINFO_CACHE.get(type); + if (cls == null) { + cls = new ClassInfo(dex, type); + CLASSINFO_CACHE.put(type, cls); + } + return cls; + } + + public static void clearCache() { + CLASSINFO_CACHE.clear(); + } + + private ClassInfo(DexNode dex, ArgType type) { + this.type = type; + + String fullObjectName = type.getObject(); + String name; + String pkg; + + assert fullObjectName.indexOf('/') == -1; + + int dot = fullObjectName.lastIndexOf('.'); + if (dot == -1) { + // rename default package if it used from class with package (often for obfuscated apps), + // TODO? if default package really needed + pkg = DEFAULT_PACKAGE_NAME; + name = fullObjectName; + } else { + pkg = fullObjectName.substring(0, dot); + name = fullObjectName.substring(dot + 1); + } + + int sep = name.lastIndexOf('$'); + if (sep > 0) { + String parClsName = pkg + '.' + name.substring(0, sep); + if (dex.root().getJadxArgs().isNotObfuscated() + || dex.root().isClassExists(parClsName)) { + parentClass = fromName(dex, parClsName); + name = name.substring(sep + 1); + } else { + // TODO for more accuracy we need full classpath class listing + // for now instead make more checks + if (sep != name.length() - 1) { + parentClass = fromName(dex, parClsName); + name = name.substring(sep + 1); + } else + parentClass = null; + } + } else { + parentClass = null; + } + + if (Character.isDigit(name.charAt(0))) + name = "InnerClass_" + name; + + // TODO rename classes with reserved names + if (NameMapper.isReserved(name)) + name += "_"; + + if (parentClass != null) + fullName = parentClass.getFullName() + '.' + name; + else + fullName = pkg + '.' + name; + + this.clsName = name; + this.clsPackage = pkg; + } + + public String getFullPath() { + return clsPackage.replace('.', File.separatorChar) + File.separatorChar + + getNameWithoutPackage().replace('.', '_'); + } + + public String getFullName() { + return fullName; + } + + public String getShortName() { + return clsName; + } + + public String getPackage() { + return clsPackage; + } + + public boolean isPackageDefault() { + return clsPackage.equals(DEFAULT_PACKAGE_NAME); + } + + public String getNameWithoutPackage() { + return (parentClass != null ? parentClass.getNameWithoutPackage() + "." : "") + clsName; + } + + public ClassInfo getParentClass() { + return parentClass; + } + + public boolean isInner() { + return parentClass != null; + } + + public ArgType getType() { + return type; + } + + @Override + public String toString() { + return getFullName(); + } + + @Override + public int hashCode() { + return this.getFullName().hashCode(); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (obj instanceof ClassInfo) { + ClassInfo cls = (ClassInfo) obj; + return this.getFullName().equals(cls.getFullName()); + } + return false; + } +} diff --git a/src/main/java/jadx/dex/info/FieldInfo.java b/src/main/java/jadx/dex/info/FieldInfo.java new file mode 100644 index 000000000..6164de28b --- /dev/null +++ b/src/main/java/jadx/dex/info/FieldInfo.java @@ -0,0 +1,47 @@ +package jadx.dex.info; + +import jadx.dex.attributes.AttrNode; +import jadx.dex.instructions.args.ArgType; +import jadx.dex.nodes.DexNode; + +import com.android.dx.io.FieldId; + +public class FieldInfo extends AttrNode { + + private final String name; + private final ArgType type; + + private final ClassInfo declClass; + + public static FieldInfo fromDex(DexNode dex, int index) { + return new FieldInfo(dex, index); + } + + protected FieldInfo(DexNode dex, int ind) { + FieldId field = dex.getFieldId(ind); + this.name = dex.getString(field.getNameIndex()); + this.type = dex.getType(field.getTypeIndex()); + this.declClass = ClassInfo.fromDex(dex, field.getDeclaringClassIndex()); + } + + public static String getNameById(DexNode dex, int ind) { + return dex.getString(dex.getFieldId(ind).getNameIndex()); + } + + public String getName() { + return name; + } + + public ArgType getType() { + return type; + } + + public ClassInfo getDeclClass() { + return declClass; + } + + @Override + public String toString() { + return declClass + "." + name + " " + type; + } +} diff --git a/src/main/java/jadx/dex/info/LocalVarInfo.java b/src/main/java/jadx/dex/info/LocalVarInfo.java new file mode 100644 index 000000000..b79134bcf --- /dev/null +++ b/src/main/java/jadx/dex/info/LocalVarInfo.java @@ -0,0 +1,49 @@ +package jadx.dex.info; + +import jadx.dex.instructions.args.ArgType; +import jadx.dex.instructions.args.RegisterArg; +import jadx.dex.instructions.args.TypedVar; +import jadx.dex.nodes.DexNode; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class LocalVarInfo extends RegisterArg { + + private final static Logger LOG = LoggerFactory.getLogger(LocalVarInfo.class); + private boolean isEnd; + + public LocalVarInfo(DexNode dex, int rn, int nameId, int typeId, int signId) { + super(rn); + String name = (nameId == DexNode.NO_INDEX ? null : dex.getString(nameId)); + ArgType type = (typeId == DexNode.NO_INDEX ? null : dex.getType(typeId)); + String sign = (signId == DexNode.NO_INDEX ? null : dex.getString(signId)); + + init(name, type, sign); + } + + public LocalVarInfo(DexNode dex, int rn, String name, ArgType type, String sign) { + super(rn); + init(name, type, sign); + } + + private void init(String name, ArgType type, String sign) { + TypedVar tv = new TypedVar(type); + tv.setName(name); + setTypedVar(tv); + + // LOG.trace("local var: {}, sign: {}", tv, sign); + } + + public void start(int addr, int line) { + this.isEnd = false; + } + + public void end(int addr, int line) { + this.isEnd = true; + } + + public boolean isEnd() { + return isEnd; + } +} diff --git a/src/main/java/jadx/dex/info/MethodInfo.java b/src/main/java/jadx/dex/info/MethodInfo.java new file mode 100644 index 000000000..1ff0746df --- /dev/null +++ b/src/main/java/jadx/dex/info/MethodInfo.java @@ -0,0 +1,85 @@ +package jadx.dex.info; + +import jadx.codegen.TypeGen; +import jadx.dex.instructions.args.ArgType; +import jadx.dex.nodes.DexNode; +import jadx.utils.Utils; + +import java.util.List; + +import com.android.dx.io.MethodId; +import com.android.dx.io.ProtoId; + +public final class MethodInfo { + + private final String name; + private final ArgType retType; + private final List args; + private final ClassInfo declClass; + private final String shortId; + + public static MethodInfo fromDex(DexNode dex, int mthIndex) { + return new MethodInfo(dex, mthIndex); + } + + private MethodInfo(DexNode dex, int mthIndex) { + MethodId mthId = dex.getMethodId(mthIndex); + name = dex.getString(mthId.getNameIndex()); + declClass = ClassInfo.fromDex(dex, mthId.getDeclaringClassIndex()); + + ProtoId proto = dex.getProtoId(mthId.getProtoIndex()); + retType = dex.getType(proto.getReturnTypeIndex()); + args = dex.readParamList(proto.getParametersOffset()); + + StringBuilder strArg = new StringBuilder(); + strArg.append('('); + for (ArgType arg : args) + strArg.append(TypeGen.signature(arg)); + strArg.append(')'); + // strArg.append(TypeGen.signature(retType)); + + shortId = name + strArg; + } + + public String getName() { + return name; + } + + public String getFullName() { + return declClass.getFullName() + "." + name; + } + + /** + * Method name and signature + */ + public String getShortId() { + return shortId; + } + + public ClassInfo getDeclClass() { + return declClass; + } + + public ArgType getReturnType() { + return retType; + } + + public List getArgumentsTypes() { + return args; + } + + public boolean isConstructor() { + return name.equals(""); + } + + public boolean isClassInit() { + return name.equals(""); + } + + @Override + public String toString() { + return retType + " " + declClass.getFullName() + "." + name + + "(" + Utils.listToString(args) + ")"; + } + +} diff --git a/src/main/java/jadx/dex/instructions/ArithNode.java b/src/main/java/jadx/dex/instructions/ArithNode.java new file mode 100644 index 000000000..4bd52c230 --- /dev/null +++ b/src/main/java/jadx/dex/instructions/ArithNode.java @@ -0,0 +1,74 @@ +package jadx.dex.instructions; + +import jadx.dex.instructions.args.ArgType; +import jadx.dex.instructions.args.InsnArg; +import jadx.dex.instructions.args.RegisterArg; +import jadx.dex.nodes.InsnNode; +import jadx.dex.nodes.MethodNode; +import jadx.utils.InsnUtils; + +import com.android.dx.io.instructions.DecodedInstruction; + +public class ArithNode extends InsnNode { + + private final ArithOp op; + + public ArithNode(MethodNode mth, DecodedInstruction insn, ArithOp op, ArgType type, + boolean literal) { + super(mth, InsnType.ARITH, 2); + this.op = op; + setResult(InsnArg.reg(insn, 0, type)); + + int rc = insn.getRegisterCount(); + if (literal) { + if (rc == 1) { + // self + addReg(insn, 0, type); + addLit(insn, type); + } else if (rc == 2) { + // normal + addReg(insn, 1, type); + addLit(insn, type); + } + } else { + if (rc == 2) { + // self + addReg(insn, 0, type); + addReg(insn, 1, type); + } else if (rc == 3) { + // normal + addReg(insn, 1, type); + addReg(insn, 2, type); + } + } + assert getArgsCount() == 2; + } + + public ArithNode(MethodNode mth, ArithOp op, RegisterArg res, InsnArg a, InsnArg b) { + super(mth, InsnType.ARITH, 2); + setResult(res); + addArg(a); + addArg(b); + this.op = op; + } + + public ArithNode(MethodNode mth, ArithOp op, RegisterArg res, InsnArg a) { + super(mth, InsnType.ARITH, 1); + setResult(res); + addArg(a); + this.op = op; + } + + public ArithOp getOp() { + return op; + } + + @Override + public String toString() { + return InsnUtils.formatOffset(offset) + ": " + + InsnUtils.insnTypeToString(insnType) + + getResult() + " = " + + getArg(0) + " " + op.getSymbol() + " " + getArg(1); + } + +} diff --git a/src/main/java/jadx/dex/instructions/ArithOp.java b/src/main/java/jadx/dex/instructions/ArithOp.java new file mode 100644 index 000000000..55294a48f --- /dev/null +++ b/src/main/java/jadx/dex/instructions/ArithOp.java @@ -0,0 +1,31 @@ +package jadx.dex.instructions; + +public enum ArithOp { + ADD("+"), + SUB("-"), + MUL("*"), + DIV("/"), + REM("%"), + + INC("++"), + DEC("--"), + + AND("&"), + OR("|"), + XOR("^"), + + SHL("<<"), + SHR(">>"), + USHR(">>>"); + + private ArithOp(String symbol) { + this.symbol = symbol; + } + + private final String symbol; + + public String getSymbol() { + return this.symbol; + } + +} diff --git a/src/main/java/jadx/dex/instructions/FillArrayOp.java b/src/main/java/jadx/dex/instructions/FillArrayOp.java new file mode 100644 index 000000000..d5740247c --- /dev/null +++ b/src/main/java/jadx/dex/instructions/FillArrayOp.java @@ -0,0 +1,44 @@ +package jadx.dex.instructions; + +import jadx.dex.instructions.args.ArgType; +import jadx.dex.instructions.args.InsnArg; +import jadx.dex.instructions.args.PrimitiveType; +import jadx.dex.nodes.InsnNode; +import jadx.dex.nodes.MethodNode; + +import com.android.dx.io.instructions.FillArrayDataPayloadDecodedInstruction; + +public class FillArrayOp extends InsnNode { + + private final Object data; + + public FillArrayOp(MethodNode method, int resReg, FillArrayDataPayloadDecodedInstruction payload) { + super(method, InsnType.FILL_ARRAY, 0); + + this.data = payload.getData(); + + ArgType elType; + switch (payload.getElementWidthUnit()) { + case 1: + elType = ArgType.unknown(PrimitiveType.BOOLEAN, PrimitiveType.BYTE); + break; + case 2: + elType = ArgType.unknown(PrimitiveType.SHORT, PrimitiveType.CHAR); + break; + case 4: + elType = ArgType.unknown(PrimitiveType.INT, PrimitiveType.FLOAT); + break; + case 8: + elType = ArgType.unknown(PrimitiveType.LONG, PrimitiveType.DOUBLE); + break; + + default: + throw new AssertionError(); + } + setResult(InsnArg.reg(resReg, ArgType.array(elType))); + } + + public Object getData() { + return data; + } +} diff --git a/src/main/java/jadx/dex/instructions/GotoNode.java b/src/main/java/jadx/dex/instructions/GotoNode.java new file mode 100644 index 000000000..8dbd34f6b --- /dev/null +++ b/src/main/java/jadx/dex/instructions/GotoNode.java @@ -0,0 +1,28 @@ +package jadx.dex.instructions; + +import jadx.dex.nodes.InsnNode; +import jadx.dex.nodes.MethodNode; +import jadx.utils.InsnUtils; + +public class GotoNode extends InsnNode { + + protected int target; + + public GotoNode(MethodNode mth, int target) { + this(mth, InsnType.GOTO, target); + } + + protected GotoNode(MethodNode mth, InsnType type, int target) { + super(mth, type); + this.target = target; + } + + public int getTarget() { + return target; + } + + @Override + public String toString() { + return super.toString() + "-> " + InsnUtils.formatOffset(target); + } +} diff --git a/src/main/java/jadx/dex/instructions/IfNode.java b/src/main/java/jadx/dex/instructions/IfNode.java new file mode 100644 index 000000000..4b1b54c8e --- /dev/null +++ b/src/main/java/jadx/dex/instructions/IfNode.java @@ -0,0 +1,78 @@ +package jadx.dex.instructions; + +import jadx.dex.instructions.args.ArgType; +import jadx.dex.instructions.args.InsnArg; +import jadx.dex.instructions.args.LiteralArg; +import jadx.dex.instructions.args.PrimitiveType; +import jadx.dex.nodes.MethodNode; +import jadx.utils.InsnUtils; + +import com.android.dx.io.instructions.DecodedInstruction; + +public class IfNode extends GotoNode { + + protected boolean zeroCmp; + protected IfOp op; + + public IfNode(MethodNode mth, IfOp op, int targ, InsnArg then, InsnArg els) { + super(mth, InsnType.IF, targ); + addArg(then); + if (els == null) { + zeroCmp = true; + } else { + zeroCmp = false; + addArg(els); + } + } + + public IfNode(MethodNode mth, DecodedInstruction insn, IfOp op) { + super(mth, InsnType.IF, insn.getTarget()); + this.op = op; + + ArgType type = ArgType.unknown( + PrimitiveType.INT, PrimitiveType.OBJECT, PrimitiveType.ARRAY, + PrimitiveType.BOOLEAN, PrimitiveType.SHORT, PrimitiveType.CHAR); + + addReg(insn, 0, type); + if (insn.getRegisterCount() == 1) { + zeroCmp = true; + } else { + zeroCmp = false; + addReg(insn, 1, type); + } + } + + public IfOp getOp() { + return op; + } + + public boolean isZeroCmp() { + return zeroCmp; + } + + public void invertOp(int targ) { + op = op.invert(); + target = targ; + } + + public void changeCondition(InsnArg arg1, InsnArg arg2, IfOp op) { + this.op = op; + this.zeroCmp = arg2.isLiteral() && ((LiteralArg) arg2).getLiteral() == 0; + setArg(0, arg1); + if (!zeroCmp) { + if (getArgsCount() == 2) + setArg(1, arg2); + else + addArg(arg2); + } + } + + @Override + public String toString() { + return InsnUtils.formatOffset(offset) + ": " + + InsnUtils.insnTypeToString(insnType) + + getArg(0) + " " + op.getSymbol() + + " " + (zeroCmp ? "0" : getArg(1)) + + " -> " + InsnUtils.formatOffset(target); + } +} diff --git a/src/main/java/jadx/dex/instructions/IfOp.java b/src/main/java/jadx/dex/instructions/IfOp.java new file mode 100644 index 000000000..454766c26 --- /dev/null +++ b/src/main/java/jadx/dex/instructions/IfOp.java @@ -0,0 +1,42 @@ +package jadx.dex.instructions; + +public enum IfOp { + EQ("=="), + NE("!="), + LT("<"), + LE("<="), + GT(">"), + GE(">="); + + private final String symbol; + + private IfOp(String symbol) { + this.symbol = symbol; + } + + public String getSymbol() { + return symbol; + } + + public IfOp invert() { + switch (this) { + case EQ: + return IfOp.NE; + case NE: + return IfOp.EQ; + + case LT: + return IfOp.GE; + case LE: + return IfOp.GT; + + case GT: + return IfOp.LE; + case GE: + return IfOp.LT; + + default: + return null; + } + } +} diff --git a/src/main/java/jadx/dex/instructions/IndexInsnNode.java b/src/main/java/jadx/dex/instructions/IndexInsnNode.java new file mode 100644 index 000000000..5f20b123f --- /dev/null +++ b/src/main/java/jadx/dex/instructions/IndexInsnNode.java @@ -0,0 +1,24 @@ +package jadx.dex.instructions; + +import jadx.dex.nodes.InsnNode; +import jadx.dex.nodes.MethodNode; +import jadx.utils.InsnUtils; + +public class IndexInsnNode extends InsnNode { + + protected final Object index; + + public IndexInsnNode(MethodNode mth, InsnType type, Object index, int argCount) { + super(mth, type, argCount); + this.index = index; + } + + public Object getIndex() { + return index; + } + + @Override + public String toString() { + return super.toString() + " " + InsnUtils.indexToString(index); + } +} diff --git a/src/main/java/jadx/dex/instructions/InsnDecoder.java b/src/main/java/jadx/dex/instructions/InsnDecoder.java new file mode 100644 index 000000000..d61c7c5f0 --- /dev/null +++ b/src/main/java/jadx/dex/instructions/InsnDecoder.java @@ -0,0 +1,725 @@ +package jadx.dex.instructions; + +import jadx.dex.info.FieldInfo; +import jadx.dex.instructions.args.ArgType; +import jadx.dex.instructions.args.InsnArg; +import jadx.dex.instructions.args.PrimitiveType; +import jadx.dex.instructions.args.RegisterArg; +import jadx.dex.nodes.DexNode; +import jadx.dex.nodes.InsnNode; +import jadx.dex.nodes.MethodNode; +import jadx.utils.exceptions.DecodeException; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.android.dx.io.Code; +import com.android.dx.io.OpcodeInfo; +import com.android.dx.io.Opcodes; +import com.android.dx.io.instructions.DecodedInstruction; +import com.android.dx.io.instructions.FillArrayDataPayloadDecodedInstruction; +import com.android.dx.io.instructions.PackedSwitchPayloadDecodedInstruction; +import com.android.dx.io.instructions.SparseSwitchPayloadDecodedInstruction; + +public class InsnDecoder { + + private final static Logger LOG = LoggerFactory.getLogger(InsnDecoder.class); + + private final MethodNode method; + private final DecodedInstruction[] insnArr; + private final DexNode dex; + + public InsnDecoder(MethodNode mthNode, Code mthCode) { + this.method = mthNode; + this.dex = method.dex(); + this.insnArr = DecodedInstruction.decodeAll(mthCode.getInstructions()); + } + + public InsnNode[] run() { + InsnNode[] instructions = new InsnNode[insnArr.length]; + + for (int i = 0; i < insnArr.length; i++) { + try { + DecodedInstruction rawInsn = insnArr[i]; + if (rawInsn != null) { + InsnNode insn = decode(rawInsn, i); + if (insn != null) { + insn.setOffset(i); + insn.setInsnHashCode(calcHashCode(rawInsn)); + } + instructions[i] = insn; + } else { + instructions[i] = null; + } + } catch (DecodeException e) { + LOG.error("Instruction decode error", e); + System.exit(1); + } + } + return instructions; + } + + private int calcHashCode(DecodedInstruction insn) { + int hash = insn.getOpcode(); + hash = hash * 31 + insn.getClass().getName().hashCode(); + hash = hash * 31 + insn.getFormat().ordinal(); + hash = hash * 31 + insn.getRegisterCount(); + hash = hash * 31 + insn.getIndex(); + hash = hash * 31 + insn.getTarget(); + hash = hash * 31 + insn.getA(); + hash = hash * 31 + insn.getB(); + hash = hash * 31 + insn.getC(); + hash = hash * 31 + insn.getD(); + hash = hash * 31 + insn.getE(); + return hash; + } + + private InsnNode decode(DecodedInstruction insn, int offset) throws DecodeException { + switch (insn.getOpcode()) { + case Opcodes.NOP: + case Opcodes.PACKED_SWITCH_PAYLOAD: + case Opcodes.SPARSE_SWITCH_PAYLOAD: + case Opcodes.FILL_ARRAY_DATA_PAYLOAD: + return null; + + // move-result will be process in invoke and filled-new-array instructions + case Opcodes.MOVE_RESULT: + case Opcodes.MOVE_RESULT_WIDE: + case Opcodes.MOVE_RESULT_OBJECT: + return new InsnNode(method, InsnType.NOP, 0); + + case Opcodes.CONST: + case Opcodes.CONST_4: + case Opcodes.CONST_16: + case Opcodes.CONST_HIGH16: + return insn(InsnType.CONST, InsnArg.reg(insn, 0, ArgType.NARROW), + InsnArg.lit(insn, ArgType.NARROW)); + + case Opcodes.CONST_WIDE: + case Opcodes.CONST_WIDE_16: + case Opcodes.CONST_WIDE_32: + case Opcodes.CONST_WIDE_HIGH16: + return insn(InsnType.CONST, InsnArg.reg(insn, 0, ArgType.WIDE), + InsnArg.lit(insn, ArgType.WIDE)); + + case Opcodes.CONST_STRING: + case Opcodes.CONST_STRING_JUMBO: { + InsnNode node = new IndexInsnNode(method, InsnType.CONST, dex.getString(insn.getIndex()), 0); + node.setResult(InsnArg.reg(insn, 0, ArgType.STRING)); + return node; + } + + case Opcodes.CONST_CLASS: { + InsnNode node = new IndexInsnNode(method, InsnType.CONST, dex.getType(insn.getIndex()), 0); + node.setResult(InsnArg.reg(insn, 0, ArgType.CLASS)); + return node; + } + + case Opcodes.MOVE: + case Opcodes.MOVE_16: + case Opcodes.MOVE_FROM16: + return insn(InsnType.MOVE, + InsnArg.reg(insn, 0, ArgType.NARROW), + InsnArg.reg(insn, 1, ArgType.NARROW)); + + case Opcodes.MOVE_WIDE: + case Opcodes.MOVE_WIDE_16: + case Opcodes.MOVE_WIDE_FROM16: + return insn(InsnType.MOVE, + InsnArg.reg(insn, 0, ArgType.WIDE), + InsnArg.reg(insn, 1, ArgType.WIDE)); + + case Opcodes.MOVE_OBJECT: + case Opcodes.MOVE_OBJECT_16: + case Opcodes.MOVE_OBJECT_FROM16: + return insn(InsnType.MOVE, + InsnArg.reg(insn, 0, ArgType.UNKNOWN_OBJECT), + InsnArg.reg(insn, 1, ArgType.UNKNOWN_OBJECT)); + + case Opcodes.ADD_INT: + case Opcodes.ADD_INT_2ADDR: + return arith(insn, ArithOp.ADD, ArgType.INT); + + case Opcodes.ADD_DOUBLE: + case Opcodes.ADD_DOUBLE_2ADDR: + return arith(insn, ArithOp.ADD, ArgType.DOUBLE); + + case Opcodes.ADD_FLOAT: + case Opcodes.ADD_FLOAT_2ADDR: + return arith(insn, ArithOp.ADD, ArgType.FLOAT); + + case Opcodes.ADD_LONG: + case Opcodes.ADD_LONG_2ADDR: + return arith(insn, ArithOp.ADD, ArgType.LONG); + + case Opcodes.ADD_INT_LIT8: + case Opcodes.ADD_INT_LIT16: + return arith_lit(insn, ArithOp.ADD, ArgType.INT); + + case Opcodes.SUB_INT: + case Opcodes.SUB_INT_2ADDR: + return arith(insn, ArithOp.SUB, ArgType.INT); + + case Opcodes.RSUB_INT: + return new ArithNode(method, ArithOp.SUB, + InsnArg.reg(insn, 0, ArgType.INT), + InsnArg.reg(insn, 2, ArgType.INT), + InsnArg.reg(insn, 1, ArgType.INT)); + + case Opcodes.RSUB_INT_LIT8: + return new ArithNode(method, ArithOp.SUB, + InsnArg.reg(insn, 0, ArgType.INT), + InsnArg.lit(insn, ArgType.INT), + InsnArg.reg(insn, 1, ArgType.INT)); + + case Opcodes.SUB_LONG: + case Opcodes.SUB_LONG_2ADDR: + return arith(insn, ArithOp.SUB, ArgType.LONG); + + case Opcodes.SUB_FLOAT: + case Opcodes.SUB_FLOAT_2ADDR: + return arith(insn, ArithOp.SUB, ArgType.FLOAT); + + case Opcodes.SUB_DOUBLE: + case Opcodes.SUB_DOUBLE_2ADDR: + return arith(insn, ArithOp.SUB, ArgType.DOUBLE); + + case Opcodes.MUL_INT: + case Opcodes.MUL_INT_2ADDR: + return arith(insn, ArithOp.MUL, ArgType.INT); + + case Opcodes.MUL_DOUBLE: + case Opcodes.MUL_DOUBLE_2ADDR: + return arith(insn, ArithOp.MUL, ArgType.DOUBLE); + + case Opcodes.MUL_FLOAT: + case Opcodes.MUL_FLOAT_2ADDR: + return arith(insn, ArithOp.MUL, ArgType.FLOAT); + + case Opcodes.MUL_LONG: + case Opcodes.MUL_LONG_2ADDR: + return arith(insn, ArithOp.MUL, ArgType.LONG); + + case Opcodes.MUL_INT_LIT8: + case Opcodes.MUL_INT_LIT16: + return arith_lit(insn, ArithOp.MUL, ArgType.INT); + + case Opcodes.DIV_INT: + case Opcodes.DIV_INT_2ADDR: + return arith(insn, ArithOp.DIV, ArgType.INT); + + case Opcodes.REM_INT: + case Opcodes.REM_INT_2ADDR: + return arith(insn, ArithOp.REM, ArgType.INT); + + case Opcodes.REM_LONG: + case Opcodes.REM_LONG_2ADDR: + return arith(insn, ArithOp.REM, ArgType.LONG); + + case Opcodes.REM_FLOAT: + case Opcodes.REM_FLOAT_2ADDR: + return arith(insn, ArithOp.REM, ArgType.FLOAT); + + case Opcodes.REM_DOUBLE: + case Opcodes.REM_DOUBLE_2ADDR: + return arith(insn, ArithOp.REM, ArgType.DOUBLE); + + case Opcodes.DIV_DOUBLE: + case Opcodes.DIV_DOUBLE_2ADDR: + return arith(insn, ArithOp.DIV, ArgType.DOUBLE); + + case Opcodes.DIV_FLOAT: + case Opcodes.DIV_FLOAT_2ADDR: + return arith(insn, ArithOp.DIV, ArgType.FLOAT); + + case Opcodes.DIV_LONG: + case Opcodes.DIV_LONG_2ADDR: + return arith(insn, ArithOp.DIV, ArgType.LONG); + + case Opcodes.DIV_INT_LIT8: + case Opcodes.DIV_INT_LIT16: + return arith_lit(insn, ArithOp.DIV, ArgType.INT); + + case Opcodes.REM_INT_LIT8: + case Opcodes.REM_INT_LIT16: + return arith_lit(insn, ArithOp.REM, ArgType.INT); + + case Opcodes.AND_INT: + case Opcodes.AND_INT_2ADDR: + return arith(insn, ArithOp.AND, ArgType.INT); + + case Opcodes.AND_INT_LIT8: + case Opcodes.AND_INT_LIT16: + return arith_lit(insn, ArithOp.AND, ArgType.INT); + + case Opcodes.XOR_INT_LIT8: + case Opcodes.XOR_INT_LIT16: + return arith_lit(insn, ArithOp.XOR, ArgType.INT); + + case Opcodes.AND_LONG: + case Opcodes.AND_LONG_2ADDR: + return arith(insn, ArithOp.AND, ArgType.LONG); + + case Opcodes.OR_INT: + case Opcodes.OR_INT_2ADDR: + return arith(insn, ArithOp.OR, ArgType.INT); + + case Opcodes.OR_INT_LIT8: + case Opcodes.OR_INT_LIT16: + return arith_lit(insn, ArithOp.OR, ArgType.INT); + + case Opcodes.XOR_INT: + case Opcodes.XOR_INT_2ADDR: + return arith(insn, ArithOp.XOR, ArgType.INT); + + case Opcodes.OR_LONG: + case Opcodes.OR_LONG_2ADDR: + return arith(insn, ArithOp.OR, ArgType.LONG); + + case Opcodes.XOR_LONG: + case Opcodes.XOR_LONG_2ADDR: + return arith(insn, ArithOp.XOR, ArgType.LONG); + + case Opcodes.USHR_INT: + case Opcodes.USHR_INT_2ADDR: + return arith(insn, ArithOp.USHR, ArgType.INT); + + case Opcodes.USHR_LONG: + case Opcodes.USHR_LONG_2ADDR: + return arith(insn, ArithOp.USHR, ArgType.LONG); + + case Opcodes.SHL_INT: + case Opcodes.SHL_INT_2ADDR: + return arith(insn, ArithOp.SHL, ArgType.INT); + + case Opcodes.SHL_LONG: + case Opcodes.SHL_LONG_2ADDR: + return arith(insn, ArithOp.SHL, ArgType.LONG); + + case Opcodes.SHR_INT: + case Opcodes.SHR_INT_2ADDR: + return arith(insn, ArithOp.SHR, ArgType.INT); + + case Opcodes.SHR_LONG: + case Opcodes.SHR_LONG_2ADDR: + return arith(insn, ArithOp.SHR, ArgType.LONG); + + case Opcodes.SHL_INT_LIT8: + return arith_lit(insn, ArithOp.SHL, ArgType.INT); + case Opcodes.SHR_INT_LIT8: + return arith_lit(insn, ArithOp.SHR, ArgType.INT); + case Opcodes.USHR_INT_LIT8: + return arith_lit(insn, ArithOp.USHR, ArgType.INT); + + case Opcodes.NEG_INT: + return neg(insn, ArgType.INT); + case Opcodes.NEG_LONG: + return neg(insn, ArgType.LONG); + case Opcodes.NEG_FLOAT: + return neg(insn, ArgType.FLOAT); + case Opcodes.NEG_DOUBLE: + return neg(insn, ArgType.DOUBLE); + + case Opcodes.INT_TO_BYTE: + return cast(insn, ArgType.INT, ArgType.BYTE); + case Opcodes.INT_TO_CHAR: + return cast(insn, ArgType.INT, ArgType.CHAR); + case Opcodes.INT_TO_SHORT: + return cast(insn, ArgType.INT, ArgType.SHORT); + case Opcodes.INT_TO_FLOAT: + return cast(insn, ArgType.INT, ArgType.FLOAT); + case Opcodes.INT_TO_DOUBLE: + return cast(insn, ArgType.INT, ArgType.DOUBLE); + case Opcodes.INT_TO_LONG: + return cast(insn, ArgType.INT, ArgType.LONG); + + case Opcodes.FLOAT_TO_INT: + return cast(insn, ArgType.FLOAT, ArgType.INT); + case Opcodes.FLOAT_TO_DOUBLE: + return cast(insn, ArgType.FLOAT, ArgType.DOUBLE); + case Opcodes.FLOAT_TO_LONG: + return cast(insn, ArgType.FLOAT, ArgType.LONG); + + case Opcodes.DOUBLE_TO_INT: + return cast(insn, ArgType.DOUBLE, ArgType.INT); + case Opcodes.DOUBLE_TO_FLOAT: + return cast(insn, ArgType.DOUBLE, ArgType.FLOAT); + case Opcodes.DOUBLE_TO_LONG: + return cast(insn, ArgType.DOUBLE, ArgType.LONG); + + case Opcodes.LONG_TO_INT: + return cast(insn, ArgType.LONG, ArgType.INT); + case Opcodes.LONG_TO_FLOAT: + return cast(insn, ArgType.LONG, ArgType.FLOAT); + case Opcodes.LONG_TO_DOUBLE: + return cast(insn, ArgType.LONG, ArgType.DOUBLE); + + case Opcodes.IF_EQ: + case Opcodes.IF_EQZ: + return new IfNode(method, insn, IfOp.EQ); + + case Opcodes.IF_NE: + case Opcodes.IF_NEZ: + return new IfNode(method, insn, IfOp.NE); + + case Opcodes.IF_GT: + case Opcodes.IF_GTZ: + return new IfNode(method, insn, IfOp.GT); + + case Opcodes.IF_GE: + case Opcodes.IF_GEZ: + return new IfNode(method, insn, IfOp.GE); + + case Opcodes.IF_LT: + case Opcodes.IF_LTZ: + return new IfNode(method, insn, IfOp.LT); + + case Opcodes.IF_LE: + case Opcodes.IF_LEZ: + return new IfNode(method, insn, IfOp.LE); + + case Opcodes.CMP_LONG: + return cmp(insn, InsnType.CMP_L, ArgType.LONG); + case Opcodes.CMPL_FLOAT: + return cmp(insn, InsnType.CMP_L, ArgType.FLOAT); + case Opcodes.CMPL_DOUBLE: + return cmp(insn, InsnType.CMP_L, ArgType.DOUBLE); + + case Opcodes.CMPG_FLOAT: + return cmp(insn, InsnType.CMP_G, ArgType.FLOAT); + case Opcodes.CMPG_DOUBLE: + return cmp(insn, InsnType.CMP_G, ArgType.DOUBLE); + + case Opcodes.GOTO: + case Opcodes.GOTO_16: + case Opcodes.GOTO_32: + return new GotoNode(method, insn.getTarget()); + + case Opcodes.THROW: + return insn(InsnType.THROW, null, + InsnArg.reg(insn, 0, ArgType.unknown(PrimitiveType.OBJECT))); + + case Opcodes.MOVE_EXCEPTION: + return insn(InsnType.MOVE_EXCEPTION, + InsnArg.reg(insn, 0, ArgType.unknown(PrimitiveType.OBJECT))); + + case Opcodes.RETURN_VOID: + return new InsnNode(method, InsnType.RETURN, 0); + + case Opcodes.RETURN: + case Opcodes.RETURN_WIDE: + case Opcodes.RETURN_OBJECT: + return insn(InsnType.RETURN, + null, + InsnArg.reg(insn, 0, method.getMethodInfo().getReturnType())); + + case Opcodes.INSTANCE_OF: { + InsnNode node = new IndexInsnNode(method, InsnType.INSTANCE_OF, dex.getType(insn.getIndex()), 1); + node.setResult(InsnArg.reg(insn, 0, ArgType.BOOLEAN)); + node.addArg(InsnArg.reg(insn, 1, ArgType.UNKNOWN_OBJECT)); + return node; + } + + case Opcodes.CHECK_CAST: { + ArgType castType = dex.getType(insn.getIndex()); + InsnNode node = new IndexInsnNode(method, InsnType.CHECK_CAST, castType, 1); + node.setResult(InsnArg.reg(insn, 0, castType)); + node.addArg(InsnArg.reg(insn, 0, ArgType.UNKNOWN_OBJECT)); + return node; + } + + case Opcodes.IGET: + case Opcodes.IGET_BOOLEAN: + case Opcodes.IGET_BYTE: + case Opcodes.IGET_CHAR: + case Opcodes.IGET_SHORT: + case Opcodes.IGET_WIDE: + case Opcodes.IGET_OBJECT: { + FieldInfo field = FieldInfo.fromDex(dex, insn.getIndex()); + InsnNode node = new IndexInsnNode(method, InsnType.IGET, field, 1); + node.setResult(InsnArg.reg(insn, 0, field.getType())); + node.addArg(InsnArg.reg(insn, 1, field.getDeclClass().getType())); + return node; + } + + case Opcodes.IPUT: + case Opcodes.IPUT_BOOLEAN: + case Opcodes.IPUT_BYTE: + case Opcodes.IPUT_CHAR: + case Opcodes.IPUT_SHORT: + case Opcodes.IPUT_WIDE: + case Opcodes.IPUT_OBJECT: { + FieldInfo field = FieldInfo.fromDex(dex, insn.getIndex()); + InsnNode node = new IndexInsnNode(method, InsnType.IPUT, field, 2); + node.addArg(InsnArg.reg(insn, 0, field.getType())); + node.addArg(InsnArg.reg(insn, 1, field.getDeclClass().getType())); + return node; + } + + case Opcodes.SGET: + case Opcodes.SGET_BOOLEAN: + case Opcodes.SGET_BYTE: + case Opcodes.SGET_CHAR: + case Opcodes.SGET_SHORT: + case Opcodes.SGET_WIDE: + case Opcodes.SGET_OBJECT: { + FieldInfo field = FieldInfo.fromDex(dex, insn.getIndex()); + InsnNode node = new IndexInsnNode(method, InsnType.SGET, field, 0); + node.setResult(InsnArg.reg(insn, 0, field.getType())); + return node; + } + + case Opcodes.SPUT: + case Opcodes.SPUT_BOOLEAN: + case Opcodes.SPUT_BYTE: + case Opcodes.SPUT_CHAR: + case Opcodes.SPUT_SHORT: + case Opcodes.SPUT_WIDE: + case Opcodes.SPUT_OBJECT: { + FieldInfo field = FieldInfo.fromDex(dex, insn.getIndex()); + InsnNode node = new IndexInsnNode(method, InsnType.SPUT, field, 1); + node.addArg(InsnArg.reg(insn, 0, field.getType())); + return node; + } + + case Opcodes.ARRAY_LENGTH: { + InsnNode node = new InsnNode(method, InsnType.ARRAY_LENGTH, 1); + node.setResult(InsnArg.reg(insn, 0, ArgType.INT)); + node.addArg(InsnArg.reg(insn, 1, ArgType.unknown(PrimitiveType.ARRAY))); + return node; + } + + case Opcodes.AGET: + return arrayGet(insn, ArgType.NARROW); + case Opcodes.AGET_BOOLEAN: + return arrayGet(insn, ArgType.BOOLEAN); + case Opcodes.AGET_BYTE: + return arrayGet(insn, ArgType.BYTE); + case Opcodes.AGET_CHAR: + return arrayGet(insn, ArgType.CHAR); + case Opcodes.AGET_SHORT: + return arrayGet(insn, ArgType.SHORT); + case Opcodes.AGET_WIDE: + return arrayGet(insn, ArgType.WIDE); + case Opcodes.AGET_OBJECT: + return arrayGet(insn, ArgType.UNKNOWN_OBJECT); + + case Opcodes.APUT: + return arrayPut(insn, ArgType.NARROW); + case Opcodes.APUT_BOOLEAN: + return arrayPut(insn, ArgType.BOOLEAN); + case Opcodes.APUT_BYTE: + return arrayPut(insn, ArgType.BYTE); + case Opcodes.APUT_CHAR: + return arrayPut(insn, ArgType.CHAR); + case Opcodes.APUT_SHORT: + return arrayPut(insn, ArgType.SHORT); + case Opcodes.APUT_WIDE: + return arrayPut(insn, ArgType.WIDE); + case Opcodes.APUT_OBJECT: + return arrayPut(insn, ArgType.UNKNOWN_OBJECT); + + case Opcodes.INVOKE_STATIC: + return invoke(insn, offset, InvokeType.STATIC, false); + + case Opcodes.INVOKE_STATIC_RANGE: + return invoke(insn, offset, InvokeType.STATIC, true); + + case Opcodes.INVOKE_DIRECT: + return invoke(insn, offset, InvokeType.DIRECT, false); + case Opcodes.INVOKE_INTERFACE: + return invoke(insn, offset, InvokeType.INTERFACE, false); + case Opcodes.INVOKE_SUPER: + return invoke(insn, offset, InvokeType.SUPER, false); + case Opcodes.INVOKE_VIRTUAL: + return invoke(insn, offset, InvokeType.VIRTUAL, false); + + case Opcodes.INVOKE_DIRECT_RANGE: + return invoke(insn, offset, InvokeType.DIRECT, true); + case Opcodes.INVOKE_INTERFACE_RANGE: + return invoke(insn, offset, InvokeType.INTERFACE, true); + case Opcodes.INVOKE_SUPER_RANGE: + return invoke(insn, offset, InvokeType.SUPER, true); + case Opcodes.INVOKE_VIRTUAL_RANGE: + return invoke(insn, offset, InvokeType.VIRTUAL, true); + + case Opcodes.NEW_INSTANCE: + return insn(InsnType.NEW_INSTANCE, + InsnArg.reg(insn, 0, dex.getType(insn.getIndex()))); + + case Opcodes.NEW_ARRAY: + return insn(InsnType.NEW_ARRAY, + InsnArg.reg(insn, 0, dex.getType(insn.getIndex())), + InsnArg.reg(insn, 1, ArgType.INT)); + + case Opcodes.FILL_ARRAY_DATA: + return fillArray(insn); + + case Opcodes.FILLED_NEW_ARRAY: + return filledNewArray(insn, offset, false); + case Opcodes.FILLED_NEW_ARRAY_RANGE: + return filledNewArray(insn, offset, true); + + case Opcodes.PACKED_SWITCH: + return decodeSwitch(insn, offset, true); + + case Opcodes.SPARSE_SWITCH: + return decodeSwitch(insn, offset, false); + + case Opcodes.MONITOR_ENTER: + return insn(InsnType.MONITOR_ENTER, + null, + InsnArg.reg(insn, 0, ArgType.UNKNOWN_OBJECT)); + + case Opcodes.MONITOR_EXIT: + return insn(InsnType.MONITOR_EXIT, + null, + InsnArg.reg(insn, 0, ArgType.UNKNOWN_OBJECT)); + } + + throw new DecodeException("Unknown instruction: " + OpcodeInfo.getName(insn.getOpcode())); + } + + private InsnNode decodeSwitch(DecodedInstruction insn, int offset, boolean packed) { + int payloadOffset = insn.getTarget(); + DecodedInstruction payload = insnArr[payloadOffset]; + int[] keys; + int[] targets; + if (packed) { + PackedSwitchPayloadDecodedInstruction ps = (PackedSwitchPayloadDecodedInstruction) payload; + targets = ps.getTargets(); + keys = new int[targets.length]; + int k = ps.getFirstKey(); + for (int i = 0; i < keys.length; i++) + keys[i] = k++; + } else { + SparseSwitchPayloadDecodedInstruction ss = (SparseSwitchPayloadDecodedInstruction) payload; + targets = ss.getTargets(); + keys = ss.getKeys(); + } + // convert from relative to absolute offsets + for (int i = 0; i < targets.length; i++) { + targets[i] = targets[i] - payloadOffset + offset; + } + int nextOffset = getNextInsnOffset(insnArr, offset); + return new SwitchNode(method, InsnArg.reg(insn, 0, ArgType.NARROW), + keys, targets, nextOffset); + } + + private InsnNode fillArray(DecodedInstruction insn) { + DecodedInstruction payload = insnArr[insn.getTarget()]; + return new FillArrayOp(method, insn.getA(), (FillArrayDataPayloadDecodedInstruction) payload); + } + + private InsnNode filledNewArray(DecodedInstruction insn, int offset, boolean isRange) { + int resReg = getMoveResultRegister(insnArr, offset); + ArgType arrType = dex.getType(insn.getIndex()); + ArgType elType = arrType.getArrayElement(); + InsnArg[] regs = new InsnArg[insn.getRegisterCount()]; + if (isRange) { + int r = insn.getA(); + for (int i = 0; i < insn.getRegisterCount(); i++) { + regs[i] = InsnArg.reg(r, elType); + r++; + } + } else { + for (int i = 0; i < insn.getRegisterCount(); i++) + regs[i] = InsnArg.reg(insn, i, elType); + } + return insn(InsnType.FILLED_NEW_ARRAY, + resReg == -1 ? null : InsnArg.reg(resReg, arrType), + regs); + } + + private InsnNode cmp(DecodedInstruction insn, InsnType itype, ArgType argType) { + InsnNode inode = new InsnNode(method, itype, 2); + inode.setResult(InsnArg.reg(insn, 0, ArgType.INT)); + inode.addArg(InsnArg.reg(insn, 1, argType)); + inode.addArg(InsnArg.reg(insn, 2, argType)); + return inode; + } + + private InsnNode cast(DecodedInstruction insn, ArgType from, ArgType to) { + InsnNode inode = new IndexInsnNode(method, InsnType.CAST, to, 1); + inode.setResult(InsnArg.reg(insn, 0, to)); + inode.addArg(InsnArg.reg(insn, 1, from)); + return inode; + } + + private InsnNode invoke(DecodedInstruction insn, int offset, InvokeType type, boolean isRange) { + int resReg = getMoveResultRegister(insnArr, offset); + return new InvokeNode(method, insn, type, isRange, resReg); + } + + private InsnNode arrayGet(DecodedInstruction insn, ArgType argType) { + InsnNode inode = new InsnNode(method, InsnType.AGET, 2); + inode.setResult(InsnArg.reg(insn, 0, argType)); + inode.addArg(InsnArg.reg(insn, 1, ArgType.unknown(PrimitiveType.ARRAY))); + inode.addArg(InsnArg.reg(insn, 2, ArgType.INT)); + return inode; + } + + private InsnNode arrayPut(DecodedInstruction insn, ArgType argType) { + InsnNode inode = new InsnNode(method, InsnType.APUT, 3); + inode.addArg(InsnArg.reg(insn, 1, ArgType.unknown(PrimitiveType.ARRAY))); + inode.addArg(InsnArg.reg(insn, 2, ArgType.INT)); + inode.addArg(InsnArg.reg(insn, 0, argType)); + return inode; + } + + private InsnNode arith(DecodedInstruction insn, ArithOp op, ArgType type) { + return new ArithNode(method, insn, op, type, false); + } + + private InsnNode arith_lit(DecodedInstruction insn, ArithOp op, ArgType type) { + return new ArithNode(method, insn, op, type, true); + } + + private InsnNode neg(DecodedInstruction insn, ArgType type) { + InsnNode inode = new InsnNode(method, InsnType.NEG, 1); + inode.setResult(InsnArg.reg(insn, 0, type)); + inode.addArg(InsnArg.reg(insn, 1, type)); + return inode; + } + + private InsnNode insn(InsnType type, RegisterArg res, InsnArg... args) { + InsnNode inode = new InsnNode(method, type, args == null ? 0 : args.length); + inode.setResult(res); + if (args != null) + for (InsnArg arg : args) + inode.addArg(arg); + return inode; + } + + private int getMoveResultRegister(DecodedInstruction[] insnArr, int offset) { + int nextOffset = getNextInsnOffset(insnArr, offset); + if (nextOffset >= 0) { + DecodedInstruction next = insnArr[nextOffset]; + int opc = next.getOpcode(); + if (opc == Opcodes.MOVE_RESULT + || opc == Opcodes.MOVE_RESULT_WIDE + || opc == Opcodes.MOVE_RESULT_OBJECT) { + return next.getA(); + } + } + return -1; + } + + public static int getPrevInsnOffset(Object[] insnArr, int offset) { + int i = offset - 1; + while (i >= 0 && insnArr[i] == null) + i--; + if (i < 0) + return -1; + return i; + } + + public static int getNextInsnOffset(Object[] insnArr, int offset) { + int i = offset + 1; + while (i < insnArr.length && insnArr[i] == null) + i++; + if (i >= insnArr.length) + return -1; + return i; + } +} diff --git a/src/main/java/jadx/dex/instructions/InsnType.java b/src/main/java/jadx/dex/instructions/InsnType.java new file mode 100644 index 000000000..074748f70 --- /dev/null +++ b/src/main/java/jadx/dex/instructions/InsnType.java @@ -0,0 +1,55 @@ +package jadx.dex.instructions; + +public enum InsnType { + NOP, // replacement for removed instructions + + CONST, + + ARITH, + NEG, + + MOVE, + CAST, + + RETURN, + GOTO, + + THROW, + MOVE_EXCEPTION, + + CMP_L, + CMP_G, + IF, + SWITCH, + + MONITOR_ENTER, + MONITOR_EXIT, + + CHECK_CAST, + INSTANCE_OF, + + ARRAY_LENGTH, + FILL_ARRAY, + FILLED_NEW_ARRAY, + + AGET, + APUT, + + NEW_ARRAY, + NEW_INSTANCE, + + IGET, + IPUT, + + SGET, + SPUT, + + INVOKE, + + // additional instructions + CONSTRUCTOR, + BREAK, + CONTINUE, + TERNARY, + NEW_MULTIDIM_ARRAY // TODO: now multidimensional arrays created using Array.newInstance function +} diff --git a/src/main/java/jadx/dex/instructions/InvokeNode.java b/src/main/java/jadx/dex/instructions/InvokeNode.java new file mode 100644 index 000000000..f0c8a7bae --- /dev/null +++ b/src/main/java/jadx/dex/instructions/InvokeNode.java @@ -0,0 +1,57 @@ +package jadx.dex.instructions; + +import jadx.dex.info.MethodInfo; +import jadx.dex.instructions.args.ArgType; +import jadx.dex.instructions.args.InsnArg; +import jadx.dex.nodes.InsnNode; +import jadx.dex.nodes.MethodNode; +import jadx.utils.InsnUtils; +import jadx.utils.Utils; + +import com.android.dx.io.instructions.DecodedInstruction; + +public class InvokeNode extends InsnNode { + + private final InvokeType type; + private final MethodInfo mth; + + public InvokeNode(MethodNode method, DecodedInstruction insn, InvokeType type, boolean isRange, + int resReg) { + super(method, InsnType.INVOKE); + this.mth = MethodInfo.fromDex(method.dex(), insn.getIndex()); + this.type = type; + + if (resReg >= 0) + setResult(InsnArg.reg(resReg, mth.getReturnType())); + + int k = isRange ? insn.getA() : 0; + if (type != InvokeType.STATIC) { + int r = isRange ? k : InsnUtils.getArg(insn, k); + addReg(r, mth.getDeclClass().getType()); + k++; + } + + for (ArgType arg : mth.getArgumentsTypes()) { + addReg(isRange ? k : InsnUtils.getArg(insn, k), arg); + k += arg.getRegCount(); + } + } + + public InvokeType getInvokeType() { + return type; + } + + public MethodInfo getCallMth() { + return mth; + } + + @Override + public String toString() { + return InsnUtils.formatOffset(offset) + ": " + + InsnUtils.insnTypeToString(insnType) + + (getResult() == null ? "" : getResult() + " = ") + + Utils.listToString(getArguments()) + + " " + mth + + " type: " + type; + } +} diff --git a/src/main/java/jadx/dex/instructions/InvokeType.java b/src/main/java/jadx/dex/instructions/InvokeType.java new file mode 100644 index 000000000..726d491ee --- /dev/null +++ b/src/main/java/jadx/dex/instructions/InvokeType.java @@ -0,0 +1,9 @@ +package jadx.dex.instructions; + +public enum InvokeType { + STATIC, + DIRECT, + VIRTUAL, + INTERFACE, + SUPER, +} diff --git a/src/main/java/jadx/dex/instructions/SwitchNode.java b/src/main/java/jadx/dex/instructions/SwitchNode.java new file mode 100644 index 000000000..92a9b62a6 --- /dev/null +++ b/src/main/java/jadx/dex/instructions/SwitchNode.java @@ -0,0 +1,52 @@ +package jadx.dex.instructions; + +import jadx.dex.instructions.args.InsnArg; +import jadx.dex.nodes.InsnNode; +import jadx.dex.nodes.MethodNode; +import jadx.utils.InsnUtils; + +import java.util.Arrays; + +public class SwitchNode extends InsnNode { + + private final int[] keys; + private final int[] targets; + private final int def; // next instruction + + public SwitchNode(MethodNode mth, InsnArg arg, int[] keys, int[] targets, int def) { + super(mth, InsnType.SWITCH, 1); + this.keys = keys; + this.targets = targets; + this.def = def; + addArg(arg); + } + + public int getCasesCount() { + return keys.length; + } + + public int[] getKeys() { + return keys; + } + + public int[] getTargets() { + return targets; + } + + public int getDefaultCaseOffset() { + return def; + } + + @Override + public String toString() { + StringBuilder targ = new StringBuilder(); + targ.append('['); + for (int i = 0; i < targets.length; i++) { + targ.append(InsnUtils.formatOffset(targets[i])); + if (i < targets.length - 1) + targ.append(", "); + } + targ.append(']'); + return super.toString() + " k:" + Arrays.toString(keys) + " t:" + targ; + } +} diff --git a/src/main/java/jadx/dex/instructions/args/ArgType.java b/src/main/java/jadx/dex/instructions/args/ArgType.java new file mode 100644 index 000000000..3c3af8dee --- /dev/null +++ b/src/main/java/jadx/dex/instructions/args/ArgType.java @@ -0,0 +1,299 @@ +package jadx.dex.instructions.args; + +import jadx.Consts; +import jadx.utils.Utils; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +public final class ArgType { + + public static final ArgType INT = primitive(PrimitiveType.INT); + public static final ArgType BOOLEAN = primitive(PrimitiveType.BOOLEAN); + public static final ArgType BYTE = primitive(PrimitiveType.BYTE); + public static final ArgType SHORT = primitive(PrimitiveType.SHORT); + public static final ArgType CHAR = primitive(PrimitiveType.CHAR); + public static final ArgType FLOAT = primitive(PrimitiveType.FLOAT); + public static final ArgType DOUBLE = primitive(PrimitiveType.DOUBLE); + public static final ArgType LONG = primitive(PrimitiveType.LONG); + public static final ArgType VOID = primitive(PrimitiveType.VOID); + + public static final ArgType OBJECT = object(Consts.CLASS_OBJECT); + public static final ArgType CLASS = object(Consts.CLASS_CLASS); + public static final ArgType STRING = object(Consts.CLASS_STRING); + public static final ArgType THROWABLE = object(Consts.CLASS_THROWABLE); + + public static final ArgType UNKNOWN = unknown(PrimitiveType.values()); + + public static final ArgType NARROW = unknown( + PrimitiveType.INT, PrimitiveType.FLOAT, + PrimitiveType.BOOLEAN, PrimitiveType.SHORT, PrimitiveType.BYTE, PrimitiveType.CHAR, + PrimitiveType.OBJECT, PrimitiveType.ARRAY); + + public static final ArgType WIDE = unknown(PrimitiveType.LONG, PrimitiveType.DOUBLE); + + public static final ArgType UNKNOWN_OBJECT = unknown(PrimitiveType.OBJECT, PrimitiveType.ARRAY); + + private final PrimitiveType type; + private final String object; + private final ArgType arrayElement; + private final PrimitiveType possibleTypes[]; + + private final int hash; + + private ArgType(PrimitiveType type, String object, ArgType arrayElement) { + this.type = type; + this.object = (object == null ? null : Utils.cleanObjectName(object)); + this.arrayElement = arrayElement; + this.possibleTypes = null; + this.hash = calcHashCode(); + } + + private ArgType(PrimitiveType[] posTypes) { + this.type = null; + this.object = null; + this.arrayElement = null; + this.possibleTypes = posTypes; + this.hash = calcHashCode(); + } + + public static ArgType primitive(PrimitiveType stype) { + assert stype != PrimitiveType.OBJECT && stype != PrimitiveType.ARRAY; + return new ArgType(stype, null, null); + } + + public static ArgType object(String obj) { + assert obj != null; + return new ArgType(PrimitiveType.OBJECT, obj, null); + } + + public static ArgType array(ArgType vtype) { + return new ArgType(PrimitiveType.ARRAY, null, vtype); + } + + public static ArgType unknown(PrimitiveType... types) { + return new ArgType(types); + } + + public boolean isTypeKnown() { + return type != null; + } + + public PrimitiveType getPrimitiveType() { + return type; + } + + public boolean isPrimitive() { + return type != null && type != PrimitiveType.OBJECT && type != PrimitiveType.ARRAY; + } + + public String getObject() { + return object; + } + + public boolean isObject() { + return type == PrimitiveType.OBJECT; + } + + public ArgType getArrayElement() { + return arrayElement; + } + + public boolean isArray() { + return type == PrimitiveType.ARRAY; + } + + public int getArrayDimension() { + if (isArray()) + return 1 + arrayElement.getArrayDimension(); + else + return 0; + } + + public ArgType getArrayRootElement() { + if (isArray()) + return arrayElement.getArrayRootElement(); + else + return this; + } + + public boolean contains(PrimitiveType type) { + for (PrimitiveType t : possibleTypes) + if (t == type) + return true; + return false; + } + + public ArgType selectFirst() { + assert possibleTypes != null; + PrimitiveType f = possibleTypes[0]; + if (f == PrimitiveType.OBJECT || f == PrimitiveType.ARRAY) + return object(Consts.CLASS_OBJECT); + else + return primitive(f); + } + + public static ArgType merge(ArgType a, ArgType b) { + if (a == b) + return a; + + if (b == null || a == null) + return null; + + ArgType res = mergeInternal(a, b); + if (res == null) + res = mergeInternal(b, a); // swap + return res; + } + + private static ArgType mergeInternal(ArgType a, ArgType b) { + if (a == UNKNOWN) + return b; + + if (a.possibleTypes != null) { + if (b.isTypeKnown()) { + if (a.contains(b.getPrimitiveType())) + return b; + else + return null; + } else { + // both types unknown + List types = new ArrayList(); + for (PrimitiveType type : a.possibleTypes) { + if (b.contains(type)) + types.add(type); + } + if (types.size() == 0) { + return null; + } else if (types.size() == 1) { + PrimitiveType nt = types.get(0); + if (nt == PrimitiveType.OBJECT || nt == PrimitiveType.ARRAY) + return unknown(nt); + else + return primitive(nt); + } else { + return unknown(types.toArray(new PrimitiveType[types.size()])); + } + } + } else { + if (a.isObject() && b.isObject()) { + if (a.getObject().equals(b.getObject())) + return a; + else if (a.getObject().equals(OBJECT.getObject())) + return b; + else if (b.getObject().equals(OBJECT.getObject())) + return a; + else + // different objects + return OBJECT; + // return null; + } + + if (a.isArray() && b.isArray()) { + ArgType res = merge(a.getArrayElement(), b.getArrayElement()); + return (res == null ? null : ArgType.array(res)); + } + + if (a.isPrimitive() && b.isPrimitive()) { + if (a.getRegCount() == b.getRegCount()) + // return primitive(PrimitiveType.getWidest(a.getPrimitiveType(), b.getPrimitiveType())); + return primitive(PrimitiveType.getSmaller(a.getPrimitiveType(), b.getPrimitiveType())); + } + } + return null; + } + + public static ArgType parse(String type) { + assert type.length() > 0 : "Empty type"; + char f = type.charAt(0); + if (f == 'L') + return object(type); + else if (f == '[') + return array(parse(type.substring(1))); + else + return parse(f); + } + + private static ArgType parse(char f) { + switch (f) { + case 'Z': + return BOOLEAN; + case 'B': + return BYTE; + case 'C': + return CHAR; + case 'S': + return SHORT; + case 'I': + return INT; + case 'J': + return LONG; + case 'F': + return FLOAT; + case 'D': + return DOUBLE; + case 'V': + return VOID; + } + throw new RuntimeException("Unknown type: " + f); + } + + public int getRegCount() { + if (type == PrimitiveType.LONG || type == PrimitiveType.DOUBLE) + return 2; + else + return 1; + } + + @Override + public String toString() { + if (this == UNKNOWN) + return "ANY"; + + if (type != null) { + if (type == PrimitiveType.OBJECT) + return object; + else if (type == PrimitiveType.ARRAY) + return arrayElement + "[]"; + else + return type.toString(); + } else { + return "?" + Arrays.asList(possibleTypes).toString(); + } + } + + private int calcHashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((type == null) ? 0 : type.hashCode()); + result = prime * result + ((object == null) ? 0 : object.hashCode()); + result = prime * result + ((arrayElement == null) ? 0 : arrayElement.hashCode()); + result = prime * result + Arrays.hashCode(possibleTypes); + return result; + } + + @Override + public int hashCode() { + return hash; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (obj == null) return false; + if (hash != obj.hashCode()) return false; + if (getClass() != obj.getClass()) return false; + ArgType other = (ArgType) obj; + if (type != other.type) return false; + if (!Arrays.equals(possibleTypes, other.possibleTypes)) return false; + if (arrayElement == null) { + if (other.arrayElement != null) return false; + } else if (!arrayElement.equals(other.arrayElement)) return false; + if (object == null) { + if (other.object != null) return false; + } else if (!object.equals(other.object)) return false; + return true; + } + +} diff --git a/src/main/java/jadx/dex/instructions/args/InsnArg.java b/src/main/java/jadx/dex/instructions/args/InsnArg.java new file mode 100644 index 000000000..078a2ead5 --- /dev/null +++ b/src/main/java/jadx/dex/instructions/args/InsnArg.java @@ -0,0 +1,75 @@ +package jadx.dex.instructions.args; + +import jadx.dex.nodes.InsnNode; +import jadx.utils.InsnUtils; + +import com.android.dx.io.instructions.DecodedInstruction; + +/** + * Instruction argument, + * argument can be register, literal or instruction + */ +public abstract class InsnArg extends Typed { + + protected InsnNode parentInsn; + + public static RegisterArg reg(int regNum, ArgType type) { + assert regNum >= 0 : "Register number must be positive"; + return new RegisterArg(regNum, type); + } + + public static RegisterArg reg(DecodedInstruction insn, int argNum, ArgType type) { + return reg(InsnUtils.getArg(insn, argNum), type); + } + + public static LiteralArg lit(long literal, ArgType type) { + return new LiteralArg(literal, type); + } + + public static LiteralArg lit(DecodedInstruction insn, ArgType type) { + return lit(insn.getLiteral(), type); + } + + public static InsnWrapArg wrap(InsnNode insn) { + return new InsnWrapArg(insn); + } + + public boolean isRegister() { + return false; + } + + public boolean isLiteral() { + return false; + } + + public boolean isInsnWrap() { + return false; + } + + public InsnNode getParentInsn() { + return parentInsn; + } + + public void setParentInsn(InsnNode parentInsn) { + this.parentInsn = parentInsn; + } + + public InsnWrapArg wrapInstruction(InsnNode insn) { + assert parentInsn != insn : "Can't wrap instruction info itself"; + int count = parentInsn.getArgsCount(); + for (int i = 0; i < count; i++) { + if (parentInsn.getArg(i) == this) { + InsnWrapArg arg = wrap(insn); + parentInsn.setArg(i, arg); + return arg; + } + } + return null; + } + + public boolean isThis() { + // must be implemented in RegisterArg + return false; + } + +} diff --git a/src/main/java/jadx/dex/instructions/args/InsnWrapArg.java b/src/main/java/jadx/dex/instructions/args/InsnWrapArg.java new file mode 100644 index 000000000..1c38e01e7 --- /dev/null +++ b/src/main/java/jadx/dex/instructions/args/InsnWrapArg.java @@ -0,0 +1,34 @@ +package jadx.dex.instructions.args; + +import jadx.dex.nodes.InsnNode; + +public class InsnWrapArg extends InsnArg { + + private final InsnNode wrappedInsn; + + public InsnWrapArg(InsnNode insn) { + ArgType type = (insn.getResult() == null ? ArgType.VOID : insn.getResult().getType()); + this.typedVar = new TypedVar(type); + this.wrappedInsn = insn; + } + + public InsnNode getWrapInsn() { + return wrappedInsn; + } + + @Override + public void setParentInsn(InsnNode parentInsn) { + assert parentInsn != wrappedInsn : "Can't wrap instruction info itself: " + parentInsn; + this.parentInsn = parentInsn; + } + + @Override + public boolean isInsnWrap() { + return true; + } + + @Override + public String toString() { + return "(wrap: " + typedVar + "\n " + wrappedInsn + ")"; + } +} diff --git a/src/main/java/jadx/dex/instructions/args/LiteralArg.java b/src/main/java/jadx/dex/instructions/args/LiteralArg.java new file mode 100644 index 000000000..f8cef9ac7 --- /dev/null +++ b/src/main/java/jadx/dex/instructions/args/LiteralArg.java @@ -0,0 +1,35 @@ +package jadx.dex.instructions.args; + +import jadx.codegen.TypeGen; +import jadx.utils.exceptions.JadxRuntimeException; + +public class LiteralArg extends InsnArg { + + private final long literal; + + public LiteralArg(long value, ArgType type) { + this.literal = value; + this.typedVar = new TypedVar(type); + if (literal != 0 && type.isObject()) + throw new RuntimeException("wrong literal type"); + } + + public long getLiteral() { + return literal; + } + + @Override + public boolean isLiteral() { + return true; + } + + @Override + public String toString() { + try { + return "(" + TypeGen.literalToString(literal, getType()) + " " + typedVar + ")"; + } catch (JadxRuntimeException ex) { + // can't convert literal to string + return "(" + literal + " " + typedVar + ")"; + } + } +} diff --git a/src/main/java/jadx/dex/instructions/args/PrimitiveType.java b/src/main/java/jadx/dex/instructions/args/PrimitiveType.java new file mode 100644 index 000000000..9af332f4b --- /dev/null +++ b/src/main/java/jadx/dex/instructions/args/PrimitiveType.java @@ -0,0 +1,50 @@ +package jadx.dex.instructions.args; + +public enum PrimitiveType { + BOOLEAN("Z", "boolean"), + CHAR("C", "char"), + BYTE("B", "byte"), + SHORT("S", "short"), + INT("I", "int"), + FLOAT("F", "float"), + LONG("J", "long"), + DOUBLE("D", "double"), + OBJECT("L", "OBJECT"), + ARRAY("[", "ARRAY"), + VOID("V", "void"); + + private final String shortName; + private final String longName; + + private PrimitiveType(String shortName, String longName) { + this.shortName = shortName; + this.longName = longName; + } + + public String getShortName() { + return shortName; + } + + public String getLongName() { + return longName; + } + + public static PrimitiveType getWidest(PrimitiveType a, PrimitiveType b) { + if (a.ordinal() > b.ordinal()) + return a; + else + return b; + } + + public static PrimitiveType getSmaller(PrimitiveType a, PrimitiveType b) { + if (a.ordinal() < b.ordinal()) + return a; + else + return b; + } + + @Override + public String toString() { + return this.name().toLowerCase(); + } +} diff --git a/src/main/java/jadx/dex/instructions/args/RegisterArg.java b/src/main/java/jadx/dex/instructions/args/RegisterArg.java new file mode 100644 index 000000000..e871afdc1 --- /dev/null +++ b/src/main/java/jadx/dex/instructions/args/RegisterArg.java @@ -0,0 +1,102 @@ +package jadx.dex.instructions.args; + +import jadx.dex.instructions.IndexInsnNode; +import jadx.dex.instructions.InsnType; +import jadx.dex.nodes.InsnNode; +import jadx.dex.visitors.InstructionRemover; + +public class RegisterArg extends InsnArg { + protected final int regNum; + + public RegisterArg(int rn) { + this.regNum = rn; + } + + public RegisterArg(int rn, ArgType type) { + this.typedVar = new TypedVar(type); + this.regNum = rn; + } + + public int getRegNum() { + return regNum; + } + + @Override + public boolean isRegister() { + return true; + } + + public InsnNode getAssignInsn() { + for (InsnArg arg : getTypedVar().getUseList()) { + InsnNode assignInsn = arg.getParentInsn(); + if (assignInsn == null) + // assign as function argument + return null; + else if (assignInsn.getResult() != null + && assignInsn.getResult().getRegNum() == regNum) + return assignInsn; + } + return null; + } + + /** + * Return constant value from register assign or null if not constant + * + * @return LiteralArg, String or ArgType + */ + public Object getConstValue() { + InsnNode parInsn = getAssignInsn(); + if (parInsn != null && parInsn.getType() == InsnType.CONST) { + if (parInsn.getArgsCount() == 0) { + // const in 'index' - string or class + return ((IndexInsnNode) parInsn).getIndex(); + } else { + return parInsn.getArg(0); + } + } + return null; + } + + @Override + public boolean isThis() { + if (isRegister()) { + String name = getTypedVar().getName(); + if (name != null && name.equals("this")) + return true; + + // maybe it was moved from 'this' register + InsnNode ai = getAssignInsn(); + if (ai != null && ai.getType() == InsnType.MOVE) { + if (ai.getArg(0).isThis()) { + // actually we need to remove this instruction but we can't + // because of iterating on instructions list + // so unbind insn and rely on code shrinker + InstructionRemover.unbindInsn(ai); + return true; + } + } + } + return false; + } + + @Override + public int hashCode() { + return regNum * 31 + typedVar.hashCode(); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (obj == null) return false; + if (getClass() != obj.getClass()) return false; + RegisterArg other = (RegisterArg) obj; + if (regNum != other.regNum) return false; + if (!typedVar.equals(other.typedVar)) return false; + return true; + } + + @Override + public String toString() { + return "(r" + regNum + " " + typedVar + ")"; + } +} diff --git a/src/main/java/jadx/dex/instructions/args/Typed.java b/src/main/java/jadx/dex/instructions/args/Typed.java new file mode 100644 index 000000000..8496d82e9 --- /dev/null +++ b/src/main/java/jadx/dex/instructions/args/Typed.java @@ -0,0 +1,50 @@ +package jadx.dex.instructions.args; + +public abstract class Typed { + + protected TypedVar typedVar; + + public TypedVar getTypedVar() { + return typedVar; + } + + public void setTypedVar(TypedVar arg) { + this.typedVar = arg; + } + + public ArgType getType() { + return typedVar.getType(); + } + + public boolean merge(Typed var) { + return typedVar.merge(var.getTypedVar()); + } + + public boolean merge(ArgType var) { + return typedVar.merge(var); + } + + public void replace(Typed var) { + replace(var.getTypedVar()); + } + + public void replace(TypedVar newVar) { + assert newVar != null; + if (typedVar == newVar) + return; + + if (typedVar != null) { + newVar.merge(typedVar); + for (InsnArg arg : typedVar.getUseList()) { + if (arg != this) + arg.setTypedVar(newVar); + } + newVar.getUseList().addAll(typedVar.getUseList()); + if (typedVar.getName() != null) + newVar.setName(typedVar.getName()); + typedVar.getUseList().clear(); + } + typedVar = newVar; + } + +} diff --git a/src/main/java/jadx/dex/instructions/args/TypedVar.java b/src/main/java/jadx/dex/instructions/args/TypedVar.java new file mode 100644 index 000000000..c2acea41c --- /dev/null +++ b/src/main/java/jadx/dex/instructions/args/TypedVar.java @@ -0,0 +1,82 @@ +package jadx.dex.instructions.args; + +import java.util.ArrayList; +import java.util.List; + +public class TypedVar { + + private ArgType type; + private final List useList = new ArrayList(2); + private String name; + + public TypedVar(ArgType initType) { + this.type = initType; + } + + public ArgType getType() { + return type; + } + + /** + * This method must be used very carefully + */ + public boolean forceSetType(ArgType type) { + if (type != null && !type.equals(type)) { + this.type = type; + return true; + } else { + return false; + } + } + + public boolean merge(TypedVar typedVar) { + return merge(typedVar.getType()); + } + + public boolean merge(ArgType mtype) { + ArgType res = ArgType.merge(type, mtype); + if (res != null && !type.equals(res)) { + this.type = res; + return true; + } else { + return false; + } + } + + public List getUseList() { + return useList; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + @Override + public int hashCode() { + return type.hashCode() * 31 + ((name == null) ? 0 : name.hashCode()); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (obj == null) return false; + if (getClass() != obj.getClass()) return false; + TypedVar other = (TypedVar) obj; + if (name == null) { + if (other.name != null) return false; + } else if (!name.equals(other.name)) return false; + if (type == null) { + if (other.type != null) return false; + } else if (!type.equals(other.type)) return false; + return true; + } + + @Override + public String toString() { + return (name != null ? "'" + name + "' " : "") + type.toString(); + } +} diff --git a/src/main/java/jadx/dex/instructions/mods/ConstructorInsn.java b/src/main/java/jadx/dex/instructions/mods/ConstructorInsn.java new file mode 100644 index 000000000..1ff26bc8c --- /dev/null +++ b/src/main/java/jadx/dex/instructions/mods/ConstructorInsn.java @@ -0,0 +1,75 @@ +package jadx.dex.instructions.mods; + +import jadx.dex.info.ClassInfo; +import jadx.dex.info.MethodInfo; +import jadx.dex.instructions.InsnType; +import jadx.dex.instructions.InvokeNode; +import jadx.dex.instructions.args.RegisterArg; +import jadx.dex.nodes.InsnNode; +import jadx.dex.nodes.MethodNode; + +public class ConstructorInsn extends InsnNode { + + private final MethodInfo callMth; + + private static enum CallType { + CONSTRUCTOR, // just new instance + SUPER, // super call + THIS, // call constructor from other constructor + SELF // call itself + } + + private final CallType callType; + + public ConstructorInsn(MethodNode mth, InvokeNode invoke) { + super(mth, InsnType.CONSTRUCTOR, invoke.getArgsCount() - 1); + this.callMth = invoke.getCallMth(); + ClassInfo classType = callMth.getDeclClass(); + + if (invoke.getArg(0).isThis()) { + if (classType.equals(mth.getParentClass().getClassInfo())) { + // self constructor + if (callMth.getShortId().equals(mth.getMethodInfo().getShortId())) { + callType = CallType.SELF; + } else { + callType = CallType.THIS; + } + } else { + callType = CallType.SUPER; + } + } else { + callType = CallType.CONSTRUCTOR; + setResult((RegisterArg) invoke.getArg(0)); + } + + for (int i = 1; i < invoke.getArgsCount(); i++) { + addArg(invoke.getArg(i)); + } + offset = invoke.getOffset(); + } + + public MethodInfo getCallMth() { + return callMth; + } + + public ClassInfo getClassType() { + return callMth.getDeclClass(); + } + + public boolean isSuper() { + return callType == CallType.SUPER; + } + + public boolean isThis() { + return callType == CallType.THIS; + } + + public boolean isSelf() { + return callType == CallType.SELF; + } + + @Override + public String toString() { + return super.toString() + " " + callMth + " " + callType; + } +} diff --git a/src/main/java/jadx/dex/instructions/mods/TernaryInsn.java b/src/main/java/jadx/dex/instructions/mods/TernaryInsn.java new file mode 100644 index 000000000..42456bad3 --- /dev/null +++ b/src/main/java/jadx/dex/instructions/mods/TernaryInsn.java @@ -0,0 +1,31 @@ +package jadx.dex.instructions.mods; + +import jadx.dex.instructions.IfNode; +import jadx.dex.instructions.IfOp; +import jadx.dex.instructions.InsnType; +import jadx.dex.instructions.args.InsnArg; +import jadx.dex.nodes.InsnNode; +import jadx.dex.nodes.MethodNode; +import jadx.utils.InsnUtils; +import jadx.utils.Utils; + +public class TernaryInsn extends IfNode { + + public TernaryInsn(MethodNode mth, IfOp op, InsnNode then, InsnNode els) { + super(mth, op, then.getOffset(), + InsnArg.wrap(then), + els == null ? null : InsnArg.wrap(els)); + } + + @Override + public InsnType getType() { + return InsnType.TERNARY; + } + + @Override + public String toString() { + return InsnUtils.formatOffset(offset) + ": TERNARY" + + getResult() + " = " + + Utils.listToString(getArguments()); + } +} diff --git a/src/main/java/jadx/dex/nodes/BlockNode.java b/src/main/java/jadx/dex/nodes/BlockNode.java new file mode 100644 index 000000000..a21155c6f --- /dev/null +++ b/src/main/java/jadx/dex/nodes/BlockNode.java @@ -0,0 +1,173 @@ +package jadx.dex.nodes; + +import jadx.dex.attributes.AttrNode; +import jadx.dex.attributes.AttributeType; +import jadx.dex.attributes.BlockRegState; +import jadx.dex.attributes.LoopAttr; +import jadx.utils.InsnUtils; + +import java.util.ArrayList; +import java.util.BitSet; +import java.util.Collections; +import java.util.List; + +public class BlockNode extends AttrNode implements IBlock { + + private final int startOffset; + private final List instructions = new ArrayList(2); + + private List predecessors = new ArrayList(1); + private List successors = new ArrayList(1); + + private BitSet doms; // all dominators + private BlockNode idom; // immediate dominator + private final List dominatesOn = new ArrayList(1); + + private BlockRegState startState; + private BlockRegState endState; + + private int id; + public static int initialID; + + public BlockNode(MethodNode mth, int offset) { + this.startOffset = offset; + this.id = ++initialID; + } + + public void setId(int id) { + this.id = id; + } + + public int getId() { + return id; + } + + public List getPredecessors() { + return predecessors; + } + + public List getSuccessors() { + return successors; + } + + private List cleanSuccessors; + + public List getCleanSuccessors() { + return cleanSuccessors; + } + + public void updateCleanSuccessors() { + cleanSuccessors = cleanSuccessors(this); + } + + public void lock() { + cleanSuccessors = Collections.unmodifiableList(cleanSuccessors); + successors = Collections.unmodifiableList(successors); + predecessors = Collections.unmodifiableList(predecessors); + } + + /** + * Return all successor which are not exception handler or followed by loop back edge + */ + private static List cleanSuccessors(BlockNode block) { + List sucList = block.getSuccessors(); + List nodes = new ArrayList(sucList.size()); + LoopAttr loop = (LoopAttr) block.getAttributes().get(AttributeType.LOOP); + if (loop == null) { + for (BlockNode b : sucList) { + if (!b.getAttributes().contains(AttributeType.EXC_HANDLER)) + nodes.add(b); + } + } else { + for (BlockNode b : sucList) { + if (!b.getAttributes().contains(AttributeType.EXC_HANDLER)) { + // don't follow back edge + if (loop.getStart() == b && loop.getEnd() == block) + continue; + nodes.add(b); + } + } + } + return (nodes.size() == sucList.size() ? sucList : nodes); + } + + @Override + public List getInstructions() { + return instructions; + } + + public int getStartOffset() { + return startOffset; + } + + /** + * Check if 'block' dominated on this node + */ + public boolean isDominator(BlockNode block) { + return doms.get(block.getId()); + } + + /** + * Dominators of this node (exclude itself) + */ + public BitSet getDoms() { + return doms; + } + + public void setDoms(BitSet doms) { + this.doms = doms; + } + + /** + * Immediate dominator + */ + public BlockNode getIDom() { + return idom; + } + + public void setIDom(BlockNode idom) { + this.idom = idom; + } + + public List getDominatesOn() { + return dominatesOn; + } + + public BlockRegState getStartState() { + return startState; + } + + public void setStartState(BlockRegState startState) { + this.startState = startState; + } + + public BlockRegState getEndState() { + return endState; + } + + public void setEndState(BlockRegState endState) { + this.endState = endState; + } + + @Override + public int hashCode() { + return id; // TODO id can change during reindex + } + + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (obj == null) return false; + if (hashCode() != obj.hashCode()) return false; + if (!(obj instanceof BlockNode)) return false; + BlockNode other = (BlockNode) obj; + if (id != other.id) return false; + if (startOffset != other.startOffset) return false; + return true; + } + + @Override + public String toString() { + return "B:" + id + ":" + InsnUtils.formatOffset(startOffset); + } +} diff --git a/src/main/java/jadx/dex/nodes/ClassNode.java b/src/main/java/jadx/dex/nodes/ClassNode.java new file mode 100644 index 000000000..aa75b9a3c --- /dev/null +++ b/src/main/java/jadx/dex/nodes/ClassNode.java @@ -0,0 +1,258 @@ +package jadx.dex.nodes; + +import jadx.dex.attributes.AttrNode; +import jadx.dex.attributes.AttributeType; +import jadx.dex.attributes.IAttribute; +import jadx.dex.attributes.annotations.Annotation; +import jadx.dex.attributes.annotations.AnnotationsList; +import jadx.dex.info.AccessInfo; +import jadx.dex.info.AccessInfo.AFType; +import jadx.dex.info.ClassInfo; +import jadx.dex.info.FieldInfo; +import jadx.dex.info.MethodInfo; +import jadx.dex.instructions.args.ArgType; +import jadx.dex.nodes.parser.AnnotationsParser; +import jadx.dex.nodes.parser.FieldValueAttr; +import jadx.dex.nodes.parser.StaticValuesParser; +import jadx.utils.exceptions.DecodeException; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.android.dx.io.ClassData; +import com.android.dx.io.ClassData.Field; +import com.android.dx.io.ClassData.Method; +import com.android.dx.io.ClassDef; + +public class ClassNode extends AttrNode implements ILoadable { + + private final static Logger LOG = LoggerFactory.getLogger(ClassNode.class); + + private final DexNode dex; + private final ClassInfo clsInfo; + private final ClassInfo superClass; + private final List interfaces; + + private final List methods = new ArrayList(); + private final List fields = new ArrayList(); + + private final AccessInfo accessFlags; + private List innerClasses = Collections.emptyList(); + + private final Map constFields = new HashMap(); + + public ClassNode(DexNode dex, ClassDef cls) throws DecodeException { + this.dex = dex; + this.clsInfo = ClassInfo.fromDex(dex, cls.getTypeIndex()); + try { + this.superClass = cls.getSupertypeIndex() == DexNode.NO_INDEX + ? null + : ClassInfo.fromDex(dex, cls.getSupertypeIndex()); + + this.interfaces = new ArrayList(cls.getInterfaces().length); + for (short interfaceIdx : cls.getInterfaces()) { + this.interfaces.add(ClassInfo.fromDex(dex, interfaceIdx)); + } + + if (cls.getClassDataOffset() == 0) { + // nothing to load + } else { + ClassData clsData = dex.readClassData(cls); + + for (Method mth : clsData.getDirectMethods()) + methods.add(new MethodNode(this, mth)); + + for (Method mth : clsData.getVirtualMethods()) + methods.add(new MethodNode(this, mth)); + + for (Field f : clsData.getStaticFields()) + fields.add(new FieldNode(this, f)); + + loadStaticValues(cls, fields); + + for (Field f : clsData.getInstanceFields()) + fields.add(new FieldNode(this, f)); + } + + loadAnnotations(cls); + + int accFlagsValue = cls.getAccessFlags(); + + IAttribute annotations = getAttributes().get(AttributeType.ANNOTATION_LIST); + if (annotations != null) { + AnnotationsList list = (AnnotationsList) annotations; + Annotation iCls = list.get("dalvik.annotation.InnerClass"); + if (iCls != null) + accFlagsValue = (Integer) iCls.getValues().get("accessFlags"); + } + + this.accessFlags = new AccessInfo(accFlagsValue, AFType.CLASS); + + } catch (Exception e) { + throw new DecodeException("Error decode class: " + getFullName(), e); + } + } + + private void loadAnnotations(ClassDef cls) { + int offset = cls.getAnnotationsOffset(); + if (offset != 0) { + try { + new AnnotationsParser(this, offset); + } catch (DecodeException e) { + LOG.error("Error parsing annotations in " + this, e); + } + } + } + + private void loadStaticValues(ClassDef cls, List staticFields) throws DecodeException { + for (FieldNode f : staticFields) { + if (f.getAccessFlags().isFinal()) { + FieldValueAttr nullValue = new FieldValueAttr(null); + f.getAttributes().add(nullValue); + } + } + + int offset = cls.getStaticValuesOffset(); + if (offset != 0) { + StaticValuesParser parser = new StaticValuesParser(dex, dex.openSection(offset)); + parser.processFields(staticFields); + + for (FieldNode f : staticFields) { + if (f.getType().equals(ArgType.STRING)) { + FieldValueAttr fv = (FieldValueAttr) f.getAttributes().get(AttributeType.FIELD_VALUE); + if (fv != null && fv.getValue() != null) { + constFields.put(fv.getValue(), f); + } + } + } + } + } + + @Override + public void load() throws DecodeException { + for (MethodNode mth : getMethods()) { + mth.load(); + } + for (ClassNode innerCls : getInnerClasses()) { + innerCls.load(); + } + } + + @Override + public void unload() { + for (MethodNode mth : getMethods()) { + mth.unload(); + } + for (ClassNode innerCls : getInnerClasses()) { + innerCls.unload(); + } + } + + public ClassInfo getSuperClass() { + return superClass; + } + + public List getInterfaces() { + return interfaces; + } + + public List getMethods() { + return methods; + } + + public List getFields() { + return fields; + } + + public FieldNode searchFieldById(int id) { + String name = FieldInfo.getNameById(dex, id); + for (FieldNode f : fields) { + if (f.getName().equals(name)) + return f; + } + return null; + } + + public FieldNode searchField(FieldInfo field) { + String name = field.getName(); + for (FieldNode f : fields) { + if (f.getName().equals(name)) + return f; + } + return null; + } + + public MethodNode searchMethodById(String shortId) { + for (MethodNode m : methods) { + if (m.getMethodInfo().getShortId().equals(shortId)) + return m; + } + return null; + } + + public MethodNode searchMethodById(int id) { + return searchMethodById(MethodInfo.fromDex(dex, id).getShortId()); + } + + public List getInnerClasses() { + return innerClasses; + } + + public void addInnerClass(ClassNode cls) { + if (innerClasses.isEmpty()) + innerClasses = new ArrayList(3); + innerClasses.add(cls); + } + + public boolean isAnonymous() { + boolean simple = false; + for (MethodNode m : methods) { + MethodInfo mi = m.getMethodInfo(); + if (mi.isConstructor() && mi.getArgumentsTypes().size() == 0) { + simple = true; + break; + } + } + return simple && Character.isDigit(getShortName().charAt(0)); + } + + public AccessInfo getAccessFlags() { + return accessFlags; + } + + public Map getConstFields() { + return constFields; + } + + public DexNode dex() { + return dex; + } + + public ClassInfo getClassInfo() { + return clsInfo; + } + + public String getShortName() { + return clsInfo.getShortName(); + } + + public String getFullName() { + return clsInfo.getFullName(); + } + + public String getPackage() { + return clsInfo.getPackage(); + } + + @Override + public String toString() { + return getFullName(); + } + +} diff --git a/src/main/java/jadx/dex/nodes/DexNode.java b/src/main/java/jadx/dex/nodes/DexNode.java new file mode 100644 index 000000000..cccb51ed7 --- /dev/null +++ b/src/main/java/jadx/dex/nodes/DexNode.java @@ -0,0 +1,119 @@ +package jadx.dex.nodes; + +import jadx.dex.info.ClassInfo; +import jadx.dex.instructions.args.ArgType; +import jadx.utils.Utils; +import jadx.utils.exceptions.DecodeException; +import jadx.utils.files.InputFile; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import com.android.dx.io.ClassData; +import com.android.dx.io.ClassData.Method; +import com.android.dx.io.ClassDef; +import com.android.dx.io.Code; +import com.android.dx.io.DexBuffer; +import com.android.dx.io.DexBuffer.Section; +import com.android.dx.io.FieldId; +import com.android.dx.io.MethodId; +import com.android.dx.io.ProtoId; +import com.android.dx.merge.TypeList; + +public class DexNode { + + public final static int NO_INDEX = -1; + + private final RootNode root; + private final DexBuffer dexBuf; + private final List classes = new ArrayList(); + private final String[] strings; + + public DexNode(RootNode root, InputFile input) throws IOException, DecodeException { + this.root = root; + this.dexBuf = input.getDexBuffer(); + + List stringList = dexBuf.strings(); + this.strings = stringList.toArray(new String[stringList.size()]); + } + + public void loadClasses(RootNode root) throws DecodeException { + for (ClassDef cls : dexBuf.classDefs()) { + classes.add(new ClassNode(this, cls)); + } + } + + public List getClasses() { + return classes; + } + + public ClassNode resolveClass(ClassInfo clsInfo) { + return root.resolveClass(clsInfo); + } + + // DexBuffer wrappers + + public String getString(int index) { + return strings[index]; + } + + public ArgType getType(int index) { + return ArgType.parse(getString(dexBuf.typeIds().get(index))); + } + + public List getAllClassesNames() { + List types = dexBuf.typeIds(); + int size = types.size(); + List list = new ArrayList(size); + for (Integer typeId : types) { + String type = getString(typeId); + if (type.length() > 0 && type.charAt(0) == 'L') + list.add(Utils.cleanObjectName(type)); + } + return list; + } + + public MethodId getMethodId(int mthIndex) { + return dexBuf.methodIds().get(mthIndex); + } + + public FieldId getFieldId(int fieldIndex) { + return dexBuf.fieldIds().get(fieldIndex); + } + + public ProtoId getProtoId(int protoIndex) { + return dexBuf.protoIds().get(protoIndex); + } + + public ClassData readClassData(ClassDef cls) { + return dexBuf.readClassData(cls); + } + + public List readParamList(int parametersOffset) { + TypeList paramList = dexBuf.readTypeList(parametersOffset); + List args = new ArrayList(paramList.getTypes().length); + for (short t : paramList.getTypes()) { + args.add(getType(t)); + } + return args; + } + + public Code readCode(Method mth) { + return dexBuf.readCode(mth); + } + + public Section openSection(int offset) { + return dexBuf.open(offset); + } + + public RootNode root() { + return root; + } + + @Override + public String toString() { + return "DEX"; + } + +} diff --git a/src/main/java/jadx/dex/nodes/FieldNode.java b/src/main/java/jadx/dex/nodes/FieldNode.java new file mode 100644 index 000000000..a7e912a7a --- /dev/null +++ b/src/main/java/jadx/dex/nodes/FieldNode.java @@ -0,0 +1,22 @@ +package jadx.dex.nodes; + +import jadx.dex.info.AccessInfo; +import jadx.dex.info.AccessInfo.AFType; +import jadx.dex.info.FieldInfo; + +import com.android.dx.io.ClassData.Field; + +public class FieldNode extends FieldInfo { + + private final AccessInfo accFlags; + + public FieldNode(ClassNode cls, Field field) { + super(cls.dex(), field.getFieldIndex()); + this.accFlags = new AccessInfo(field.getAccessFlags(), AFType.FIELD); + } + + public AccessInfo getAccessFlags() { + return accFlags; + } + +} diff --git a/src/main/java/jadx/dex/nodes/IBlock.java b/src/main/java/jadx/dex/nodes/IBlock.java new file mode 100644 index 000000000..9368d2e00 --- /dev/null +++ b/src/main/java/jadx/dex/nodes/IBlock.java @@ -0,0 +1,8 @@ +package jadx.dex.nodes; + +import java.util.List; + +public interface IBlock extends IContainer { + + public List getInstructions(); +} diff --git a/src/main/java/jadx/dex/nodes/IContainer.java b/src/main/java/jadx/dex/nodes/IContainer.java new file mode 100644 index 000000000..dcb971df6 --- /dev/null +++ b/src/main/java/jadx/dex/nodes/IContainer.java @@ -0,0 +1,11 @@ +package jadx.dex.nodes; + +import jadx.dex.attributes.AttributesList; +import jadx.dex.attributes.IAttributeNode; + +public interface IContainer extends IAttributeNode { + + @Override + public AttributesList getAttributes(); + +} diff --git a/src/main/java/jadx/dex/nodes/ILoadable.java b/src/main/java/jadx/dex/nodes/ILoadable.java new file mode 100644 index 000000000..729f06331 --- /dev/null +++ b/src/main/java/jadx/dex/nodes/ILoadable.java @@ -0,0 +1,19 @@ +package jadx.dex.nodes; + +import jadx.utils.exceptions.DecodeException; + +public interface ILoadable { + + /** + * On demand loading + * + * @throws DecodeException + */ + public void load() throws DecodeException; + + /** + * Free resources + */ + public void unload(); + +} diff --git a/src/main/java/jadx/dex/nodes/IRegion.java b/src/main/java/jadx/dex/nodes/IRegion.java new file mode 100644 index 000000000..6dfe0fd47 --- /dev/null +++ b/src/main/java/jadx/dex/nodes/IRegion.java @@ -0,0 +1,11 @@ +package jadx.dex.nodes; + +import java.util.List; + +public interface IRegion extends IContainer { + + public IRegion getParent(); + + public List getSubBlocks(); + +} diff --git a/src/main/java/jadx/dex/nodes/InsnContainer.java b/src/main/java/jadx/dex/nodes/InsnContainer.java new file mode 100644 index 000000000..db481d9fa --- /dev/null +++ b/src/main/java/jadx/dex/nodes/InsnContainer.java @@ -0,0 +1,20 @@ +package jadx.dex.nodes; + +import jadx.dex.attributes.AttrNode; + +import java.util.List; + +public class InsnContainer extends AttrNode implements IBlock { + + private List insns; + + public void setInstructions(List insns) { + this.insns = insns; + } + + @Override + public List getInstructions() { + return insns; + } + +} diff --git a/src/main/java/jadx/dex/nodes/InsnNode.java b/src/main/java/jadx/dex/nodes/InsnNode.java new file mode 100644 index 000000000..1bfced068 --- /dev/null +++ b/src/main/java/jadx/dex/nodes/InsnNode.java @@ -0,0 +1,162 @@ +package jadx.dex.nodes; + +import jadx.dex.attributes.AttrNode; +import jadx.dex.instructions.InsnType; +import jadx.dex.instructions.args.ArgType; +import jadx.dex.instructions.args.InsnArg; +import jadx.dex.instructions.args.InsnWrapArg; +import jadx.dex.instructions.args.RegisterArg; +import jadx.utils.InsnUtils; +import jadx.utils.Utils; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import com.android.dx.io.instructions.DecodedInstruction; + +public class InsnNode extends AttrNode { + + protected final InsnType insnType; + + private RegisterArg result; + private final List arguments; + protected int offset; + protected int insnHashCode = super.hashCode(); + + protected InsnNode(MethodNode mth, InsnType type) { + this(mth, type, 3); + } + + public InsnNode(MethodNode mth, InsnType type, int argsCount) { + this.insnType = type; + this.offset = -1; + + if (argsCount == 0) + this.arguments = Collections.emptyList(); + else + this.arguments = new ArrayList(argsCount); + } + + public void setResult(RegisterArg res) { + if (res != null) + res.setParentInsn(this); + this.result = res; + } + + public void addArg(InsnArg arg) { + arg.setParentInsn(this); + arguments.add(arg); + } + + public InsnType getType() { + return insnType; + } + + public RegisterArg getResult() { + return result; + } + + public Iterable getArguments() { + return arguments; + } + + public int getArgsCount() { + return arguments.size(); + } + + public InsnArg getArg(int n) { + return arguments.get(n); + } + + public boolean containsArg(RegisterArg arg) { + for (InsnArg a : arguments) { + if (a == arg || (a.isRegister() && ((RegisterArg) a).getRegNum() == arg.getRegNum())) + return true; + } + return false; + } + + public void setArg(int n, InsnArg arg) { + arg.setParentInsn(this); + arguments.set(n, arg); + } + + public boolean replaceArg(InsnArg from, InsnArg to) { + int count = getArgsCount(); + for (int i = 0; i < count; i++) { + if (arguments.get(i) == from) { + // TODO correct remove from use list + // from.getTypedVar().getUseList().remove(from); + setArg(i, to); + return true; + } + } + return false; + } + + protected void addReg(DecodedInstruction insn, int i, ArgType type) { + addArg(InsnArg.reg(insn, i, type)); + } + + protected void addReg(int regNum, ArgType type) { + addArg(InsnArg.reg(regNum, type)); + } + + protected void addLit(long literal, ArgType type) { + addArg(InsnArg.lit(literal, type)); + } + + protected void addLit(DecodedInstruction insn, ArgType type) { + addArg(InsnArg.lit(insn, type)); + } + + public int getOffset() { + return offset; + } + + public void setOffset(int offset) { + this.offset = offset; + } + + public void getRegisterArgs(List list) { + for (InsnArg arg : this.getArguments()) { + if (arg.isRegister()) + list.add((RegisterArg) arg); + else if (arg.isInsnWrap()) + ((InsnWrapArg) arg).getWrapInsn().getRegisterArgs(list); + } + } + + @Override + public String toString() { + return InsnUtils.formatOffset(offset) + ": " + + InsnUtils.insnTypeToString(insnType) + + (result == null ? "" : result + " = ") + + Utils.listToString(arguments); + } + + public void setInsnHashCode(int insnHashCode) { + this.insnHashCode = insnHashCode; + } + + @Override + public int hashCode() { + return insnHashCode; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (hashCode() != obj.hashCode()) return false; + if (!(obj instanceof InsnNode)) return false; + + InsnNode other = (InsnNode) obj; + if (insnType != other.insnType) return false; + if (arguments.size() != other.arguments.size()) return false; + + // TODO !!! finish equals + return true; + } + +} diff --git a/src/main/java/jadx/dex/nodes/MethodNode.java b/src/main/java/jadx/dex/nodes/MethodNode.java new file mode 100644 index 000000000..bea744ce0 --- /dev/null +++ b/src/main/java/jadx/dex/nodes/MethodNode.java @@ -0,0 +1,403 @@ +package jadx.dex.nodes; + +import jadx.dex.attributes.AttrNode; +import jadx.dex.attributes.AttributeFlag; +import jadx.dex.attributes.JumpAttribute; +import jadx.dex.info.AccessInfo; +import jadx.dex.info.AccessInfo.AFType; +import jadx.dex.info.ClassInfo; +import jadx.dex.info.MethodInfo; +import jadx.dex.instructions.GotoNode; +import jadx.dex.instructions.InsnDecoder; +import jadx.dex.instructions.SwitchNode; +import jadx.dex.instructions.args.ArgType; +import jadx.dex.instructions.args.InsnArg; +import jadx.dex.instructions.args.RegisterArg; +import jadx.dex.instructions.mods.ConstructorInsn; +import jadx.dex.nodes.parser.DebugInfoParser; +import jadx.dex.trycatch.ExcHandlerAttr; +import jadx.dex.trycatch.ExceptionHandler; +import jadx.dex.trycatch.TryCatchBlock; +import jadx.utils.Utils; +import jadx.utils.exceptions.DecodeException; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.android.dx.io.ClassData.Method; +import com.android.dx.io.Code; +import com.android.dx.io.Code.CatchHandler; +import com.android.dx.io.Code.Try; + +public class MethodNode extends AttrNode implements ILoadable { + + private final static Logger LOG = LoggerFactory.getLogger(MethodNode.class); + + private final MethodInfo mthInfo; + private final ClassNode parentClass; + private final AccessInfo accFlags; + + private final Method methodData; + private int regsCount; + private List instructions; + private boolean noCode; + + private RegisterArg thisArg; + private List argsList; + + private List blocks; + private BlockNode enterBlock; + private List exitBlocks; + + private ConstructorInsn superCall; + + private IContainer region; + private List exceptionHandlers; + + public MethodNode(ClassNode classNode, Method mth) { + this.mthInfo = MethodInfo.fromDex(classNode.dex(), mth.getMethodIndex()); + this.parentClass = classNode; + this.accFlags = new AccessInfo(mth.getAccessFlags(), AFType.METHOD); + this.methodData = mth; + + if (methodData.getCodeOffset() == 0) { + noCode = true; + regsCount = 0; + initArguments(); + } else { + noCode = false; + } + } + + @Override + public void load() throws DecodeException { + if (noCode) + return; + + try { + DexNode dex = parentClass.dex(); + Code mthCode = dex.readCode(methodData); + regsCount = mthCode.getRegistersSize(); + + InsnDecoder decoder = new InsnDecoder(this, mthCode); + InsnNode[] insnByOffset = decoder.run(); + instructions = new ArrayList(); + for (InsnNode insn : insnByOffset) { + if (insn != null) + instructions.add(insn); + } + ((ArrayList) instructions).trimToSize(); + + initArguments(); + initTryCatches(mthCode, insnByOffset); + initJumps(insnByOffset); + + if (mthCode.getDebugInfoOffset() > 0) { + DebugInfoParser debugInfo = new DebugInfoParser(this, dex.openSection(mthCode.getDebugInfoOffset())); + debugInfo.process(insnByOffset); + } + } catch (Exception e) { + throw new DecodeException(this, "Load method exception", e); + } + } + + @Override + public void unload() { + if (noCode) + return; + + if (instructions != null) instructions.clear(); + // if (blocks != null) blocks.clear(); + // if (exitBlocks != null) exitBlocks.clear(); + blocks = null; + exitBlocks = null; + if (exceptionHandlers != null) exceptionHandlers.clear(); + getAttributes().clear(); + noCode = true; + } + + private void initArguments() { + List args = mthInfo.getArgumentsTypes(); + int pos; + if (!noCode) { + pos = regsCount; + for (ArgType arg : args) + pos -= arg.getRegCount(); + } else { + pos = 2 * args.size() + 1; + } + + if (accFlags.isStatic()) { + thisArg = null; + } else { + thisArg = InsnArg.reg(pos - 1, parentClass.getClassInfo().getType()); + thisArg.getTypedVar().setName("this"); + } + + argsList = new ArrayList(args.size()); + for (ArgType arg : args) { + argsList.add(InsnArg.reg(pos, arg)); + pos += arg.getRegCount(); + } + } + + public List getArguments(boolean includeThis) { + if (includeThis && thisArg != null) { + List list = new ArrayList(argsList.size() + 1); + list.add(thisArg); + list.addAll(argsList); + return list; + } else { + return argsList; + } + } + + public RegisterArg getThisArg() { + return thisArg; + } + + // TODO: args types can change during type resolving => reset and copy back names + @Deprecated + public void resetArgsTypes() { + List modArgs = new ArrayList(argsList); + initArguments(); + + for (int i = 0; i < argsList.size(); i++) { + argsList.get(i).getTypedVar().setName(modArgs.get(i).getTypedVar().getName()); + } + } + + // move to external class + private void initTryCatches(Code mthCode, InsnNode[] insnByOffset) { + CatchHandler[] catchBlocks = mthCode.getCatchHandlers(); + Try[] tries = mthCode.getTries(); + + // Bug in dx library already fixed (Try.getHandlerOffset() replaced by Try.getCatchHandlerIndex()) + // and we don't need this mapping anymore, + // but in maven repository still old version + Set handlerSet = new HashSet(tries.length); + for (Try try_ : tries) { + handlerSet.add(try_.getHandlerOffset()); + } + List handlerList = new ArrayList(catchBlocks.length); + handlerList.addAll(handlerSet); + Collections.sort(handlerList); + handlerSet = null; + // ------------------- + + int hc = 0; + Set addrs = new HashSet(); + List catches = new ArrayList(catchBlocks.length); + + for (CatchHandler catch_ : catchBlocks) { + TryCatchBlock tcBlock = new TryCatchBlock(); + catches.add(tcBlock); + for (int i = 0; i < catch_.getAddresses().length; i++) { + int addr = catch_.getAddresses()[i]; + ClassInfo type = ClassInfo.fromDex(parentClass.dex(), catch_.getTypeIndexes()[i]); + tcBlock.addHandler(this, addr, type); + addrs.add(addr); + hc++; + } + int addr = catch_.getCatchAllAddress(); + if (addr >= 0) { + tcBlock.addHandler(this, addr, null); + addrs.add(addr); + hc++; + } + } + + if (hc > 0 && hc != addrs.size()) { + // resolve nested try blocks: + // inner block contains all handlers from outer block => remove these handlers from inner block + // each handler must be only in one try/catch block + for (TryCatchBlock ct1 : catches) + for (TryCatchBlock ct2 : catches) + if (ct1 != ct2 && ct2.getHandlers().containsAll(ct1.getHandlers())) + for (ExceptionHandler h : ct1.getHandlers()) + ct2.removeHandler(this, h); + } + + // attach EXC_HANDLER attributes to instructions + addrs.clear(); + for (TryCatchBlock ct : catches) { + for (ExceptionHandler eh : ct.getHandlers()) { + int addr = eh.getHandleOffset(); + // assert addrs.add(addr) : "Instruction already contains EXC_HANDLER attribute"; + ExcHandlerAttr ehAttr = new ExcHandlerAttr(ct, eh); + insnByOffset[addr].getAttributes().add(ehAttr); + } + } + + // attach TRY_ENTER, TRY_LEAVE attributes to instructions + for (Try try_ : tries) { + int catchNum = handlerList.indexOf(try_.getHandlerOffset()); + TryCatchBlock block = catches.get(catchNum); + int offset = try_.getStartAddress(); + int end = offset + try_.getInstructionCount() - 1; + + insnByOffset[offset].getAttributes().add(AttributeFlag.TRY_ENTER); + while (offset <= end && offset >= 0) { + block.addInsn(insnByOffset[offset]); + offset = InsnDecoder.getNextInsnOffset(insnByOffset, offset); + } + if (insnByOffset[end] != null) + insnByOffset[end].getAttributes().add(AttributeFlag.TRY_LEAVE); + } + } + + private void initJumps(InsnNode[] insnByOffset) { + for (InsnNode insn : getInstructions()) { + int offset = insn.getOffset(); + switch (insn.getType()) { + case SWITCH: { + SwitchNode sw = (SwitchNode) insn; + for (int target : sw.getTargets()) { + insnByOffset[target].getAttributes().add(new JumpAttribute(offset, target)); + } + // default case + int next = InsnDecoder.getNextInsnOffset(insnByOffset, offset); + if (next != -1) + insnByOffset[next].getAttributes().add(new JumpAttribute(offset, next)); + break; + } + + case IF: + int next = InsnDecoder.getNextInsnOffset(insnByOffset, offset); + if (next != -1) + insnByOffset[next].getAttributes().add(new JumpAttribute(offset, next)); + // no break + case GOTO: + int target = ((GotoNode) insn).getTarget(); + insnByOffset[target].getAttributes().add(new JumpAttribute(offset, target)); + break; + + default: + break; + } + } + } + + public String getName() { + String name = mthInfo.getName(); + if (name.equals(parentClass.getShortName())) + return name + "_"; + else + return name; + } + + public ClassNode getParentClass() { + return parentClass; + } + + public boolean isNoCode() { + return noCode; + } + + public List getInstructions() { + return instructions; + } + + public void initBasicBlocks() { + blocks = new ArrayList(); + exitBlocks = new ArrayList(1); + } + + public void finishBasicBlocks() { + // after filling basic blocks we don't need instructions list anymore + instructions.clear(); + instructions = null; + + ((ArrayList) blocks).trimToSize(); + ((ArrayList) exitBlocks).trimToSize(); + + blocks = Collections.unmodifiableList(blocks); + exitBlocks = Collections.unmodifiableList(exitBlocks); + + for (BlockNode block : blocks) + block.lock(); + } + + public List getBasicBlocks() { + return blocks; + } + + public BlockNode getEnterBlock() { + return enterBlock; + } + + public void setEnterBlock(BlockNode enterBlock) { + this.enterBlock = enterBlock; + } + + public List getExitBlocks() { + return exitBlocks; + } + + public void addExitBlock(BlockNode exitBlock) { + this.exitBlocks.add(exitBlock); + } + + public ExceptionHandler addExceptionHandler(ExceptionHandler handler) { + if (exceptionHandlers == null) { + exceptionHandlers = new ArrayList(2); + } else { + for (ExceptionHandler h : exceptionHandlers) { + if (h == handler || h.getHandleOffset() == handler.getHandleOffset()) + return h; + } + } + exceptionHandlers.add(handler); + return handler; + } + + public List getExceptionHandlers() { + return exceptionHandlers; + } + + public int getRegsCount() { + return regsCount; + } + + public AccessInfo getAccessFlags() { + return accFlags; + } + + public void setSuperCall(ConstructorInsn insn) { + this.superCall = insn; + } + + public ConstructorInsn getSuperCall() { + return this.superCall; + } + + public IContainer getRegion() { + return region; + } + + public void setRegion(IContainer region) { + this.region = region; + } + + public DexNode dex() { + return parentClass.dex(); + } + + public MethodInfo getMethodInfo() { + return mthInfo; + } + + @Override + public String toString() { + return mthInfo.getReturnType() + + " " + parentClass.getFullName() + "." + mthInfo.getName() + + "(" + Utils.listToString(mthInfo.getArgumentsTypes()) + ")"; + } + +} diff --git a/src/main/java/jadx/dex/nodes/RootNode.java b/src/main/java/jadx/dex/nodes/RootNode.java new file mode 100644 index 000000000..701297c08 --- /dev/null +++ b/src/main/java/jadx/dex/nodes/RootNode.java @@ -0,0 +1,103 @@ +package jadx.dex.nodes; + +import jadx.JadxArgs; +import jadx.dex.info.ClassInfo; +import jadx.utils.exceptions.DecodeException; +import jadx.utils.files.InputFile; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class RootNode { + private final static Logger LOG = LoggerFactory.getLogger(RootNode.class); + + private final JadxArgs jadxArgs; + + private List dexNodes; + private final List classes = new ArrayList(); + + private final Set earlyClassList = new HashSet(); + private final Map names = new HashMap(); + + public RootNode(JadxArgs args) { + this.jadxArgs = args; + } + + public void load() throws DecodeException { + List dexFiles = jadxArgs.getInput(); + dexNodes = new ArrayList(dexFiles.size()); + for (InputFile dex : dexFiles) { + DexNode dexNode; + try { + dexNode = new DexNode(this, dex); + } catch (Exception e) { + throw new DecodeException("Error decode file: " + dex, e); + } + dexNodes.add(dexNode); + } + + for (DexNode dexNode : dexNodes) + earlyClassList.addAll(dexNode.getAllClassesNames()); + + for (DexNode dexNode : dexNodes) + dexNode.loadClasses(this); + + for (DexNode dexNode : dexNodes) { + for (ClassNode cls : dexNode.getClasses()) + names.put(cls.getFullName(), cls); + classes.addAll(dexNode.getClasses()); + } + } + + public void init() { + // move inner classes + List inner = new ArrayList(); + for (ClassNode cls : getClasses()) { + if (cls.getClassInfo().isInner()) + inner.add(cls); + } + getClasses().removeAll(inner); + + for (ClassNode cls : inner) { + ClassNode parent = resolveClass(cls.getClassInfo().getParentClass()); + if (parent == null) + LOG.warn("Can't add inner class: {} to {}", cls, cls.getClassInfo().getParentClass()); + else + parent.addInnerClass(cls); + } + inner.clear(); + } + + public List getClasses() { + return classes; + } + + public ClassNode searchClassByName(String fullName) { + return names.get(fullName); + } + + public boolean isClassExists(String fullName) { + return earlyClassList.contains(fullName); + } + + public ClassNode resolveClass(ClassInfo cls) { + String fullName = cls.getFullName(); + ClassNode rCls = searchClassByName(fullName); + return rCls; + } + + public List getDexNodes() { + return dexNodes; + } + + public JadxArgs getJadxArgs() { + return jadxArgs; + } +} diff --git a/src/main/java/jadx/dex/nodes/parser/AnnotationsParser.java b/src/main/java/jadx/dex/nodes/parser/AnnotationsParser.java new file mode 100644 index 000000000..8a7cd8ced --- /dev/null +++ b/src/main/java/jadx/dex/nodes/parser/AnnotationsParser.java @@ -0,0 +1,103 @@ +package jadx.dex.nodes.parser; + +import jadx.dex.attributes.annotations.Annotation; +import jadx.dex.attributes.annotations.Annotation.Visibility; +import jadx.dex.attributes.annotations.AnnotationsList; +import jadx.dex.attributes.annotations.MethodParameters; +import jadx.dex.nodes.ClassNode; +import jadx.dex.nodes.DexNode; +import jadx.dex.nodes.FieldNode; +import jadx.dex.nodes.MethodNode; +import jadx.utils.exceptions.DecodeException; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.android.dx.io.DexBuffer.Section; + +public class AnnotationsParser { + + private final static Logger LOG = LoggerFactory.getLogger(AnnotationsParser.class); + + private final DexNode dex; + + public AnnotationsParser(ClassNode cls, int offset) throws DecodeException { + this.dex = cls.dex(); + Section section = dex.openSection(offset); + + // TODO read as unsigned int + int class_annotations_off = section.readInt(); + int fields_size = section.readInt(); + int annotated_methods_size = section.readInt(); + int annotated_parameters_size = section.readInt(); + + if (class_annotations_off != 0) { + cls.getAttributes().add(readAnnotationSet(class_annotations_off)); + } + + for (int i = 0; i < fields_size; i++) { + FieldNode f = cls.searchFieldById(section.readInt()); + f.getAttributes().add(readAnnotationSet(section.readInt())); + } + + for (int i = 0; i < annotated_methods_size; i++) { + MethodNode m = cls.searchMethodById(section.readInt()); + m.getAttributes().add(readAnnotationSet(section.readInt())); + // LOG.info(m + " " + m.getAttributes()); + } + + for (int i = 0; i < annotated_parameters_size; i++) { + MethodNode mth = cls.searchMethodById(section.readInt()); + // read annotation ref list + Section ss = dex.openSection(section.readInt()); + int size = ss.readInt(); + MethodParameters params = new MethodParameters(size); + for (int j = 0; j < size; j++) { + params.getParamList().add(readAnnotationSet(ss.readInt())); + } + mth.getAttributes().add(params); + } + } + + private AnnotationsList readAnnotationSet(int offset) throws DecodeException { + Section section = dex.openSection(offset); + int size = section.readInt(); + if (size > 100) + section.toString(); + List list = new ArrayList(size); + for (int i = 0; i < size; i++) { + Section anSection = dex.openSection(section.readInt()); + Annotation a = readAnnotation(dex, anSection, true); + list.add(a); + // LOG.debug(" + " + a); + } + return new AnnotationsList(list); + } + + private static final Annotation.Visibility[] visibilities = new Annotation.Visibility[] { + Annotation.Visibility.BUILD, + Annotation.Visibility.RUNTIME, + Annotation.Visibility.SYSTEM + }; + + public static Annotation readAnnotation(DexNode dex, Section s, boolean readVisibility) throws DecodeException { + EncValueParser ep = new EncValueParser(dex, s); + Visibility visibility = null; + if (readVisibility) + visibility = visibilities[s.readByte()]; + + int typeIndex = s.readUleb128(); + int size = s.readUleb128(); + Map values = new HashMap(size); + for (int i = 0; i < size; i++) { + String name = dex.getString(s.readUleb128()); + values.put(name, ep.parseValue()); + } + return new Annotation(visibility, dex.getType(typeIndex), values); + } +} diff --git a/src/main/java/jadx/dex/nodes/parser/DebugInfoParser.java b/src/main/java/jadx/dex/nodes/parser/DebugInfoParser.java new file mode 100644 index 000000000..c7a6e2dea --- /dev/null +++ b/src/main/java/jadx/dex/nodes/parser/DebugInfoParser.java @@ -0,0 +1,157 @@ +package jadx.dex.nodes.parser; + +import jadx.dex.info.LocalVarInfo; +import jadx.dex.instructions.args.InsnArg; +import jadx.dex.instructions.args.RegisterArg; +import jadx.dex.nodes.DexNode; +import jadx.dex.nodes.InsnNode; +import jadx.dex.nodes.MethodNode; +import jadx.utils.exceptions.DecodeException; + +import java.util.List; + +import com.android.dx.io.DexBuffer.Section; + +public class DebugInfoParser { + + private final static int DBG_END_SEQUENCE = 0x00; + private final static int DBG_ADVANCE_PC = 0x01; + private final static int DBG_ADVANCE_LINE = 0x02; + private final static int DBG_START_LOCAL = 0x03; + private final static int DBG_START_LOCAL_EXTENDED = 0x04; + private final static int DBG_END_LOCAL = 0x05; + private final static int DBG_RESTART_LOCAL = 0x06; + private final static int DBG_SET_PROLOGUE_END = 0x07; + private final static int DBG_SET_EPILOGUE_BEGIN = 0x08; + private final static int DBG_SET_FILE = 0x09; + + private final static int DBG_FIRST_SPECIAL = 0x0a; // the smallest special opcode + private final static int DBG_LINE_BASE = -4; // the smallest line number increment + private final static int DBG_LINE_RANGE = 15; // the number of line increments represented + + private final MethodNode mth; + private final Section section; + private final DexNode dex; + + public DebugInfoParser(MethodNode mth, Section section) { + this.mth = mth; + this.section = section; + this.dex = mth.dex(); + } + + public void process(InsnNode[] insnByOffset) throws DecodeException { + int addr = 0; + int line; + // String source_file; + + line = section.readUleb128(); + int param_size = section.readUleb128(); // exclude 'this' + List mthArgs = mth.getArguments(false); + assert param_size == mthArgs.size(); + + for (int i = 0; i < param_size; i++) { + int id = section.readUleb128() - 1; + if (id != DexNode.NO_INDEX) { + String name = dex.getString(id); + mthArgs.get(i).getTypedVar().setName(name); + } + } + + LocalVarInfo[] locals = new LocalVarInfo[mth.getRegsCount()]; + for (RegisterArg arg : mthArgs) { + locals[arg.getRegNum()] = new LocalVarInfo(dex, arg.getRegNum(), + arg.getTypedVar().getName(), arg.getType(), null); + } + + int c = section.readByte() & 0xFF; + while (c != DBG_END_SEQUENCE) { + switch (c) { + case DBG_ADVANCE_PC: + addr += section.readUleb128(); + break; + case DBG_ADVANCE_LINE: + line += section.readSleb128(); + break; + + case DBG_START_LOCAL: { + int regNum = section.readUleb128(); + int nameId = section.readUleb128() - 1; + int type = section.readUleb128() - 1; + locals[regNum] = new LocalVarInfo(dex, regNum, nameId, type, DexNode.NO_INDEX); + locals[regNum].start(addr, line); + break; + } + case DBG_START_LOCAL_EXTENDED: { + int regNum = section.readUleb128(); + int nameId = section.readUleb128() - 1; + int type = section.readUleb128() - 1; + int sign = section.readUleb128() - 1; + locals[regNum] = new LocalVarInfo(dex, regNum, nameId, type, sign); + locals[regNum].start(addr, line); + break; + } + case DBG_RESTART_LOCAL: { + int regNum = section.readUleb128(); + if (locals[regNum] != null) + locals[regNum].start(addr, line); + break; + } + case DBG_END_LOCAL: { + int regNum = section.readUleb128(); + if (locals[regNum] != null) + locals[regNum].end(addr, line); + break; + } + + case DBG_SET_PROLOGUE_END: + break; + case DBG_SET_EPILOGUE_BEGIN: + break; + + case DBG_SET_FILE: + section.readUleb128(); + // source_file = dex.getString(idx); + break; + + default: + if (c >= DBG_FIRST_SPECIAL) { + + int adjusted_opcode = c - DBG_FIRST_SPECIAL; + line += DBG_LINE_BASE + (adjusted_opcode % DBG_LINE_RANGE); + addr += (adjusted_opcode / DBG_LINE_RANGE); + + fillLocals(insnByOffset[addr], locals); + } else { + throw new DecodeException("Unknown debug insn code: " + c); + } + break; + } + + c = section.readByte() & 0xFF; + } + } + + private void fillLocals(InsnNode insn, LocalVarInfo[] locals) { + if (insn == null) + return; + + if (insn.getResult() != null) + merge(insn.getResult(), locals); + + for (InsnArg arg : insn.getArguments()) + merge(arg, locals); + } + + private void merge(InsnArg arg, LocalVarInfo[] locals) { + if (arg.isRegister()) { + int rn = ((RegisterArg) arg).getRegNum(); + + for (LocalVarInfo var : locals) { + if (var != null && !var.isEnd()) { + if (var.getRegNum() == rn) + arg.replace(var); + } + } + } + } +} diff --git a/src/main/java/jadx/dex/nodes/parser/EncValueParser.java b/src/main/java/jadx/dex/nodes/parser/EncValueParser.java new file mode 100644 index 000000000..167770368 --- /dev/null +++ b/src/main/java/jadx/dex/nodes/parser/EncValueParser.java @@ -0,0 +1,87 @@ +package jadx.dex.nodes.parser; + +import jadx.dex.info.FieldInfo; +import jadx.dex.info.MethodInfo; +import jadx.dex.nodes.DexNode; +import jadx.utils.exceptions.DecodeException; + +import java.util.ArrayList; +import java.util.List; + +import com.android.dx.io.DexBuffer.Section; +import com.android.dx.io.EncodedValueReader; +import com.android.dx.util.Leb128Utils; + +public class EncValueParser extends EncodedValueReader { + + private final DexNode dex; + + public EncValueParser(DexNode dex, Section in) { + super(in); + this.dex = dex; + } + + public Object parseValue() throws DecodeException { + int argAndType = in.readByte() & 0xff; + int type = argAndType & 0x1f; + int arg = (argAndType & 0xe0) >> 5; + int size = arg + 1; + + switch (type) { + case ENCODED_NULL: + return null; + + case ENCODED_BOOLEAN: + return Boolean.valueOf(arg == 1); + case ENCODED_BYTE: + return Byte.valueOf((byte) parseNumber(size)); + case ENCODED_SHORT: + return Short.valueOf((short) parseNumber(size)); + case ENCODED_CHAR: + return Character.valueOf((char) parseNumber(size)); + case ENCODED_INT: + return Integer.valueOf((int) parseNumber(size)); + case ENCODED_LONG: + return Long.valueOf(parseNumber(size)); + case ENCODED_FLOAT: + return Float.intBitsToFloat((int) parseNumber(size)); + case ENCODED_DOUBLE: + return Double.longBitsToDouble(parseNumber(size)); + + case ENCODED_STRING: + return dex.getString((int) parseNumber(size)); + + case ENCODED_TYPE: + return dex.getType((int) parseNumber(size)); + + case ENCODED_METHOD: + return MethodInfo.fromDex(dex, (int) parseNumber(size)); + + case ENCODED_FIELD: + case ENCODED_ENUM: + return FieldInfo.fromDex(dex, (int) parseNumber(size)); + + case ENCODED_ARRAY: + int count = Leb128Utils.readUnsignedLeb128(in); + List values = new ArrayList(count); + for (int i = 0; i < count; i++) { + values.add(parseValue()); + } + return values; + + case ENCODED_ANNOTATION: + return AnnotationsParser.readAnnotation(dex, (Section) in, false); + } + throw new DecodeException("Unknown encoded value type: 0x" + Integer.toHexString(type)); + } + + private long parseNumber(int byteCount) { + long result = 0; + int shift = 0; + for (int i = 0; i < byteCount; i++) { + result |= (long) (in.readByte() & 0xff) << shift; + shift += 8; + } + return result; + } +} diff --git a/src/main/java/jadx/dex/nodes/parser/FieldValueAttr.java b/src/main/java/jadx/dex/nodes/parser/FieldValueAttr.java new file mode 100644 index 000000000..23e7c3f4a --- /dev/null +++ b/src/main/java/jadx/dex/nodes/parser/FieldValueAttr.java @@ -0,0 +1,27 @@ +package jadx.dex.nodes.parser; + +import jadx.dex.attributes.AttributeType; +import jadx.dex.attributes.IAttribute; + +public class FieldValueAttr implements IAttribute { + + private final Object value; + + public FieldValueAttr(Object value) { + this.value = value; + } + + @Override + public AttributeType getType() { + return AttributeType.FIELD_VALUE; + } + + public Object getValue() { + return value; + } + + @Override + public String toString() { + return "V=" + value; + } +} diff --git a/src/main/java/jadx/dex/nodes/parser/StaticValuesParser.java b/src/main/java/jadx/dex/nodes/parser/StaticValuesParser.java new file mode 100644 index 000000000..774a79ae1 --- /dev/null +++ b/src/main/java/jadx/dex/nodes/parser/StaticValuesParser.java @@ -0,0 +1,27 @@ +package jadx.dex.nodes.parser; + +import jadx.dex.nodes.DexNode; +import jadx.dex.nodes.FieldNode; +import jadx.utils.exceptions.DecodeException; + +import java.util.List; + +import com.android.dx.io.DexBuffer.Section; +import com.android.dx.util.Leb128Utils; + +public class StaticValuesParser extends EncValueParser { + + public StaticValuesParser(DexNode dex, Section in) { + super(dex, in); + } + + public void processFields(List fields) throws DecodeException { + int size = Leb128Utils.readUnsignedLeb128(in); + visitArray(size); + + for (int i = 0; i < size; i++) { + Object value = parseValue(); + fields.get(i).getAttributes().add(new FieldValueAttr(value)); + } + } +} diff --git a/src/main/java/jadx/dex/regions/AbstractRegion.java b/src/main/java/jadx/dex/regions/AbstractRegion.java new file mode 100644 index 000000000..eeb1124fb --- /dev/null +++ b/src/main/java/jadx/dex/regions/AbstractRegion.java @@ -0,0 +1,19 @@ +package jadx.dex.regions; + +import jadx.dex.attributes.AttrNode; +import jadx.dex.nodes.IRegion; + +public abstract class AbstractRegion extends AttrNode implements IRegion { + + private final IRegion parent; + + public AbstractRegion(IRegion parent) { + this.parent = parent; + } + + @Override + public IRegion getParent() { + return parent; + } + +} diff --git a/src/main/java/jadx/dex/regions/IfRegion.java b/src/main/java/jadx/dex/regions/IfRegion.java new file mode 100644 index 000000000..97d123c25 --- /dev/null +++ b/src/main/java/jadx/dex/regions/IfRegion.java @@ -0,0 +1,62 @@ +package jadx.dex.regions; + +import jadx.dex.instructions.IfNode; +import jadx.dex.nodes.BlockNode; +import jadx.dex.nodes.IContainer; +import jadx.dex.nodes.IRegion; + +import java.util.ArrayList; +import java.util.List; + +public final class IfRegion extends AbstractRegion { + + protected BlockNode header; + protected IContainer thenRegion; + protected IContainer elseRegion; + + public IfRegion(IRegion parent, BlockNode header) { + super(parent); + assert header.getInstructions().size() == 1; + this.header = header; + } + + public IfNode getIfInsn() { + return (IfNode) header.getInstructions().get(0); + } + + public BlockNode getHeader() { + return header; + } + + public IContainer getThenRegion() { + return thenRegion; + } + + public void setThenRegion(IContainer thenRegion) { + this.thenRegion = thenRegion; + } + + public IContainer getElseRegion() { + return elseRegion; + } + + public void setElseRegion(IContainer elseRegion) { + this.elseRegion = elseRegion; + } + + @Override + public List getSubBlocks() { + ArrayList all = new ArrayList(3); + all.add(header); + if (thenRegion != null) + all.add(thenRegion); + if (elseRegion != null) + all.add(elseRegion); + return all; + } + + @Override + public String toString() { + return "IF(" + header + ") then " + thenRegion + " else " + elseRegion; + } +} diff --git a/src/main/java/jadx/dex/regions/LoopRegion.java b/src/main/java/jadx/dex/regions/LoopRegion.java new file mode 100644 index 000000000..8e0fe6eba --- /dev/null +++ b/src/main/java/jadx/dex/regions/LoopRegion.java @@ -0,0 +1,122 @@ +package jadx.dex.regions; + +import jadx.dex.instructions.IfNode; +import jadx.dex.instructions.args.RegisterArg; +import jadx.dex.nodes.BlockNode; +import jadx.dex.nodes.IContainer; +import jadx.dex.nodes.IRegion; +import jadx.dex.nodes.InsnNode; + +import java.util.ArrayList; +import java.util.List; + +public final class LoopRegion extends AbstractRegion { + + // loop header contains one 'if' insn, equals null for infinite loop + private final BlockNode conditionBlock; + // instruction which must be executed before condition in every loop + private BlockNode preCondition = null; + private IContainer body; + private final boolean conditionAtEnd; + + public LoopRegion(IRegion parent, BlockNode header, boolean reversed) { + super(parent); + this.conditionBlock = header; + this.conditionAtEnd = reversed; + } + + public IfNode getIfInsn() { + return (IfNode) conditionBlock.getInstructions().get(conditionBlock.getInstructions().size() - 1); + } + + public BlockNode getHeader() { + return conditionBlock; + } + + public BlockNode getConditionBlock() { + return conditionBlock; + } + + public IContainer getBody() { + return body; + } + + public void setBody(IContainer body) { + this.body = body; + } + + public boolean isConditionAtEnd() { + return conditionAtEnd; + } + + /** + * Set instructions which must be executed before condition in every loop + */ + public void setPreCondition(BlockNode preCondition) { + this.preCondition = preCondition; + } + + /** + * Check if pre-conditions can be inlined into loop condition + */ + public boolean checkPreCondition() { + List insns = preCondition.getInstructions(); + if (insns.isEmpty()) + return true; + + IfNode ifInsn = getIfInsn(); + int size = insns.size(); + for (int i = 0; i < size; i++) { + InsnNode insn = insns.get(i); + if (insn.getResult() == null) { + return false; + } else { + RegisterArg res = insn.getResult(); + if (res.getTypedVar().getUseList().size() > 2) + return false; + + boolean found = false; + // search result arg in other insns + for (int j = i + 1; j < size; j++) { + if (insns.get(i).containsArg(res)) + found = true; + } + // or in if insn + if (!found && ifInsn.containsArg(res)) + found = true; + + if (!found) + return false; + } + } + return true; + } + + /** + * Move all preCondition block instructions before conditionBlock instructions + */ + public void mergePreCondition() { + if (preCondition != null && conditionBlock != null) { + preCondition.getInstructions().addAll(conditionBlock.getInstructions()); + conditionBlock.getInstructions().clear(); + conditionBlock.getInstructions().addAll(preCondition.getInstructions()); + preCondition.getInstructions().clear(); + } + } + + @Override + public List getSubBlocks() { + List all = new ArrayList(3); + if (preCondition != null) + all.add(preCondition); + if (conditionBlock != null) + all.add(conditionBlock); + all.add(body); + return all; + } + + @Override + public String toString() { + return "LOOP"; + } +} diff --git a/src/main/java/jadx/dex/regions/Region.java b/src/main/java/jadx/dex/regions/Region.java new file mode 100644 index 000000000..ee70eb6ba --- /dev/null +++ b/src/main/java/jadx/dex/regions/Region.java @@ -0,0 +1,38 @@ +package jadx.dex.regions; + +import jadx.dex.nodes.BlockNode; +import jadx.dex.nodes.IContainer; +import jadx.dex.nodes.IRegion; + +import java.util.ArrayList; +import java.util.List; + +public final class Region extends AbstractRegion { + + private final List blocks; + + public Region(IRegion parent) { + super(parent); + this.blocks = new ArrayList(1); + } + + @Override + public List getSubBlocks() { + return blocks; + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("R:"); + sb.append(blocks.size()); + if (blocks.size() != 0) { + for (IContainer cont : blocks) { + if (cont instanceof BlockNode) + sb.append(((BlockNode) cont).getId()); + } + } + return sb.toString(); + } + +} diff --git a/src/main/java/jadx/dex/regions/SwitchRegion.java b/src/main/java/jadx/dex/regions/SwitchRegion.java new file mode 100644 index 000000000..f8bfefe1f --- /dev/null +++ b/src/main/java/jadx/dex/regions/SwitchRegion.java @@ -0,0 +1,64 @@ +package jadx.dex.regions; + +import jadx.dex.nodes.BlockNode; +import jadx.dex.nodes.IContainer; +import jadx.dex.nodes.IRegion; + +import java.util.ArrayList; +import java.util.List; + +public final class SwitchRegion extends AbstractRegion { + + private final BlockNode header; + + private final List> keys; + private final List cases; + private IContainer defCase; + + public SwitchRegion(IRegion parent, BlockNode header) { + super(parent); + this.header = header; + this.keys = new ArrayList>(); + this.cases = new ArrayList(); + } + + public BlockNode getHeader() { + return header; + } + + public void addCase(List keysList, IContainer c) { + keys.add(keysList); + cases.add(c); + } + + public void setDefaultCase(IContainer block) { + defCase = block; + } + + public IContainer getDefaultCase() { + return defCase; + } + + public List> getKeys() { + return keys; + } + + public List getCases() { + return cases; + } + + @Override + public List getSubBlocks() { + List all = new ArrayList(cases.size() + 2); + all.add(header); + all.addAll(cases); + if (defCase != null) + all.add(defCase); + return all; + } + + @Override + public String toString() { + return "Switch: " + cases.size() + ", default: " + defCase; + } +} diff --git a/src/main/java/jadx/dex/regions/SynchronizedRegion.java b/src/main/java/jadx/dex/regions/SynchronizedRegion.java new file mode 100644 index 000000000..f29f020bb --- /dev/null +++ b/src/main/java/jadx/dex/regions/SynchronizedRegion.java @@ -0,0 +1,37 @@ +package jadx.dex.regions; + +import jadx.dex.instructions.args.RegisterArg; +import jadx.dex.nodes.IContainer; +import jadx.dex.nodes.IRegion; + +import java.util.List; + +public final class SynchronizedRegion extends AbstractRegion { + + private final RegisterArg arg; + private final Region region; + + public SynchronizedRegion(IRegion parent, RegisterArg arg) { + super(parent); + this.arg = arg; + this.region = new Region(this); + } + + public RegisterArg getArg() { + return arg; + } + + public Region getRegion() { + return region; + } + + @Override + public List getSubBlocks() { + return region.getSubBlocks(); + } + + @Override + public String toString() { + return "Synchronized:" + region; + } +} diff --git a/src/main/java/jadx/dex/trycatch/CatchAttr.java b/src/main/java/jadx/dex/trycatch/CatchAttr.java new file mode 100644 index 000000000..dfdc2c604 --- /dev/null +++ b/src/main/java/jadx/dex/trycatch/CatchAttr.java @@ -0,0 +1,28 @@ +package jadx.dex.trycatch; + +import jadx.dex.attributes.AttributeType; +import jadx.dex.attributes.IAttribute; + +public class CatchAttr implements IAttribute { + + private final TryCatchBlock tryBlock; + + public CatchAttr(TryCatchBlock block) { + this.tryBlock = block; + } + + @Override + public AttributeType getType() { + return AttributeType.CATCH_BLOCK; + } + + public TryCatchBlock getTryBlock() { + return tryBlock; + } + + @Override + public String toString() { + return tryBlock.toString(); + } + +} diff --git a/src/main/java/jadx/dex/trycatch/ExcHandlerAttr.java b/src/main/java/jadx/dex/trycatch/ExcHandlerAttr.java new file mode 100644 index 000000000..5330f869e --- /dev/null +++ b/src/main/java/jadx/dex/trycatch/ExcHandlerAttr.java @@ -0,0 +1,33 @@ +package jadx.dex.trycatch; + +import jadx.dex.attributes.AttributeType; +import jadx.dex.attributes.IAttribute; + +public class ExcHandlerAttr implements IAttribute { + + private final TryCatchBlock tryBlock; + private final ExceptionHandler handler; + + public ExcHandlerAttr(TryCatchBlock block, ExceptionHandler handler) { + this.tryBlock = block; + this.handler = handler; + } + + @Override + public AttributeType getType() { + return AttributeType.EXC_HANDLER; + } + + public TryCatchBlock getTryBlock() { + return tryBlock; + } + + public ExceptionHandler getHandler() { + return handler; + } + + @Override + public String toString() { + return "ExcHandler: " + (handler.isCatchAll() ? "all" : handler.getCatchType()); + } +} diff --git a/src/main/java/jadx/dex/trycatch/ExceptionHandler.java b/src/main/java/jadx/dex/trycatch/ExceptionHandler.java new file mode 100644 index 000000000..e86d4b206 --- /dev/null +++ b/src/main/java/jadx/dex/trycatch/ExceptionHandler.java @@ -0,0 +1,95 @@ +package jadx.dex.trycatch; + +import jadx.Consts; +import jadx.dex.info.ClassInfo; +import jadx.dex.instructions.args.InsnArg; +import jadx.dex.nodes.BlockNode; +import jadx.dex.nodes.IContainer; +import jadx.utils.InsnUtils; + +import java.util.ArrayList; +import java.util.List; + +public class ExceptionHandler { + + private final ClassInfo catchType; + private final int handleOffset; + + private BlockNode handleBlock; + private final List blocks = new ArrayList(); + private IContainer handlerRegion; + private InsnArg arg; + + public ExceptionHandler(int addr, ClassInfo type) { + this.handleOffset = addr; + this.catchType = type; + } + + public ClassInfo getCatchType() { + return catchType; + } + + public boolean isCatchAll() { + return catchType == null || catchType.getFullName().equals(Consts.CLASS_THROWABLE); + } + + public int getHandleOffset() { + return handleOffset; + } + + public BlockNode getHandleBlock() { + return handleBlock; + } + + public void setHandleBlock(BlockNode handleBlock) { + this.handleBlock = handleBlock; + } + + public List getBlocks() { + return blocks; + } + + public void addBlock(BlockNode node) { + blocks.add(node); + } + + public IContainer getHandlerRegion() { + return handlerRegion; + } + + public void setHandlerRegion(IContainer handlerRegion) { + this.handlerRegion = handlerRegion; + } + + public InsnArg getArg() { + return arg; + } + + public void setArg(InsnArg arg) { + this.arg = arg; + } + + @Override + public int hashCode() { + return 31 * (catchType == null ? 0 : catchType.hashCode()) + handleOffset; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (obj == null) return false; + if (getClass() != obj.getClass()) return false; + ExceptionHandler other = (ExceptionHandler) obj; + if (catchType == null) { + if (other.catchType != null) return false; + } else if (!catchType.equals(other.catchType)) return false; + return handleOffset == other.handleOffset; + } + + @Override + public String toString() { + return (catchType == null ? "all" : catchType.getShortName()) + + " -> " + InsnUtils.formatOffset(handleOffset); + } + +} diff --git a/src/main/java/jadx/dex/trycatch/SplitterBlockAttr.java b/src/main/java/jadx/dex/trycatch/SplitterBlockAttr.java new file mode 100644 index 000000000..340c62d41 --- /dev/null +++ b/src/main/java/jadx/dex/trycatch/SplitterBlockAttr.java @@ -0,0 +1,29 @@ +package jadx.dex.trycatch; + +import jadx.dex.attributes.AttributeType; +import jadx.dex.attributes.IAttribute; +import jadx.dex.nodes.BlockNode; + +public class SplitterBlockAttr implements IAttribute { + + private final BlockNode block; + + public SplitterBlockAttr(BlockNode block) { + this.block = block; + } + + public BlockNode getBlock() { + return block; + } + + @Override + public AttributeType getType() { + return AttributeType.SPLITTER_BLOCK; + } + + @Override + public String toString() { + return "Splitter: " + block; + } + +} diff --git a/src/main/java/jadx/dex/trycatch/TryCatchBlock.java b/src/main/java/jadx/dex/trycatch/TryCatchBlock.java new file mode 100644 index 000000000..f271f4fae --- /dev/null +++ b/src/main/java/jadx/dex/trycatch/TryCatchBlock.java @@ -0,0 +1,131 @@ +package jadx.dex.trycatch; + +import jadx.dex.attributes.AttributeType; +import jadx.dex.info.ClassInfo; +import jadx.dex.nodes.BlockNode; +import jadx.dex.nodes.IContainer; +import jadx.dex.nodes.InsnNode; +import jadx.dex.nodes.MethodNode; +import jadx.utils.Utils; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; + +public class TryCatchBlock { + + private final List handlers; + private IContainer finalBlock; + + // references for fast remove/modify + private final List insns; + private final CatchAttr attr; + + public TryCatchBlock() { + handlers = new ArrayList(2); + insns = new ArrayList(); + attr = new CatchAttr(this); + } + + public Collection getHandlers() { + return Collections.unmodifiableCollection(handlers); + } + + public ExceptionHandler addHandler(MethodNode mth, int addr, ClassInfo type) { + ExceptionHandler handler = new ExceptionHandler(addr, type); + handler = mth.addExceptionHandler(handler); + handlers.add(handler); + return handler; + } + + public void removeHandler(MethodNode mth, ExceptionHandler handler) { + for (int i = 0; i < handlers.size(); i++) { + if (handlers.get(i) == handler) { + handlers.remove(i); + break; + } + } + if (handlers.isEmpty()) { + removeWholeBlock(mth); + } + } + + private void removeWholeBlock(MethodNode mth) { + // self destruction + for (InsnNode insn : insns) + insn.getAttributes().remove(AttributeType.CATCH_BLOCK); + + insns.clear(); + for (BlockNode block : mth.getBasicBlocks()) { + block.getAttributes().remove(AttributeType.CATCH_BLOCK); + } + } + + public void addInsn(InsnNode insn) { + insns.add(insn); + insn.getAttributes().add(attr); + } + + public void removeInsn(InsnNode insn) { + insns.remove(insn); + insn.getAttributes().remove(attr.getType()); + } + + public Iterable getInsns() { + return insns; + } + + public int getInsnsCount() { + return insns.size(); + } + + public CatchAttr getCatchAttr() { + return attr; + } + + public IContainer getFinalBlock() { + return finalBlock; + } + + public void setFinalBlock(IContainer finalBlock) { + this.finalBlock = finalBlock; + } + + public void merge(MethodNode mth, TryCatchBlock tryBlock) { + for (InsnNode insn : tryBlock.getInsns()) + this.addInsn(insn); + + this.handlers.addAll(tryBlock.getHandlers()); + + // clear + tryBlock.handlers.clear(); + tryBlock.removeWholeBlock(mth); + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((handlers == null) ? 0 : handlers.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (obj == null) return false; + if (getClass() != obj.getClass()) return false; + TryCatchBlock other = (TryCatchBlock) obj; + if (handlers == null) { + if (other.handlers != null) return false; + } else if (!handlers.equals(other.handlers)) return false; + return true; + } + + @Override + public String toString() { + return "Catch:{ " + Utils.listToString(handlers) + " }"; + } + +} diff --git a/src/main/java/jadx/dex/visitors/AbstractVisitor.java b/src/main/java/jadx/dex/visitors/AbstractVisitor.java new file mode 100644 index 000000000..40bb30941 --- /dev/null +++ b/src/main/java/jadx/dex/visitors/AbstractVisitor.java @@ -0,0 +1,18 @@ +package jadx.dex.visitors; + +import jadx.dex.nodes.ClassNode; +import jadx.dex.nodes.MethodNode; +import jadx.utils.exceptions.JadxException; + +public class AbstractVisitor implements IDexTreeVisitor { + + @Override + public boolean visit(ClassNode cls) throws JadxException { + return true; + } + + @Override + public void visit(MethodNode mth) throws JadxException { + } + +} diff --git a/src/main/java/jadx/dex/visitors/BlockMakerVisitor.java b/src/main/java/jadx/dex/visitors/BlockMakerVisitor.java new file mode 100644 index 000000000..de9dc64a2 --- /dev/null +++ b/src/main/java/jadx/dex/visitors/BlockMakerVisitor.java @@ -0,0 +1,393 @@ +package jadx.dex.visitors; + +import jadx.dex.attributes.AttributeFlag; +import jadx.dex.attributes.AttributeType; +import jadx.dex.attributes.AttributesList; +import jadx.dex.attributes.IAttribute; +import jadx.dex.attributes.JumpAttribute; +import jadx.dex.attributes.LoopAttr; +import jadx.dex.instructions.IfNode; +import jadx.dex.instructions.InsnType; +import jadx.dex.instructions.args.InsnArg; +import jadx.dex.instructions.args.RegisterArg; +import jadx.dex.nodes.BlockNode; +import jadx.dex.nodes.InsnNode; +import jadx.dex.nodes.MethodNode; +import jadx.dex.trycatch.CatchAttr; +import jadx.dex.trycatch.ExceptionHandler; +import jadx.dex.trycatch.SplitterBlockAttr; +import jadx.utils.exceptions.JadxRuntimeException; + +import java.util.ArrayList; +import java.util.BitSet; +import java.util.EnumSet; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class BlockMakerVisitor extends AbstractVisitor { + private final static Logger LOG = LoggerFactory.getLogger(BlockMakerVisitor.class); + + // leave these instructions alone in block node + private final static Set separateInsns = EnumSet.of( + InsnType.IF, + InsnType.SWITCH, + InsnType.MONITOR_ENTER, + InsnType.MONITOR_EXIT); + + @Override + public void visit(MethodNode mth) { + if (mth.isNoCode()) + return; + + mth.initBasicBlocks(); + BlockNode.initialID = 0; + + InsnNode prevInsn = null; + Map blocksMap = new HashMap(); + BlockNode curBlock = startNewBlock(mth, 0); + mth.setEnterBlock(curBlock); + + // split into blocks + for (InsnNode insn : mth.getInstructions()) { + boolean startNew = false; + if (prevInsn != null) { + InsnType type = prevInsn.getType(); + if (type == InsnType.RETURN + || type == InsnType.GOTO + || type == InsnType.THROW + || separateInsns.contains(type)) { + + if (type == InsnType.RETURN || type == InsnType.THROW) + mth.addExitBlock(curBlock); + + BlockNode block = startNewBlock(mth, insn.getOffset()); + if (type == InsnType.MONITOR_ENTER || type == InsnType.MONITOR_EXIT) + connect(curBlock, block); + curBlock = block; + startNew = true; + } else { + type = insn.getType(); + startNew = separateInsns.contains(type); + + List pjumps = prevInsn.getAttributes().getAll(AttributeType.JUMP); + if (pjumps.size() > 0) { + for (IAttribute j : pjumps) { + JumpAttribute jump = (JumpAttribute) j; + if (jump.getSrc() == prevInsn.getOffset()) + startNew = true; + } + } + + List cjumps = insn.getAttributes().getAll(AttributeType.JUMP); + if (cjumps.size() > 0) { + for (IAttribute j : cjumps) { + JumpAttribute jump = (JumpAttribute) j; + if (jump.getDest() == insn.getOffset()) + startNew = true; + } + } + + // split 'do-while' block (last instruction: 'if', target this block) + if (type == InsnType.IF) { + IfNode ifs = (IfNode) (insn); + BlockNode targBlock = blocksMap.get(ifs.getTarget()); + if (targBlock == curBlock) + startNew = true; + } + + if (startNew) { + BlockNode block = startNewBlock(mth, insn.getOffset()); + connect(curBlock, block); + curBlock = block; + } + } + } + + // for try/catch make empty block for connect handlers + if (insn.getAttributes().contains(AttributeFlag.TRY_ENTER)) { + BlockNode block; + if (insn.getOffset() != 0 && !startNew) { + block = startNewBlock(mth, insn.getOffset()); + connect(curBlock, block); + curBlock = block; + } + blocksMap.put(insn.getOffset(), curBlock); + + // add this insn in new block + block = startNewBlock(mth, -1); + curBlock.getAttributes().add(AttributeFlag.SYNTHETIC); + block.getAttributes().add(new SplitterBlockAttr(curBlock)); + connect(curBlock, block); + curBlock = block; + } else { + blocksMap.put(insn.getOffset(), curBlock); + } + + curBlock.getInstructions().add(insn); + prevInsn = insn; + } + + // setup missing connections + for (BlockNode block : mth.getBasicBlocks()) { + for (InsnNode insn : block.getInstructions()) { + List jumps = insn.getAttributes().getAll(AttributeType.JUMP); + for (IAttribute attr : jumps) { + JumpAttribute jump = (JumpAttribute) attr; + BlockNode srcBlock = getBlock(mth, jump.getSrc(), blocksMap); + BlockNode thisblock = getBlock(mth, jump.getDest(), blocksMap); + connect(srcBlock, thisblock); + } + + // connect exception handlers + CatchAttr catches = (CatchAttr) insn.getAttributes().get(AttributeType.CATCH_BLOCK); + if (catches != null) { + // get synthetic block for handlers + IAttribute spl = block.getAttributes().get(AttributeType.SPLITTER_BLOCK); + if (spl != null) { + BlockNode connBlock = ((SplitterBlockAttr) spl).getBlock(); + for (ExceptionHandler h : catches.getTryBlock().getHandlers()) { + BlockNode destBlock = getBlock(mth, h.getHandleOffset(), blocksMap); + // skip self loop in handler + if (connBlock != destBlock) + // && !connBlock.getPredecessors().contains(destBlock)) + connect(connBlock, destBlock); + } + } + } + } + } + + computeDominators(mth); + + for (BlockNode block : mth.getBasicBlocks()) { + markReturnBlocks(mth, block); + } + + int i = 0; + while (modifyBlocksTree(mth)) { + // revert calculations + cleanDomTree(mth); + // recalculate dominators tree + computeDominators(mth); + + i++; + if (i > 100) + throw new AssertionError("Can't fix method cfg: " + mth); + } + + BlockProcessingHelper.visit(mth); + + mth.finishBasicBlocks(); + } + + private static BlockNode getBlock(MethodNode mth, int offset, Map blocksMap) { + BlockNode block = blocksMap.get(offset); + assert block != null; + return block; + } + + private static void connect(BlockNode from, BlockNode to) { + if (!from.getSuccessors().contains(to)) + from.getSuccessors().add(to); + if (!to.getPredecessors().contains(from)) + to.getPredecessors().add(from); + } + + private static void removeConnection(BlockNode from, BlockNode to) { + from.getSuccessors().remove(to); + to.getPredecessors().remove(from); + } + + private static BlockNode startNewBlock(MethodNode mth, int offset) { + BlockNode block = new BlockNode(mth, offset); + mth.getBasicBlocks().add(block); + return block; + } + + private static void computeDominators(MethodNode mth) { + int nBlocks = mth.getBasicBlocks().size(); + for (int i = 0; i < nBlocks; i++) { + BlockNode block = mth.getBasicBlocks().get(i); + block.setId(i); + block.setDoms(new BitSet(nBlocks)); + block.getDoms().set(0, nBlocks); + } + + BlockNode entryBlock = mth.getEnterBlock(); + entryBlock.getDoms().clear(); + entryBlock.getDoms().set(entryBlock.getId()); + + BitSet dset = new BitSet(nBlocks); + boolean changed; + do { + changed = false; + for (BlockNode block : mth.getBasicBlocks()) { + if (block == entryBlock) + continue; + + BitSet d = block.getDoms(); + + dset.clear(); + dset.or(d); + for (BlockNode pred : block.getPredecessors()) { + d.and(pred.getDoms()); + } + d.set(block.getId()); + + if (!d.equals(dset)) + changed = true; + } + } while (changed); + + markLoops(mth); + + // clear self dominance + for (BlockNode block : mth.getBasicBlocks()) { + block.getDoms().clear(block.getId()); + } + + // calculate immediate dominators + for (BlockNode block : mth.getBasicBlocks()) { + if (block == entryBlock) + continue; + + if (block.getPredecessors().size() == 1) { + block.setIDom(block.getPredecessors().get(0)); + } else { + BitSet bs = new BitSet(block.getDoms().length()); + bs.or(block.getDoms()); + + for (int i = bs.nextSetBit(0); i >= 0; i = bs.nextSetBit(i + 1)) { + BlockNode dom = mth.getBasicBlocks().get(i); + bs.andNot(dom.getDoms()); + } + + int c = bs.cardinality(); + if (c == 1) { + int id = bs.nextSetBit(0); + BlockNode idom = mth.getBasicBlocks().get(id); + block.setIDom(idom); + idom.getDominatesOn().add(block); + } else if (block != entryBlock) { + throw new JadxRuntimeException("Can't find immediate dominator for block " + block + + " in " + bs + " prec:" + block.getPredecessors()); + } + } + } + } + + private static void markLoops(MethodNode mth) { + for (BlockNode block : mth.getBasicBlocks()) { + for (BlockNode succ : block.getSuccessors()) { + // Every successor that dominates its predecessor + // must be the header of a loop. + // That is, block -> succ is a back edge. + if (block.getDoms().get(succ.getId())) { + succ.getAttributes().add(AttributeFlag.LOOP_START); + block.getAttributes().add(AttributeFlag.LOOP_END); + + LoopAttr loop = new LoopAttr(succ, block); + succ.getAttributes().add(loop); + block.getAttributes().add(loop); + } + } + } + } + + private static void markReturnBlocks(MethodNode mth, BlockNode block) { + if (block.getInstructions().size() == 1) { + if (block.getInstructions().get(0).getType() == InsnType.RETURN) + block.getAttributes().add(AttributeFlag.RETURN); + } + } + + private static boolean modifyBlocksTree(MethodNode mth) { + for (BlockNode block : mth.getBasicBlocks()) { + if (block.getPredecessors().isEmpty() && block != mth.getEnterBlock()) { + throw new JadxRuntimeException("Unreachable block: " + block); + } + + // check loops + List loops = block.getAttributes().getAll(AttributeType.LOOP); + if (loops.size() > 1) { + boolean oneHeader = true; + for (IAttribute a : loops) { + LoopAttr loop = (LoopAttr) a; + if (loop.getStart() != block) { + oneHeader = false; + break; + } + } + if (oneHeader) { + // several back edges connected to one loop header => make additional block + BlockNode newLoopHeader = startNewBlock(mth, block.getStartOffset()); + connect(newLoopHeader, block); + for (IAttribute a : loops) { + LoopAttr la = (LoopAttr) a; + BlockNode node = la.getEnd(); + removeConnection(node, block); + connect(node, newLoopHeader); + } + return true; + } + } + + // splice return block + if (block.getAttributes().contains(AttributeFlag.RETURN) + && block.getPredecessors().size() > 1 + && !block.getInstructions().get(0).getAttributes().contains(AttributeType.CATCH_BLOCK)) { + List preds = new ArrayList(block.getPredecessors()); + + BlockNode origRetBlock = block; + origRetBlock.getPredecessors().clear(); + origRetBlock.getPredecessors().add(preds.get(0)); + preds.remove(0); + + InsnNode origReturnInsn = origRetBlock.getInstructions().get(0); + RegisterArg retArg = null; + if (origReturnInsn.getArgsCount() != 0) + retArg = (RegisterArg) origReturnInsn.getArg(0); + + for (BlockNode pred : preds) { + pred.getSuccessors().remove(origRetBlock); + // make copy of return block and connect to predecessor + BlockNode newRetBlock = startNewBlock(mth, origRetBlock.getStartOffset()); + + InsnNode ret = new InsnNode(mth, InsnType.RETURN, 1); + if (retArg != null) + ret.addArg(InsnArg.reg(retArg.getRegNum(), retArg.getType())); + ret.getAttributes().addAll(origReturnInsn.getAttributes()); + + newRetBlock.getInstructions().add(ret); + newRetBlock.getAttributes().add(AttributeFlag.RETURN); + + connect(pred, newRetBlock); + mth.addExitBlock(newRetBlock); + } + return true; + } + + // TODO detect ternary operator + } + return false; + } + + private void cleanDomTree(MethodNode mth) { + for (BlockNode block : mth.getBasicBlocks()) { + AttributesList attrs = block.getAttributes(); + attrs.remove(AttributeType.LOOP); + attrs.remove(AttributeFlag.LOOP_START); + attrs.remove(AttributeFlag.LOOP_END); + + block.setDoms(null); + block.setIDom(null); + block.getDominatesOn().clear(); + } + } + +} diff --git a/src/main/java/jadx/dex/visitors/BlockProcessingHelper.java b/src/main/java/jadx/dex/visitors/BlockProcessingHelper.java new file mode 100644 index 000000000..e747bd741 --- /dev/null +++ b/src/main/java/jadx/dex/visitors/BlockProcessingHelper.java @@ -0,0 +1,127 @@ +package jadx.dex.visitors; + +import jadx.dex.attributes.AttributeType; +import jadx.dex.instructions.InsnType; +import jadx.dex.instructions.args.ArgType; +import jadx.dex.instructions.args.RegisterArg; +import jadx.dex.nodes.BlockNode; +import jadx.dex.nodes.InsnNode; +import jadx.dex.nodes.MethodNode; +import jadx.dex.trycatch.CatchAttr; +import jadx.dex.trycatch.ExcHandlerAttr; +import jadx.dex.trycatch.ExceptionHandler; +import jadx.utils.BlockUtils; + +public class BlockProcessingHelper { + + public static void visit(MethodNode mth) { + if (mth.isNoCode()) + return; + + for (BlockNode block : mth.getBasicBlocks()) { + markExceptionHandlers(mth, block); + } + for (BlockNode block : mth.getBasicBlocks()) { + block.updateCleanSuccessors(); + } + for (BlockNode block : mth.getBasicBlocks()) { + processExceptionHandlers(mth, block); + } + for (BlockNode block : mth.getBasicBlocks()) { + processTryCatchBlocks(mth, block); + } + } + + /** + * Set exception handler attribute for whole block + */ + private static void markExceptionHandlers(MethodNode mth, BlockNode block) { + if (!block.getInstructions().isEmpty()) { + InsnNode me = block.getInstructions().get(0); + ExcHandlerAttr handlerAttr = (ExcHandlerAttr) me.getAttributes().get(AttributeType.EXC_HANDLER); + if (handlerAttr != null) { + ExceptionHandler excHandler = handlerAttr.getHandler(); + assert me.getType() == InsnType.MOVE_EXCEPTION && me.getOffset() == excHandler.getHandleOffset(); + // set correct type for 'move-exception' operation + RegisterArg excArg = me.getResult(); + if (excHandler.isCatchAll()) + excArg.getTypedVar().merge(ArgType.THROWABLE); + else + excArg.getTypedVar().merge(excHandler.getCatchType().getType()); + + excHandler.setArg(excArg); + block.getAttributes().add(handlerAttr); + } + } + } + + private static void processExceptionHandlers(MethodNode mth, BlockNode block) { + ExcHandlerAttr handlerAttr = (ExcHandlerAttr) block.getAttributes().get(AttributeType.EXC_HANDLER); + if (handlerAttr != null) { + ExceptionHandler excHandler = handlerAttr.getHandler(); + excHandler.addBlock(block); + for (BlockNode node : BlockUtils.collectBlocksDominatedBy(block, block)) { + excHandler.addBlock(node); + } + + for (BlockNode excBlock : excHandler.getBlocks()) { + // remove 'monitor-exit' from exception handler blocks + InstructionRemover remover = new InstructionRemover(excBlock.getInstructions()); + for (InsnNode insn : excBlock.getInstructions()) { + if (insn.getType() == InsnType.MONITOR_ENTER) + break; + + if (insn.getType() == InsnType.MONITOR_EXIT) + remover.add(insn); + } + remover.perform(); + + // if 'throw' in exception handler block have 'catch' - merge these catch blocks + for (InsnNode insn : excBlock.getInstructions()) { + if (insn.getType() == InsnType.THROW) { + CatchAttr catchAttr = (CatchAttr) insn.getAttributes().get(AttributeType.CATCH_BLOCK); + if (catchAttr != null) { + handlerAttr.getTryBlock().merge(mth, catchAttr.getTryBlock()); + catchAttr.getTryBlock().removeInsn(insn); + } + } + } + } + } + } + + private static void processTryCatchBlocks(MethodNode mth, BlockNode block) { + // if all instructions in block have same 'catch' attribute mark it as 'TryCatch' block + CatchAttr commonCatchAttr = null; + for (InsnNode insn : block.getInstructions()) { + CatchAttr catchAttr = (CatchAttr) insn.getAttributes().get(AttributeType.CATCH_BLOCK); + if (catchAttr == null) + continue; + + if (commonCatchAttr == null) { + commonCatchAttr = catchAttr; + } else if (commonCatchAttr != catchAttr) { + commonCatchAttr = null; + break; + } + } + if (commonCatchAttr != null) { + block.getAttributes().add(commonCatchAttr); + // connect handler to block + for (ExceptionHandler handler : commonCatchAttr.getTryBlock().getHandlers()) { + connectHandler(mth, handler); + } + } + } + + private static void connectHandler(MethodNode mth, ExceptionHandler handler) { + int addr = handler.getHandleOffset(); + for (BlockNode block : mth.getBasicBlocks()) { + ExcHandlerAttr bh = (ExcHandlerAttr) block.getAttributes().get(AttributeType.EXC_HANDLER); + if (bh != null && bh.getHandler().getHandleOffset() == addr) { + handler.setHandleBlock(block); + break; + } + } + } +} diff --git a/src/main/java/jadx/dex/visitors/ClassCheck.java b/src/main/java/jadx/dex/visitors/ClassCheck.java new file mode 100644 index 000000000..8f5d585e2 --- /dev/null +++ b/src/main/java/jadx/dex/visitors/ClassCheck.java @@ -0,0 +1,30 @@ +package jadx.dex.visitors; + +import jadx.dex.info.AccessInfo; +import jadx.dex.nodes.ClassNode; +import jadx.dex.nodes.MethodNode; +import jadx.utils.exceptions.JadxException; + +import java.util.Iterator; + +public class ClassCheck extends AbstractVisitor { + + @Override + public boolean visit(ClassNode cls) throws JadxException { + for (ClassNode inner : cls.getInnerClasses()) { + visit(inner); + } + + for (Iterator it = cls.getMethods().iterator(); it.hasNext();) { + MethodNode mth = it.next(); + AccessInfo af = mth.getAccessFlags(); + + // remove bridge methods + if (af.isBridge() && af.isSynthetic()) { + // TODO make some checks before deleting + it.remove(); + } + } + return false; + } +} diff --git a/src/main/java/jadx/dex/visitors/CodeShrinker.java b/src/main/java/jadx/dex/visitors/CodeShrinker.java new file mode 100644 index 000000000..8f0805d41 --- /dev/null +++ b/src/main/java/jadx/dex/visitors/CodeShrinker.java @@ -0,0 +1,199 @@ +package jadx.dex.visitors; + +import jadx.dex.attributes.AttributeFlag; +import jadx.dex.instructions.ArithNode; +import jadx.dex.instructions.ArithOp; +import jadx.dex.instructions.IfNode; +import jadx.dex.instructions.InsnType; +import jadx.dex.instructions.args.InsnArg; +import jadx.dex.instructions.args.InsnWrapArg; +import jadx.dex.instructions.args.LiteralArg; +import jadx.dex.instructions.args.RegisterArg; +import jadx.dex.nodes.BlockNode; +import jadx.dex.nodes.InsnNode; +import jadx.dex.nodes.MethodNode; +import jadx.utils.BlockUtils; +import jadx.utils.exceptions.JadxRuntimeException; + +import java.util.ArrayList; +import java.util.List; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class CodeShrinker extends AbstractVisitor { + + private final static Logger LOG = LoggerFactory.getLogger(CodeShrinker.class); + + @Override + public void visit(MethodNode mth) { + if (mth.isNoCode() || mth.getAttributes().contains(AttributeFlag.DONT_SHRINK)) + return; + + // TODO pretify required inlined consts (made by shrink) + // 'dec' and 'inc' don't need to be inlined => must be replaced before shrink + pretify(mth); + + shrink(mth); + pretify(mth); + } + + private static void shrink(MethodNode mth) { + for (BlockNode block : mth.getBasicBlocks()) { + InstructionRemover remover = new InstructionRemover(block.getInstructions()); + for (int i = 0; i < block.getInstructions().size(); i++) { + InsnNode insn = block.getInstructions().get(i); + // wrap instructions + if (insn.getResult() != null) { + List use = insn.getResult().getTypedVar().getUseList(); + if (use.size() == 1) { + // variable is used only in this instruction + // TODO not correct sometimes :( + remover.add(insn); + } else if (use.size() == 2) { + InsnArg useInsnArg = use.get(1); + InsnNode useInsn = useInsnArg.getParentInsn(); + if (useInsn == null) { + LOG.debug("parent insn null in " + useInsnArg + " from " + insn + " mth: " + mth); + } else if (useInsn != insn) { + boolean wrap = false; + // wrap insn from current block + if (BlockUtils.blockContains(block, useInsn)) { + wrap = true; + } else { + // TODO implement rules for shrink insn from different blocks + BlockNode useBlock = BlockUtils.getBlockByInsn(mth, useInsn); + if (useBlock != null && useBlock.getPredecessors().contains(block)) { + wrap = true; + } + } + if (wrap) { + useInsnArg.wrapInstruction(insn); + remover.add(insn); + } + } + } + } + } + remover.perform(); + } + } + + private static void pretify(MethodNode mth) { + for (BlockNode block : mth.getBasicBlocks()) { + for (int i = 0; i < block.getInstructions().size(); i++) { + InsnNode insn = block.getInstructions().get(i); + + InsnNode ni = pretifyInsn(mth, insn); + if (ni != null) + block.getInstructions().set(i, ni); + } + } + } + + private static InsnNode pretifyInsn(MethodNode mth, InsnNode insn) { + for (InsnArg arg : insn.getArguments()) { + if (arg.isInsnWrap()) { + InsnNode ni = pretifyInsn(mth, ((InsnWrapArg) arg).getWrapInsn()); + if (ni != null) + arg.wrapInstruction(ni); + } + } + switch (insn.getType()) { + case ARITH: + ArithNode arith = (ArithNode) insn; + if (arith.getArgsCount() == 2) { + InsnArg litArg = null; + + if (arith.getArg(1).isInsnWrap()) { + InsnNode wr = ((InsnWrapArg) arith.getArg(1)).getWrapInsn(); + if (wr.getType() == InsnType.CONST) + litArg = wr.getArg(0); + } else if (arith.getArg(1).isLiteral()) { + litArg = arith.getArg(1); + } + + if (litArg != null) { + long lit = ((LiteralArg) litArg).getLiteral(); + boolean invert = false; + + if (arith.getOp() == ArithOp.ADD && lit < 0) + invert = true; + + if (false && (lit == 1 || lit == -1) && arith.getArg(0).isRegister()) { + ArithOp op = arith.getOp(); + if (op == ArithOp.ADD || op == ArithOp.SUB) { + RegisterArg res = arith.getResult(); + RegisterArg v0 = (RegisterArg) arith.getArg(0); + if (v0.equals(res)) { + // use 'a++' instead 'a = a + 1' (similar for minus) + boolean inc = ((op == ArithOp.ADD && lit == 1) + || (op == ArithOp.SUB && lit == -1)); + return new ArithNode(mth, inc ? ArithOp.INC : ArithOp.DEC, null, v0); + } + } + } + + // fix 'c + (-1)' => 'c - (1)' + if (invert) { + return new ArithNode(mth, ArithOp.SUB, + arith.getResult(), insn.getArg(0), + InsnArg.lit(-lit, litArg.getType())); + } + } + } + break; + + case IF: + // simplify 'cmp' instruction in if condition + IfNode ifb = (IfNode) insn; + InsnArg f = ifb.getArg(0); + if (f.isInsnWrap()) { + InsnNode wi = ((InsnWrapArg) f).getWrapInsn(); + if (wi.getType() == InsnType.CMP_L || wi.getType() == InsnType.CMP_G) { + if (ifb.isZeroCmp() + || ((LiteralArg) ifb.getArg(1)).getLiteral() == 0) { + ifb.changeCondition(wi.getArg(0), wi.getArg(1), ifb.getOp()); + } else { + LOG.warn("TODO: cmp" + ifb); + } + } + } + break; + + default: + break; + } + return null; + } + + public static InsnArg inlineArgument(MethodNode mth, RegisterArg arg) { + InsnNode assignInsn = arg.getAssignInsn(); + if (assignInsn == null) + return null; + + // recursively wrap all instructions + List list = new ArrayList(); + List args = mth.getArguments(false); + int i = 0; + do { + list.clear(); + assignInsn.getRegisterArgs(list); + for (RegisterArg rarg : list) { + InsnNode ai = rarg.getAssignInsn(); + if (ai != assignInsn && ai != null + && rarg.getParentInsn() != ai) + rarg.wrapInstruction(ai); + } + // remove method args + if (list.size() != 0 & args.size() != 0) { + list.removeAll(args); + } + i++; + if (i > 10000) + throw new JadxRuntimeException("Can't inline arguments for: " + arg + " insn:" + assignInsn); + } while (!list.isEmpty()); + + return arg.wrapInstruction(assignInsn); + } +} diff --git a/src/main/java/jadx/dex/visitors/ConstInlinerVisitor.java b/src/main/java/jadx/dex/visitors/ConstInlinerVisitor.java new file mode 100644 index 000000000..f137d737d --- /dev/null +++ b/src/main/java/jadx/dex/visitors/ConstInlinerVisitor.java @@ -0,0 +1,167 @@ +package jadx.dex.visitors; + +import jadx.dex.info.FieldInfo; +import jadx.dex.instructions.IfNode; +import jadx.dex.instructions.IndexInsnNode; +import jadx.dex.instructions.InsnType; +import jadx.dex.instructions.InvokeNode; +import jadx.dex.instructions.args.ArgType; +import jadx.dex.instructions.args.InsnArg; +import jadx.dex.instructions.args.LiteralArg; +import jadx.dex.nodes.BlockNode; +import jadx.dex.nodes.InsnNode; +import jadx.dex.nodes.MethodNode; +import jadx.utils.BlockUtils; +import jadx.utils.exceptions.JadxException; + +import java.util.Iterator; +import java.util.List; +import java.util.Set; + +public class ConstInlinerVisitor extends AbstractVisitor { + + @Override + public void visit(MethodNode mth) throws JadxException { + if (mth.isNoCode()) + return; + + for (BlockNode block : mth.getBasicBlocks()) { + for (Iterator it = block.getInstructions().iterator(); it.hasNext();) { + InsnNode insn = it.next(); + if (checkInsn(mth, block, insn)) + it.remove(); + } + } + } + + public static boolean checkInsn(MethodNode mth, BlockNode block, InsnNode insn) { + if (insn.getType() == InsnType.CONST) { + if (insn.getArgsCount() == 1 + && insn.getArg(0).isLiteral() + && insn.getResult().getType().getRegCount() == 1 /* process only narrow types */) { + // literal arg + LiteralArg litArg = (LiteralArg) insn.getArg(0); + long lit = litArg.getLiteral(); + if (lit == 0 || lit == 1) { + return replaceConst(mth, block, insn, lit); + } + } + // TODO process string const + } + return false; + } + + private static boolean replaceConst(MethodNode mth, BlockNode block, InsnNode insn, long literal) { + List use = insn.getResult().getTypedVar().getUseList(); + + int replace = 0; + for (InsnArg arg : use) { + InsnNode useInsn = arg.getParentInsn(); + if (useInsn == null) + continue; + + BlockNode useBlock = BlockUtils.getBlockByInsn(mth, useInsn); + if (useBlock == block || useBlock.isDominator(block)) { + if (arg != insn.getResult() + && !registerReassignOnPath(block, useBlock, insn)) { + // in most cases type not equal arg.getType() + // just set unknown type and run type fixer + LiteralArg litArg = InsnArg.lit(literal, ArgType.NARROW); + if (useInsn.replaceArg(arg, litArg)) { + // if (useInsn.getType() == InsnType.MOVE) { + // // 'move' became 'const' + // InsnNode constInsn = new InsnNode(mth, InsnType.CONST, 1); + // constInsn.setResult(useInsn.getResult()); + // constInsn.addArg(litArg); + // ModVisitor.replaceInsn(useBlock, useInsn, constInsn); + // fixTypes(mth, constInsn); + // } + fixTypes(mth, useInsn); + replace++; + } + } + } + } + return (replace + 1) == use.size(); + } + + private static boolean registerReassignOnPath(BlockNode block, BlockNode useBlock, InsnNode assignInsn) { + if (block == useBlock) + return false; + + Set blocks = BlockUtils.getAllPathsBlocks(block, useBlock); + // TODO store list of assign insn for each register + int regNum = assignInsn.getResult().getRegNum(); + for (BlockNode b : blocks) { + for (InsnNode insn : b.getInstructions()) { + if (insn.getResult() != null + && insn != assignInsn + && insn.getResult().getRegNum() == regNum) + return true; + } + } + return false; + } + + /** + * This is method similar to PostTypeResolver.visit method, + * but contains some expensive operations needed only after consts inlining + */ + private static void fixTypes(MethodNode mth, InsnNode insn) { + switch (insn.getType()) { + case CONST: + if (insn.getArgsCount() > 0) { + insn.getArg(0).merge(insn.getResult()); + } + break; + + case MOVE: + insn.getResult().merge(insn.getArg(0)); + insn.getArg(0).merge(insn.getResult()); + break; + + case IPUT: + case SPUT: { + IndexInsnNode node = (IndexInsnNode) insn; + insn.getArg(0).merge(((FieldInfo) node.getIndex()).getType()); + break; + } + + case IF: + IfNode ifnode = (IfNode) insn; + if (!ifnode.isZeroCmp()) { + insn.getArg(1).merge(insn.getArg(0)); + insn.getArg(0).merge(insn.getArg(1)); + } + break; + + case RETURN: + if (insn.getArgsCount() != 0) { + insn.getArg(0).merge(mth.getMethodInfo().getReturnType()); + } + break; + + case INVOKE: + InvokeNode inv = (InvokeNode) insn; + List types = inv.getCallMth().getArgumentsTypes(); + int count = insn.getArgsCount(); + int k = (types.size() == count ? 0 : -1); + for (int i = 0; i < count; i++) { + InsnArg arg = insn.getArg(i); + if (!arg.getType().isTypeKnown()) { + ArgType type; + if (k >= 0) + type = types.get(k); + else + type = mth.getParentClass().getClassInfo().getType(); + arg.merge(type); + } + k++; + } + break; + + default: + break; + } + } +} diff --git a/src/main/java/jadx/dex/visitors/DepthTraverser.java b/src/main/java/jadx/dex/visitors/DepthTraverser.java new file mode 100644 index 000000000..5f4b08dcb --- /dev/null +++ b/src/main/java/jadx/dex/visitors/DepthTraverser.java @@ -0,0 +1,33 @@ +package jadx.dex.visitors; + +import jadx.dex.nodes.ClassNode; +import jadx.dex.nodes.MethodNode; +import jadx.utils.ErrorsCounter; + +public class DepthTraverser { + + public static void visit(IDexTreeVisitor visitor, ClassNode cls) { + // if (!cls.toString().contains("ProcessClass")) + // return; + try { + if (visitor.visit(cls)) { + for (ClassNode inCls : cls.getInnerClasses()) + visit(visitor, inCls); + for (MethodNode mth : cls.getMethods()) + visit(visitor, mth); + } + } catch (Throwable e) { + ErrorsCounter.classError(cls, + e.getClass().getSimpleName() + " in pass: " + visitor.getClass().getSimpleName(), e); + } + } + + public static void visit(IDexTreeVisitor visitor, MethodNode mth) { + try { + visitor.visit(mth); + } catch (Throwable e) { + ErrorsCounter.methodError(mth, + e.getClass().getSimpleName() + " in pass: " + visitor.getClass().getSimpleName(), e); + } + } +} diff --git a/src/main/java/jadx/dex/visitors/DotGraphVisitor.java b/src/main/java/jadx/dex/visitors/DotGraphVisitor.java new file mode 100644 index 000000000..1e02cc20e --- /dev/null +++ b/src/main/java/jadx/dex/visitors/DotGraphVisitor.java @@ -0,0 +1,183 @@ +package jadx.dex.visitors; + +import jadx.codegen.CodeWriter; +import jadx.codegen.MethodGen; +import jadx.dex.attributes.IAttributeNode; +import jadx.dex.nodes.BlockNode; +import jadx.dex.nodes.IContainer; +import jadx.dex.nodes.IRegion; +import jadx.dex.nodes.InsnNode; +import jadx.dex.nodes.MethodNode; +import jadx.dex.trycatch.ExceptionHandler; +import jadx.utils.InsnUtils; +import jadx.utils.Utils; + +import java.io.File; + +public class DotGraphVisitor extends AbstractVisitor { + + private static final String NL = "\\l"; + private static final boolean PRINT_REGISTERS_STATES = false; + + private final File dir; + private final boolean useRegions; + private final boolean rawInsn; + + public DotGraphVisitor(File outDir, boolean useRegions, boolean rawInsn) { + this.dir = outDir; + this.useRegions = useRegions; + this.rawInsn = rawInsn; + } + + public DotGraphVisitor(File outDir, boolean useRegions) { + this(outDir, useRegions, false); + } + + @Override + public void visit(MethodNode mth) { + if (mth.isNoCode()) + return; + + CodeWriter dot = new CodeWriter(); + CodeWriter conn = new CodeWriter(); + + dot.startLine("digraph \"CFG for" + + escape(mth.getParentClass().getFullName() + "." + mth.getMethodInfo().getShortId()) + + "\" {"); + + if (useRegions) { + if (mth.getRegion() == null) + return; + + processRegion(mth, mth.getRegion(), dot, conn); + if (mth.getExceptionHandlers() != null) { + for (ExceptionHandler h : mth.getExceptionHandlers()) + if (h.getHandlerRegion() != null) + processRegion(mth, h.getHandlerRegion(), dot, conn); + } + } else { + for (BlockNode block : mth.getBasicBlocks()) + processBlock(mth, block, dot, conn); + } + + String attrs = attributesString(mth); + + dot.startLine("MethodNode[shape=record,label=\"{" + + escape(mth.getAccessFlags().makeString()) + + escape(mth.getMethodInfo().getReturnType() + " " + + mth.getParentClass().getFullName() + "." + mth.getName() + + "(" + Utils.listToString(mth.getArguments(true)) + ") ") + + (attrs.length() == 0 ? "" : " | " + attrs) + + (mth.getSuperCall() != null ? "| Super call: " + escape(mth.getSuperCall().toString()) : "") + + "}\"];"); + + dot.startLine("MethodNode -> " + makeName(mth.getEnterBlock()) + ";"); + + dot.add(conn); + + dot.startLine("}"); + dot.startLine(); + + String fileName = Utils.escape(mth.getMethodInfo().getShortId()) + + (useRegions ? ".regions" : "") + + (rawInsn ? ".raw" : "") + + ".dot"; + dot.save(dir, mth.getParentClass().getClassInfo().getFullPath() + "_graphs", fileName); + } + + private void processRegion(MethodNode mth, IContainer region, CodeWriter dot, CodeWriter conn) { + if (region instanceof IRegion) { + IRegion r = (IRegion) region; + String attrs = attributesString(r); + dot.startLine("subgraph " + makeName(region) + " {"); + dot.startLine("label = \"" + r.toString() + + (attrs.length() == 0 ? "" : " | " + attrs) + + "\";"); + dot.startLine("node [shape=record,color=blue];"); + + for (IContainer c : r.getSubBlocks()) { + processRegion(mth, c, dot, conn); + } + + dot.startLine("}"); + } else if (region instanceof BlockNode) { + processBlock(mth, (BlockNode) region, dot, conn); + } + } + + private void processBlock(MethodNode mth, BlockNode block, CodeWriter dot, CodeWriter conn) { + String attrs = attributesString(block); + if (PRINT_REGISTERS_STATES) { + if (block.getStartState() != null) { + if (attrs.length() != 0) + attrs += "|"; + attrs += escape("RS: " + block.getStartState()) + NL; + attrs += escape("RE: " + block.getEndState()) + NL; + } + } + + String insns = insertInsns(mth, block); + + dot.startLine(makeName(block) + " [shape=record,label=\"{" + + block.getId() + "\\:\\ " + + InsnUtils.formatOffset(block.getStartOffset()) + + (attrs.length() == 0 ? "" : "|" + attrs) + + (insns.length() == 0 ? "" : "|" + insns) + + "}\"];"); + + for (BlockNode next : block.getSuccessors()) + conn.startLine(makeName(block) + " -> " + makeName(next) + ";"); + + for (BlockNode next : block.getDominatesOn()) + conn.startLine(makeName(block) + " -> " + makeName(next) + "[style=dotted];"); + + // // add all dominators connections + // for (BlockNode next : BlockUtils.bitsetToBlocks(mth, block.getDoms())) + // conn.startLine(makeName(block) + " -> " + makeName(next) + "[style=dotted, color=green];"); + } + + private String attributesString(IAttributeNode block) { + StringBuilder attrs = new StringBuilder(); + for (String attr : block.getAttributes().getAttributeStrings()) { + attrs.append(escape(attr)).append(NL); + } + return attrs.toString(); + } + + private String makeName(IContainer c) { + String name; + if (c instanceof BlockNode) { + name = "Node_" + ((BlockNode) c).getId(); + } else { + name = "cluster_" + c.getClass().getSimpleName() + "_" + c.hashCode(); + } + return name; + } + + private String insertInsns(MethodNode mth, BlockNode block) { + if (rawInsn) { + StringBuilder str = new StringBuilder(); + for (InsnNode insn : block.getInstructions()) { + str.append(escape(insn.toString() + " " + insn.getAttributes())); + str.append(NL); + } + return str.toString(); + } else { + CodeWriter code = new CodeWriter(0); + MethodGen.makeFallbackInsns(code, mth, block.getInstructions(), false); + String str = escape(code.endl().toString()); + if (str.startsWith(NL)) + str = str.substring(NL.length()); + return str; + } + } + + private String escape(String string) { + return string + .replace(">", "\\>").replace("<", "\\<") + .replace("{", "\\{").replace("}", "\\}") + .replace("\"", "\\\"") + .replace("-", "\\-") + .replace("\n", NL); + } +} diff --git a/src/main/java/jadx/dex/visitors/EnumVisitor.java b/src/main/java/jadx/dex/visitors/EnumVisitor.java new file mode 100644 index 000000000..74b87c584 --- /dev/null +++ b/src/main/java/jadx/dex/visitors/EnumVisitor.java @@ -0,0 +1,144 @@ +package jadx.dex.visitors; + +import jadx.dex.attributes.AttributeFlag; +import jadx.dex.attributes.EnumClassAttr; +import jadx.dex.attributes.EnumClassAttr.EnumField; +import jadx.dex.info.ClassInfo; +import jadx.dex.info.FieldInfo; +import jadx.dex.info.MethodInfo; +import jadx.dex.instructions.IndexInsnNode; +import jadx.dex.instructions.InsnType; +import jadx.dex.instructions.args.InsnArg; +import jadx.dex.instructions.args.RegisterArg; +import jadx.dex.instructions.mods.ConstructorInsn; +import jadx.dex.nodes.BlockNode; +import jadx.dex.nodes.ClassNode; +import jadx.dex.nodes.FieldNode; +import jadx.dex.nodes.InsnNode; +import jadx.dex.nodes.MethodNode; +import jadx.utils.exceptions.JadxException; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +public class EnumVisitor extends AbstractVisitor { + + @Override + public boolean visit(ClassNode cls) throws JadxException { + if (!cls.getAccessFlags().isEnum() + || !cls.getSuperClass().getFullName().equals("java.lang.Enum")) + return true; + + // collect enum fields, remove synthetic + List enumFields = new ArrayList(); + for (Iterator it = cls.getFields().iterator(); it.hasNext();) { + FieldNode f = it.next(); + if (f.getAccessFlags().isEnum()) { + enumFields.add(f); + it.remove(); + } else if (f.getAccessFlags().isSynthetic()) { + it.remove(); + } + } + + MethodNode staticMethod = null; + + // remove synthetic methods + for (Iterator it = cls.getMethods().iterator(); it.hasNext();) { + MethodNode mth = it.next(); + MethodInfo mi = mth.getMethodInfo(); + if (mi.isClassInit()) { + staticMethod = mth; + } else if (mi.isConstructor() && !mth.getAccessFlags().isSynthetic()) { + if (mi.getShortId().equals("(Ljava/lang/String;I)")) + it.remove(); + } else if (mth.getAccessFlags().isSynthetic() + || mi.getShortId().equals("values()") + || mi.getShortId().equals("valueOf(Ljava/lang/String;)")) { + it.remove(); + } + } + + if (staticMethod == null) + throw new JadxException("Enum class init method not found"); + + EnumClassAttr attr = new EnumClassAttr(enumFields.size()); + cls.getAttributes().add(attr); + attr.setStaticMethod(staticMethod); + + // move enum specific instruction from static method to separate list + BlockNode staticBlock = staticMethod.getBasicBlocks().get(0); + List insns = new ArrayList(); + List list = staticBlock.getInstructions(); + int size = list.size(); + for (int i = 0; i < size; i++) { + InsnNode insn = list.get(i); + insns.add(insn); + if (insn.getType() == InsnType.SPUT) { + IndexInsnNode fp = (IndexInsnNode) insn; + FieldInfo f = (FieldInfo) fp.getIndex(); + if (f.getName().equals("$VALUES")) { + if (i == size - 1) + cls.getMethods().remove(staticMethod); + else + list.subList(0, i + 1).clear(); + break; + } + } + } + + for (InsnNode insn : insns) { + if (insn.getType() == InsnType.CONSTRUCTOR) { + ConstructorInsn co = (ConstructorInsn) insn; + + if (insn.getArgsCount() < 2) + continue; + + ClassInfo clsInfo = co.getClassType(); + ClassNode constrCls = cls.dex().resolveClass(clsInfo); + if (constrCls == null) + continue; + + if (!clsInfo.equals(cls.getClassInfo()) && !constrCls.getAccessFlags().isEnum()) + continue; + + RegisterArg nameArg = (RegisterArg) insn.getArg(0); + InsnArg pos = insn.getArg(1); + // TODO add check: pos == j + String name = (String) nameArg.getConstValue(); + + EnumField field = new EnumField(name, insn.getArgsCount() - 2); + attr.getFields().add(field); + for (int i = 2; i < insn.getArgsCount(); i++) { + InsnArg constrArg; + InsnArg iArg = insn.getArg(i); + if (iArg.isLiteral()) { + constrArg = iArg; + } else { + constrArg = CodeShrinker.inlineArgument(staticMethod, (RegisterArg) iArg); + assert constrArg != null; + } + field.getArgs().add(constrArg); + } + + if (co.getClassType() != cls.getClassInfo()) { + // enum contains additional methods + for (ClassNode innerCls : cls.getInnerClasses()) { + if (innerCls.getClassInfo().equals(co.getClassType())) { + // remove constructor, because it is anonymous class + for (Iterator mit = innerCls.getMethods().iterator(); mit.hasNext();) { + MethodNode innerMth = (MethodNode) mit.next(); + if (innerMth.getAccessFlags().isConstructor()) + mit.remove(); + } + field.setCls(innerCls); + innerCls.getAttributes().add(AttributeFlag.DONT_GENERATE); + } + } + } + } + } + return false; + } +} diff --git a/src/main/java/jadx/dex/visitors/FallbackModeVisitor.java b/src/main/java/jadx/dex/visitors/FallbackModeVisitor.java new file mode 100644 index 000000000..c24d045f5 --- /dev/null +++ b/src/main/java/jadx/dex/visitors/FallbackModeVisitor.java @@ -0,0 +1,40 @@ +package jadx.dex.visitors; + +import jadx.dex.attributes.AttributeType; +import jadx.dex.nodes.InsnNode; +import jadx.dex.nodes.MethodNode; +import jadx.dex.trycatch.CatchAttr; +import jadx.utils.exceptions.JadxException; + +public class FallbackModeVisitor extends AbstractVisitor { + + @Override + public void visit(MethodNode mth) throws JadxException { + if (mth.isNoCode()) + return; + + for (InsnNode insn : mth.getInstructions()) { + // remove 'exception catch' for instruction which don't throw any exceptions + CatchAttr catchAttr = (CatchAttr) insn.getAttributes().get(AttributeType.CATCH_BLOCK); + if (catchAttr != null) { + switch (insn.getType()) { + case RETURN: + case IF: + case GOTO: + case MOVE: + case MOVE_EXCEPTION: + case ARITH: // ?? + case NEG: + case CONST: + case CMP_L: + case CMP_G: + catchAttr.getTryBlock().removeInsn(insn); + break; + + default: + break; + } + } + } + } +} diff --git a/src/main/java/jadx/dex/visitors/IDexTreeVisitor.java b/src/main/java/jadx/dex/visitors/IDexTreeVisitor.java new file mode 100644 index 000000000..d5df2acc3 --- /dev/null +++ b/src/main/java/jadx/dex/visitors/IDexTreeVisitor.java @@ -0,0 +1,26 @@ +package jadx.dex.visitors; + +import jadx.dex.nodes.ClassNode; +import jadx.dex.nodes.MethodNode; +import jadx.utils.exceptions.JadxException; + +/** + * Visitor interface for traverse dex tree + */ +public interface IDexTreeVisitor { + + /** + * Visit class + * + * @return false for disable child methods and inner classes traversal + * @throws JadxException + */ + boolean visit(ClassNode cls) throws JadxException; + + /** + * Visit method + * + * @throws JadxException + */ + void visit(MethodNode mth) throws JadxException; +} diff --git a/src/main/java/jadx/dex/visitors/InstructionRemover.java b/src/main/java/jadx/dex/visitors/InstructionRemover.java new file mode 100644 index 000000000..f2e87ff40 --- /dev/null +++ b/src/main/java/jadx/dex/visitors/InstructionRemover.java @@ -0,0 +1,101 @@ +package jadx.dex.visitors; + +import jadx.dex.instructions.args.InsnArg; +import jadx.dex.nodes.BlockNode; +import jadx.dex.nodes.InsnNode; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +/** + * Helper class for correct instructions removing, + * can be used while iterating over instructions list + */ +public class InstructionRemover { + + private final List insns; + private final List toRemove; + + public InstructionRemover(List instructions) { + this.insns = instructions; + this.toRemove = new ArrayList(); + } + + public void add(InsnNode insn) { + toRemove.add(insn); + } + + public void perform() { + removeAll(insns, toRemove); + toRemove.clear(); + } + + public static void unbindInsnList(List unbind) { + for (InsnNode rem : unbind) + unbindInsn(rem); + } + + public static void unbindInsn(InsnNode insn) { + if (insn.getResult() != null) { + InsnArg res = insn.getResult(); + res.getTypedVar().getUseList().remove(res); + } + for (InsnArg arg : insn.getArguments()) { + if (arg.isRegister()) { + arg.getTypedVar().getUseList().remove(arg); + } + } + } + + public static void removeAll(BlockNode block, List toRemove) { + removeAll(block.getInstructions(), toRemove); + } + + // Don't use 'insns.removeAll(toRemove)' because it will remove instructions by content + // and here can be several instructions with same content + public static void removeAll(List insns, List toRemove) { + if (insns == toRemove) { + for (InsnNode rem : toRemove) + unbindInsn(rem); + return; + } + + for (InsnNode rem : toRemove) { + unbindInsn(rem); + for (Iterator it = insns.iterator(); it.hasNext();) { + InsnNode insn = it.next(); + if (insn == rem) { + it.remove(); + break; + } + } + } + } + + public static void remove(BlockNode block, InsnNode insn) { + unbindInsn(insn); + // remove by pointer (don't use equals) + for (Iterator it = block.getInstructions().iterator(); it.hasNext();) { + InsnNode ir = it.next(); + if (ir == insn) { + it.remove(); + return; + } + } + } + + public static void removeAllByContent(BlockNode block, List toRemove) { + for (InsnNode rem : toRemove) { + unbindInsn(rem); + } + block.getInstructions().removeAll(toRemove); + } + + public static void remove(BlockNode block, int index) { + InsnNode insn = block.getInstructions().get(index); + unbindInsn(insn); + block.getInstructions().remove(index); + } + +} diff --git a/src/main/java/jadx/dex/visitors/ModVisitor.java b/src/main/java/jadx/dex/visitors/ModVisitor.java new file mode 100644 index 000000000..ced47637e --- /dev/null +++ b/src/main/java/jadx/dex/visitors/ModVisitor.java @@ -0,0 +1,235 @@ +package jadx.dex.visitors; + +import jadx.Consts; +import jadx.dex.attributes.AttributeType; +import jadx.dex.attributes.IAttribute; +import jadx.dex.info.MethodInfo; +import jadx.dex.instructions.IndexInsnNode; +import jadx.dex.instructions.InsnType; +import jadx.dex.instructions.InvokeNode; +import jadx.dex.instructions.args.InsnArg; +import jadx.dex.instructions.args.RegisterArg; +import jadx.dex.instructions.mods.ConstructorInsn; +import jadx.dex.nodes.BlockNode; +import jadx.dex.nodes.FieldNode; +import jadx.dex.nodes.InsnContainer; +import jadx.dex.nodes.InsnNode; +import jadx.dex.nodes.MethodNode; +import jadx.dex.trycatch.ExcHandlerAttr; +import jadx.dex.trycatch.ExceptionHandler; +import jadx.dex.trycatch.TryCatchBlock; +import jadx.utils.BlockUtils; + +import java.util.ArrayList; +import java.util.List; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Visitor for modify method instructions + * (remove, replace, process exception handlers) + */ +public class ModVisitor extends AbstractVisitor { + private final static Logger LOG = LoggerFactory.getLogger(ModVisitor.class); + + @Override + public void visit(MethodNode mth) { + if (mth.isNoCode()) + return; + + removeStep(mth); + + replaceStep(mth); + + for (BlockNode block : mth.getBasicBlocks()) { + processExceptionHander(mth, block); + } + } + + private void replaceStep(MethodNode mth) { + for (BlockNode block : mth.getBasicBlocks()) { + InstructionRemover remover = new InstructionRemover(block.getInstructions()); + + int size = block.getInstructions().size(); + for (int i = 0; i < size; i++) { + InsnNode insn = block.getInstructions().get(i); + + switch (insn.getType()) { + case INVOKE: + InvokeNode inv = (InvokeNode) insn; + MethodInfo callMth = inv.getCallMth(); + if (callMth.isConstructor()) { + ConstructorInsn co = new ConstructorInsn(mth, inv); + // don't call 'super' if parent 'Object' + if (co.isSuper()) { + if (!co.getClassType().getFullName().equals(Consts.CLASS_OBJECT)) { + for (int j = 0; j < co.getArgsCount(); j++) { + InsnArg arg = co.getArg(j); + if (arg.isRegister()) { + CodeShrinker.inlineArgument(mth, (RegisterArg) arg); + } + } + if (!mth.getParentClass().getAccessFlags().isEnum()) + mth.setSuperCall(co); + } + remover.add(insn); + } else if (co.isThis() && co.getArgsCount() == 0) { + MethodNode defCo = mth.getParentClass().searchMethodById(co.getCallMth().getShortId()); + if (defCo == null || defCo.isNoCode()) { + // default constructor not implemented + remover.add(insn); + } else { + replaceInsn(block, i, co); + } + } else { + replaceInsn(block, i, co); + } + } + break; + + case CONST: + if (insn.getArgsCount() == 0) { + // const-string + IndexInsnNode node = (IndexInsnNode) insn; + FieldNode f = mth.getParentClass().getConstFields().get(node.getIndex()); + if (f != null) { + InsnNode inode = new IndexInsnNode(mth, InsnType.SGET, f, 0); + inode.setResult(insn.getResult()); + replaceInsn(block, i, inode); + } + } + break; + + default: + break; + } + } + remover.perform(); + } + } + + /** + * Remove unnecessary instructions + */ + private void removeStep(MethodNode mth) { + for (BlockNode block : mth.getBasicBlocks()) { + InstructionRemover remover = new InstructionRemover(block.getInstructions()); + + int size = block.getInstructions().size(); + for (int i = 0; i < size; i++) { + InsnNode insn = block.getInstructions().get(i); + + switch (insn.getType()) { + case NOP: + case GOTO: + case NEW_INSTANCE: + remover.add(insn); + break; + + case NEW_ARRAY: + // create array in 'fill-array' instruction + int next = i + 1; + if (next < size) { + InsnNode ni = block.getInstructions().get(next); + if (ni.getType() == InsnType.FILL_ARRAY) { + ni.getResult().merge(insn.getResult()); + remover.add(insn); + } + } + break; + + case RETURN: + if (insn.getArgsCount() == 0 + && mth.getBasicBlocks().size() == 1 + && i == size - 1) + remover.add(insn); + break; + + default: + break; + } + } + remover.perform(); + } + } + + private void processExceptionHander(MethodNode mth, BlockNode block) { + ExcHandlerAttr handlerAttr = (ExcHandlerAttr) block.getAttributes().get(AttributeType.EXC_HANDLER); + if (handlerAttr == null) + return; + + TryCatchBlock tryBlock = handlerAttr.getTryBlock(); + ExceptionHandler excHandler = handlerAttr.getHandler(); + List blockInsns = block.getInstructions(); + int size = blockInsns.size(); + if (size > 0 && blockInsns.get(0).getType() == InsnType.MOVE_EXCEPTION) { + InstructionRemover.remove(block, 0); + } + + int totalSize = 0; + boolean noExitNode = true; // check if handler has exit edge to block not from this handler + for (BlockNode excBlock : excHandler.getBlocks()) { + List insns = excBlock.getInstructions(); + size = insns.size(); + if (noExitNode) + noExitNode = excHandler.getBlocks().containsAll(excBlock.getCleanSuccessors()); + + if (excHandler.isCatchAll() + && size > 0 + && insns.get(size - 1).getType() == InsnType.THROW) { + + InstructionRemover.remove(excBlock, size - 1); + size = insns.size(); + + // move not removed instructions to 'finally' block + if (size != 0) { + InsnContainer cont = new InsnContainer(); + List finalBlockInsns = new ArrayList(insns); + cont.setInstructions(finalBlockInsns); + tryBlock.setFinalBlock(cont); + + InstructionRemover.unbindInsnList(finalBlockInsns); + + // remove these instructions from other handlers + for (ExceptionHandler h : tryBlock.getHandlers()) { + for (BlockNode ehb : h.getBlocks()) + ehb.getInstructions().removeAll(finalBlockInsns); + } + // remove from blocks with this catch + for (BlockNode b : mth.getBasicBlocks()) { + IAttribute ca = b.getAttributes().get(AttributeType.CATCH_BLOCK); + if (tryBlock.getCatchAttr() == ca) + b.getInstructions().removeAll(finalBlockInsns); + } + size = insns.size(); + } + } + totalSize += size; + } + if (totalSize == 0 && noExitNode) + tryBlock.removeHandler(mth, excHandler); + } + + /** + * Replace insn by index i in block, + * for proper copy attributes, assume attributes are not overlap + */ + private static void replaceInsn(BlockNode block, int i, InsnNode insn) { + InsnNode prevInsn = block.getInstructions().get(i); + insn.getAttributes().addAll(prevInsn.getAttributes()); + block.getInstructions().set(i, insn); + } + + /** + * Replace oldInsn in block by newInsn, + */ + public static boolean replaceInsn(BlockNode block, InsnNode oldInsn, InsnNode newInsn) { + int pos = BlockUtils.insnIndex(block, oldInsn); + if (pos == -1) + return false; + + replaceInsn(block, pos, newInsn); + return true; + } +} diff --git a/src/main/java/jadx/dex/visitors/regions/AbstractRegionVisitor.java b/src/main/java/jadx/dex/visitors/regions/AbstractRegionVisitor.java new file mode 100644 index 000000000..ac2d41067 --- /dev/null +++ b/src/main/java/jadx/dex/visitors/regions/AbstractRegionVisitor.java @@ -0,0 +1,21 @@ +package jadx.dex.visitors.regions; + +import jadx.dex.nodes.IBlock; +import jadx.dex.nodes.IRegion; +import jadx.dex.nodes.MethodNode; + +public abstract class AbstractRegionVisitor implements IRegionVisitor { + + @Override + public void enterRegion(MethodNode mth, IRegion region) { + } + + @Override + public void processBlock(MethodNode mth, IBlock container) { + } + + @Override + public void leaveRegion(MethodNode mth, IRegion region) { + } + +} diff --git a/src/main/java/jadx/dex/visitors/regions/CheckRegions.java b/src/main/java/jadx/dex/visitors/regions/CheckRegions.java new file mode 100644 index 000000000..a692427b5 --- /dev/null +++ b/src/main/java/jadx/dex/visitors/regions/CheckRegions.java @@ -0,0 +1,60 @@ +package jadx.dex.visitors.regions; + +import jadx.dex.attributes.AttributeFlag; +import jadx.dex.nodes.BlockNode; +import jadx.dex.nodes.IBlock; +import jadx.dex.nodes.IRegion; +import jadx.dex.nodes.MethodNode; +import jadx.dex.regions.LoopRegion; +import jadx.dex.visitors.AbstractVisitor; +import jadx.utils.ErrorsCounter; +import jadx.utils.exceptions.JadxException; + +import java.util.HashSet; +import java.util.Set; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class CheckRegions extends AbstractVisitor { + private static final Logger LOG = LoggerFactory.getLogger(CheckRegions.class); + + @Override + public void visit(MethodNode mth) throws JadxException { + if (mth.isNoCode() || mth.getBasicBlocks().size() == 0) + return; + + // check if all blocks included in regions + final Set blocksInRegions = new HashSet(); + IRegionVisitor collectBlocks = new AbstractRegionVisitor() { + @Override + public void processBlock(MethodNode mth, IBlock container) { + if (container instanceof BlockNode) + blocksInRegions.add((BlockNode) container); + else + LOG.warn("Not block node : " + container.getClass().getSimpleName()); + } + }; + DepthRegionTraverser.traverseAll(mth, collectBlocks); + + if (mth.getBasicBlocks().size() != blocksInRegions.size()) + mth.getAttributes().add(AttributeFlag.INCONSISTENT_CODE); + + // check loop conditions + IRegionVisitor checkLoops = new AbstractRegionVisitor() { + @Override + public void enterRegion(MethodNode mth, IRegion region) { + if (region instanceof LoopRegion) { + LoopRegion loop = (LoopRegion) region; + if (loop.getConditionBlock() != null + && loop.getConditionBlock().getInstructions().size() != 1) { + ErrorsCounter.methodError(mth, "Incorrect condition in loop: " + loop.getConditionBlock()); + mth.getAttributes().add(AttributeFlag.INCONSISTENT_CODE); + } + } + } + }; + DepthRegionTraverser.traverseAll(mth, checkLoops); + + } +} diff --git a/src/main/java/jadx/dex/visitors/regions/DepthRegionTraverser.java b/src/main/java/jadx/dex/visitors/regions/DepthRegionTraverser.java new file mode 100644 index 000000000..151fa999d --- /dev/null +++ b/src/main/java/jadx/dex/visitors/regions/DepthRegionTraverser.java @@ -0,0 +1,32 @@ +package jadx.dex.visitors.regions; + +import jadx.dex.nodes.IBlock; +import jadx.dex.nodes.IContainer; +import jadx.dex.nodes.IRegion; +import jadx.dex.nodes.MethodNode; +import jadx.dex.trycatch.ExceptionHandler; + +public class DepthRegionTraverser { + + public static void traverse(MethodNode mth, IRegionVisitor visitor, IContainer container) { + if (container instanceof IBlock) { + visitor.processBlock(mth, (IBlock) container); + } else if (container instanceof IRegion) { + IRegion region = (IRegion) container; + visitor.enterRegion(mth, region); + for (IContainer subCont : region.getSubBlocks()) { + traverse(mth, visitor, subCont); + } + visitor.leaveRegion(mth, region); + } + } + + public static void traverseAll(MethodNode mth, IRegionVisitor visitor) { + traverse(mth, visitor, mth.getRegion()); + + if (mth.getExceptionHandlers() != null) { + for (ExceptionHandler h : mth.getExceptionHandlers()) + traverse(mth, visitor, h.getHandlerRegion()); + } + } +} diff --git a/src/main/java/jadx/dex/visitors/regions/FinishRegions.java b/src/main/java/jadx/dex/visitors/regions/FinishRegions.java new file mode 100644 index 000000000..0874db3de --- /dev/null +++ b/src/main/java/jadx/dex/visitors/regions/FinishRegions.java @@ -0,0 +1,41 @@ +package jadx.dex.visitors.regions; + +import jadx.dex.instructions.InsnType; +import jadx.dex.nodes.IBlock; +import jadx.dex.nodes.IRegion; +import jadx.dex.nodes.InsnNode; +import jadx.dex.nodes.MethodNode; +import jadx.dex.regions.LoopRegion; + +import java.util.List; + +public class FinishRegions implements IRegionVisitor { + + @Override + public void processBlock(MethodNode mth, IBlock block) { + + // remove return from class init method + if (mth.getMethodInfo().isClassInit()) { + List insns = block.getInstructions(); + if (insns.size() != 0) { + InsnNode last = insns.get(insns.size() - 1); + if (last.getType() == InsnType.RETURN) { + insns.remove(insns.size() - 1); + } + } + } + } + + @Override + public void enterRegion(MethodNode mth, IRegion region) { + } + + @Override + public void leaveRegion(MethodNode mth, IRegion region) { + if (region instanceof LoopRegion) { + LoopRegion loop = (LoopRegion) region; + loop.mergePreCondition(); + } + } + +} diff --git a/src/main/java/jadx/dex/visitors/regions/IRegionVisitor.java b/src/main/java/jadx/dex/visitors/regions/IRegionVisitor.java new file mode 100644 index 000000000..9bc6580ee --- /dev/null +++ b/src/main/java/jadx/dex/visitors/regions/IRegionVisitor.java @@ -0,0 +1,15 @@ +package jadx.dex.visitors.regions; + +import jadx.dex.nodes.IBlock; +import jadx.dex.nodes.IRegion; +import jadx.dex.nodes.MethodNode; + +public interface IRegionVisitor { + + public void processBlock(MethodNode mth, IBlock container); + + public void enterRegion(MethodNode mth, IRegion region); + + public void leaveRegion(MethodNode mth, IRegion region); + +} diff --git a/src/main/java/jadx/dex/visitors/regions/MarkTryCatchRegions.java b/src/main/java/jadx/dex/visitors/regions/MarkTryCatchRegions.java new file mode 100644 index 000000000..09bcea36b --- /dev/null +++ b/src/main/java/jadx/dex/visitors/regions/MarkTryCatchRegions.java @@ -0,0 +1,144 @@ +package jadx.dex.visitors.regions; + +import jadx.dex.attributes.AttributeType; +import jadx.dex.nodes.BlockNode; +import jadx.dex.nodes.IContainer; +import jadx.dex.nodes.IRegion; +import jadx.dex.nodes.MethodNode; +import jadx.dex.regions.Region; +import jadx.dex.trycatch.CatchAttr; +import jadx.dex.trycatch.ExceptionHandler; +import jadx.dex.trycatch.TryCatchBlock; +import jadx.utils.BlockUtils; +import jadx.utils.RegionUtils; +import jadx.utils.exceptions.JadxRuntimeException; + +import java.util.BitSet; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Extract blocks to separate try/catch region + */ +public class MarkTryCatchRegions extends AbstractRegionVisitor { + private final static Logger LOG = LoggerFactory.getLogger(MarkTryCatchRegions.class); + private final static boolean DEBUG = false; + + static { + if (DEBUG) + LOG.debug("Debug enabled for " + MarkTryCatchRegions.class); + } + + private final Map tryBlocksMap = new HashMap(2); + + public MarkTryCatchRegions(MethodNode mth) { + if (mth.isNoCode() || mth.getExceptionHandlers() == null) + return; + + Set tryBlocks = new HashSet(); + // collect all try/catch blocks + for (BlockNode block : mth.getBasicBlocks()) { + CatchAttr c = (CatchAttr) block.getAttributes().get(AttributeType.CATCH_BLOCK); + if (c != null) + tryBlocks.add(c.getTryBlock()); + } + + // for each try block search nearest dominator block + for (TryCatchBlock tb : tryBlocks) { + BitSet bs = null; + // build bitset with dominators of blocks covered with this try/catch block + for (BlockNode block : mth.getBasicBlocks()) { + CatchAttr c = (CatchAttr) block.getAttributes().get(AttributeType.CATCH_BLOCK); + if (c != null && c.getTryBlock() == tb) { + if (bs == null) { + bs = (BitSet) block.getDoms().clone(); + } else { + bs.and(block.getDoms()); + } + } + } + assert bs != null; + + // intersect to get dominator of dominators + List domBlocks = BlockUtils.bitsetToBlocks(mth, bs); + for (BlockNode block : domBlocks) { + bs.andNot(block.getDoms()); + } + domBlocks = BlockUtils.bitsetToBlocks(mth, bs); + if (domBlocks.size() != 1) + throw new JadxRuntimeException( + "Exception block dominator not found, method:" + mth + ". bs: " + bs); + + BlockNode domBlock = domBlocks.get(0); + + TryCatchBlock prevTB = tryBlocksMap.put(domBlock, tb); + if (prevTB != null) { + LOG.info("!!! TODO merge try blocks"); + } + } + + if (DEBUG && !tryBlocksMap.isEmpty()) + LOG.debug("MarkTryCatchRegions: \n {} \n {}", mth, tryBlocksMap); + } + + @Override + public void leaveRegion(MethodNode mth, IRegion region) { + if (tryBlocksMap.isEmpty()) + return; + + // search dominator blocks in this region (don't need to go deeper) + for (BlockNode dominator : tryBlocksMap.keySet()) { + if (region.getSubBlocks().contains(dominator)) { + wrapBlocks(mth, region, dominator); + tryBlocksMap.remove(dominator); + // if region is modified rerun this method + leaveRegion(mth, region); + return; + } + } + } + + /** + * Extract all block dominated by 'dominator' to separate region and mark as try/catch block + */ + private void wrapBlocks(MethodNode mth, IRegion region, BlockNode dominator) { + Region newRegion = new Region(region); + TryCatchBlock tb = tryBlocksMap.get(dominator); + assert tb != null; + + for (IContainer cont : region.getSubBlocks()) { + if (RegionUtils.isDominaterBy(dominator, cont)) { + boolean pathFromExcHandler = false; + for (ExceptionHandler h : tb.getHandlers()) { + if (RegionUtils.hasPathThruBlock(h.getHandleBlock(), cont)) { + pathFromExcHandler = true; + break; + } + } + if (!pathFromExcHandler) { + newRegion.getSubBlocks().add(cont); + } else { + break; + } + } + } + if (newRegion.getSubBlocks().size() != 0) { + if (DEBUG) + LOG.debug("MarkTryCatchRegions mark: {}", newRegion); + // replace first node by region + IContainer firstNode = newRegion.getSubBlocks().get(0); + int i = region.getSubBlocks().indexOf(firstNode); + region.getSubBlocks().set(i, newRegion); + region.getSubBlocks().removeAll(newRegion.getSubBlocks()); + + newRegion.getAttributes().add(tb.getCatchAttr()); + } + } + +} diff --git a/src/main/java/jadx/dex/visitors/regions/PostRegionVisitor.java b/src/main/java/jadx/dex/visitors/regions/PostRegionVisitor.java new file mode 100644 index 000000000..19800237a --- /dev/null +++ b/src/main/java/jadx/dex/visitors/regions/PostRegionVisitor.java @@ -0,0 +1,59 @@ +package jadx.dex.visitors.regions; + +import jadx.dex.instructions.InsnType; +import jadx.dex.instructions.args.ArgType; +import jadx.dex.nodes.BlockNode; +import jadx.dex.nodes.IContainer; +import jadx.dex.nodes.InsnNode; +import jadx.dex.nodes.MethodNode; +import jadx.dex.regions.Region; +import jadx.dex.visitors.AbstractVisitor; +import jadx.utils.exceptions.JadxException; + +import java.util.List; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class PostRegionVisitor extends AbstractVisitor { + private final static Logger LOG = LoggerFactory.getLogger(PostRegionVisitor.class); + + @Override + public void visit(MethodNode mth) throws JadxException { + if (mth.isNoCode() || mth.getRegion() == null) + return; + + DepthRegionTraverser.traverse(mth, new MarkTryCatchRegions(mth), mth.getRegion()); + + DepthRegionTraverser.traverse(mth, new FinishRegions(), mth.getRegion()); + + // removeReturn(mth); + } + + /** + * Remove useless return at end + */ + private void removeReturn(MethodNode mth) { + if (!mth.getMethodInfo().getReturnType().equals(ArgType.VOID)) + return; + + if (!(mth.getRegion() instanceof Region)) + return; + + Region rootRegion = (Region) mth.getRegion(); + if (rootRegion.getSubBlocks().isEmpty()) + return; + + IContainer lastCont = rootRegion.getSubBlocks().get(rootRegion.getSubBlocks().size() - 1); + if (lastCont instanceof BlockNode) { + BlockNode lastBlock = (BlockNode) lastCont; + List insns = lastBlock.getInstructions(); + int last = insns.size() - 1; + if (last >= 0 + && insns.get(last).getType() == InsnType.RETURN + && insns.get(last).getArgsCount() == 0) { + insns.remove(last); + } + } + } +} diff --git a/src/main/java/jadx/dex/visitors/regions/ProcessVariables.java b/src/main/java/jadx/dex/visitors/regions/ProcessVariables.java new file mode 100644 index 000000000..fe1ed48f2 --- /dev/null +++ b/src/main/java/jadx/dex/visitors/regions/ProcessVariables.java @@ -0,0 +1,188 @@ +package jadx.dex.visitors.regions; + +import jadx.dex.attributes.AttributeType; +import jadx.dex.attributes.DeclareVariableAttr; +import jadx.dex.instructions.args.RegisterArg; +import jadx.dex.nodes.IBlock; +import jadx.dex.nodes.IContainer; +import jadx.dex.nodes.IRegion; +import jadx.dex.nodes.InsnNode; +import jadx.dex.nodes.MethodNode; +import jadx.dex.visitors.AbstractVisitor; +import jadx.utils.RegionUtils; +import jadx.utils.exceptions.JadxException; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class ProcessVariables extends AbstractVisitor { + private final static Logger LOG = LoggerFactory.getLogger(ProcessVariables.class); + + private static class Usage { + private RegisterArg arg; + private final Set usage = new HashSet(2); + private final Set assigns = new HashSet(2); + + public void setArg(RegisterArg arg) { + this.arg = arg; + } + + public RegisterArg getArg() { + return arg; + } + + public Set getAssigns() { + return assigns; + } + + public Set getUseRegions() { + return usage; + } + + @Override + public String toString() { + return arg + " " + assigns + " " + usage; + } + } + + @Override + public void visit(MethodNode mth) throws JadxException { + final Map usageMap = new HashMap(); + + // collect all variables usage + IRegionVisitor collect = new TracedRegionVisitor() { + @Override + public void processBlockTraced(MethodNode mth, IBlock container, IRegion curRegion) { + int len = container.getInstructions().size(); + List args = new ArrayList(); + for (int i = 0; i < len; i++) { + InsnNode insn = container.getInstructions().get(i); + // result + RegisterArg result = insn.getResult(); + if (result != null) { + Usage u = usageMap.get(result); + if (u == null) { + u = new Usage(); + u.setArg(result); + usageMap.put(result, u); + } + if (u.getArg() == null) + u.setArg(result); + u.getAssigns().add(curRegion); + } + // args + args.clear(); + insn.getRegisterArgs(args); + for (RegisterArg arg : args) { + Usage u = usageMap.get(arg); + if (u == null) { + u = new Usage(); + usageMap.put(arg, u); + } + u.getUseRegions().add(curRegion); + } + } + } + }; + DepthRegionTraverser.traverseAll(mth, collect); + + List mthArgs = mth.getArguments(true); + // reduce assigns map + for (Iterator> it = usageMap.entrySet().iterator(); it.hasNext();) { + Entry entry = it.next(); + Usage u = entry.getValue(); + RegisterArg r = u.getArg(); + + if (u.getAssigns().isEmpty()) { + it.remove(); + continue; + } + + int i; + if ((i = mthArgs.indexOf(r)) != -1) { + // if (mthArgs.get(i).getTypedVar() == r.getTypedVar()) { + it.remove(); + continue; + // } + } + // suppose variables always initialized + if (u.getAssigns().size() == 1 && u.getUseRegions().size() <= 1) { + r.getParentInsn().getAttributes().add(new DeclareVariableAttr()); + it.remove(); + continue; + } + // check if we can declare variable at current assigns + for (IRegion assignRegion : u.getAssigns()) { + if (canDeclareInRegion(u, assignRegion)) { + r.getParentInsn().getAttributes().add(new DeclareVariableAttr()); + it.remove(); + break; + } + } + } + + // apply + for (Entry entry : usageMap.entrySet()) { + Usage u = entry.getValue(); + // find common region which contains all usage regions + + Set set = u.getUseRegions(); + for (Iterator it = set.iterator(); it.hasNext();) { + IRegion r = (IRegion) it.next(); + IRegion parent = r.getParent(); + if (parent != null && set.contains(parent)) + it.remove(); + } + if (set.isEmpty()) + continue; + + IRegion region = set.iterator().next(); + IRegion parent = region; + boolean declare = false; + while (parent != null) { + if (canDeclareInRegion(u, region)) { + declareVar(region, u.getArg()); + declare = true; + break; + } + region = parent; + parent = region.getParent(); + } + + if (!declare) { + declareVar(mth.getRegion(), u.getArg()); + } + } + } + + private void declareVar(IContainer region, RegisterArg arg) { + DeclareVariableAttr dv = (DeclareVariableAttr) region.getAttributes().get( + AttributeType.DECLARE_VARIABLE); + if (dv == null) { + dv = new DeclareVariableAttr(new ArrayList()); + region.getAttributes().add(dv); + } + dv.addVar(arg); + } + + private boolean canDeclareInRegion(Usage u, IRegion region) { + for (IRegion r : u.getAssigns()) { + if (!RegionUtils.isRegionContainsRegion(region, r)) + return false; + } + for (IRegion r : u.getUseRegions()) { + if (!RegionUtils.isRegionContainsRegion(region, r)) + return false; + } + return true; + } +} diff --git a/src/main/java/jadx/dex/visitors/regions/RegionMaker.java b/src/main/java/jadx/dex/visitors/regions/RegionMaker.java new file mode 100644 index 000000000..637006818 --- /dev/null +++ b/src/main/java/jadx/dex/visitors/regions/RegionMaker.java @@ -0,0 +1,476 @@ +package jadx.dex.visitors.regions; + +import jadx.dex.attributes.AttributeFlag; +import jadx.dex.attributes.AttributeType; +import jadx.dex.attributes.AttributesList; +import jadx.dex.attributes.ForceReturnAttr; +import jadx.dex.attributes.IAttribute; +import jadx.dex.attributes.LoopAttr; +import jadx.dex.instructions.IfNode; +import jadx.dex.instructions.InsnType; +import jadx.dex.instructions.SwitchNode; +import jadx.dex.instructions.args.RegisterArg; +import jadx.dex.nodes.BlockNode; +import jadx.dex.nodes.IRegion; +import jadx.dex.nodes.InsnNode; +import jadx.dex.nodes.MethodNode; +import jadx.dex.regions.IfRegion; +import jadx.dex.regions.LoopRegion; +import jadx.dex.regions.Region; +import jadx.dex.regions.SwitchRegion; +import jadx.dex.regions.SynchronizedRegion; +import jadx.dex.trycatch.ExceptionHandler; +import jadx.dex.visitors.InstructionRemover; +import jadx.utils.BlockUtils; +import jadx.utils.ErrorsCounter; +import jadx.utils.RegionUtils; + +import java.util.ArrayList; +import java.util.BitSet; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class RegionMaker { + private final static Logger LOG = LoggerFactory.getLogger(RegionMaker.class); + + private final MethodNode mth; + + public RegionMaker(MethodNode mth) { + this.mth = mth; + } + + public Region makeRegion(BlockNode startBlock, RegionStack stack) { + Region r = new Region(stack.peekRegion()); + BlockNode next = startBlock; + while (next != null) { + next = traverse(r, next, stack); + } + return r; + } + + /** + * Recursively traverse all blocks from 'block' until block from 'exits' + */ + private BlockNode traverse(IRegion r, BlockNode block, RegionStack stack) { + BlockNode next = null; + boolean processed = false; + + AttributesList attrs = block.getAttributes(); + int loopCount = attrs.getCount(AttributeType.LOOP); + if (loopCount != 0 && attrs.contains(AttributeFlag.LOOP_START)) { + if (loopCount == 1) { + LoopAttr loop = (LoopAttr) attrs.get(AttributeType.LOOP); + next = processLoop(r, loop, stack); + processed = true; + } else { + List loops = attrs.getAll(AttributeType.LOOP); + for (IAttribute a : loops) { + LoopAttr loop = (LoopAttr) a; + if (loop.getStart() == block) { + next = processLoop(r, loop, stack); + processed = true; + break; + } + } + } + } + + if (!processed) { + int size = block.getInstructions().size(); + if (size == 1) { + InsnNode insn = block.getInstructions().get(0); + switch (insn.getType()) { + case IF: + next = processIf(r, block, (IfNode) insn, stack); + processed = true; + break; + + case SWITCH: + next = processSwitch(r, block, (SwitchNode) insn, stack); + processed = true; + break; + + case MONITOR_ENTER: + next = processMonitorEnter(r, block, insn, stack); + processed = true; + break; + + default: + break; + } + } + } + + if (!processed) { + r.getSubBlocks().add(block); + next = BlockUtils.getNextBlock(block); + } + + if (!stack.containsExit(block) && next != null && !stack.containsExit(next)) + return next; + else + return null; + } + + private BlockNode processLoop(IRegion curRegion, LoopAttr loop, RegionStack stack) { + BlockNode loopStart = loop.getStart(); + IfNode ifnode = null; + LoopRegion loopRegion = null; + Set exitBlocksSet = loop.getExitNodes(); + // set exit blocks scan order by priority + // this can help if we have several exit from loop (after using 'break' or 'return' in loop) + List exitBlocks = new ArrayList(exitBlocksSet.size()); + if (exitBlocksSet.contains(loop.getEnd())) { + exitBlocks.add(loop.getEnd()); + exitBlocksSet.remove(loop.getEnd()); + } + if (exitBlocksSet.contains(loopStart)) { + exitBlocks.add(loopStart); + exitBlocksSet.remove(loopStart); + } + exitBlocks.addAll(exitBlocksSet); + exitBlocksSet = null; + + BlockNode condBlock = null; // exit block with loop condition + + for (BlockNode exit : exitBlocks) { + if (exit.getAttributes().contains(AttributeType.EXC_HANDLER) + || exit.getInstructions().size() != 1) + continue; + + InsnNode insn = exit.getInstructions().get(0); + if (insn.getType() == InsnType.IF) { + ifnode = (IfNode) insn; + condBlock = exit; + loopRegion = new LoopRegion(curRegion, condBlock, condBlock == loop.getEnd()); + if (!loopRegion.isConditionAtEnd() && condBlock != loop.getStart() + && condBlock.getPredecessors().contains(loopStart)) { + loopRegion.setPreCondition(loopStart); + // if we can't merge pre-condition this is not correct header + if (!loopRegion.checkPreCondition()) { + ifnode = null; + loopRegion = null; + // try another exit + continue; + } + } + break; + } + } + + // endless loop + if (loopRegion == null) { + loopRegion = new LoopRegion(curRegion, null, false); + curRegion.getSubBlocks().add(loopRegion); + + loopStart.getAttributes().remove(AttributeType.LOOP); + stack.push(loopRegion); + Region body = makeRegion(loopStart, stack); + if (!RegionUtils.isRegionContainsBlock(body, loop.getEnd())) + body.getSubBlocks().add(loop.getEnd()); + loopRegion.setBody(body); + stack.pop(); + loopStart.getAttributes().add(loop); + + return BlockUtils.getNextBlock(loop.getEnd()); + } + + stack.push(loopRegion); + curRegion.getSubBlocks().add(loopRegion); + + exitBlocks.remove(condBlock); + if (exitBlocks.size() > 0) { + // set BREAK or FORCE_RETURN attributes + // before path cross between main loop exit and subexit + BlockNode loopExit = BlockUtils.getNextBlock(condBlock); + for (BlockNode exit : exitBlocks) { + BlockNode next = BlockUtils.getNextBlock(exit); + while (next != null) { + if (BlockUtils.isPathExists(loopExit, next)) { + // found cross + if (next.getCleanSuccessors().size() == 1) { + BlockNode r = BlockUtils.getNextBlock(next); + if (r != null && r.getAttributes().contains(AttributeFlag.RETURN)) { + next.getAttributes().add(new ForceReturnAttr(r.getInstructions().get(0))); + } else { + next.getAttributes().add(AttributeFlag.BREAK); + } + } + stack.addExit(next); + break; + } + next = BlockUtils.getNextBlock(next); + } + } + } + + BlockNode bThen = BlockUtils.getBlockByOffset(ifnode.getTarget(), condBlock.getSuccessors()); + BlockNode out; + if (loopRegion.isConditionAtEnd()) { + BlockNode bElse = BlockUtils.selectOther(bThen, condBlock.getSuccessors()); + out = (bThen == loopStart ? bElse : bThen); + + loopStart.getAttributes().remove(AttributeType.LOOP); + stack.addExit(loop.getEnd()); + loopRegion.setBody(makeRegion(loopStart, stack)); + loopStart.getAttributes().add(loop); + } else { + Set loopBlocks = loop.getLoopBlocks(); + BlockNode loopBody = null; + for (BlockNode s : condBlock.getSuccessors()) { + if (loopBlocks.contains(s)) { + loopBody = s; + break; + } + } + if (bThen != loopBody) + ifnode.invertOp(bThen.getStartOffset()); + + out = BlockUtils.selectOther(loopBody, condBlock.getSuccessors()); + AttributesList outAttrs = out.getAttributes(); + if (outAttrs.contains(AttributeFlag.LOOP_START) + && outAttrs.get(AttributeType.LOOP) != loop) { + // exit to outer loop which already processed + out = null; + } + stack.addExit(out); + loopRegion.setBody(makeRegion(loopBody, stack)); + } + stack.pop(); + return out; + } + + private final static Set cacheSet = new HashSet(); + + private BlockNode processMonitorEnter(IRegion curRegion, BlockNode block, InsnNode insn, RegionStack stack) { + RegisterArg arg = (RegisterArg) insn.getArg(0); + SynchronizedRegion synchRegion = new SynchronizedRegion(curRegion, arg); + synchRegion.getSubBlocks().add(block); + curRegion.getSubBlocks().add(synchRegion); + + Set exits = new HashSet(); + cacheSet.clear(); + traverseMonitorExits(arg, block, exits, cacheSet); + + block = BlockUtils.getNextBlock(block); + BlockNode exit; + if (exits.size() == 1) { + exit = BlockUtils.getNextBlock(exits.iterator().next()); + } else { + cacheSet.clear(); + exit = traverseMonitorExitsCross(block, exits, cacheSet); + // LOG.debug("synchronized exits: " + exits + ", cross: " + exit); + } + + stack.push(synchRegion); + stack.addExit(exit); + synchRegion.getSubBlocks().add(makeRegion(block, stack)); + stack.pop(); + return exit; + } + + /** + * Traverse from monitor-enter thru successors and collect blocks contains monitor-exit + */ + private void traverseMonitorExits(RegisterArg arg, BlockNode block, Set exits, Set visited) { + visited.add(block); + for (InsnNode insn : block.getInstructions()) { + if (insn.getType() == InsnType.MONITOR_EXIT + && insn.getArg(0).equals(arg)) { + exits.add(block); + InstructionRemover.remove(block, insn); + return; + } + } + for (BlockNode node : block.getCleanSuccessors()) { + if (!visited.contains(node)) + traverseMonitorExits(arg, node, exits, visited); + } + } + + /** + * Traverse from monitor-enter thru successors and search for exit paths cross + */ + private BlockNode traverseMonitorExitsCross(BlockNode block, Set exits, Set visited) { + visited.add(block); + for (BlockNode node : block.getCleanSuccessors()) { + boolean cross = true; + for (BlockNode exitBlock : exits) { + boolean p = BlockUtils.isPathExists(exitBlock, node); + if (!p) { + cross = false; + break; + } + } + if (cross) + return node; + + if (!visited.contains(node)) { + BlockNode res = traverseMonitorExitsCross(node, exits, visited); + if (res != null) + return res; + } + } + return null; + } + + private BlockNode processIf(IRegion currentRegion, BlockNode block, IfNode ifnode, RegionStack stack) { + BlockNode bElse = BlockUtils.getBlockByOffset(ifnode.getTarget(), block.getSuccessors()); + BlockNode bThen; + if (block.getSuccessors().size() == 1) { + // TODO eliminate useless 'if' instruction + bThen = bElse; + } else { + bThen = BlockUtils.selectOther(bElse, block.getSuccessors()); + } + + ifnode.invertOp(bThen.getStartOffset()); + + BlockNode out = null; + BlockNode thenBlock; + BlockNode elseBlock = null; + + thenBlock = bThen; + // select else and exit blocks + if (block.getDominatesOn().size() == 2) { + if (bElse.getPredecessors().size() == 1) + elseBlock = bElse; + else + out = bElse; + } else { + if (bElse.getPredecessors().size() != 1) { + elseBlock = null; + out = bElse; + } else { + elseBlock = bElse; + for (BlockNode d : block.getDominatesOn()) { + if (d != bThen && d != bElse) { + out = d; + break; + } + } + } + } + + if (stack.containsExit(elseBlock)) + elseBlock = null; + + IfRegion ifRegion = new IfRegion(currentRegion, block); + currentRegion.getSubBlocks().add(ifRegion); + + stack.push(ifRegion); + stack.addExit(out); + + ifRegion.setThenRegion(makeRegion(thenBlock, stack)); + ifRegion.setElseRegion(elseBlock == null ? null : makeRegion(elseBlock, stack)); + + stack.pop(); + return out; + } + + private BlockNode processSwitch(IRegion currentRegion, BlockNode block, SwitchNode insn, RegionStack stack) { + SwitchRegion sw = new SwitchRegion(currentRegion, block); + currentRegion.getSubBlocks().add(sw); + + int len = insn.getTargets().length; + // sort by target + Map> casesMap = new LinkedHashMap>(len); + for (int i = 0; i < len; i++) { + int key = insn.getKeys()[i]; + int targ = insn.getTargets()[i]; + List keys = casesMap.get(targ); + if (keys == null) { + keys = new ArrayList(1); + casesMap.put(targ, keys); + } + keys.add(key); + } + + Map> blocksMap = new LinkedHashMap>(len); + + for (Entry> entry : casesMap.entrySet()) { + BlockNode c = BlockUtils.getBlockByOffset(entry.getKey(), block.getSuccessors()); + assert c != null; + blocksMap.put(c, entry.getValue()); + } + + BitSet succ = BlockUtils.blocksToBitSet(mth, block.getSuccessors()); + BitSet domsOn = BlockUtils.blocksToBitSet(mth, block.getDominatesOn()); + domsOn.andNot(succ); // filter 'out' block + + BlockNode defCase = BlockUtils.getBlockByOffset(insn.getDefaultCaseOffset(), block.getSuccessors()); + if (defCase != null) { + blocksMap.remove(defCase); + } + + int outCount = domsOn.cardinality(); + if (outCount > 1) { + // remove exception handlers + BlockUtils.cleanBitSet(mth, domsOn); + outCount = domsOn.cardinality(); + } + if (outCount > 1) { + // filter successors of other blocks + for (int i = domsOn.nextSetBit(0); i >= 0; i = domsOn.nextSetBit(i + 1)) { + BlockNode b = mth.getBasicBlocks().get(i); + for (BlockNode s : b.getCleanSuccessors()) + if (domsOn.get(s.getId())) + domsOn.clear(s.getId()); + } + outCount = domsOn.cardinality(); + } + + BlockNode out = null; + if (outCount == 1) { + out = mth.getBasicBlocks().get(domsOn.nextSetBit(0)); + } else if (outCount == 0) { + // default and out blocks are same + out = defCase; + } + + stack.push(sw); + if (out != null) { + stack.addExit(out); + } else { + for (BlockNode e : BlockUtils.bitsetToBlocks(mth, domsOn)) + stack.addExit(e); + } + + if (!stack.containsExit(defCase)) { + sw.setDefaultCase(makeRegion(defCase, stack)); + } + for (Entry> entry : blocksMap.entrySet()) { + BlockNode c = entry.getKey(); + sw.addCase(entry.getValue(), makeRegion(c, stack)); + } + + stack.pop(); + return out; + } + + public void processExcHandler(ExceptionHandler handler, RegionStack stack) { + BlockNode start = handler.getHandleBlock(); + if (start == null) { + LOG.debug(ErrorsCounter.formatErrorMsg(mth, "No exception handler block: " + handler)); + return; + } + + BlockNode out = BlockUtils.traverseWhileDominates(start, start); + if (out != null) + stack.addExit(out); + // TODO extract finally part which exists in all handlers from same try block + // TODO add blocks common for several handlers to some region + handler.setHandlerRegion(makeRegion(start, stack)); + + IAttribute excHandlerAttr = start.getAttributes().get(AttributeType.EXC_HANDLER); + handler.getHandlerRegion().getAttributes().add(excHandlerAttr); + } + +} diff --git a/src/main/java/jadx/dex/visitors/regions/RegionMakerVisitor.java b/src/main/java/jadx/dex/visitors/regions/RegionMakerVisitor.java new file mode 100644 index 000000000..9eff8e231 --- /dev/null +++ b/src/main/java/jadx/dex/visitors/regions/RegionMakerVisitor.java @@ -0,0 +1,31 @@ +package jadx.dex.visitors.regions; + +import jadx.dex.nodes.MethodNode; +import jadx.dex.trycatch.ExceptionHandler; +import jadx.dex.visitors.AbstractVisitor; +import jadx.utils.exceptions.JadxException; + +/** + * Pack blocks into regions for code generation + */ +public class RegionMakerVisitor extends AbstractVisitor { + + @Override + public void visit(MethodNode mth) throws JadxException { + if (mth.isNoCode()) + return; + + RegionMaker rm = new RegionMaker(mth); + RegionStack state = new RegionStack(mth); + + // fill region structure + mth.setRegion(rm.makeRegion(mth.getEnterBlock(), state)); + + if (mth.getExceptionHandlers() != null) { + state = new RegionStack(mth); + for (ExceptionHandler handler : mth.getExceptionHandlers()) { + rm.processExcHandler(handler, state); + } + } + } +} diff --git a/src/main/java/jadx/dex/visitors/regions/RegionStack.java b/src/main/java/jadx/dex/visitors/regions/RegionStack.java new file mode 100644 index 000000000..bec8f50a5 --- /dev/null +++ b/src/main/java/jadx/dex/visitors/regions/RegionStack.java @@ -0,0 +1,92 @@ +package jadx.dex.visitors.regions; + +import jadx.dex.nodes.BlockNode; +import jadx.dex.nodes.IRegion; +import jadx.dex.nodes.MethodNode; + +import java.util.HashSet; +import java.util.Set; +import java.util.Stack; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class RegionStack { + private final static Logger LOG = LoggerFactory.getLogger(RegionStack.class); + private final static boolean DEBUG = false; + + static { + if (DEBUG) + LOG.debug("Debug enabled for " + RegionStack.class); + } + + private static class State { + final Set exits; + IRegion region; + + public State() { + exits = new HashSet(); + } + + public State copy() { + State c = new State(); + c.exits.addAll(exits); + return c; + } + + @Override + public String toString() { + return "Exits: " + exits; + } + } + + private final Stack stack; + private State curState; + + public RegionStack(MethodNode mth) { + if (DEBUG) + LOG.debug("New RegionStack: {}", mth); + this.stack = new Stack(); + this.curState = new State(); + } + + public void push(IRegion region) { + stack.push(curState); + if (stack.size() > 1000) + throw new StackOverflowError("Deep code hierarchy"); + + curState = curState.copy(); + curState.region = region; + if (DEBUG) + LOG.debug("Stack push: {} = {}", region, curState); + } + + public void pop() { + curState = stack.pop(); + if (DEBUG) + LOG.debug("Stack pop : {}", curState); + } + + /** + * Add boundary(exit) node for current stack frame + * + * @param exit + * boundary node, null will be ignored + */ + public void addExit(BlockNode exit) { + if (exit != null) + curState.exits.add(exit); + } + + public boolean containsExit(BlockNode exit) { + return curState.exits.contains(exit); + } + + public IRegion peekRegion() { + return curState.region; + } + + public int size() { + return stack.size(); + } +} diff --git a/src/main/java/jadx/dex/visitors/regions/TracedRegionVisitor.java b/src/main/java/jadx/dex/visitors/regions/TracedRegionVisitor.java new file mode 100644 index 000000000..4493c4800 --- /dev/null +++ b/src/main/java/jadx/dex/visitors/regions/TracedRegionVisitor.java @@ -0,0 +1,31 @@ +package jadx.dex.visitors.regions; + +import jadx.dex.nodes.IBlock; +import jadx.dex.nodes.IRegion; +import jadx.dex.nodes.MethodNode; + +import java.util.Stack; + +public abstract class TracedRegionVisitor implements IRegionVisitor { + + private final Stack regionStack = new Stack(); + + @Override + public void enterRegion(MethodNode mth, IRegion region) { + regionStack.push(region); + } + + @Override + public void processBlock(MethodNode mth, IBlock container) { + final IRegion curRegion = regionStack.peek(); + processBlockTraced(mth, container, curRegion); + } + + public abstract void processBlockTraced(MethodNode mth, IBlock container, IRegion currentRegion); + + @Override + public void leaveRegion(MethodNode mth, IRegion region) { + regionStack.pop(); + } + +} diff --git a/src/main/java/jadx/dex/visitors/typeresolver/FinishTypeResolver.java b/src/main/java/jadx/dex/visitors/typeresolver/FinishTypeResolver.java new file mode 100644 index 000000000..6276a916c --- /dev/null +++ b/src/main/java/jadx/dex/visitors/typeresolver/FinishTypeResolver.java @@ -0,0 +1,42 @@ +package jadx.dex.visitors.typeresolver; + +import jadx.dex.nodes.BlockNode; +import jadx.dex.nodes.InsnNode; +import jadx.dex.nodes.MethodNode; +import jadx.dex.visitors.AbstractVisitor; +import jadx.dex.visitors.typeresolver.finish.CheckTypeVisitor; +import jadx.dex.visitors.typeresolver.finish.PostTypeResolver; +import jadx.dex.visitors.typeresolver.finish.SelectTypeVisitor; + +public class FinishTypeResolver extends AbstractVisitor { + + @Override + public void visit(MethodNode mth) { + if (mth.isNoCode()) + return; + + boolean change; + int i = 0; + do { + change = false; + for (BlockNode block : mth.getBasicBlocks()) + for (InsnNode insn : block.getInstructions()) + if (PostTypeResolver.visit(insn)) + change = true; + + i++; + if (i > 1000) + break; + } while (change); + + // last chance to set correct value (just use first type from 'possible' list) + for (BlockNode block : mth.getBasicBlocks()) + for (InsnNode insn : block.getInstructions()) + SelectTypeVisitor.visit(insn); + + // check + for (BlockNode block : mth.getBasicBlocks()) + for (InsnNode insn : block.getInstructions()) + CheckTypeVisitor.visit(mth, insn); + } +} diff --git a/src/main/java/jadx/dex/visitors/typeresolver/TypeResolver.java b/src/main/java/jadx/dex/visitors/typeresolver/TypeResolver.java new file mode 100644 index 000000000..17b1156a0 --- /dev/null +++ b/src/main/java/jadx/dex/visitors/typeresolver/TypeResolver.java @@ -0,0 +1,98 @@ +package jadx.dex.visitors.typeresolver; + +import jadx.dex.attributes.BlockRegState; +import jadx.dex.instructions.args.InsnArg; +import jadx.dex.instructions.args.RegisterArg; +import jadx.dex.nodes.BlockNode; +import jadx.dex.nodes.InsnNode; +import jadx.dex.nodes.MethodNode; +import jadx.dex.visitors.AbstractVisitor; + +import java.util.List; + +public class TypeResolver extends AbstractVisitor { + + @Override + public void visit(MethodNode mth) { + if (mth.isNoCode()) + return; + + visitBlocks(mth); + visitEdges(mth); + + // clear register states + for (BlockNode block : mth.getBasicBlocks()) { + block.setStartState(null); + block.setEndState(null); + } + } + + private void visitBlocks(MethodNode mth) { + for (BlockNode block : mth.getBasicBlocks()) { + BlockRegState state = new BlockRegState(mth); + + if (block == mth.getEnterBlock()) { + for (RegisterArg arg : mth.getArguments(true)) { + state.assignReg(arg); + } + } + block.setStartState(new BlockRegState(state)); + + for (InsnNode insn : block.getInstructions()) { + for (InsnArg arg : insn.getArguments()) { + if (arg.isRegister()) + state.use((RegisterArg) arg); + } + if (insn.getResult() != null) + state.assignReg(insn.getResult()); + } + + if (block.getSuccessors().size() > 0) + block.setEndState(new BlockRegState(state)); + } + } + + private void visitEdges(MethodNode mth) { + List preds = mth.getBasicBlocks(); + boolean changed; + do { + changed = false; + for (BlockNode block : preds) { + for (BlockNode pred : block.getPredecessors()) { + if (connectEdges(mth, pred, block, true)) + changed = true; + } + } + } while (changed); + + for (BlockNode block : mth.getBasicBlocks()) { + for (BlockNode dest : block.getSuccessors()) { + connectEdges(mth, block, dest, false); + } + } + } + + private boolean connectEdges(MethodNode mth, BlockNode from, BlockNode to, boolean back) { + BlockRegState end = from.getEndState(); + BlockRegState start = to.getStartState(); + + boolean changed = false; + for (int r = 0; r < mth.getRegsCount(); r++) { + RegisterArg sr = start.getRegister(r); + RegisterArg er = end.getRegister(r); + + if (back) { + if (er.getTypedVar() == null && sr.getTypedVar() != null) { + er.replace(sr); + changed = true; + } + } else { + if (sr.getTypedVar() != null && er.getTypedVar() != null) { + sr.replace(er); + changed = true; + } + } + } + return changed; + } +} diff --git a/src/main/java/jadx/dex/visitors/typeresolver/finish/CheckTypeVisitor.java b/src/main/java/jadx/dex/visitors/typeresolver/finish/CheckTypeVisitor.java new file mode 100644 index 000000000..3676b5ce1 --- /dev/null +++ b/src/main/java/jadx/dex/visitors/typeresolver/finish/CheckTypeVisitor.java @@ -0,0 +1,34 @@ +package jadx.dex.visitors.typeresolver.finish; + +import jadx.dex.instructions.args.InsnArg; +import jadx.dex.nodes.InsnNode; +import jadx.dex.nodes.MethodNode; +import jadx.utils.ErrorsCounter; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class CheckTypeVisitor { + private final static Logger LOG = LoggerFactory.getLogger(CheckTypeVisitor.class); + + public static void visit(MethodNode mth, InsnNode insn) { + if (insn.getResult() != null) { + if (!insn.getResult().getType().isTypeKnown()) { + error("Wrong return type", mth, insn); + return; + } + } + + for (InsnArg arg : insn.getArguments()) { + if (!arg.getType().isTypeKnown()) { + error("Wrong type", mth, insn); + return; + } + } + } + + private static void error(String msg, MethodNode mth, InsnNode insn) { + // LOG.warn(msg + ": " + insn + " " + insn.getMethod()); + ErrorsCounter.methodError(mth, msg + ": " + insn); + } +} diff --git a/src/main/java/jadx/dex/visitors/typeresolver/finish/PostTypeResolver.java b/src/main/java/jadx/dex/visitors/typeresolver/finish/PostTypeResolver.java new file mode 100644 index 000000000..85b6f2212 --- /dev/null +++ b/src/main/java/jadx/dex/visitors/typeresolver/finish/PostTypeResolver.java @@ -0,0 +1,72 @@ +package jadx.dex.visitors.typeresolver.finish; + +import jadx.dex.instructions.IfNode; +import jadx.dex.instructions.args.ArgType; +import jadx.dex.instructions.args.InsnArg; +import jadx.dex.instructions.args.LiteralArg; +import jadx.dex.instructions.args.RegisterArg; +import jadx.dex.nodes.InsnNode; + +public class PostTypeResolver { + + public static boolean visit(InsnNode insn) { + switch (insn.getType()) { + case CONST: + if (insn.getArgsCount() > 0) { + RegisterArg res = insn.getResult(); + LiteralArg litArg = (LiteralArg) insn.getArg(0); + if (res.getType().isObject()) { + long lit = litArg.getLiteral(); + if (lit != 0) { + // incorrect literal value for object + ArgType type = (lit == 1 ? ArgType.BOOLEAN : ArgType.INT); + // can't merge with object -> force it + litArg.getTypedVar().forceSetType(type); + res.getTypedVar().forceSetType(type); + return true; + } + } + // return litArg.getTypedVar().forceSetType(res.getType()); + return litArg.merge(res); + } + break; + + case MOVE: { + boolean change = false; + if (insn.getResult().merge(insn.getArg(0))) + change = true; + if (insn.getArg(0).merge(insn.getResult())) + change = true; + return change; + } + + case AGET: { + boolean change = false; + RegisterArg elem = insn.getResult(); + InsnArg array = insn.getArg(0); + if (!elem.getType().isTypeKnown() && elem.merge(array.getType().getArrayElement())) + change = true; + if (!array.getType().isTypeKnown() && array.merge(ArgType.array(elem.getType()))) + change = true; + return change; + } + + case IF: { + boolean change = false; + IfNode ifnode = (IfNode) insn; + if (!ifnode.isZeroCmp()) { + if (insn.getArg(1).merge(insn.getArg(0))) + change = true; + if (insn.getArg(0).merge(insn.getArg(1))) + change = true; + } + return change; + } + + default: + break; + } + return false; + + } +} diff --git a/src/main/java/jadx/dex/visitors/typeresolver/finish/SelectTypeVisitor.java b/src/main/java/jadx/dex/visitors/typeresolver/finish/SelectTypeVisitor.java new file mode 100644 index 000000000..8dd2ab373 --- /dev/null +++ b/src/main/java/jadx/dex/visitors/typeresolver/finish/SelectTypeVisitor.java @@ -0,0 +1,27 @@ +package jadx.dex.visitors.typeresolver.finish; + +import jadx.dex.instructions.args.ArgType; +import jadx.dex.instructions.args.InsnArg; +import jadx.dex.nodes.InsnNode; + +public class SelectTypeVisitor { + + public static void visit(InsnNode insn) { + InsnArg res = insn.getResult(); + if (res != null && !res.getType().isTypeKnown()) { + selectType(res); + } + + for (InsnArg arg : insn.getArguments()) { + if (!arg.getType().isTypeKnown()) + selectType(arg); + } + } + + private static void selectType(InsnArg arg) { + ArgType t = arg.getType(); + ArgType nt = t.selectFirst(); + arg.getTypedVar().merge(nt); + } + +} diff --git a/src/main/java/jadx/utils/BlockUtils.java b/src/main/java/jadx/utils/BlockUtils.java new file mode 100644 index 000000000..ede78e105 --- /dev/null +++ b/src/main/java/jadx/utils/BlockUtils.java @@ -0,0 +1,226 @@ +package jadx.utils; + +import jadx.dex.attributes.AttributeType; +import jadx.dex.instructions.InsnType; +import jadx.dex.nodes.BlockNode; +import jadx.dex.nodes.InsnNode; +import jadx.dex.nodes.MethodNode; +import jadx.utils.exceptions.JadxRuntimeException; + +import java.util.ArrayList; +import java.util.BitSet; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +public class BlockUtils { + + public static BlockNode getBlockByOffset(int offset, Iterable casesBlocks) { + for (BlockNode block : casesBlocks) { + if (block.getStartOffset() == offset) + return block; + } + throw new JadxRuntimeException("Can'r find block by offset: " + + InsnUtils.formatOffset(offset) + + " in list " + casesBlocks); + } + + public static BlockNode selectOther(BlockNode node, List blocks) { + List list = blocks; + if (list.size() > 2) { + list = cleanBlockList(list); + } + + assert list.size() == 2 : "too many nodes for selectOther: " + node + " in " + list; + BlockNode first = list.get(0); + if (first != node) + return first; + else + return list.get(1); + } + + public static List cleanBlockList(List list) { + List ret = new ArrayList(list.size()); + for (BlockNode block : list) { + if (!block.getAttributes().contains(AttributeType.EXC_HANDLER)) + ret.add(block); + } + return ret; + } + + /** + * Remove exception handlers from block nodes bitset + */ + public static void cleanBitSet(MethodNode mth, BitSet bs) { + for (int i = bs.nextSetBit(0); i >= 0; i = bs.nextSetBit(i + 1)) { + BlockNode block = mth.getBasicBlocks().get(i); + if (block.getAttributes().contains(AttributeType.EXC_HANDLER)) + bs.clear(i); + } + } + + public static BlockNode canMergeNextBlock(MethodNode mth, BlockNode block) { + BlockNode next = getNextBlock(block); + if (next != null) { + if (next.getIDom() == block) { + return next; + } + } + return null; + } + + /** + * Check if insn contains in block (use == for comparison, not equals) + */ + public static boolean blockContains(BlockNode block, InsnNode insn) { + for (InsnNode bi : block.getInstructions()) { + if (bi == insn) + return true; + } + return false; + } + + /** + * Return position of instruction in block (use == for comparison, not equals) + */ + public static int insnIndex(BlockNode block, InsnNode insn) { + int size = block.getInstructions().size(); + for (int i = 0; i < size; i++) { + if (block.getInstructions().get(i) == insn) + return i; + } + return -1; + } + + public static boolean lastInsnType(BlockNode block, InsnType type) { + List insns = block.getInstructions(); + if (insns.isEmpty()) + return false; + + InsnNode insn = insns.get(insns.size() - 1); + return insn.getType() == type; + } + + public static BlockNode getBlockByInsn(MethodNode mth, InsnNode insn) { + assert insn != null; + for (BlockNode bn : mth.getBasicBlocks()) { + if (blockContains(bn, insn)) + return bn; + } + return null; + } + + public static BitSet blocksToBitSet(MethodNode mth, List blocks) { + BitSet bs = new BitSet(mth.getBasicBlocks().size()); + for (BlockNode block : blocks) { + bs.set(block.getId()); + } + return bs; + } + + public static List bitsetToBlocks(MethodNode mth, BitSet bs) { + List blocks = new ArrayList(bs.cardinality()); + for (int i = bs.nextSetBit(0); i >= 0; i = bs.nextSetBit(i + 1)) { + BlockNode block = mth.getBasicBlocks().get(i); + blocks.add(block); + } + return blocks; + } + + /** + * Return first successor which not exception handler or followed by loop back edge + */ + public static BlockNode getNextBlock(BlockNode block) { + List s = block.getCleanSuccessors(); + return s.isEmpty() ? null : s.get(0); + } + + /** + * Collect blocks from all possible execution paths from 'start' to 'end' + */ + public static Set getAllPathsBlocks(BlockNode start, BlockNode end) { + Set set = new HashSet(); + set.add(start); + if (start != end) + addPredcessors(set, end, start); + return set; + } + + private static void addPredcessors(Set set, BlockNode from, BlockNode until) { + set.add(from); + for (BlockNode pred : from.getPredecessors()) { + if (pred != until && !set.contains(pred)) { + addPredcessors(set, pred, until); + } + } + } + + private static boolean addSuccessorsUntil(BlockNode from, BlockNode until) { + if (from == until) + return true; + + for (BlockNode s : from.getCleanSuccessors()) { + if (addSuccessorsUntil(s, until)) + return true; + } + return false; + } + + private static boolean addPredcessorsUntil(Set set, BlockNode from, BlockNode until) { + set.add(from); + for (BlockNode pred : from.getPredecessors()) { + if (pred == until) + return true; + if (pred != until && !set.contains(pred)) { + if (addPredcessorsUntil(set, pred, until)) + return true; + } + } + return false; + } + + public static boolean isPathExists(BlockNode start, BlockNode end) { + if (start == end) + return true; + + if (end.isDominator(start)) + return true; + + return addSuccessorsUntil(start, end); + // return addPredcessorsUntil(new HashSet(), end, start); + } + + /** + * Search for first node which not dominated by block, starting from child + */ + public static BlockNode traverseWhileDominates(BlockNode block, BlockNode child) { + for (BlockNode node : child.getCleanSuccessors()) { + if (!node.isDominator(block)) { + return node; + } else { + BlockNode out = traverseWhileDominates(block, node); + if (out != null) + return out; + } + } + return null; + } + + /** + * Collect all block dominated by 'dominator', starting from 'start' + */ + public static List collectBlocksDominatedBy(BlockNode dominator, BlockNode start) { + List result = new ArrayList(); + collectWhileDominates(dominator, start, result); + return result; + } + + private static void collectWhileDominates(BlockNode dominator, BlockNode child, List result) { + for (BlockNode node : child.getCleanSuccessors()) { + if (node.isDominator(dominator)) { + result.add(node); + collectWhileDominates(dominator, node, result); + } + } + } +} diff --git a/src/main/java/jadx/utils/ErrorsCounter.java b/src/main/java/jadx/utils/ErrorsCounter.java new file mode 100644 index 000000000..4022ed321 --- /dev/null +++ b/src/main/java/jadx/utils/ErrorsCounter.java @@ -0,0 +1,95 @@ +package jadx.utils; + +import jadx.dex.attributes.IAttributeNode; +import jadx.dex.attributes.JadxErrorAttr; +import jadx.dex.nodes.ClassNode; +import jadx.dex.nodes.MethodNode; + +import java.util.HashSet; +import java.util.Set; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class ErrorsCounter { + private final static Logger LOG = LoggerFactory.getLogger(ErrorsCounter.class); + + private final static Set errorNodes = new HashSet(); + private static int errorsCount = 0; + + public static int getErrorCount() { + return errorsCount; + } + + public static void reset() { + errorNodes.clear(); + errorsCount = 0; + } + + private static void addError(IAttributeNode node, String msg, Throwable e) { + errorNodes.add(node); + errorsCount++; + + if (e != null) { + if (e.getClass() == StackOverflowError.class) { + // don't print full stack trace + e = new StackOverflowError(e.getMessage()); + LOG.error(msg); + } else { + LOG.error(msg, e); + } + node.getAttributes().add(new JadxErrorAttr(e)); + } else { + LOG.error(msg); + } + } + + public static String classError(ClassNode cls, String errorMsg, Throwable e) { + String msg = formatErrorMsg(cls, errorMsg); + addError(cls, msg, e); + return msg; + } + + public static String methodError(MethodNode mth, String errorMsg, Throwable e) { + String msg = formatErrorMsg(mth, errorMsg); + addError(mth, msg, e); + return msg; + } + + public static String methodError(MethodNode mth, String errorMsg) { + return methodError(mth, errorMsg, null); + } + + public static void printReport() { + if (getErrorCount() > 0) { + LOG.error(getErrorCount() + " errors occured in following nodes:"); + for (Object node : errorNodes) { + LOG.error(" " + node.getClass().getSimpleName() + ": " + node); + } + // LOG.error("You can run jadx with '-f' option to view low level instructions"); + } + } + + public static String formatErrorMsg(ClassNode cls, String msg) { + return msg + " in class: " + cls; + } + + public static String formatErrorMsg(MethodNode mth, String msg) { + return msg + " in method: " + mth; + } + + private static String formatException(Throwable e) { + if (e == null || e.getMessage() == null) + return ""; + else + return "\n error: " + e.getMessage(); + } + + public static String formatErrorMsg(ClassNode cls, String msg, Throwable e) { + return formatErrorMsg(cls, msg) + formatException(e); + } + + public static String formatErrorMsg(MethodNode mth, String msg, Throwable e) { + return formatErrorMsg(mth, msg) + formatException(e); + } +} diff --git a/src/main/java/jadx/utils/InsnUtils.java b/src/main/java/jadx/utils/InsnUtils.java new file mode 100644 index 000000000..2cf2a4c27 --- /dev/null +++ b/src/main/java/jadx/utils/InsnUtils.java @@ -0,0 +1,46 @@ +package jadx.utils; + +import jadx.dex.instructions.InsnType; + +import com.android.dx.io.instructions.DecodedInstruction; + +public class InsnUtils { + + public static int getArg(DecodedInstruction insn, int arg) { + switch (arg) { + case 0: + return insn.getA(); + case 1: + return insn.getB(); + case 2: + return insn.getC(); + case 3: + return insn.getD(); + case 4: + return insn.getE(); + } + throw new RuntimeException("Wrong argument number: " + arg); + } + + public static String formatOffset(int offset) { + return String.format("0x%04x", offset); + } + + public static String insnTypeToString(InsnType type) { + return insnTypeToString(type.toString()); + } + + public static String insnTypeToString(String str) { + return String.format("%s ", str); + } + + public static String indexToString(Object index) { + if (index == null) + return ""; + + if (index instanceof String) + return "\"" + index + "\""; + else + return " " + index.toString(); + } +} diff --git a/src/main/java/jadx/utils/RegionUtils.java b/src/main/java/jadx/utils/RegionUtils.java new file mode 100644 index 000000000..52ff70c42 --- /dev/null +++ b/src/main/java/jadx/utils/RegionUtils.java @@ -0,0 +1,164 @@ +package jadx.utils; + +import jadx.dex.attributes.AttributeType; +import jadx.dex.nodes.BlockNode; +import jadx.dex.nodes.IContainer; +import jadx.dex.nodes.IRegion; +import jadx.dex.trycatch.CatchAttr; +import jadx.dex.trycatch.ExceptionHandler; +import jadx.dex.trycatch.TryCatchBlock; +import jadx.utils.exceptions.JadxRuntimeException; + +import java.util.List; + +public class RegionUtils { + + public static boolean hasExitEdge(IContainer container) { + if (container instanceof BlockNode) { + return ((BlockNode) container).getSuccessors().size() != 0; + } else if (container instanceof IRegion) { + IRegion region = (IRegion) container; + List blocks = region.getSubBlocks(); + return hasExitEdge(blocks.get(blocks.size() - 1)); + } else { + throw new JadxRuntimeException("Unknown container type: " + container.getClass()); + } + } + + public static boolean notEmpty(IContainer container) { + if (container instanceof BlockNode) { + return ((BlockNode) container).getInstructions().size() != 0; + } else if (container instanceof IRegion) { + IRegion region = (IRegion) container; + for (IContainer block : region.getSubBlocks()) { + if (notEmpty(block)) + return true; + } + return false; + } else { + throw new JadxRuntimeException("Unknown container type: " + container.getClass()); + } + } + + public static void getAllRegionBlocks(IContainer container, List blocks) { + if (container instanceof BlockNode) { + blocks.add((BlockNode) container); + } else if (container instanceof IRegion) { + IRegion region = (IRegion) container; + for (IContainer block : region.getSubBlocks()) { + getAllRegionBlocks(block, blocks); + } + } else { + throw new JadxRuntimeException("Unknown container type: " + container.getClass()); + } + } + + public static boolean isRegionContainsBlock(IContainer container, BlockNode block) { + if (container instanceof BlockNode) { + return container == block; + } else if (container instanceof IRegion) { + IRegion region = (IRegion) container; + for (IContainer b : region.getSubBlocks()) { + if (isRegionContainsBlock(b, block)) + return true; + } + return false; + } else { + throw new JadxRuntimeException("Unknown container type: " + container.getClass()); + } + } + + private static boolean isRegionContainsExcHandlerRegion(IContainer container, IRegion region) { + if (container == region) + return true; + + if (container instanceof IRegion) { + IRegion r = (IRegion) container; + + // process sub blocks + for (IContainer b : r.getSubBlocks()) { + // process try block + CatchAttr cb = (CatchAttr) b.getAttributes().get(AttributeType.CATCH_BLOCK); + if (cb != null && (b instanceof IRegion)) { + TryCatchBlock tb = cb.getTryBlock(); + for (ExceptionHandler eh : tb.getHandlers()) { + if (isRegionContainsRegion(eh.getHandlerRegion(), region)) + return true; + } + if (tb.getFinalBlock() != null) { + if (isRegionContainsRegion(tb.getFinalBlock(), region)) + return true; + } + } + if (isRegionContainsRegion(b, region)) + return true; + } + } + return false; + } + + /** + * Check if region contains in container + * + * For simple region (not from exception handlers) search in parents + * otherwise run recursive search because exception handlers can have several parents + */ + public static boolean isRegionContainsRegion(IContainer container, IRegion region) { + if (container == region) return true; + if (region == null) return false; + + IRegion parent = region.getParent(); + while (container != parent) { + if (parent == null) { + if (region.getAttributes().contains(AttributeType.EXC_HANDLER)) + return isRegionContainsExcHandlerRegion(container, region); + else + return false; + } + region = parent; + parent = region.getParent(); + } + return true; + } + + public static boolean isDominaterBy(BlockNode dom, IContainer cont) { + assert cont != null; + + if (dom == cont) + return true; + + if (cont instanceof BlockNode) { + BlockNode block = (BlockNode) cont; + return block.isDominator(dom); + } else if (cont instanceof IRegion) { + IRegion region = (IRegion) cont; + for (IContainer c : region.getSubBlocks()) { + if (!isDominaterBy(dom, c)) { + return false; + } + } + return true; + } else { + throw new JadxRuntimeException("Unknown container type: " + cont.getClass()); + } + } + + public static boolean hasPathThruBlock(BlockNode block, IContainer cont) { + if (block == cont) + return true; + + if (cont instanceof BlockNode) { + return BlockUtils.isPathExists(block, (BlockNode) cont); + } else if (cont instanceof IRegion) { + IRegion region = (IRegion) cont; + for (IContainer c : region.getSubBlocks()) { + if (!hasPathThruBlock(block, c)) + return false; + } + return true; + } else { + throw new JadxRuntimeException("Unknown container type: " + cont.getClass()); + } + } + +} diff --git a/src/main/java/jadx/utils/StringUtils.java b/src/main/java/jadx/utils/StringUtils.java new file mode 100644 index 000000000..1acdf7fe6 --- /dev/null +++ b/src/main/java/jadx/utils/StringUtils.java @@ -0,0 +1,61 @@ +package jadx.utils; + +public class StringUtils { + + public static String unescapeString(String str) { + int len = str.length(); + StringBuilder res = new StringBuilder(); + + for (int i = 0; i < len; i++) { + int c = str.charAt(i) & 0xFFFF; + processChar(c, res); + } + return '"' + res.toString() + '"'; + } + + public static String unescapeChar(char ch) { + if (ch == '\'') + return "'\\\''"; + + StringBuilder res = new StringBuilder(); + processChar(ch, res); + return '\'' + res.toString() + '\''; + } + + public static void processChar(int c, StringBuilder res) { + switch (c) { + case '\n': + res.append("\\n"); + break; + case '\r': + res.append("\\r"); + break; + case '\t': + res.append("\\t"); + break; + case '\b': + res.append("\\b"); + break; + case '\f': + res.append("\\f"); + break; + case '\'': + res.append('\''); + break; + case '"': + res.append("\\\""); + break; + case '\\': + res.append("\\\\"); + break; + + default: + if (32 <= c && c <= 126) { + res.append((char) c); + } else { + res.append("\\u").append(String.format("%04x", c)); + } + break; + } + } +} diff --git a/src/main/java/jadx/utils/Utils.java b/src/main/java/jadx/utils/Utils.java new file mode 100644 index 000000000..50da859ce --- /dev/null +++ b/src/main/java/jadx/utils/Utils.java @@ -0,0 +1,69 @@ +package jadx.utils; + +import java.io.File; +import java.io.PrintWriter; +import java.io.StringWriter; +import java.util.Iterator; +import java.util.List; + +public class Utils { + + public static String cleanObjectName(String obj) { + int last = obj.length() - 1; + if (obj.charAt(0) == 'L' && obj.charAt(last) == ';') + return obj.substring(1, last).replace('/', '.'); + else + return obj; + } + + public static String makeQualifiedObjectName(String obj) { + return 'L' + obj.replace('.', '/') + ';'; + } + + public static String escape(String str) { + return str.replace('.', '_').replace('/', '_').replace(';', '_') + .replace('$', '_').replace("[]", "_A"); + } + + public static String listToString(Iterable list) { + if (list == null) + return ""; + + StringBuilder str = new StringBuilder(); + for (Iterator it = list.iterator(); it.hasNext();) { + Object o = it.next(); + str.append(o.toString()); + if (it.hasNext()) + str.append(", "); + } + return str.toString(); + } + + public static boolean deleteFolder(File dir) { + File[] files = dir.listFiles(); + if (files != null) { + for (File f : files) { + if (f.isDirectory()) + deleteFolder(f); + else + f.delete(); + } + } + return dir.delete(); + } + + public static String getStackTrace(Throwable throwable) { + StringWriter sw = new StringWriter(); + PrintWriter pw = new PrintWriter(sw, true); + throwable.printStackTrace(pw); + return sw.getBuffer().toString(); + } + + public static String mergeSignature(List list) { + StringBuilder sb = new StringBuilder(); + for (String s : list) { + sb.append(s); + } + return sb.toString(); + } +} diff --git a/src/main/java/jadx/utils/exceptions/CodegenException.java b/src/main/java/jadx/utils/exceptions/CodegenException.java new file mode 100644 index 000000000..4cf00a45b --- /dev/null +++ b/src/main/java/jadx/utils/exceptions/CodegenException.java @@ -0,0 +1,34 @@ +package jadx.utils.exceptions; + +import jadx.dex.nodes.ClassNode; +import jadx.dex.nodes.MethodNode; + +public class CodegenException extends JadxException { + + private static final long serialVersionUID = 39344288912966824L; + + public CodegenException(String message) { + super(message); + } + + public CodegenException(String message, Throwable cause) { + super(message, cause); + } + + public CodegenException(ClassNode mth, String msg) { + super(mth, msg, null); + } + + public CodegenException(ClassNode mth, String msg, Throwable th) { + super(mth, msg, th); + } + + public CodegenException(MethodNode mth, String msg) { + super(mth, msg, null); + } + + public CodegenException(MethodNode mth, String msg, Throwable th) { + super(mth, msg, th); + } + +} diff --git a/src/main/java/jadx/utils/exceptions/DecodeException.java b/src/main/java/jadx/utils/exceptions/DecodeException.java new file mode 100644 index 000000000..1aa575f2e --- /dev/null +++ b/src/main/java/jadx/utils/exceptions/DecodeException.java @@ -0,0 +1,25 @@ +package jadx.utils.exceptions; + +import jadx.dex.nodes.MethodNode; + +public class DecodeException extends JadxException { + + private static final long serialVersionUID = -6611189094923499636L; + + public DecodeException(String message) { + super(message); + } + + public DecodeException(String message, Throwable cause) { + super(message, cause); + } + + public DecodeException(MethodNode mth, String msg) { + super(mth, msg, null); + } + + public DecodeException(MethodNode mth, String msg, Throwable th) { + super(mth, msg, th); + } + +} diff --git a/src/main/java/jadx/utils/exceptions/JadxException.java b/src/main/java/jadx/utils/exceptions/JadxException.java new file mode 100644 index 000000000..1cfdbaef3 --- /dev/null +++ b/src/main/java/jadx/utils/exceptions/JadxException.java @@ -0,0 +1,27 @@ +package jadx.utils.exceptions; + +import jadx.dex.nodes.ClassNode; +import jadx.dex.nodes.MethodNode; +import jadx.utils.ErrorsCounter; + +public class JadxException extends Exception { + + private static final long serialVersionUID = 3577449089978463557L; + + public JadxException(String message) { + super(message); + } + + public JadxException(String message, Throwable cause) { + super(message, cause); + } + + public JadxException(ClassNode cls, String msg, Throwable th) { + super(ErrorsCounter.formatErrorMsg(cls, msg), th); + } + + public JadxException(MethodNode mth, String msg, Throwable th) { + super(ErrorsCounter.formatErrorMsg(mth, msg), th); + } + +} diff --git a/src/main/java/jadx/utils/exceptions/JadxRuntimeException.java b/src/main/java/jadx/utils/exceptions/JadxRuntimeException.java new file mode 100644 index 000000000..475416aeb --- /dev/null +++ b/src/main/java/jadx/utils/exceptions/JadxRuntimeException.java @@ -0,0 +1,11 @@ +package jadx.utils.exceptions; + + +public class JadxRuntimeException extends RuntimeException { + + private static final long serialVersionUID = -7410848445429898248L; + + public JadxRuntimeException(String message) { + super(message); + } +} diff --git a/src/main/java/jadx/utils/files/InputFile.java b/src/main/java/jadx/utils/files/InputFile.java new file mode 100644 index 000000000..84f7ba98e --- /dev/null +++ b/src/main/java/jadx/utils/files/InputFile.java @@ -0,0 +1,85 @@ +package jadx.utils.files; + +import jadx.utils.exceptions.DecodeException; +import jadx.utils.exceptions.JadxException; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.util.zip.ZipEntry; +import java.util.zip.ZipFile; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.android.dx.io.DexBuffer; + +public class InputFile { + private final static Logger LOG = LoggerFactory.getLogger(InputFile.class); + + private final File file; + private final DexBuffer dexBuf; + + public InputFile(File file) throws IOException, DecodeException { + this.file = file; + + String fileName = file.getName(); + + if (fileName.endsWith(".dex")) { + this.dexBuf = new DexBuffer(file); + } else if (fileName.endsWith(".apk")) { + this.dexBuf = new DexBuffer(openDexFromApk(file)); + } else if (fileName.endsWith(".class") || fileName.endsWith(".jar")) { + try { + LOG.info("converting to dex: {} ...", fileName); + JavaToDex j2d = new JavaToDex(); + byte[] ba = j2d.convert(file.getAbsolutePath()); + if (ba.length == 0) { + throw new JadxException( + j2d.isError() ? j2d.getDxErrors() : "Empty dx output"); + } else if (j2d.isError()) { + LOG.warn("dx message: " + j2d.getDxErrors()); + } + this.dexBuf = new DexBuffer(ba); + } catch (Throwable e) { + throw new DecodeException( + "java class to dex conversion error:\n " + e.getMessage(), e); + } + } else + throw new DecodeException("Unsupported input file: " + file); + } + + private byte[] openDexFromApk(File file) throws IOException { + ZipFile zf = new ZipFile(file); + ZipEntry dex = zf.getEntry("classes.dex"); + ByteArrayOutputStream bytesOut = new ByteArrayOutputStream(); + try { + InputStream in = zf.getInputStream(dex); + + byte[] buffer = new byte[8192]; + int count; + while ((count = in.read(buffer)) != -1) { + bytesOut.write(buffer, 0, count); + } + in.close(); + } finally { + zf.close(); + } + return bytesOut.toByteArray(); + } + + public File getFile() { + return file; + } + + public DexBuffer getDexBuffer() { + return dexBuf; + } + + @Override + public String toString() { + return file.toString(); + } + +} diff --git a/src/main/java/jadx/utils/files/JavaToDex.java b/src/main/java/jadx/utils/files/JavaToDex.java new file mode 100644 index 000000000..c3d746fb1 --- /dev/null +++ b/src/main/java/jadx/utils/files/JavaToDex.java @@ -0,0 +1,58 @@ +package jadx.utils.files; + +import jadx.utils.exceptions.JadxException; + +import java.io.ByteArrayOutputStream; +import java.io.PrintStream; + +import com.android.dx.command.DxConsole; +import com.android.dx.command.dexer.Main; +import com.android.dx.command.dexer.Main.Arguments; + +public class JavaToDex { + + public static class DxArgs extends Arguments { + public DxArgs(String dexFile, String[] input) { + outName = dexFile; + fileNames = input; + jarOutput = false; + + optimize = true; + localInfo = true; + coreLibrary = true; + } + } + + private String dxErrors; + + public byte[] convert(String javaFile) throws JadxException { + + ByteArrayOutputStream err_out = new ByteArrayOutputStream(); + DxConsole.err = new PrintStream(err_out); + + PrintStream old_out = System.out; + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + try { + System.setOut(new PrintStream(baos)); + DxArgs args = new DxArgs("-", new String[] { javaFile }); + Main.run(args); + baos.close(); + } catch (Throwable e) { + throw new JadxException("dx exception: " + e.getMessage(), e); + } finally { + System.setOut(old_out); + } + + // err_out also contains warnings + dxErrors = err_out.toString(); + return baos.toByteArray(); + } + + public String getDxErrors() { + return dxErrors; + } + + public boolean isError() { + return dxErrors != null && dxErrors.length() > 0; + } +} diff --git a/src/main/resources/logback.xml b/src/main/resources/logback.xml new file mode 100644 index 000000000..f4e91a312 --- /dev/null +++ b/src/main/resources/logback.xml @@ -0,0 +1,13 @@ + + + + + %d{HH:mm:ss} %-5level - %msg%n + + + + + + + + \ No newline at end of file diff --git a/src/samples/java/jadx/samples/AbstractTest.java b/src/samples/java/jadx/samples/AbstractTest.java new file mode 100644 index 000000000..5c126b987 --- /dev/null +++ b/src/samples/java/jadx/samples/AbstractTest.java @@ -0,0 +1,18 @@ +package jadx.samples; + +public abstract class AbstractTest { + + public abstract boolean testRun() throws Exception; + + public static void assertTrue(boolean condition) { + if (!condition) { + throw new AssertionError(); + } + } + + public static void assertEquals(int a1, int a2) { + if (a1 != a2) { + throw new AssertionError(a1 + " != " + a2); + } + } +} diff --git a/src/samples/java/jadx/samples/RunTests.java b/src/samples/java/jadx/samples/RunTests.java new file mode 100644 index 000000000..fc68deafa --- /dev/null +++ b/src/samples/java/jadx/samples/RunTests.java @@ -0,0 +1,114 @@ +package jadx.samples; + +import java.io.File; +import java.io.FilenameFilter; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.net.URL; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Timer; +import java.util.TimerTask; + +public class RunTests { + + public static void main(String[] args) { + ClassLoader clsLoader = ClassLoader.getSystemClassLoader(); + + List clsList = getClasses(clsLoader, "jadx.samples"); + if (clsList.isEmpty()) { + System.err.println("No tests found"); + System.exit(1); + } + + int timeout = 3 * clsList.size(); + System.err.println("Set timeout to " + timeout + " seconds"); + new Timer().schedule(new TerminateTask(), timeout * 1000); + + Collections.sort(clsList); + int passed = 0; + for (String cls : clsList) { + if (runTest(cls)) + passed++; + } + int failed = clsList.size() - passed; + System.err.println("---"); + System.err.println("Total " + clsList.size() + + ", Passed: " + passed + + ", Failed: " + failed); + + System.exit(failed); + } + + private static boolean runTest(String clsName) { + try { + boolean pass = false; + String msg = null; + Throwable exc = null; + + Class cls = Class.forName(clsName); + if (cls.getSuperclass() == AbstractTest.class) { + Method mth = cls.getMethod("testRun"); + + AbstractTest test = (AbstractTest) cls.getConstructor().newInstance(); + try { + pass = (Boolean) mth.invoke(test); + } catch (InvocationTargetException e) { + pass = false; + exc = e.getCause(); + } catch (Throwable e) { + pass = false; + exc = e; + } + } else { + msg = "not extends AbstractTest"; + } + System.err.println(">> " + + (pass ? "PASS" : "FAIL") + "\t" + + clsName + + (msg == null ? "" : "\t - " + msg)); + if (exc != null) + exc.printStackTrace(); + + return pass; + } catch (ClassNotFoundException e) { + System.err.println("Class '" + clsName + "' not found"); + } catch (Exception e) { + e.printStackTrace(); + } + return false; + } + + private static class TerminateTask extends TimerTask { + @Override + public void run() { + System.err.println("Test timed out"); + System.exit(1); + } + } + + private static class TestFilter implements FilenameFilter { + @Override + public boolean accept(File dir, String name) { + return name.startsWith("Test") && name.endsWith(".class") && !name.contains("$"); + } + } + + private static List getClasses(ClassLoader clsLoader, String packageName) { + List clsList = new ArrayList(); + URL resource = clsLoader.getResource(packageName.replace('.', '/')); + if (resource != null) { + File path = new File(resource.getFile()); + if (path.exists() && path.isDirectory()) { + System.out.println("Test classes path: " + path.getAbsolutePath()); + String[] files = path.list(new TestFilter()); + for (String file : files) { + String clsName = packageName + '.' + file.replace(".class", ""); + clsList.add(clsName); + } + } + } + return clsList; + } +} diff --git a/src/samples/java/jadx/samples/TestAnnotations.java b/src/samples/java/jadx/samples/TestAnnotations.java new file mode 100644 index 000000000..4c720258e --- /dev/null +++ b/src/samples/java/jadx/samples/TestAnnotations.java @@ -0,0 +1,102 @@ +package jadx.samples; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.lang.reflect.Method; +import java.util.Arrays; + +public class TestAnnotations extends AbstractTest { + + @Deprecated + public int a; + + public void error() throws Exception { + throw new Exception("error"); + } + + @Deprecated + public static Object depr(String[] a) { + return Arrays.asList(a); + } + + public @interface SimpleAnnotation { + boolean value(); + } + + @Documented + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.METHOD) + public @interface MyAnnotation { + String name() default "a"; + + String str() default "str"; + + int num(); + + float value(); + + double[] doubles(); + + Class cls(); + + SimpleAnnotation simple(); + + Thread.State state() default Thread.State.TERMINATED; + } + + @MyAnnotation(name = "b", + num = 7, + cls = Exception.class, + doubles = { 0.0, 1.1 }, + value = 9.87f, + simple = @SimpleAnnotation(false)) + public static Object test(String[] a) { + return Arrays.asList(a); + } + + public static Object test2(@Deprecated String a, @SimpleAnnotation(value = false) Object b) { + @Deprecated + Object c = a; + return c; + } + + public @interface ClassesAnnotation { + Class[] value(); + } + + @ClassesAnnotation({ + int.class, int[].class, int[][][].class, + String.class, String[].class, String[][].class + }) + public static Object test3(Object b) { + return b.toString(); + } + + @Override + public boolean testRun() throws Exception { + Class cls = TestAnnotations.class; + new Thread(); + + Method err = cls.getMethod("error"); + assertTrue(err.getExceptionTypes().length > 0); + assertTrue(err.getExceptionTypes()[0] == Exception.class); + + Method d = cls.getMethod("depr", String[].class); + assertTrue(d.getAnnotations().length > 0); + assertTrue(d.getAnnotations()[0].annotationType() == Deprecated.class); + + Method ma = cls.getMethod("test", String[].class); + assertTrue(ma.getAnnotations().length > 0); + MyAnnotation a = (MyAnnotation) ma.getAnnotations()[0]; + assertTrue(a.num() == 7); + assertTrue(a.state() == Thread.State.TERMINATED); + return true; + } + + public static void main(String[] args) throws Exception { + new TestAnnotations().testRun(); + } +} diff --git a/src/samples/java/jadx/samples/TestArrays.java b/src/samples/java/jadx/samples/TestArrays.java new file mode 100644 index 000000000..2d4e55b99 --- /dev/null +++ b/src/samples/java/jadx/samples/TestArrays.java @@ -0,0 +1,34 @@ +package jadx.samples; + +public class TestArrays extends AbstractTest { + + public int test1(int i) { + // fill-array-data + int[] a = new int[] { 1, 2, 3, 5 }; + return a[i]; + } + + public int test2(int i) { + // filled-new-array + int[][] a = new int[i][i + 1]; + return a.length; + } + + public int test3(int i) { + // filled-new-array/range + boolean[][][][][][][][] a = new boolean[i][i][i][i][i][i][i][i]; + return a.length; + } + + @Override + public boolean testRun() throws Exception { + assertEquals(test1(2), 3); + assertEquals(test2(2), 2); + assertEquals(test3(2), 2); + return true; + } + + public static void main(String[] args) throws Exception { + new TestArrays().testRun(); + } +} diff --git a/src/samples/java/jadx/samples/TestCF.java b/src/samples/java/jadx/samples/TestCF.java new file mode 100644 index 000000000..fe84499ec --- /dev/null +++ b/src/samples/java/jadx/samples/TestCF.java @@ -0,0 +1,183 @@ +package jadx.samples; + +public class TestCF extends AbstractTest { + + public int test1(int a) { + if (a > 0) { + return 1; + } else { + a += 2; + return a * 3; + } + } + + public int test1a(int a) { + if (a > 0) { + a++; + } + a *= 2; + return a + 3; + + } + + public int test1b(int a) { + if (a > 0) { + if (a < 5) + a++; + else + a -= 2; + } + a *= 2; + return a + 3; + + } + + public int test1c(int a, int b) { + if (a > 0) { + long c = 5; + a = (int) (a + c); + } else { + double f = 7.7; + a *= f; + } + return a + b; + + } + + public int test2(int a, int b) { + int c = a + b; + for (int i = a; i < b; i++) { + c *= 2; + } + c--; + return c; + } + + public int test2a(int a, int b) { + int c = a + b; + for (int i = a; i < b; i++) { + if (i == 7) { + c += 2; + } else { + c *= 2; + } + } + c--; + return c; + } + + public int test3(int a, int b) { + int c = 0; + for (int i = a; i < b; i++) { + int z = a * i + 5; + if (i == 7) { + c += z + a; + } else { + c *= z + b; + } + } + return c; + } + + public int test4(int a, int b) { + int c = 0; + for (int i = a; i < b; i++) { + int z = (i == 7 ? a : b); + c *= z + b; + if (i == 7) { + c += z + a; + } else { + c *= z + b; + } + } + return c; + } + + public int test5(int a, int b) { + int c = b; + do { + int z = c + a; + if (z >= 7) { + break; + } + c = z; + } while (true); + return c; + } + + public int test6(int a, int b) { + int c = b; + int z; + while ((z = c + a) >= 7) { + c = z; + } + return c; + } + + public int test7(int a, int b) { + int c = b; + int z; + + do { + z = c + a; + if (z >= 7) { + break; + } + c = z; + } while (true); + + while ((z = c + a) >= 7) { + c = z; + } + return c; + } + + public void testInfiniteLoop() { + while (true) { + System.out.println("test"); + } + } + + public static void test_hello(String[] args) { + System.out.println("Hello world!"); + } + + public static void test_print(String[] args) { + for (String arg : args) { + System.out.println(arg); + } + } + + @Override + public boolean testRun() throws Exception { + TestCF c = new TestCF(); + assertEquals(c.test1(1), 1); + assertEquals(c.test1(-1), 3); + + assertEquals(c.test1a(12), 29); + + assertEquals(c.test1b(-1), 1); + assertEquals(c.test1b(3), 11); + assertEquals(c.test1b(12), 23); + + assertEquals(c.test1c(-1, 1), -6); + assertEquals(c.test1c(3, 2), 10); + + assertEquals(c.test2(2, 4), 23); + assertEquals(c.test2(6, 4), 9); + + assertEquals(c.test2a(5, 9), 115); + assertEquals(c.test2a(8, 23), 1015807); + + assertEquals(c.test3(5, 9), 2430); + assertEquals(c.test3(8, 23), 0); + + assertEquals(c.test4(5, 9), 3240); + assertEquals(c.test4(8, 15), 0); + return true; + } + + public static void main(String[] args) throws Exception { + System.out.println("TestCF: " + new TestCF().testRun()); + } +} diff --git a/src/samples/java/jadx/samples/TestCF2.java b/src/samples/java/jadx/samples/TestCF2.java new file mode 100644 index 000000000..988e496c1 --- /dev/null +++ b/src/samples/java/jadx/samples/TestCF2.java @@ -0,0 +1,102 @@ +package jadx.samples; + +public class TestCF2 extends AbstractTest { + private final Object ready_mutex = new Object(); + private boolean ready = false; + + /** + * Test infinite loop + */ + public void run() throws InterruptedException { + while (true) { + if (!ready) + ready_mutex.wait(); + ready = false; + func(); + } + } + + private void func() { + ready = true; + } + + public void do_while() throws InterruptedException { + int i = 3; + do { + func(); + i++; + } while (i < 5); + } + + public void do_while_2(long k) throws InterruptedException { + if (k > 5) { + long i = 3; + do { + func(); + i++; + } while (i < 5); + } + } + + public void do_while_3(int k) throws InterruptedException { + int i = 3; + do { + if (k > 9) { + func(); + } + i++; + } while (i < 5); + } + + public int do_while_break(int k) throws InterruptedException { + int i = 3; + do { + if (k > 9) { + i = 0; + break; + } + i++; + } while (i < 5); + + return i; + } + + public int do_while_continue(int k) throws InterruptedException { + int i = 0; + do { + if (k > 9) { + i = k + 1; + continue; + } + i++; + } while (i < k); + return i; + } + + public void do_while_return2(boolean k) throws InterruptedException { + int i = 3; + do { + if (k) + return; + i++; + } while (i < 5); + } + + public void while_iterator(String[] args, int k) throws InterruptedException { + for (String arg : args) { + if (arg.length() > 9) { + func(); + } + } + } + + @Override + public boolean testRun() throws Exception { + // TODO add checks + return true; + } + + public static void main(String[] args) throws Exception { + System.out.println("TestCF2: " + new TestCF2().testRun()); + } +} diff --git a/src/samples/java/jadx/samples/TestCF3.java b/src/samples/java/jadx/samples/TestCF3.java new file mode 100644 index 000000000..12adef9c0 --- /dev/null +++ b/src/samples/java/jadx/samples/TestCF3.java @@ -0,0 +1,114 @@ +package jadx.samples; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Iterator; +import java.util.List; + +public class TestCF3 extends AbstractTest { + + public String f = "str//ing"; + private boolean enabled; + + private void setEnabled(boolean b) { + this.enabled = b; + } + + private int next() { + return 1; + } + + public void testSwitchInLoop() throws Exception { + while (true) { + int n = next(); + switch (n) { + case 0: + setEnabled(false); + break; + + case 1: + setEnabled(true); + return; + } + } + } + + private void testIfInLoop() { + int j = 0; + for (int i = 0; i < f.length(); i++) { + char ch = f.charAt(i); + if (ch == '/') { + j++; + if (j == 2) { + setEnabled(true); + return; + } + } + } + setEnabled(false); + } + + public boolean testNestedLoops(List l1, List l2) { + Iterator it1 = l1.iterator(); + while (it1.hasNext()) { + String s1 = it1.next(); + Iterator it2 = l2.iterator(); + while (it2.hasNext()) { + String s2 = it2.next(); + if (s1.equals(s2)) { + if (s1.length() == 5) + l2.add(s1); + else + l1.remove(s2); + } + } + } + if (l2.size() > 0) + l1.clear(); + return l1.size() == 0; + } + + public static boolean testLabeledBreakContinue() { + String searchMe = "Look for a substring in me"; + String substring = "sub"; + boolean foundIt = false; + + // int max = searchMe.length() - substring.length(); + // test: for (int i = 0; i <= max; i++) { + // int n = substring.length(); + // int j = i; + // int k = 0; + // while (n-- != 0) { + // if (searchMe.charAt(j++) != substring.charAt(k++)) { + // continue test; + // } + // } + // foundIt = true; + // break test; + // } + // System.out.println(foundIt ? "Found it" : "Didn't find it"); + return foundIt; + } + + @Override + public boolean testRun() throws Exception { + setEnabled(false); + testSwitchInLoop(); + assertTrue(enabled); + + setEnabled(false); + testIfInLoop(); + assertTrue(enabled); + + assertTrue(testNestedLoops( + new ArrayList(Arrays.asList("a1", "a2")), + new ArrayList(Arrays.asList("a1", "b2")))); + + // assertTrue(testLabeledBreakContinue()); + return true; + } + + public static void main(String[] args) throws Exception { + new TestCF3().testRun(); + } +} diff --git a/src/samples/java/jadx/samples/TestDeadCode.java b/src/samples/java/jadx/samples/TestDeadCode.java new file mode 100644 index 000000000..94b2328d3 --- /dev/null +++ b/src/samples/java/jadx/samples/TestDeadCode.java @@ -0,0 +1,15 @@ +package jadx.samples; + +public class TestDeadCode extends AbstractTest { + + private void test1(int i) { + if (i == 0) + return; + return; + } + + @Override + public boolean testRun() throws Exception { + return true; + } +} diff --git a/src/samples/java/jadx/samples/TestEnum.java b/src/samples/java/jadx/samples/TestEnum.java new file mode 100644 index 000000000..de811f4b6 --- /dev/null +++ b/src/samples/java/jadx/samples/TestEnum.java @@ -0,0 +1,128 @@ +package jadx.samples; + +import java.util.ArrayList; +import java.util.EnumSet; +import java.util.List; +import java.util.Set; + +public class TestEnum extends AbstractTest { + + public enum Direction { + NORTH, SOUTH, EAST, WEST + }; + + private static int three = 3; + + public enum Numbers { + ONE(1), TWO(2), THREE(three), FOUR(three + 1); + + private final int num; + + private Numbers(int n) { + this.num = n; + } + + public int getNum() { + return num; + } + }; + + public enum Operation { + PLUS { + @Override + int apply(int x, int y) { + return x + y; + } + }, + MINUS { + @Override + int apply(int x, int y) { + return x - y; + } + }; + + abstract int apply(int x, int y); + } + + public interface IOps { + double apply(double x, double y); + } + + public enum DoubleOperations implements IOps { + TIMES("*") { + @Override + public double apply(double x, double y) { + return x * y; + } + }, + DIVIDE("/") { + @Override + public double apply(double x, double y) { + return x / y; + } + }; + + private final String op; + + private DoubleOperations(String op) { + this.op = op; + } + + public String getOp() { + return op; + } + } + + public enum Types { + INT, FLOAT, + LONG, DOUBLE, + OBJECT, ARRAY; + + private static Set primitives = EnumSet.of(INT, FLOAT, LONG, DOUBLE); + public static List references = new ArrayList(); + + static { + references.add(OBJECT); + references.add(ARRAY); + } + + public static Set getPrimitives() { + return primitives; + } + } + + public enum EmptyEnum { + ; + + public static String getOp() { + return "op"; + } + } + + public enum Singleton { + INSTANCE; + public String test(String arg) { + return arg.concat("test"); + } + } + + @Override + public boolean testRun() throws Exception { + Direction d = Direction.EAST; + assertTrue(d.toString().equals("EAST")); + assertTrue(d.ordinal() == 2); + assertTrue(Numbers.THREE.getNum() == 3); + assertTrue(Operation.PLUS.apply(2, 2) == 4); + assertTrue(DoubleOperations.TIMES.apply(1, 1) == 1); + assertTrue(Types.getPrimitives().contains(Types.INT)); + assertTrue(Types.references.size() == 2); + assertTrue(EmptyEnum.values().length == 0); + assertTrue(EmptyEnum.getOp().equals("op")); + assertTrue(Singleton.INSTANCE.test("a").equals("atest")); + return true; + } + + public static void main(String[] args) throws Exception { + new TestEnum().testRun(); + } +} diff --git a/src/samples/java/jadx/samples/TestFields.java b/src/samples/java/jadx/samples/TestFields.java new file mode 100644 index 000000000..098c970b1 --- /dev/null +++ b/src/samples/java/jadx/samples/TestFields.java @@ -0,0 +1,25 @@ +package jadx.samples; + +import java.util.Arrays; + +public class TestFields extends AbstractTest { + + private final static boolean fbz = false; + private final static boolean fb = true; + private final static int fi = 5; + private final static int fiz = 0; + + private final static String fstr = "final string"; + + private final static double fd = 3.14; + private final static double[] fda = new double[] { 3.14, 2.7 }; + + private static int si = 5; + + @Override + public boolean testRun() throws Exception { + String str = "" + fbz + fiz + fb + fi + fstr + fd + Arrays.toString(fda) + si; + return str.equals("false0true5final string3.14[3.14, 2.7]5"); + } + +} diff --git a/src/samples/java/jadx/samples/TestInner.java b/src/samples/java/jadx/samples/TestInner.java new file mode 100644 index 000000000..8f1ed2aea --- /dev/null +++ b/src/samples/java/jadx/samples/TestInner.java @@ -0,0 +1,76 @@ +package jadx.samples; + +public class TestInner extends AbstractTest { + + public static int count = -2; + + public static class MyThread extends Thread { + @Override + public void run() { + count++; + super.run(); + } + } + + public static class MyInceptionThread extends Thread { + + public static class MyThread2 extends Thread { + @Override + public void run() { + count += 2; + } + } + + @Override + public void run() { + MyThread2 thr = new MyThread2(); + thr.start(); + try { + thr.join(); + } catch (InterruptedException e) { + assertTrue(false); + } + } + } + + public void func() { + new Runnable() { + @Override + public void run() { + count += 4; + } + }.run(); + } + + public static class MyException extends Exception { + public MyException(String str, Exception e) { + super("msg:" + str, e); + } + } + + @Override + public boolean testRun() throws Exception { + TestInner c = new TestInner(); + TestInner.count = 0; + c.func(); + + Runnable myRunnable = new Runnable() { + @Override + public void run() { + TestInner.count += 8; + } + }; + myRunnable.run(); + + MyThread thread = new TestInner.MyThread(); + thread.start(); + + MyInceptionThread thread2 = new TestInner.MyInceptionThread(); + thread2.start(); + + thread.join(); + thread2.join(); + + return TestInner.count == 15; + } +} diff --git a/src/samples/java/jadx/samples/TestInvoke.java b/src/samples/java/jadx/samples/TestInvoke.java new file mode 100644 index 000000000..de9698533 --- /dev/null +++ b/src/samples/java/jadx/samples/TestInvoke.java @@ -0,0 +1,50 @@ +package jadx.samples; + +import java.util.Arrays; + +public class TestInvoke extends AbstractTest { + + private int f; + + private void parse(String[] args) { + if (args.length > 0) + f = Integer.parseInt(args[0]); + else + f = 20; + } + + public int getF() { + return f; + } + + private boolean testVarArgs(String s1, String... args) { + String str = Arrays.toString(args); + return s1.length() + str.length() > 0; + } + + private String testVarArgs2(char[]... args) { + String s = ""; + for (char[] ca : args) { + s += new String(ca); + } + return s; + } + + @Override + public boolean testRun() throws Exception { + TestInvoke inv = new TestInvoke(); + + inv.parse(new String[] { "12", "35" }); + assertTrue(inv.getF() == 12); + inv.parse(new String[0]); + assertTrue(inv.getF() == 20); + + assertTrue(inv.testVarArgs("a", "2", "III")); + assertTrue(inv.testVarArgs2("a".toCharArray(), new char[] { '1', '2' }).equals("a12")); + return true; + } + + public static void main(String[] args) throws Exception { + new TestInvoke().testRun(); + } +} diff --git a/src/samples/java/jadx/samples/TestStringProcessing.java b/src/samples/java/jadx/samples/TestStringProcessing.java new file mode 100644 index 000000000..797a4040d --- /dev/null +++ b/src/samples/java/jadx/samples/TestStringProcessing.java @@ -0,0 +1,15 @@ +package jadx.samples; + +public class TestStringProcessing extends AbstractTest { + + @Override + public boolean testRun() { + String str = "test\tstr\n"; + assertTrue(str.length() == 9); + + str = "test\bunicode\u1234"; + assertTrue(str.charAt(4) == '\b'); + return true; + } + +} diff --git a/src/samples/java/jadx/samples/TestSwitch.java b/src/samples/java/jadx/samples/TestSwitch.java new file mode 100644 index 000000000..38c40b5db --- /dev/null +++ b/src/samples/java/jadx/samples/TestSwitch.java @@ -0,0 +1,90 @@ +package jadx.samples; + +public class TestSwitch extends AbstractTest { + + public static final int test1(int i) { + int k = i * 4; + + switch (k) { + case 1: + return 0; + case 10: + return 1; + case 100: + return 2; + case 1000: + return 3; + } + i -= 77; + return i; + } + + public static final int test2(int i) { + int k = i; + switch (k) { + case 1: + return 0; + case 2: + return 1; + case 3: + return 2; + case 5: + return 3; + case 7: + return 4; + case 9: + return 5; + } + i /= 2; + return -i; + } + + public static final int test3(int i, int j) { + int k = i; + switch (k) { + case 1: + if (j == 0) + return 0; + else + return -1; + case 2: + return 1; + } + return -1; + } + + public static final int test4(int i) { + int k = i; + switch (k) { + case 1: + throw new RuntimeException("test4"); + case 2: + return 1; + } + return -1; + } + + public static final int test5(int i, int b) { + int k = i; + switch (k) { + case 1: + if (b == 0) + return 3; + + case 2: + b++; + return b; + } + return -1; + } + + @Override + public boolean testRun() { + assertTrue(test1(25) == 2); + assertTrue(test2(5) == 3); + assertTrue(test3(1, 0) == 0); + assertTrue(test4(2) == 1); + return true; + } + +} diff --git a/src/samples/java/jadx/samples/TestTryCatch.java b/src/samples/java/jadx/samples/TestTryCatch.java new file mode 100644 index 000000000..cef0ddd83 --- /dev/null +++ b/src/samples/java/jadx/samples/TestTryCatch.java @@ -0,0 +1,156 @@ +package jadx.samples; + +import java.io.IOException; + +public class TestTryCatch extends AbstractTest { + + private static boolean exc(Object obj) throws Exception { + if (obj == null) + throw new Exception("test"); + return (obj instanceof Object); + } + + private static boolean exc2(Object obj) throws IOException { + if (obj == null) + throw new IOException(); + return true; + } + + private static boolean test0(Object obj) { + try { + synchronized (obj) { + obj.wait(5); + } + } catch (InterruptedException e) { + return false; + } + return true; + } + + private static boolean test1(Object obj) { + boolean res = false; + try { + res = exc(obj); + } catch (Exception e) { + return false; + } + return res; + } + + private static boolean test2(Object obj) { + try { + return exc(obj); + } catch (Exception e) { + if (obj != null) + return true; + else + return false; + } + } + + private static boolean test3(Object obj) { + boolean res = false; + try { + res = exc(obj); + } catch (Exception e) { + res = false; + } finally { + test0(obj); + } + return res; + } + + private static String test4(Object obj) { + String res = "good"; + try { + res += exc(obj); + exc2("a"); + } catch (IOException e) { + res = "io exc"; + } catch (Exception e) { + res = "exc"; + } + return res; + } + + private static String test5(Object obj) { + String res = "good"; + try { + res = "" + exc(obj); + boolean f = exc2("a"); + if (!f) + res = "f == false"; + } catch (Exception e) { + res = "exc"; + } + return res; + } + + private static boolean test6(Object obj) { + boolean res = false; + while (true) { + try { + res = exc2(obj); + return res; + } catch (IOException e) { + res = true; + } catch (Throwable e) { + if (obj == null) + obj = new Object(); + } + } + } + + private static boolean test7() { + boolean res = false; + Object obj = null; + while (true) { + try { + res = exc2(obj); + return res; + } catch (IOException e) { + res = true; + obj = new Object(); + } catch (Throwable e) { + if (obj == null) + res = false; + } + } + } + + private static boolean testSynchronize(Object obj) throws InterruptedException { + synchronized (obj) { + if (obj instanceof String) + return false; + obj.wait(5); + } + return true; + } + + private synchronized static boolean testSynchronize2(Object obj) throws InterruptedException { + return obj.toString() != null; + } + + @Override + public boolean testRun() throws Exception { + Object obj = new Object(); + assertTrue(test0(obj)); + assertTrue(test1(obj)); + assertTrue(test2(obj)); + assertTrue(test3(obj)); + assertTrue(test4(obj) != null); + assertTrue(test5(null) != null); + assertTrue(test6(obj)); + assertTrue(test7()); + + assertTrue(testSynchronize(obj) == true); + assertTrue(testSynchronize("str") == false); + + assertTrue(testSynchronize2("str")); + return true; + } + + public static void main(String[] args) throws Exception { + new TestTryCatch().testRun(); + } +} diff --git a/src/samples/java/jadx/samples/TestTypeResolver.java b/src/samples/java/jadx/samples/TestTypeResolver.java new file mode 100644 index 000000000..5238f1069 --- /dev/null +++ b/src/samples/java/jadx/samples/TestTypeResolver.java @@ -0,0 +1,54 @@ +package jadx.samples; + +public class TestTypeResolver extends AbstractTest { + + private final int f1; + + public TestTypeResolver() { + this.f1 = 2; + } + + public TestTypeResolver(int b1, int b2) { + // test 'this' move and constructor invocation on moved register + this(b1, b2, 0, 0, 0); + } + + public TestTypeResolver(int a1, int a2, int a3, int a4, int a5) { + this.f1 = a1; + } + + public static class TestTernaryInSuper extends TestTypeResolver { + + public TestTernaryInSuper(int c) { + super(c > 0 ? c : -c, 1); + } + } + + // public static Object testVarsPropagation(int a) { + // Object b = new Exception(); + // if (a == 5) + // b = 1; + // return b; + // } + // + // public Object testMoveThis(int a) { + // TestTypeResolver t = this; + // if (a == 0) + // return t; + // + // return t.testMoveThis(--a); + // } + + @Override + public boolean testRun() throws Exception { + // assertTrue((Integer) testVarsPropagation(5) == 1); + // assertTrue(testVarsPropagation(1).getClass() == Exception.class); + // + // assertTrue(testMoveThis(f1) == this); + return true; + } + + public static void main(String[] args) throws Exception { + new TestTypeResolver().testRun(); + } +} diff --git a/src/test/java/jadx/tests/StringUtilsTest.java b/src/test/java/jadx/tests/StringUtilsTest.java new file mode 100644 index 000000000..48d660c94 --- /dev/null +++ b/src/test/java/jadx/tests/StringUtilsTest.java @@ -0,0 +1,31 @@ +package jadx.tests; + +import jadx.utils.StringUtils; +import junit.framework.TestCase; + +public class StringUtilsTest extends TestCase { + + public void testUnescape() { + unescapeTest("\n", "\\n"); + unescapeTest("\t", "\\t"); + unescapeTest("\r", "\\r"); + unescapeTest("\b", "\\b"); + unescapeTest("\f", "\\f"); + unescapeTest("\\", "\\\\"); + unescapeTest("\"", "\\\""); + unescapeTest("'", "'"); + + unescapeTest("\u1234", "\\u1234"); + + unescapeCharTest('\'', "'\\\''"); + } + + private void unescapeTest(String input, String expected) { + assertEquals("\"" + expected + "\"", StringUtils.unescapeString(input)); + } + + private void unescapeCharTest(char input, String expected) { + assertEquals(expected, StringUtils.unescapeChar(input)); + } + +} diff --git a/src/test/java/jadx/tests/TypeMergeTest.java b/src/test/java/jadx/tests/TypeMergeTest.java new file mode 100644 index 000000000..25764a9f5 --- /dev/null +++ b/src/test/java/jadx/tests/TypeMergeTest.java @@ -0,0 +1,76 @@ +package jadx.tests; + +import static jadx.dex.instructions.args.ArgType.BOOLEAN; +import static jadx.dex.instructions.args.ArgType.CHAR; +import static jadx.dex.instructions.args.ArgType.INT; +import static jadx.dex.instructions.args.ArgType.LONG; +import static jadx.dex.instructions.args.ArgType.NARROW; +import static jadx.dex.instructions.args.ArgType.OBJECT; +import static jadx.dex.instructions.args.ArgType.UNKNOWN; +import static jadx.dex.instructions.args.ArgType.UNKNOWN_OBJECT; +import static jadx.dex.instructions.args.ArgType.object; +import static jadx.dex.instructions.args.ArgType.unknown; +import jadx.dex.instructions.args.ArgType; +import jadx.dex.instructions.args.PrimitiveType; +import junit.framework.TestCase; + +public class TypeMergeTest extends TestCase { + + public void testMerge() { + first(INT, INT); + first(BOOLEAN, INT); + reject(INT, LONG); + first(INT, UNKNOWN); + reject(INT, UNKNOWN_OBJECT); + + first(INT, NARROW); + first(CHAR, INT); + + merge(unknown(PrimitiveType.INT, PrimitiveType.BOOLEAN, PrimitiveType.FLOAT), + unknown(PrimitiveType.INT, PrimitiveType.BOOLEAN), + unknown(PrimitiveType.INT, PrimitiveType.BOOLEAN)); + + merge(unknown(PrimitiveType.INT, PrimitiveType.FLOAT), + unknown(PrimitiveType.INT, PrimitiveType.BOOLEAN), + INT); + + merge(unknown(PrimitiveType.INT, PrimitiveType.OBJECT), + unknown(PrimitiveType.OBJECT, PrimitiveType.ARRAY), + unknown(PrimitiveType.OBJECT)); + + first(object("Lsomeobj;"), object("Lsomeobj;")); + merge(object("Lsomeobj;"), object("Lotherobj;"), OBJECT); + first(object("Lsomeobj;"), OBJECT); + + // first(object("Lsomeobj;"), object("Lsomeobj;")); + // merge(object("Lsomeobj;"), object("Lotherobj;"), null); + // merge(object("Lsomeobj;"), OBJECT, null); + } + + private void first(ArgType t1, ArgType t2) { + check(t1, t2, t1); + } + + private void reject(ArgType t1, ArgType t2) { + check(t1, t2, null); + } + + private void check(ArgType t1, ArgType t2, ArgType exp) { + merge(t1, t2, exp); + merge(t2, t1, exp); + } + + private void merge(ArgType t1, ArgType t2, ArgType exp) { + ArgType res = ArgType.merge(t1, t2); + if (exp == null) { + assertNull("Incorrect accept: " + format(t1, t2, exp, res), res); + } else { + assertNotNull("Incorrect reject: " + format(t1, t2, exp, res), res); + assertTrue("Incorrect result: " + format(t1, t2, exp, res), exp.equals(res)); + } + } + + private String format(ArgType t1, ArgType t2, ArgType exp, ArgType res) { + return t1 + " <+> " + t2 + " = '" + res + "', expected: " + exp; + } +}