feat: add 'simple' decompilation mode

This commit is contained in:
Skylot 2022-03-05 17:18:07 +00:00
parent 909cf0a576
commit d8306cb1c0
No known key found for this signature in database
GPG Key ID: 1E23F5B52567AA39
32 changed files with 686 additions and 150 deletions

View File

@ -84,6 +84,11 @@ options:
--output-format - can be 'java' or 'json', default: java
-e, --export-gradle - save as android gradle project
-j, --threads-count - processing threads count, default: 4
-m, --decompilation-mode - code output mode:
'auto' - trying best options (default)
'restructure' - restore code structure (normal java code)
'simple' - simplified instructions (linear, with goto's)
'fallback' - raw instructions without modifications
--show-bad-code - show inconsistent code (incorrectly decompiled)
--no-imports - disable use of imports, always write entire package name
--no-debug-info - disable debug info
@ -115,7 +120,7 @@ options:
--fs-case-sensitive - treat filesystem as case sensitive, false by default
--cfg - save methods control flow graph to dot file
--raw-cfg - save methods control flow graph (use raw instructions)
-f, --fallback - make simple dump (using goto instead of 'if', 'for', etc)
-f, --fallback - set '--decompilation-mode' to 'fallback' (deprecated)
--use-dx - use dx/d8 to convert java bytecode
--comments-level - set code comments level, values: error, warn, info, debug, user-only, none, default: info
--log-level - set log level, values: quiet, progress, error, warn, info, debug, default: progress

View File

@ -15,6 +15,7 @@ import com.beust.jcommander.IStringConverter;
import com.beust.jcommander.Parameter;
import jadx.api.CommentsLevel;
import jadx.api.DecompilationMode;
import jadx.api.JadxArgs;
import jadx.api.JadxArgs.RenameEnum;
import jadx.api.JadxArgs.UseKotlinMethodsForVarNames;
@ -58,6 +59,17 @@ public class JadxCLIArgs {
@Parameter(names = { "-j", "--threads-count" }, description = "processing threads count")
protected int threadsCount = JadxArgs.DEFAULT_THREADS_COUNT;
@Parameter(
names = { "-m", "--decompilation-mode" },
description = "code output mode:"
+ "\n 'auto' - trying best options (default)"
+ "\n 'restructure' - restore code structure (normal java code)"
+ "\n 'simple' - simplified instructions (linear, with goto's)"
+ "\n 'fallback' - raw instructions without modifications",
converter = RenameConverter.class
)
protected DecompilationMode decompilationMode = DecompilationMode.AUTO;
@Parameter(names = { "--show-bad-code" }, description = "show inconsistent code (incorrectly decompiled)")
protected boolean showInconsistentCode = false;
@ -148,7 +160,7 @@ public class JadxCLIArgs {
@Parameter(names = { "--raw-cfg" }, description = "save methods control flow graph (use raw instructions)")
protected boolean rawCfgOutput = false;
@Parameter(names = { "-f", "--fallback" }, description = "make simple dump (using goto instead of 'if', 'for', etc)")
@Parameter(names = { "-f", "--fallback" }, description = "set '--decompilation-mode' to 'fallback' (deprecated)")
protected boolean fallbackMode = false;
@Parameter(names = { "--use-dx" }, description = "use dx/d8 to convert java bytecode")
@ -236,7 +248,11 @@ public class JadxCLIArgs {
args.setThreadsCount(threadsCount);
args.setSkipSources(skipSources);
args.setSkipResources(skipResources);
args.setFallbackMode(fallbackMode);
if (fallbackMode) {
args.setDecompilationMode(DecompilationMode.FALLBACK);
} else {
args.setDecompilationMode(decompilationMode);
}
args.setShowInconsistentCode(showInconsistentCode);
args.setCfgOutput(cfgOutput);
args.setRawCFGOutput(rawCfgOutput);
@ -313,6 +329,10 @@ public class JadxCLIArgs {
return useDx;
}
public DecompilationMode getDecompilationMode() {
return decompilationMode;
}
public boolean isShowInconsistentCode() {
return showInconsistentCode;
}
@ -493,6 +513,19 @@ public class JadxCLIArgs {
}
}
public static class DecompilationModeConverter implements IStringConverter<DecompilationMode> {
@Override
public DecompilationMode convert(String value) {
try {
return DecompilationMode.valueOf(value.toUpperCase());
} catch (Exception e) {
throw new IllegalArgumentException(
'\'' + value + "' is unknown, possible values are: "
+ JadxCLIArgs.enumValuesString(DecompilationMode.values()));
}
}
}
public static String enumValuesString(Enum<?>[] values) {
return Stream.of(values)
.map(v -> v.name().replace('_', '-').toLowerCase(Locale.ROOT))

View File

@ -0,0 +1,23 @@
package jadx.api;
public enum DecompilationMode {
/**
* Trying best options (default)
*/
AUTO,
/**
* Restore code structure (normal java code)
*/
RESTRUCTURE,
/**
* Simplified instructions (linear with goto's)
*/
SIMPLE,
/**
* Raw instructions without modifications
*/
FALLBACK
}

View File

@ -38,7 +38,6 @@ public class JadxArgs {
private boolean cfgOutput = false;
private boolean rawCFGOutput = false;
private boolean fallbackMode = false;
private boolean showInconsistentCode = false;
private boolean useImports = true;
@ -85,6 +84,8 @@ public class JadxArgs {
private OutputFormatEnum outputFormat = OutputFormatEnum.JAVA;
private DecompilationMode decompilationMode = DecompilationMode.AUTO;
private ICodeData codeData;
private CommentsLevel commentsLevel = CommentsLevel.INFO;
@ -175,11 +176,17 @@ public class JadxArgs {
}
public boolean isFallbackMode() {
return fallbackMode;
return decompilationMode == DecompilationMode.FALLBACK;
}
/**
* Deprecated: use 'decompilation mode' property
*/
@Deprecated
public void setFallbackMode(boolean fallbackMode) {
this.fallbackMode = fallbackMode;
if (fallbackMode) {
this.decompilationMode = DecompilationMode.FALLBACK;
}
}
public boolean isShowInconsistentCode() {
@ -422,6 +429,14 @@ public class JadxArgs {
this.outputFormat = outputFormat;
}
public DecompilationMode getDecompilationMode() {
return decompilationMode;
}
public void setDecompilationMode(DecompilationMode decompilationMode) {
this.decompilationMode = decompilationMode;
}
public ICodeCache getCodeCache() {
return codeCache;
}
@ -493,9 +508,7 @@ public class JadxArgs {
+ ", outDirSrc=" + outDirSrc
+ ", outDirRes=" + outDirRes
+ ", threadsCount=" + threadsCount
+ ", cfgOutput=" + cfgOutput
+ ", rawCFGOutput=" + rawCFGOutput
+ ", fallbackMode=" + fallbackMode
+ ", decompilationMode=" + decompilationMode
+ ", showInconsistentCode=" + showInconsistentCode
+ ", useImports=" + useImports
+ ", skipResources=" + skipResources
@ -520,6 +533,8 @@ public class JadxArgs {
+ ", codeWriter=" + codeWriterProvider.apply(this).getClass().getSimpleName()
+ ", useDxInput=" + useDxInput
+ ", pluginOptions=" + pluginOptions
+ ", cfgOutput=" + cfgOutput
+ ", rawCFGOutput=" + rawCFGOutput
+ '}';
}
}

View File

@ -12,6 +12,7 @@ import org.slf4j.LoggerFactory;
import jadx.api.CommentsLevel;
import jadx.api.JadxArgs;
import jadx.core.dex.attributes.AFlag;
import jadx.core.dex.visitors.AnonymousClassVisitor;
import jadx.core.dex.visitors.AttachCommentsVisitor;
import jadx.core.dex.visitors.AttachMethodDetails;
@ -32,6 +33,7 @@ import jadx.core.dex.visitors.InitCodeVariables;
import jadx.core.dex.visitors.InlineMethods;
import jadx.core.dex.visitors.MarkMethodsForInline;
import jadx.core.dex.visitors.MethodInvokeVisitor;
import jadx.core.dex.visitors.MethodVisitor;
import jadx.core.dex.visitors.ModVisitor;
import jadx.core.dex.visitors.MoveInlineVisitor;
import jadx.core.dex.visitors.OverrideMethodVisitor;
@ -62,6 +64,7 @@ import jadx.core.dex.visitors.shrink.CodeShrinkVisitor;
import jadx.core.dex.visitors.ssa.SSATransform;
import jadx.core.dex.visitors.typeinference.TypeInferenceVisitor;
import jadx.core.dex.visitors.usage.UsageInfoVisitor;
import jadx.core.utils.exceptions.JadxRuntimeException;
public class Jadx {
private static final Logger LOG = LoggerFactory.getLogger(Jadx.class);
@ -69,21 +72,20 @@ public class Jadx {
private Jadx() {
}
static {
if (Consts.DEBUG) {
LOG.info("debug enabled");
public static List<IDexTreeVisitor> getPassesList(JadxArgs args) {
switch (args.getDecompilationMode()) {
case AUTO:
case RESTRUCTURE:
return getRegionsModePasses(args);
case SIMPLE:
return getSimpleModePasses(args);
case FALLBACK:
return getFallbackPassesList();
default:
throw new JadxRuntimeException("Unknown decompilation mode: " + args.getDecompilationMode());
}
}
public static List<IDexTreeVisitor> getFallbackPassesList() {
List<IDexTreeVisitor> passes = new ArrayList<>();
passes.add(new AttachTryCatchVisitor());
passes.add(new AttachCommentsVisitor());
passes.add(new ProcessInstructionsVisitor());
passes.add(new FallbackModeVisitor());
return passes;
}
public static List<IDexTreeVisitor> getPreDecompilePassesList() {
List<IDexTreeVisitor> passes = new ArrayList<>();
passes.add(new SignatureProcessor());
@ -95,12 +97,8 @@ public class Jadx {
return passes;
}
public static List<IDexTreeVisitor> getPassesList(JadxArgs args) {
if (args.isFallbackMode()) {
return getFallbackPassesList();
}
public static List<IDexTreeVisitor> getRegionsModePasses(JadxArgs args) {
List<IDexTreeVisitor> passes = new ArrayList<>();
// instructions IR
passes.add(new CheckCode());
if (args.isDebugInfo()) {
@ -178,6 +176,60 @@ public class Jadx {
return passes;
}
public static List<IDexTreeVisitor> getSimpleModePasses(JadxArgs args) {
List<IDexTreeVisitor> passes = new ArrayList<>();
if (args.isDebugInfo()) {
passes.add(new DebugInfoAttachVisitor());
}
passes.add(new AttachTryCatchVisitor());
if (args.getCommentsLevel() != CommentsLevel.NONE) {
passes.add(new AttachCommentsVisitor());
}
passes.add(new AttachMethodDetails());
passes.add(new ProcessInstructionsVisitor());
passes.add(new BlockSplitter());
passes.add(new MethodVisitor(mth -> mth.add(AFlag.DISABLE_BLOCKS_LOCK)));
if (args.isRawCFGOutput()) {
passes.add(DotGraphVisitor.dumpRaw());
}
passes.add(new MethodVisitor(mth -> mth.add(AFlag.DISABLE_BLOCKS_LOCK)));
passes.add(new BlockProcessor());
passes.add(new SSATransform());
passes.add(new MoveInlineVisitor());
passes.add(new ConstructorVisitor());
passes.add(new InitCodeVariables());
passes.add(new ConstInlineVisitor());
passes.add(new TypeInferenceVisitor());
if (args.isDebugInfo()) {
passes.add(new DebugInfoApplyVisitor());
}
passes.add(new CodeRenameVisitor());
passes.add(new DeboxingVisitor());
passes.add(new ModVisitor());
passes.add(new CodeShrinkVisitor());
passes.add(new ReSugarCode());
passes.add(new CodeShrinkVisitor());
passes.add(new SimplifyVisitor());
passes.add(new MethodVisitor(mth -> mth.remove(AFlag.DONT_GENERATE)));
if (args.isRawCFGOutput()) {
passes.add(DotGraphVisitor.dumpRaw());
}
if (args.isCfgOutput()) {
passes.add(DotGraphVisitor.dump());
}
return passes;
}
public static List<IDexTreeVisitor> getFallbackPassesList() {
List<IDexTreeVisitor> passes = new ArrayList<>();
passes.add(new AttachTryCatchVisitor());
passes.add(new AttachCommentsVisitor());
passes.add(new ProcessInstructionsVisitor());
passes.add(new FallbackModeVisitor());
return passes;
}
public static final String VERSION_DEV = "dev";
private static String version;

View File

@ -1,13 +1,19 @@
package jadx.core;
import java.util.List;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import jadx.api.ICodeInfo;
import jadx.api.JadxArgs;
import jadx.core.codegen.CodeGen;
import jadx.core.dex.attributes.AFlag;
import jadx.core.dex.nodes.ClassNode;
import jadx.core.dex.nodes.LoadStage;
import jadx.core.dex.nodes.RootNode;
import jadx.core.dex.visitors.DepthTraversal;
import jadx.core.dex.visitors.IDexTreeVisitor;
import jadx.core.utils.exceptions.JadxRuntimeException;
@ -18,13 +24,17 @@ import static jadx.core.dex.nodes.ProcessState.NOT_LOADED;
import static jadx.core.dex.nodes.ProcessState.PROCESS_COMPLETE;
import static jadx.core.dex.nodes.ProcessState.PROCESS_STARTED;
public final class ProcessClass {
public class ProcessClass {
private static final Logger LOG = LoggerFactory.getLogger(ProcessClass.class);
private ProcessClass() {
private final List<IDexTreeVisitor> passes;
public ProcessClass(JadxArgs args) {
this.passes = Jadx.getPassesList(args);
}
@Nullable
private static ICodeInfo process(ClassNode cls, boolean codegen) {
private ICodeInfo process(ClassNode cls, boolean codegen) {
if (!codegen && cls.getState() == PROCESS_COMPLETE) {
// nothing to do
return null;
@ -58,7 +68,7 @@ public final class ProcessClass {
}
if (cls.getState() == LOADED) {
cls.setState(PROCESS_STARTED);
for (IDexTreeVisitor visitor : cls.root().getPasses()) {
for (IDexTreeVisitor visitor : passes) {
DepthTraversal.visit(visitor, cls);
}
cls.setState(PROCESS_COMPLETE);
@ -83,7 +93,7 @@ public final class ProcessClass {
}
@NotNull
public static ICodeInfo generateCode(ClassNode cls) {
public ICodeInfo generateCode(ClassNode cls) {
ClassNode topParentClass = cls.getTopParentClass();
if (topParentClass != cls) {
return generateCode(topParentClass);
@ -107,4 +117,19 @@ public final class ProcessClass {
throw new JadxRuntimeException("Failed to generate code for class: " + cls.getFullName(), e);
}
}
public void initPasses(RootNode root) {
for (IDexTreeVisitor pass : passes) {
try {
pass.init(root);
} catch (Exception e) {
LOG.error("Visitor init failed: {}", pass.getClass().getSimpleName(), e);
}
}
}
// TODO: make passes list private and not visible
public List<IDexTreeVisitor> getPasses() {
return passes;
}
}

View File

@ -356,7 +356,7 @@ public class ClassGen {
badCode = false;
}
MethodGen mthGen;
if (badCode || fallback || mth.contains(AType.JADX_ERROR) || mth.getRegion() == null) {
if (badCode || fallback || mth.contains(AType.JADX_ERROR)) {
mthGen = MethodGen.getFallbackMethodGen(mth);
} else {
mthGen = new MethodGen(this, mth);

View File

@ -50,6 +50,7 @@ import jadx.core.dex.instructions.args.RegisterArg;
import jadx.core.dex.instructions.args.SSAVar;
import jadx.core.dex.instructions.mods.ConstructorInsn;
import jadx.core.dex.instructions.mods.TernaryInsn;
import jadx.core.dex.nodes.BlockNode;
import jadx.core.dex.nodes.ClassNode;
import jadx.core.dex.nodes.FieldNode;
import jadx.core.dex.nodes.InsnNode;
@ -517,7 +518,7 @@ public class InsnGen {
code.add(' ');
code.add(ifInsn.getOp().getSymbol()).add(' ');
addArg(code, insn.getArg(1));
code.add(") goto ").add(MethodGen.getLabelName(ifInsn.getTarget()));
code.add(") goto ").add(MethodGen.getLabelName(ifInsn));
break;
case GOTO:
@ -538,13 +539,24 @@ public class InsnGen {
code.add(") {");
code.incIndent();
int[] keys = sw.getKeys();
int[] targets = sw.getTargets();
for (int i = 0; i < keys.length; i++) {
code.startLine("case ").add(Integer.toString(keys[i])).add(": goto ");
code.add(MethodGen.getLabelName(targets[i])).add(';');
int size = keys.length;
BlockNode[] targetBlocks = sw.getTargetBlocks();
if (targetBlocks != null) {
for (int i = 0; i < size; i++) {
code.startLine("case ").add(Integer.toString(keys[i])).add(": goto ");
code.add(MethodGen.getLabelName(targetBlocks[i])).add(';');
}
code.startLine("default: goto ");
code.add(MethodGen.getLabelName(sw.getDefTargetBlock())).add(';');
} else {
int[] targets = sw.getTargets();
for (int i = 0; i < size; i++) {
code.startLine("case ").add(Integer.toString(keys[i])).add(": goto ");
code.add(MethodGen.getLabelName(targets[i])).add(';');
}
code.startLine("default: goto ");
code.add(MethodGen.getLabelName(sw.getDefaultCaseOffset())).add(';');
}
code.startLine("default: goto ");
code.add(MethodGen.getLabelName(sw.getDefaultCaseOffset())).add(';');
code.decIndent();
code.startLine('}');
break;

View File

@ -6,11 +6,13 @@ import java.util.List;
import java.util.Objects;
import java.util.stream.Stream;
import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import jadx.api.CommentsLevel;
import jadx.api.ICodeWriter;
import jadx.api.JadxArgs;
import jadx.api.data.annotations.InsnCodeOffset;
import jadx.api.data.annotations.VarDeclareRef;
import jadx.api.plugins.input.data.AccessFlags;
@ -32,13 +34,14 @@ import jadx.core.dex.instructions.args.ArgType;
import jadx.core.dex.instructions.args.CodeVar;
import jadx.core.dex.instructions.args.RegisterArg;
import jadx.core.dex.instructions.args.SSAVar;
import jadx.core.dex.nodes.BlockNode;
import jadx.core.dex.nodes.InsnNode;
import jadx.core.dex.nodes.MethodNode;
import jadx.core.dex.trycatch.CatchAttr;
import jadx.core.dex.trycatch.ExceptionHandler;
import jadx.core.dex.visitors.DepthTraversal;
import jadx.core.dex.visitors.IDexTreeVisitor;
import jadx.core.utils.CodeGenUtils;
import jadx.core.utils.InsnUtils;
import jadx.core.utils.Utils;
import jadx.core.utils.exceptions.CodegenException;
import jadx.core.utils.exceptions.JadxOverflowException;
@ -252,12 +255,28 @@ public class MethodGen {
}
public void addInstructions(ICodeWriter code) throws CodegenException {
if (mth.root().getArgs().isFallbackMode()) {
addFallbackMethodCode(code, FALLBACK_MODE);
} else if (classGen.isFallbackMode()) {
dumpInstructions(code);
} else {
addRegionInsns(code);
JadxArgs args = mth.root().getArgs();
switch (args.getDecompilationMode()) {
case AUTO:
if (classGen.isFallbackMode()) {
// TODO: try simple mode first
dumpInstructions(code);
} else {
addRegionInsns(code);
}
break;
case RESTRUCTURE:
addRegionInsns(code);
break;
case SIMPLE:
addSimpleMethodCode(code);
break;
case FALLBACK:
addFallbackMethodCode(code, FALLBACK_MODE);
break;
}
}
@ -279,6 +298,59 @@ public class MethodGen {
}
}
private void addSimpleMethodCode(ICodeWriter code) {
if (mth.getBasicBlocks() == null) {
code.startLine("// Blocks not ready for simple mode, using fallback");
addFallbackMethodCode(code, FALLBACK_MODE);
return;
}
JadxArgs args = mth.root().getArgs();
ICodeWriter tmpCode = args.getCodeWriterProvider().apply(args);
try {
tmpCode.setIndent(code.getIndent());
generateSimpleCode(tmpCode);
code.add(tmpCode);
} catch (Exception e) {
mth.addError("Simple mode code generation failed", e);
CodeGenUtils.addError(code, "Simple mode code generation failed", e);
dumpInstructions(code);
}
}
private void generateSimpleCode(ICodeWriter code) throws CodegenException {
SimpleModeHelper helper = new SimpleModeHelper(mth);
List<BlockNode> blocks = helper.prepareBlocks();
InsnGen insnGen = new InsnGen(this, true);
for (BlockNode block : blocks) {
if (block.contains(AFlag.DONT_GENERATE)) {
continue;
}
if (helper.isNeedStartLabel(block)) {
code.decIndent();
code.startLine(getLabelName(block)).add(':');
code.incIndent();
}
for (InsnNode insn : block.getInstructions()) {
if (!insn.contains(AFlag.DONT_GENERATE)) {
if (insn.getResult() != null) {
CodeVar codeVar = insn.getResult().getSVar().getCodeVar();
if (!codeVar.isDeclared()) {
insn.add(AFlag.DECLARE_VAR);
codeVar.setDeclared(true);
}
}
InsnCodeOffset.attach(code, insn);
insnGen.makeInsn(insn, code);
addCatchComment(code, insn, false);
CodeGenUtils.addCodeComments(code, mth, insn);
}
}
if (helper.isNeedEndGoto(block)) {
code.startLine("goto ").add(getLabelName(block.getSuccessors().get(0)));
}
}
}
public void dumpInstructions(ICodeWriter code) {
if (mth.checkCommentsLevel(CommentsLevel.ERROR)) {
code.startLine("/*");
@ -353,60 +425,81 @@ public class MethodGen {
public static void addFallbackInsns(ICodeWriter code, MethodNode mth, InsnNode[] insnArr, FallbackOption option) {
int startIndent = code.getIndent();
InsnGen insnGen = new InsnGen(getFallbackMethodGen(mth), true);
MethodGen methodGen = getFallbackMethodGen(mth);
InsnGen insnGen = new InsnGen(methodGen, true);
InsnNode prevInsn = null;
for (InsnNode insn : insnArr) {
if (insn == null) {
continue;
}
if (insn.contains(AType.JADX_ERROR)) {
for (JadxError error : insn.getAll(AType.JADX_ERROR)) {
code.startLine("// ").add(error.getError());
}
continue;
methodGen.dumpInsn(code, insnGen, option, startIndent, prevInsn, insn);
prevInsn = insn;
}
}
private boolean dumpInsn(ICodeWriter code, InsnGen insnGen, FallbackOption option, int startIndent,
@Nullable InsnNode prevInsn, InsnNode insn) {
if (insn.contains(AType.JADX_ERROR)) {
for (JadxError error : insn.getAll(AType.JADX_ERROR)) {
code.startLine("// ").add(error.getError());
}
if (option != BLOCK_DUMP && needLabel(insn, prevInsn)) {
return true;
}
if (option != BLOCK_DUMP && needLabel(insn, prevInsn)) {
code.decIndent();
code.startLine(getLabelName(insn.getOffset()) + ':');
code.incIndent();
}
if (insn.getType() == InsnType.NOP) {
return true;
}
try {
boolean escapeComment = isCommentEscapeNeeded(insn, option);
if (escapeComment) {
code.decIndent();
code.startLine(getLabelName(insn.getOffset()) + ':');
code.startLine("*/");
code.startLine("// ");
} else {
code.startLineWithNum(insn.getSourceLine());
}
InsnCodeOffset.attach(code, insn);
RegisterArg resArg = insn.getResult();
if (resArg != null) {
ArgType varType = resArg.getInitType();
if (varType.isTypeKnown()) {
code.add(varType.toString()).add(' ');
}
}
insnGen.makeInsn(insn, code, InsnGen.Flags.INLINE);
if (escapeComment) {
code.startLine("/*");
code.incIndent();
}
if (insn.getType() == InsnType.NOP) {
continue;
}
try {
boolean escapeComment = isCommentEscapeNeeded(insn, option);
if (escapeComment) {
code.decIndent();
code.startLine("*/");
code.startLine("// ");
} else {
code.startLineWithNum(insn.getSourceLine());
}
InsnCodeOffset.attach(code, insn);
RegisterArg resArg = insn.getResult();
if (resArg != null) {
ArgType varType = resArg.getInitType();
if (varType.isTypeKnown()) {
code.add(varType.toString()).add(' ');
}
}
insnGen.makeInsn(insn, code, InsnGen.Flags.INLINE);
if (escapeComment) {
code.startLine("/*");
code.incIndent();
}
addCatchComment(code, insn, true);
CodeGenUtils.addCodeComments(code, mth, insn);
} catch (Exception e) {
LOG.debug("Error generate fallback instruction: ", e.getCause());
code.setIndent(startIndent);
code.startLine("// error: " + insn);
}
return false;
}
CatchAttr catchAttr = insn.get(AType.EXC_CATCH);
if (catchAttr != null) {
code.add(" // " + catchAttr);
}
CodeGenUtils.addCodeComments(code, mth, insn);
} catch (Exception e) {
LOG.debug("Error generate fallback instruction: ", e.getCause());
code.setIndent(startIndent);
code.startLine("// error: " + insn);
private void addCatchComment(ICodeWriter code, InsnNode insn, boolean raw) {
CatchAttr catchAttr = insn.get(AType.EXC_CATCH);
if (catchAttr == null) {
return;
}
code.add(" // Catch:");
for (ExceptionHandler handler : catchAttr.getHandlers()) {
code.add(' ');
classGen.useClass(code, handler.getArgType());
code.add(" -> ");
if (raw) {
code.add(getLabelName(handler.getHandlerOffset()));
} else {
code.add(getLabelName(handler.getHandlerBlock()));
}
prevInsn = insn;
}
}
@ -449,7 +542,22 @@ public class MethodGen {
return new MethodGen(clsGen, mth);
}
public static String getLabelName(BlockNode block) {
return String.format("L%d", block.getId());
}
public static String getLabelName(IfNode insn) {
BlockNode thenBlock = insn.getThenBlock();
if (thenBlock != null) {
return getLabelName(thenBlock);
}
return getLabelName(insn.getTarget());
}
public static String getLabelName(int offset) {
return "L_" + InsnUtils.formatOffset(offset);
if (offset < 0) {
return String.format("LB_%x", -offset);
}
return String.format("L%x", offset);
}
}

View File

@ -0,0 +1,149 @@
package jadx.core.codegen;
import java.util.ArrayList;
import java.util.BitSet;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import org.jetbrains.annotations.Nullable;
import jadx.core.dex.attributes.AFlag;
import jadx.core.dex.attributes.AType;
import jadx.core.dex.instructions.IfNode;
import jadx.core.dex.instructions.TargetInsnNode;
import jadx.core.dex.nodes.BlockNode;
import jadx.core.dex.nodes.InsnNode;
import jadx.core.dex.nodes.MethodNode;
import jadx.core.dex.trycatch.ExceptionHandler;
import jadx.core.dex.visitors.blocks.BlockProcessor;
import jadx.core.dex.visitors.blocks.BlockSplitter;
import jadx.core.utils.BlockUtils;
public class SimpleModeHelper {
private final MethodNode mth;
private final BitSet startLabel;
private final BitSet endGoto;
public SimpleModeHelper(MethodNode mth) {
this.mth = mth;
this.startLabel = BlockUtils.newBlocksBitSet(mth);
this.endGoto = BlockUtils.newBlocksBitSet(mth);
}
public List<BlockNode> prepareBlocks() {
removeEmptyBlocks();
List<BlockNode> blocksList = getSortedBlocks();
blocksList.removeIf(b -> b.equals(mth.getEnterBlock()) || b.equals(mth.getExitBlock()));
unbindExceptionHandlers();
if (blocksList.isEmpty()) {
return Collections.emptyList();
}
@Nullable
BlockNode prev = null;
int blocksCount = blocksList.size();
for (int i = 0; i < blocksCount; i++) {
BlockNode block = blocksList.get(i);
BlockNode nextBlock = i + 1 == blocksCount ? null : blocksList.get(i + 1);
List<BlockNode> preds = block.getPredecessors();
int predsCount = preds.size();
if (predsCount > 1) {
startLabel.set(block.getId());
} else if (predsCount == 1 && prev != null) {
if (!prev.equals(preds.get(0))) {
startLabel.set(block.getId());
if (prev.getSuccessors().size() == 1 && !mth.isPreExitBlocks(prev)) {
endGoto.set(prev.getId());
}
}
}
InsnNode lastInsn = BlockUtils.getLastInsn(block);
if (lastInsn instanceof TargetInsnNode) {
processTargetInsn(block, lastInsn, nextBlock);
}
if (block.contains(AType.EXC_HANDLER)) {
startLabel.set(block.getId());
}
if (nextBlock == null && !mth.isPreExitBlocks(block)) {
endGoto.set(block.getId());
}
prev = block;
}
if (mth.isVoidReturn()) {
int last = blocksList.size() - 1;
if (blocksList.get(last).contains(AFlag.RETURN)) {
// remove trailing return
blocksList.remove(last);
}
}
return blocksList;
}
private void removeEmptyBlocks() {
for (BlockNode block : mth.getBasicBlocks()) {
if (block.getInstructions().isEmpty()
&& block.getPredecessors().size() > 0
&& block.getSuccessors().size() == 1) {
BlockNode successor = block.getSuccessors().get(0);
List<BlockNode> predecessors = block.getPredecessors();
BlockSplitter.removeConnection(block, successor);
if (predecessors.size() == 1) {
BlockSplitter.replaceConnection(predecessors.get(0), block, successor);
} else {
for (BlockNode pred : new ArrayList<>(predecessors)) {
BlockSplitter.replaceConnection(pred, block, successor);
}
}
block.add(AFlag.REMOVE);
}
}
BlockProcessor.removeMarkedBlocks(mth);
}
private void unbindExceptionHandlers() {
if (mth.isNoExceptionHandlers()) {
return;
}
for (ExceptionHandler handler : mth.getExceptionHandlers()) {
BlockNode handlerBlock = handler.getHandlerBlock();
if (handlerBlock != null) {
BlockSplitter.removePredecessors(handlerBlock);
}
}
}
private void processTargetInsn(BlockNode block, InsnNode lastInsn, @Nullable BlockNode next) {
if (lastInsn instanceof IfNode) {
IfNode ifInsn = (IfNode) lastInsn;
BlockNode thenBlock = ifInsn.getThenBlock();
if (Objects.equals(next, thenBlock)) {
ifInsn.invertCondition();
startLabel.set(ifInsn.getThenBlock().getId());
} else {
startLabel.set(thenBlock.getId());
}
ifInsn.normalize();
} else {
for (BlockNode successor : block.getSuccessors()) {
startLabel.set(successor.getId());
}
}
}
public boolean isNeedStartLabel(BlockNode block) {
return startLabel.get(block.getId());
}
public boolean isNeedEndGoto(BlockNode block) {
return endGoto.get(block.getId());
}
// DFS sort blocks to reduce goto count
private List<BlockNode> getSortedBlocks() {
List<BlockNode> list = new ArrayList<>(mth.getBasicBlocks().size());
BlockUtils.dfsVisit(mth, list::add);
return list;
}
}

View File

@ -81,6 +81,8 @@ public enum AFlag {
METHOD_CANDIDATE_FOR_INLINE,
DISABLE_BLOCKS_LOCK,
// Class processing flags
RESTART_CODEGEN, // codegen must be executed again
RELOAD_AT_CODEGEN_STAGE, // class can't be analyzed at 'process' stage => unload before 'codegen' stage

View File

@ -50,11 +50,7 @@ public class JadxError implements Comparable<JadxError> {
@Override
public String toString() {
StringBuilder str = new StringBuilder();
str.append("JadxError: ");
if (error != null) {
str.append(error);
str.append(' ');
}
str.append("JadxError: ").append(error).append(' ');
if (cause != null) {
str.append(cause.getClass());
str.append(':');

View File

@ -5,6 +5,7 @@ import java.util.List;
import jadx.api.plugins.input.insns.InsnData;
import jadx.core.dex.instructions.args.ArgType;
import jadx.core.dex.instructions.args.InsnArg;
import jadx.core.dex.instructions.args.LiteralArg;
import jadx.core.dex.instructions.args.PrimitiveType;
import jadx.core.dex.nodes.BlockNode;
import jadx.core.dex.nodes.InsnNode;
@ -70,6 +71,15 @@ public class IfNode extends GotoNode {
elseBlock = tmp;
}
/**
* Change 'a != false' to 'a == true'
*/
public void normalize() {
if (getOp() == IfOp.NE && getArg(1).isFalse()) {
changeCondition(IfOp.EQ, getArg(0), LiteralArg.litTrue());
}
}
public void changeCondition(IfOp op, InsnArg arg1, InsnArg arg2) {
this.op = op;
setArg(0, arg1);

View File

@ -16,9 +16,11 @@ import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import jadx.api.DecompilationMode;
import jadx.api.ICodeCache;
import jadx.api.ICodeInfo;
import jadx.api.ICodeWriter;
import jadx.api.JadxArgs;
import jadx.api.plugins.input.data.IClassData;
import jadx.api.plugins.input.data.IFieldData;
import jadx.api.plugins.input.data.IMethodData;
@ -304,6 +306,26 @@ public class ClassNode extends NotificationAttrNode implements ILoadable, ICodeN
return decompile(true);
}
/**
* WARNING: Slow operation! Use with caution!
*/
public ICodeInfo decompileWithMode(DecompilationMode mode) {
DecompilationMode baseMode = root.getArgs().getDecompilationMode();
if (mode == baseMode) {
return decompile(true);
}
JadxArgs args = root.getArgs();
try {
unload();
args.setDecompilationMode(mode);
ProcessClass process = new ProcessClass(args);
process.initPasses(root);
return process.generateCode(this);
} finally {
args.setDecompilationMode(baseMode);
}
}
public ICodeInfo getCode() {
return decompile(true);
}
@ -355,7 +377,7 @@ public class ClassNode extends NotificationAttrNode implements ILoadable, ICodeN
return code;
}
}
ICodeInfo codeInfo = ProcessClass.generateCode(this);
ICodeInfo codeInfo = root.getProcessClasses().generateCode(this);
codeCache.add(clsRawName, codeInfo);
return codeInfo;
}

View File

@ -336,6 +336,14 @@ public class MethodNode extends NotificationAttrNode implements IMethodDetails,
return exitBlock.getPredecessors();
}
public boolean isPreExitBlocks(BlockNode block) {
List<BlockNode> successors = block.getSuccessors();
if (successors.size() == 1) {
return successors.get(0).equals(exitBlock);
}
return exitBlock.getPredecessors().contains(block);
}
public void registerLoop(LoopInfo loop) {
if (loops.isEmpty()) {
loops = new ArrayList<>(5);

View File

@ -22,6 +22,7 @@ import jadx.api.data.ICodeData;
import jadx.api.plugins.input.data.IClassData;
import jadx.api.plugins.input.data.ILoadResult;
import jadx.core.Jadx;
import jadx.core.ProcessClass;
import jadx.core.clsp.ClspGraph;
import jadx.core.dex.info.ClassInfo;
import jadx.core.dex.info.ConstStorage;
@ -51,9 +52,9 @@ public class RootNode {
private final JadxArgs args;
private final List<IDexTreeVisitor> preDecompilePasses;
private final List<IDexTreeVisitor> passes;
private final List<ICodeDataUpdateListener> codeDataUpdateListeners = new ArrayList<>();
private final ProcessClass processClasses;
private final ErrorsCounter errorsCounter = new ErrorsCounter();
private final StringUtils stringUtils;
private final ConstStorage constValues;
@ -76,7 +77,7 @@ public class RootNode {
public RootNode(JadxArgs args) {
this.args = args;
this.preDecompilePasses = Jadx.getPreDecompilePassesList();
this.passes = Jadx.getPassesList(args);
this.processClasses = new ProcessClass(this.getArgs());
this.stringUtils = new StringUtils(args);
this.constValues = new ConstStorage(args);
this.typeUpdate = new TypeUpdate(this);
@ -460,18 +461,16 @@ public class RootNode {
return null;
}
public ProcessClass getProcessClasses() {
return processClasses;
}
public List<IDexTreeVisitor> getPasses() {
return passes;
return processClasses.getPasses();
}
public void initPasses() {
for (IDexTreeVisitor pass : passes) {
try {
pass.init(this);
} catch (Exception e) {
LOG.error("Visitor init failed: {}", pass.getClass().getSimpleName(), e);
}
}
processClasses.initPasses(this);
}
public ICodeWriter makeCodeWriter() {

View File

@ -4,7 +4,6 @@ import jadx.core.dex.attributes.AFlag;
import jadx.core.dex.instructions.IfNode;
import jadx.core.dex.instructions.IfOp;
import jadx.core.dex.instructions.args.InsnArg;
import jadx.core.dex.instructions.args.LiteralArg;
public final class Compare {
private final IfNode insn;
@ -35,13 +34,8 @@ public final class Compare {
return this;
}
/**
* Change 'a != false' to 'a == true'
*/
public void normalize() {
if (getOp() == IfOp.NE && getB().isFalse()) {
insn.changeCondition(IfOp.EQ, getA(), LiteralArg.litTrue());
}
insn.normalize();
}
@Override

View File

@ -0,0 +1,31 @@
package jadx.core.dex.visitors;
import java.util.function.Consumer;
import jadx.core.dex.nodes.ClassNode;
import jadx.core.dex.nodes.MethodNode;
import jadx.core.dex.nodes.RootNode;
import jadx.core.utils.exceptions.JadxException;
public class MethodVisitor implements IDexTreeVisitor {
private final Consumer<MethodNode> visitor;
public MethodVisitor(Consumer<MethodNode> visitor) {
this.visitor = visitor;
}
@Override
public void visit(MethodNode mth) throws JadxException {
visitor.accept(mth);
}
@Override
public void init(RootNode root) throws JadxException {
}
@Override
public boolean visit(ClassNode cls) throws JadxException {
return true;
}
}

View File

@ -149,12 +149,7 @@ public class BlockExceptionHandler {
continue;
}
firstInsn.remove(AType.EXC_HANDLER);
TmpEdgeAttr tmpEdgeAttr = block.get(AType.TMP_EDGE);
if (tmpEdgeAttr != null) {
// remove temp connection
BlockSplitter.removeConnection(tmpEdgeAttr.getBlock(), block);
block.remove(AType.TMP_EDGE);
}
removeTmpConnection(block);
ExceptionHandler excHandler = excHandlerAttr.getHandler();
if (block.getPredecessors().isEmpty()) {
@ -176,6 +171,19 @@ public class BlockExceptionHandler {
}
}
protected static void removeTmpConnections(MethodNode mth) {
mth.getBasicBlocks().forEach(BlockExceptionHandler::removeTmpConnection);
}
private static void removeTmpConnection(BlockNode block) {
TmpEdgeAttr tmpEdgeAttr = block.get(AType.TMP_EDGE);
if (tmpEdgeAttr != null) {
// remove temp connection
BlockSplitter.removeConnection(tmpEdgeAttr.getBlock(), block);
block.remove(AType.TMP_EDGE);
}
}
private static List<TryCatchBlockAttr> prepareTryBlocks(MethodNode mth) {
Map<ExceptionHandler, List<BlockNode>> blocksByHandler = new HashMap<>();
for (BlockNode block : mth.getBasicBlocks()) {

View File

@ -77,7 +77,9 @@ public class BlockProcessor extends AbstractVisitor {
processNestedLoops(mth);
updateCleanSuccessors(mth);
mth.finishBasicBlocks();
if (!mth.contains(AFlag.DISABLE_BLOCKS_LOCK)) {
mth.finishBasicBlocks();
}
}
static void updateCleanSuccessors(MethodNode mth) {
@ -686,7 +688,7 @@ public class BlockProcessor extends AbstractVisitor {
return false;
}
static void removeMarkedBlocks(MethodNode mth) {
public static void removeMarkedBlocks(MethodNode mth) {
mth.getBasicBlocks().removeIf(block -> {
if (block.contains(AFlag.REMOVE)) {
if (!block.getPredecessors().isEmpty() || !block.getSuccessors().isEmpty()) {

View File

@ -142,7 +142,7 @@ public class BlockSplitter extends AbstractVisitor {
return block;
}
static void connect(BlockNode from, BlockNode to) {
public static void connect(BlockNode from, BlockNode to) {
if (!from.getSuccessors().contains(to)) {
from.getSuccessors().add(to);
}
@ -151,19 +151,19 @@ public class BlockSplitter extends AbstractVisitor {
}
}
static void removeConnection(BlockNode from, BlockNode to) {
public static void removeConnection(BlockNode from, BlockNode to) {
from.getSuccessors().remove(to);
to.getPredecessors().remove(from);
}
static void removePredecessors(BlockNode block) {
public static void removePredecessors(BlockNode block) {
for (BlockNode pred : block.getPredecessors()) {
pred.getSuccessors().remove(block);
}
block.getPredecessors().clear();
}
static void replaceConnection(BlockNode source, BlockNode oldDest, BlockNode newDest) {
public static void replaceConnection(BlockNode source, BlockNode oldDest, BlockNode newDest) {
removeConnection(source, oldDest);
connect(source, newDest);
replaceTarget(source, oldDest, newDest);

View File

@ -453,6 +453,31 @@ public class BlockUtils {
}
}
public static void dfsVisit(MethodNode mth, Consumer<BlockNode> visitor) {
BitSet visited = newBlocksBitSet(mth);
Deque<BlockNode> queue = new ArrayDeque<>();
BlockNode enterBlock = mth.getEnterBlock();
queue.addLast(enterBlock);
visited.set(mth.getEnterBlock().getId());
while (true) {
BlockNode current = queue.pollLast();
if (current == null) {
return;
}
visitor.accept(current);
List<BlockNode> successors = current.getSuccessors();
int count = successors.size();
for (int i = count - 1; i >= 0; i--) { // to preserve order in queue
BlockNode next = successors.get(i);
int nextId = next.getId();
if (!visited.get(nextId)) {
queue.addLast(next);
visited.set(nextId);
}
}
}
}
public static List<BlockNode> collectPredecessors(MethodNode mth, BlockNode start, Collection<BlockNode> stopBlocks) {
BitSet bs = newBlocksBitSet(mth);
if (!stopBlocks.isEmpty()) {

View File

@ -35,18 +35,21 @@ public class CodeGenUtils {
List<JadxError> errors = node.getAll(AType.JADX_ERROR);
if (!errors.isEmpty()) {
errors.stream().distinct().sorted().forEach(err -> {
code.startLine("/* JADX ERROR: ").add(err.getError());
Throwable cause = err.getCause();
if (cause != null) {
code.incIndent();
Utils.appendStackTrace(code, cause);
code.decIndent();
}
code.add("*/");
addError(code, err.getError(), err.getCause());
});
}
}
public static void addError(ICodeWriter code, String errMsg, Throwable cause) {
code.startLine("/* JADX ERROR: ").add(errMsg);
if (cause != null) {
code.incIndent();
Utils.appendStackTrace(code, cause);
code.decIndent();
}
code.add("*/");
}
public static void addComments(ICodeWriter code, NotificationAttrNode node) {
JadxCommentsAttr commentsAttr = node.get(AType.JADX_COMMENTS);
if (commentsAttr != null) {

View File

@ -33,8 +33,8 @@ public class TestFallbackMode extends IntegrationTest {
assertThat(code, containsString("public int test(int r2) {"));
assertThat(code, containsOne("r1 = this;"));
assertThat(code, containsOne("L_0x0000:"));
assertThat(code, containsOne("L_0x0007:"));
assertThat(code, containsOne("L0:"));
assertThat(code, containsOne("L7:"));
assertThat(code, containsOne("int r2 = r2 + 1"));
assertThat(code, not(containsString("throw new UnsupportedOperationException")));
}

View File

@ -27,6 +27,7 @@ import org.slf4j.LoggerFactory;
import com.beust.jcommander.Parameter;
import jadx.api.CommentsLevel;
import jadx.api.DecompilationMode;
import jadx.api.JadxArgs;
import jadx.api.args.DeobfuscationMapFileMode;
import jadx.cli.JadxCLIArgs;
@ -44,7 +45,7 @@ public class JadxSettings extends JadxCLIArgs {
private static final Path USER_HOME = Paths.get(System.getProperty("user.home"));
private static final int RECENT_PROJECTS_COUNT = 15;
private static final int CURRENT_SETTINGS_VERSION = 16;
private static final int CURRENT_SETTINGS_VERSION = 17;
private static final Font DEFAULT_FONT = new RSyntaxTextArea().getFont();
@ -291,6 +292,10 @@ public class JadxSettings extends JadxCLIArgs {
this.skipSources = skipSources;
}
public void setDecompilationMode(DecompilationMode decompilationMode) {
this.decompilationMode = decompilationMode;
}
public void setShowInconsistentCode(boolean showInconsistentCode) {
this.showInconsistentCode = showInconsistentCode;
}
@ -672,6 +677,14 @@ public class JadxSettings extends JadxCLIArgs {
}
fromVersion++;
}
if (fromVersion == 16) {
if (fallbackMode) {
decompilationMode = DecompilationMode.FALLBACK;
} else {
decompilationMode = DecompilationMode.AUTO;
}
fromVersion++;
}
if (fromVersion != CURRENT_SETTINGS_VERSION) {
throw new JadxRuntimeException("Incorrect settings upgrade");
}

View File

@ -59,6 +59,7 @@ import com.google.gson.JsonObject;
import say.swing.JFontChooser;
import jadx.api.CommentsLevel;
import jadx.api.DecompilationMode;
import jadx.api.JadxArgs;
import jadx.api.JadxArgs.UseKotlinMethodsForVarNames;
import jadx.api.args.DeobfuscationMapFileMode;
@ -419,13 +420,6 @@ public class JadxSettingsWindow extends JDialog {
}
private SettingsGroup makeDecompilationGroup() {
JCheckBox fallback = new JCheckBox();
fallback.setSelected(settings.isFallbackMode());
fallback.addItemListener(e -> {
settings.setFallbackMode(e.getStateChange() == ItemEvent.SELECTED);
needReload();
});
JCheckBox useDx = new JCheckBox();
useDx.setSelected(settings.isUseDx());
useDx.addItemListener(e -> {
@ -433,6 +427,13 @@ public class JadxSettingsWindow extends JDialog {
needReload();
});
JComboBox<DecompilationMode> decompilationModeComboBox = new JComboBox<>(DecompilationMode.values());
decompilationModeComboBox.setSelectedItem(settings.getDecompilationMode());
decompilationModeComboBox.addActionListener(e -> {
settings.setDecompilationMode((DecompilationMode) decompilationModeComboBox.getSelectedItem());
needReload();
});
JCheckBox showInconsistentCode = new JCheckBox();
showInconsistentCode.setSelected(settings.isShowInconsistentCode());
showInconsistentCode.addItemListener(e -> {
@ -543,6 +544,7 @@ public class JadxSettingsWindow extends JDialog {
other.addRow(NLS.str("preferences.excludedPackages"), NLS.str("preferences.excludedPackages.tooltip"),
editExcludedPackages);
other.addRow(NLS.str("preferences.start_jobs"), autoStartJobs);
other.addRow(NLS.str("preferences.decompilationMode"), decompilationModeComboBox);
other.addRow(NLS.str("preferences.showInconsistentCode"), showInconsistentCode);
other.addRow(NLS.str("preferences.escapeUnicode"), escapeUnicode);
other.addRow(NLS.str("preferences.replaceConsts"), replaceConsts);
@ -551,7 +553,6 @@ public class JadxSettingsWindow extends JDialog {
other.addRow(NLS.str("preferences.inlineAnonymous"), inlineAnonymous);
other.addRow(NLS.str("preferences.inlineMethods"), inlineMethods);
other.addRow(NLS.str("preferences.fsCaseSensitive"), fsCaseSensitive);
other.addRow(NLS.str("preferences.fallback"), fallback);
other.addRow(NLS.str("preferences.useDx"), useDx);
other.addRow(NLS.str("preferences.skipResourcesDecode"), resourceDecode);
other.addRow(NLS.str("preferences.useKotlinMethodsForVarNames"), kotlinRenameVars);

View File

@ -125,8 +125,8 @@ preferences.other=Andere
preferences.language=Sprache
preferences.lineNumbersMode=Editor Zeilennummern-Modus
preferences.check_for_updates=Nach Updates beim Start suchen
preferences.fallback=Zwischencode ausgeben (einfacher Speicherauszug)
#preferences.useDx=Use dx/d8 to convert java bytecode
#preferences.decompilationMode=Decompilation mode
preferences.showInconsistentCode=Inkonsistenten Code anzeigen
preferences.escapeUnicode=Unicodezeichen escapen
preferences.replaceConsts=Konstanten ersetzen

View File

@ -125,8 +125,8 @@ preferences.other=Other
preferences.language=Language
preferences.lineNumbersMode=Editor line numbers mode
preferences.check_for_updates=Check for updates on startup
preferences.fallback=Fallback mode (simple dump)
preferences.useDx=Use dx/d8 to convert java bytecode
preferences.decompilationMode=Decompilation mode
preferences.showInconsistentCode=Show inconsistent code
preferences.escapeUnicode=Escape unicode
preferences.replaceConsts=Replace constants

View File

@ -125,8 +125,8 @@ preferences.other=Otros
preferences.language=Idioma
#preferences.lineNumbersMode=Editor line numbers mode
preferences.check_for_updates=Buscar actualizaciones al iniciar
preferences.fallback=Modo fallback (simple dump)
#preferences.useDx=Use dx/d8 to convert java bytecode
#preferences.decompilationMode=Decompilation mode
preferences.showInconsistentCode=Mostrar código inconsistente
preferences.escapeUnicode=Escape unicode
preferences.replaceConsts=Reemplazar constantes

View File

@ -125,8 +125,8 @@ preferences.other=기타
preferences.language=언어
preferences.lineNumbersMode=편집기 줄 번호 모드
preferences.check_for_updates=시작시 업데이트 확인
preferences.fallback=대체 모드 (단순 덤프)
#preferences.useDx=Use dx/d8 to convert java bytecode
#preferences.decompilationMode=Decompilation mode
preferences.showInconsistentCode=디컴파일 안된 코드 표시
preferences.escapeUnicode=유니코드 이스케이프
preferences.replaceConsts=상수 바꾸기

View File

@ -125,8 +125,8 @@ preferences.other=其他
preferences.language=语言
preferences.lineNumbersMode=编辑器行号模式
preferences.check_for_updates=启动时检查更新
preferences.fallback=输出中间代码
#preferences.useDx=Use dx/d8 to convert java bytecode
#preferences.decompilationMode=Decompilation mode
preferences.showInconsistentCode=显示不一致的代码
preferences.escapeUnicode=将 Unicode 字符转义
preferences.replaceConsts=替换常量

View File

@ -125,8 +125,8 @@ preferences.other=其他
preferences.language=語言
preferences.lineNumbersMode=編輯器行號模式
preferences.check_for_updates=啟動時檢查更新
preferences.fallback=後援模式 (簡易傾印)
preferences.useDx=使用 dx/d8 來轉換 Java 位元組碼
#preferences.decompilationMode=Decompilation mode
preferences.showInconsistentCode=顯示不一致的程式碼
preferences.escapeUnicode=Unicode 逸出
preferences.replaceConsts=替換常數