feat(gui): add a smali debugger (#1136) (PR #1137)

* add a smali debugger
* debugger: support android 11, support 9(may be) & 10 if debug_info available, add rerun.
* debugger: support get/set fields of this, change icons, fix bugs.
* debugger: add timeout to attach

Co-authored-by: tobias <tobias.hotmail.com>
This commit is contained in:
LBJ-the-GOAT
2021-03-28 18:23:07 +08:00
committed by GitHub
parent 19572a674e
commit 4705194a1d
54 changed files with 7886 additions and 964 deletions
@@ -451,6 +451,40 @@ public final class JadxDecompiler implements Closeable {
.orElse(null);
}
@Nullable
public ClassNode searchClassNodeByOrigFullName(String fullName) {
return getRoot().getClasses().stream()
.filter(cls -> cls.getClassInfo().getFullName().equals(fullName))
.findFirst()
.orElse(null);
}
// returns parent if class contains DONT_GENERATE flag.
@Nullable
public JavaClass searchJavaClassOrItsParentByOrigFullName(String fullName) {
ClassNode node = getRoot().getClasses().stream()
.filter(cls -> cls.getClassInfo().getFullName().equals(fullName))
.findFirst()
.orElse(null);
if (node != null) {
if (node.contains(AFlag.DONT_GENERATE)) {
return getJavaClassByNode(node.getTopParentClass());
} else {
return getJavaClassByNode(node);
}
}
return null;
}
@Nullable
public JavaClass searchJavaClassByAliasFullName(String fullName) {
return getRoot().getClasses().stream()
.filter(cls -> cls.getClassInfo().getAliasFullName().equals(fullName))
.findFirst()
.map(this::getJavaClassByNode)
.orElse(null);
}
@Nullable
JavaNode convertNode(Object obj) {
if (!(obj instanceof LineAttrNode)) {
@@ -54,7 +54,7 @@ public class InsnDecoder {
}
@NotNull
private InsnNode decode(InsnData insn) throws DecodeException {
protected InsnNode decode(InsnData insn) throws DecodeException {
switch (insn.getOpcode()) {
case NOP:
return new InsnNode(InsnType.NOP, 0);
@@ -575,27 +575,8 @@ public class ClassNode extends NotificationAttrNode implements ILoadable, ICodeN
sb.append(this.clsData.getDisassembledCode());
}
public String getSmaliV2() {
StringBuilder sb = new StringBuilder();
getSmaliV2(sb);
sb.append(System.lineSeparator());
Set<ClassNode> allInlinedClasses = new LinkedHashSet<>();
getInnerAndInlinedClassesRecursive(allInlinedClasses);
for (ClassNode innerClass : allInlinedClasses) {
innerClass.getSmaliV2(sb);
sb.append(System.lineSeparator());
}
return sb.toString();
}
private void getSmaliV2(StringBuilder sb) {
if (this.clsData == null) {
sb.append(String.format("###### Class %s is created by jadx", getFullName()));
return;
}
sb.append(String.format("###### Class %s (%s)", getFullName(), getRawName()));
sb.append(System.lineSeparator());
sb.append(this.clsData.getDisassembledCodeV2());
public IClassData getClsData() {
return clsData;
}
public ProcessState getState() {
@@ -1,5 +1,8 @@
package jadx.core.utils;
import java.text.SimpleDateFormat;
import java.util.Date;
import org.jetbrains.annotations.Nullable;
import jadx.api.JadxArgs;
@@ -314,4 +317,8 @@ public class StringUtils {
return WORD_SEPARATORS.indexOf(chr) != -1;
}
public static String getDateText() {
return new SimpleDateFormat("HH:mm:ss").format(new Date());
}
}
+2 -1
View File
@@ -21,7 +21,8 @@ dependencies {
implementation 'io.reactivex.rxjava2:rxjava:2.2.21'
implementation "com.github.akarnokd:rxjava2-swing:0.3.7"
implementation 'com.android.tools.build:apksig:4.1.2'
implementation 'com.android.tools.build:apksig:4.1.1'
implementation 'io.github.hqktech:jdwp:1.0'
}
application {
@@ -0,0 +1,55 @@
package jadx.gui.device.debugger;
public class ArtAdapter {
public interface Debugger {
int getRuntimeRegNum(int smaliNum, int regCount, int paramStart);
boolean readNullObject();
String typeForNull();
}
public static Debugger getAdapter(int androidReleaseVer) {
if (androidReleaseVer <= 8) {
return new AndroidOreoAndBelow();
} else {
return new AndroidPieAndAbove();
}
}
public static class AndroidOreoAndBelow implements Debugger {
@Override
public int getRuntimeRegNum(int smaliNum, int regCount, int paramStart) {
int localRegCount = regCount - paramStart;
return (smaliNum + localRegCount) % regCount;
}
@Override
public boolean readNullObject() {
return true;
}
@Override
public String typeForNull() {
return "";
}
}
public static class AndroidPieAndAbove implements Debugger {
@Override
public int getRuntimeRegNum(int smaliNum, int regCount, int paramStart) {
return smaliNum;
}
@Override
public boolean readNullObject() {
return false;
}
@Override
public String typeForNull() {
return "zero value";
}
}
}
@@ -0,0 +1,206 @@
package jadx.gui.device.debugger;
import java.io.IOException;
import java.lang.reflect.Type;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.*;
import java.util.AbstractMap.SimpleEntry;
import java.util.Map.Entry;
import com.google.gson.*;
import com.google.gson.reflect.TypeToken;
import jadx.core.dex.nodes.ClassNode;
import jadx.gui.device.debugger.smali.Smali;
import jadx.gui.treemodel.JClass;
public class BreakpointManager {
private static Gson gson = null;
private static final Type TYPE_TOKEN = new TypeToken<Map<String, List<FileBreakpoint>>>() {
}.getType();
private static Map<String, List<FileBreakpoint>> bpm;
private static Path savePath;
private static DebugController debugController;
private static Map<String, Entry<ClassNode, Listener>> listeners = Collections.emptyMap(); // class full name as key
public static void saveAndExit() {
if (bpm != null) {
if (bpm.size() == 0 && !Files.exists(savePath)) {
return; // user didn't do anything with breakpoint so don't output breakpoint file.
}
sync();
bpm = null;
savePath = null;
listeners = Collections.emptyMap();
}
}
public static void init(Path dirPath) {
if (gson == null) {
gson = new GsonBuilder()
.setPrettyPrinting()
.create();
}
savePath = dirPath.resolve("breakpoints.json");
if (Files.exists(savePath)) {
try {
byte[] bytes = Files.readAllBytes(savePath);
bpm = gson.fromJson(new String(bytes, StandardCharsets.UTF_8), TYPE_TOKEN);
} catch (IOException e) {
e.printStackTrace();
}
}
if (bpm == null) {
bpm = Collections.emptyMap();
}
}
/**
* @param listener When breakpoint is failed to set during debugging, this listener will be called.
*/
public static void addListener(JClass topCls, Listener listener) {
if (listeners == Collections.EMPTY_MAP) {
listeners = new HashMap<>();
}
listeners.put(DbgUtils.getRawFullName(topCls),
new SimpleEntry<>(topCls.getCls().getClassNode(), listener));
}
public static void removeListener(JClass topCls) {
listeners.remove(DbgUtils.getRawFullName(topCls));
}
public static List<Integer> getPositions(JClass topCls) {
List<FileBreakpoint> bps = bpm.get(DbgUtils.getRawFullName(topCls));
if (bps != null && bps.size() > 0) {
Smali smali = DbgUtils.getSmali(topCls.getCls().getClassNode());
if (smali != null) {
List<Integer> posList = new ArrayList<>(bps.size());
for (FileBreakpoint bp : bps) {
int pos = smali.getInsnPosByCodeOffset(bp.getFullMthRawID(), bp.codeOffset);
if (pos > -1) {
posList.add(pos);
}
}
return posList;
}
}
return Collections.emptyList();
}
public static boolean set(JClass topCls, int line) {
Entry<String, Integer> lineInfo = DbgUtils.getCodeOffsetInfoByLine(topCls, line);
if (lineInfo != null) {
if (bpm.isEmpty()) {
bpm = new HashMap<>();
}
String name = DbgUtils.getRawFullName(topCls);
List<FileBreakpoint> list = bpm.computeIfAbsent(name, k -> new ArrayList<>());
FileBreakpoint bkp = list.stream()
.filter(bp -> bp.codeOffset == lineInfo.getValue() && bp.getFullMthRawID().equals(lineInfo.getKey()))
.findFirst()
.orElse(null);
boolean ok = true;
if (bkp == null) {
String[] sigs = DbgUtils.sepClassAndMthSig(lineInfo.getKey());
if (sigs != null && sigs.length == 2) {
FileBreakpoint bp = new FileBreakpoint(sigs[0], sigs[1], lineInfo.getValue());
list.add(bp);
if (debugController != null) {
ok = debugController.setBreakpoint(bp);
}
}
}
return ok;
}
return false;
}
public static boolean remove(JClass topCls, int line) {
Entry<String, Integer> lineInfo = DbgUtils.getCodeOffsetInfoByLine(topCls, line);
if (lineInfo != null) {
List<FileBreakpoint> bps = bpm.get(DbgUtils.getRawFullName(topCls));
for (Iterator<FileBreakpoint> it = bps.iterator(); it.hasNext();) {
FileBreakpoint bp = it.next();
if (bp.codeOffset == lineInfo.getValue() && bp.getFullMthRawID().equals(lineInfo.getKey())) {
it.remove();
if (debugController != null) {
return debugController.removeBreakpoint(bp);
}
break;
}
}
}
return true;
}
private static void sync() {
try {
Files.write(savePath, gson.toJson(bpm).getBytes(StandardCharsets.UTF_8));
} catch (IOException e) {
e.printStackTrace();
}
}
public interface Listener {
void breakpointDisabled(int codeOffset);
}
protected static class FileBreakpoint {
final String cls;
final String mth;
final long codeOffset;
private FileBreakpoint(String cls, String mth, long codeOffset) {
this.cls = cls;
this.mth = mth;
this.codeOffset = codeOffset;
}
protected String getFullMthRawID() {
return cls + "." + mth;
}
@Override
public int hashCode() {
return (int) (31 * codeOffset + 31 * cls.hashCode() + 31 * mth.hashCode());
}
@Override
public boolean equals(Object obj) {
if (obj instanceof FileBreakpoint) {
if (obj == this) {
return true;
}
FileBreakpoint fbp = (FileBreakpoint) obj;
return fbp.codeOffset == codeOffset && fbp.cls.equals(cls) && fbp.mth.equals(mth);
}
return false;
}
}
protected static List<FileBreakpoint> getAllBreakpoints() {
List<FileBreakpoint> bpList = new ArrayList<>();
for (Entry<String, List<FileBreakpoint>> entry : bpm.entrySet()) {
bpList.addAll(entry.getValue());
}
return bpList;
}
protected static void failBreakpoint(FileBreakpoint bp) {
Entry<ClassNode, Listener> entry = listeners.get(bp.cls);
if (entry != null) {
int pos = DbgUtils.getSmali(entry.getKey())
.getInsnPosByCodeOffset(bp.getFullMthRawID(), bp.codeOffset);
pos = Math.max(0, pos);
entry.getValue().breakpointDisabled(pos);
}
}
protected static void setDebugController(DebugController controller) {
debugController = controller;
}
}
@@ -0,0 +1,167 @@
package jadx.gui.device.debugger;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;
import org.jetbrains.annotations.Nullable;
import jadx.api.JavaClass;
import jadx.api.ResourceFile;
import jadx.api.ResourceType;
import jadx.core.dex.info.ClassInfo;
import jadx.core.dex.nodes.ClassNode;
import jadx.gui.device.debugger.smali.Smali;
import jadx.gui.treemodel.JClass;
import jadx.gui.treemodel.JNode;
import jadx.gui.ui.MainWindow;
public class DbgUtils {
private static Map<ClassInfo, Smali> smaliCache = Collections.emptyMap();
protected static Smali getSmali(ClassNode topCls) {
if (smaliCache == Collections.EMPTY_MAP) {
smaliCache = new HashMap<>();
}
return smaliCache.computeIfAbsent(topCls.getTopParentClass().getClassInfo(),
c -> Smali.disassemble(topCls));
}
public static String getSmaliCode(ClassNode topCls) {
Smali smali = getSmali(topCls);
if (smali != null) {
return smali.getCode();
}
return null;
}
public static Entry<String, Integer> getCodeOffsetInfoByLine(JClass cls, int line) {
Smali smali = getSmali(cls.getCls().getClassNode().getTopParentClass());
if (smali != null) {
return smali.getMthFullIDAndCodeOffsetByLine(line);
}
return null;
}
public static String[] sepClassAndMthSig(String fullSig) {
int pos = fullSig.indexOf("(");
if (pos != -1) {
pos = fullSig.lastIndexOf(".", pos);
if (pos != -1) {
String[] sigs = new String[2];
sigs[0] = fullSig.substring(0, pos);
sigs[1] = fullSig.substring(pos + 1);
return sigs;
}
}
return null;
}
// doesn't replace $
public static String classSigToRawFullName(String clsSig) {
if (clsSig != null && clsSig.startsWith("L") && clsSig.endsWith(";")) {
clsSig = clsSig.substring(1, clsSig.length() - 1)
.replace("/", ".");
}
return clsSig;
}
// replaces $
public static String classSigToFullName(String clsSig) {
if (clsSig != null && clsSig.startsWith("L") && clsSig.endsWith(";")) {
clsSig = clsSig.substring(1, clsSig.length() - 1)
.replace("/", ".")
.replace("$", ".");
}
return clsSig;
}
public static String getRawFullName(JClass topCls) {
return topCls.getCls().getClassNode().getClassInfo().makeRawFullName();
}
public static boolean isStringObjectSig(String objectSig) {
return objectSig.equals("Ljava/lang/String;");
}
public static JClass getTopClassBySig(String clsSig, MainWindow mainWindow) {
clsSig = DbgUtils.classSigToFullName(clsSig);
JavaClass cls = mainWindow.getWrapper().getDecompiler().searchJavaClassOrItsParentByOrigFullName(clsSig);
if (cls != null) {
JClass jc = (JClass) mainWindow.getCacheObject().getNodeCache().makeFrom(cls);
return jc.getRootClass();
}
return null;
}
public static ClassNode getClassNodeBySig(String clsSig, MainWindow mainWindow) {
clsSig = DbgUtils.classSigToFullName(clsSig);
return mainWindow.getWrapper().getDecompiler().searchClassNodeByOrigFullName(clsSig);
}
public static String searchPackageName(MainWindow mainWindow) {
String content = getManifestContent(mainWindow);
int pos = content.indexOf("<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\" ");
if (pos > -1) {
pos = content.lastIndexOf(">", pos);
if (pos > -1) {
pos = content.indexOf(" package=\"", pos);
if (pos > -1) {
pos += " package=\"".length();
return content.substring(pos, content.indexOf("\"", pos));
}
}
}
return "";
}
/**
* @return the Activity class for android.intent.action.MAIN.
*/
@Nullable
public static JClass searchMainActivity(MainWindow mainWindow) {
String content = getManifestContent(mainWindow);
int pos = content.indexOf("<action android:name=\"android.intent.action.MAIN\"");
if (pos > -1) {
pos = content.lastIndexOf("<activity ", pos);
if (pos > -1) {
pos = content.indexOf(" android:name=\"", pos);
if (pos > -1) {
pos += " android:name=\"".length();
String classFullName = content.substring(pos, content.indexOf("\"", pos));
// in case the MainActivity class has been renamed before, we need raw name.
JavaClass cls = mainWindow.getWrapper().getDecompiler().searchJavaClassByAliasFullName(classFullName);
JNode jNode = mainWindow.getCacheObject().getNodeCache().makeFrom(cls);
if (jNode != null) {
return jNode.getRootClass();
}
}
}
}
return null;
}
// TODO: parse AndroidManifest.xml instead of looking for keywords
private static String getManifestContent(MainWindow mainWindow) {
try {
ResourceFile androidManifest = mainWindow.getWrapper().getDecompiler().getResources()
.stream()
.filter(res -> res.getType() == ResourceType.MANIFEST)
.findFirst()
.orElse(null);
if (androidManifest != null) {
return androidManifest.loadContent().getText().getCodeStr();
}
} catch (Exception e) {
e.printStackTrace();
}
return "";
}
public static boolean isPrintableChar(int c) {
return 32 <= c && c <= 126;
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,121 @@
package jadx.gui.device.debugger;
import java.util.*;
import java.util.AbstractMap.SimpleEntry;
import java.util.Map.Entry;
import io.reactivex.annotations.Nullable;
import jadx.core.dex.instructions.args.ArgType;
import jadx.gui.device.debugger.SmaliDebugger.RuntimeVarInfo;
import jadx.gui.device.debugger.smali.RegisterInfo;
import jadx.gui.device.debugger.smali.SmaliRegister;
public class RegisterObserver {
private Map<Long, List<Info>> infoMap;
private final List<Entry<SmaliRegister, List<RuntimeVarInfo>>> regList;
private boolean hasDbgInfo = false;
private RegisterObserver() {
regList = new ArrayList<>();
infoMap = Collections.emptyMap();
}
public static RegisterObserver merge(List<RuntimeVarInfo> rtRegs, List<SmaliRegister> smaliRegs) {
RegisterObserver adapter = new RegisterObserver();
adapter.hasDbgInfo = rtRegs.size() > 0;
if (adapter.hasDbgInfo) {
adapter.infoMap = new HashMap<>();
}
for (SmaliRegister sr : smaliRegs) {
adapter.regList.add(new SimpleEntry<>(sr, Collections.emptyList()));
}
adapter.regList.sort(Comparator.comparingInt(r -> r.getKey().getRuntimeRegNum()));
for (RuntimeVarInfo rt : rtRegs) {
Entry<SmaliRegister, List<RuntimeVarInfo>> entry = adapter.regList.get(rt.getRegNum());
if (entry.getValue().isEmpty()) {
entry.setValue(new ArrayList<>());
}
entry.getValue().add(rt);
String type = rt.getSignature();
if (type.isEmpty()) {
type = rt.getType();
}
ArgType at = ArgType.parse(type);
if (at != null) {
type = at.toString();
}
Info load = new Info(entry.getKey().getRegNum(), true,
new SimpleEntry<>(rt.getName(), type));
Info unload = new Info(entry.getKey().getRegNum(), false, null);
adapter.infoMap.computeIfAbsent((long) rt.getStartOffset(), k -> new ArrayList<>())
.add(load);
adapter.infoMap.computeIfAbsent((long) rt.getEndOffset(), k -> new ArrayList<>())
.add(unload);
}
return adapter;
}
public List<SmaliRegister> getInitializedList(long codeOffset) {
List<SmaliRegister> ret = Collections.emptyList();
for (Entry<SmaliRegister, List<RuntimeVarInfo>> info : regList) {
if (info.getKey().isInitialized(codeOffset)) {
if (ret.isEmpty()) {
ret = new ArrayList<>();
}
ret.add(info.getKey());
}
}
return ret;
}
@Nullable
public Entry<String, String> getInfo(int runtimeNum, long codeOffset) {
Entry<SmaliRegister, List<RuntimeVarInfo>> list = regList.get(runtimeNum);
for (RegisterInfo info : list.getValue()) {
if (info.getStartOffset() > codeOffset) {
break;
}
if (info.isInitialized(codeOffset)) {
return new SimpleEntry<>(info.getName(), info.getType());
}
}
return null;
}
public List<Info> getInfoAt(long codeOffset) {
if (hasDbgInfo) {
List<Info> list = infoMap.get(codeOffset);
if (list != null) {
return list;
}
}
return Collections.emptyList();
}
public static class Info {
private final int smaliRegNum;
private final boolean load;
private final Entry<String, String> info;
private Info(int smaliRegNum, boolean load, Entry<String, String> info) {
this.smaliRegNum = smaliRegNum;
this.load = load;
this.info = info;
}
public int getSmaliRegNum() {
return smaliRegNum;
}
public boolean isLoad() {
return load;
}
public Entry<String, String> getInfo() {
return info;
}
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,57 @@
package jadx.gui.device.debugger.smali;
public class MNEMONIC {
public static final String[] MNEMONICS = new String[] {
"nop", "move", "move/from16", "move/16", "move-wide",
"move-wide/from16", "move-wide/16", "move-object", "move-object/from16", "move-object/16",
"move-result", "move-result-wide", "move-result-object", "move-exception", "return-void",
"return", "return-wide", "return-object", "const/4", "const/16",
"const", "const/high16", "const-wide/16", "const-wide/32", "const-wide",
"const-wide/high16", "const-string", "const-string/jumbo", "const-class", "monitor-enter",
"monitor-exit", "check-cast", "instance-of", "array-length", "new-instance",
"new-array", "filled-new-array", "filled-new-array/range", "fill-array-data", "throw",
"goto", "goto/16", "goto/32", "packed-switch", "sparse-switch",
"cmpl-float", "cmpg-float", "cmpl-double", "cmpg-double", "cmp-long",
"if-eq", "if-ne", "if-lt", "if-ge", "if-gt",
"if-le", "if-eqz", "if-nez", "if-ltz", "if-gez",
"if-gtz", "if-lez", "(unused)", "(unused)", "(unused)",
"(unused)", "(unused)", "(unused)", "aget", "aget-wide",
"aget-object", "aget-boolean", "aget-byte", "aget-char", "aget-short",
"aput", "aput-wide", "aput-object", "aput-boolean", "aput-byte",
"aput-char", "aput-short", "iget", "iget-wide", "iget-object",
"iget-boolean", "iget-byte", "iget-char", "iget-short", "iput",
"iput-wide", "iput-object", "iput-boolean", "iput-byte", "iput-char",
"iput-short", "sget", "sget-wide", "sget-object", "sget-boolean",
"sget-byte", "sget-char", "sget-short", "sput", "sput-wide",
"sput-object", "sput-boolean", "sput-byte", "sput-char", "sput-short",
"invoke-virtual", "invoke-super", "invoke-direct", "invoke-static", "invoke-interface",
"(unused)", "invoke-virtual/range", "invoke-super/range", "invoke-direct/range", "invoke-static/range",
"invoke-interface/range", "(unused)", "(unused)", "neg-int", "not-int",
"neg-long", "not-long", "neg-float", "neg-double", "int-to-long",
"int-to-float", "int-to-double", "long-to-int", "long-to-float", "long-to-double",
"float-to-int", "float-to-long", "float-to-double", "double-to-int", "double-to-long",
"double-to-float", "int-to-byte", "int-to-char", "int-to-short", "add-int",
"sub-int", "mul-int", "div-int", "rem-int", "and-int",
"or-int", "xor-int", "shl-int", "shr-int", "ushr-int",
"add-long", "sub-long", "mul-long", "div-long", "rem-long",
"and-long", "or-long", "xor-long", "shl-long", "shr-long",
"ushr-long", "add-float", "sub-float", "mul-float", "div-float",
"rem-float", "add-double", "sub-double", "mul-double", "div-double",
"rem-double", "add-int/2addr", "sub-int/2addr", "mul-int/2addr", "div-int/2addr",
"rem-int/2addr", "and-int/2addr", "or-int/2addr", "xor-int/2addr", "shl-int/2addr",
"shr-int/2addr", "ushr-int/2addr", "add-long/2addr", "sub-long/2addr", "mul-long/2addr",
"div-long/2addr", "rem-long/2addr", "and-long/2addr", "or-long/2addr", "xor-long/2addr",
"shl-long/2addr", "shr-long/2addr", "ushr-long/2addr", "add-float/2addr", "sub-float/2addr",
"mul-float/2addr", "div-float/2addr", "rem-float/2addr", "add-double/2addr", "sub-double/2addr",
"mul-double/2addr", "div-double/2addr", "rem-double/2addr", "add-int/lit16", "rsub-int",
"mul-int/lit16", "div-int/lit16", "rem-int/lit16", "and-int/lit16", "or-int/lit16",
"xor-int/lit16", "add-int/lit8", "rsub-int/lit8", "mul-int/lit8", "div-int/lit8",
"rem-int/lit8", "and-int/lit8", "or-int/lit8", "xor-int/lit8", "shl-int/lit8",
"shr-int/lit8", "ushr-int/lit8", "(unused)", "(unused)", "(unused)",
"(unused)", "(unused)", "(unused)", "(unused)", "(unused)",
"(unused)", "(unused)", "(unused)", "(unused)", "(unused)",
"(unused)", "(unused)", "(unused)", "(unused)", "(unused)",
"(unused)", "(unused)", "(unused)", "(unused)", "(unused)",
"invoke-polymorphic", "invoke-polymorphic/range", "invoke-custom", "invoke-custom/range", "const-method-handle",
"const-method-type" };
}
@@ -0,0 +1,14 @@
package jadx.gui.device.debugger.smali;
import jadx.api.plugins.input.data.ILocalVar;
public abstract class RegisterInfo implements ILocalVar {
public boolean isInitialized(long codeOffset) {
return codeOffset >= getStartOffset() && codeOffset < getEndOffset();
}
public boolean isUnInitialized(long codeOffset) {
return codeOffset < getStartOffset() || codeOffset >= getEndOffset();
}
}
@@ -0,0 +1,998 @@
package jadx.gui.device.debugger.smali;
import java.util.*;
import java.util.AbstractMap.SimpleEntry;
import java.util.Map.Entry;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import jadx.api.ICodeInfo;
import jadx.api.plugins.input.data.*;
import jadx.api.plugins.input.data.annotations.AnnotationVisibility;
import jadx.api.plugins.input.data.annotations.EncodedValue;
import jadx.api.plugins.input.data.annotations.IAnnotation;
import jadx.api.plugins.input.insns.InsnData;
import jadx.api.plugins.input.insns.InsnIndexType;
import jadx.api.plugins.input.insns.Opcode;
import jadx.api.plugins.input.insns.custom.ISwitchPayload;
import jadx.core.codegen.TypeGen;
import jadx.core.dex.instructions.IndexInsnNode;
import jadx.core.dex.instructions.InsnDecoder;
import jadx.core.dex.instructions.InsnType;
import jadx.core.dex.instructions.InvokeNode;
import jadx.core.dex.instructions.args.ArgType;
import jadx.core.dex.instructions.args.RegisterArg;
import jadx.core.dex.nodes.ClassNode;
import jadx.core.dex.nodes.InsnNode;
import jadx.core.dex.nodes.MethodNode;
import static jadx.api.plugins.input.data.AccessFlagsScope.FIELD;
import static jadx.api.plugins.input.data.AccessFlagsScope.METHOD;
import static jadx.api.plugins.input.insns.Opcode.*;
public class Smali {
private static SmaliInsnDecoder insnDecoder = null;
private ICodeInfo codeInfo;
private final Map<String, SmaliMethodNode> insnMap = new HashMap<>(); // fullRawId of method as key
private final boolean printFileOffset = true;
private final boolean printBytecode = true;
private Smali() {
}
public static Smali disassemble(ClassNode cls) {
cls = cls.getTopParentClass();
SmaliWriter code = new SmaliWriter(cls);
Smali smali = new Smali();
smali.writeClass(code, cls);
smali.codeInfo = code.finish();
return smali;
}
public String getCode() {
return codeInfo.getCodeStr();
}
public int getMethodDefPos(String mthFullRawID) {
SmaliMethodNode info = insnMap.get(mthFullRawID);
if (info != null) {
return info.getDefPos();
}
return -1;
}
public int getRegCount(String mthFullRawID) {
SmaliMethodNode info = insnMap.get(mthFullRawID);
if (info != null) {
return info.getRegCount();
}
return -1;
}
public int getParamRegStart(String mthFullRawID) {
SmaliMethodNode info = insnMap.get(mthFullRawID);
if (info != null) {
return info.getParamRegStart();
}
return -1;
}
public int getInsnPosByCodeOffset(String mthFullRawID, long codeOffset) {
SmaliMethodNode info = insnMap.get(mthFullRawID);
if (info != null) {
return info.getInsnPos(codeOffset);
}
return -1;
}
@Nullable
public Entry<String, Integer> getMthFullIDAndCodeOffsetByLine(int line) {
for (Entry<String, SmaliMethodNode> entry : insnMap.entrySet()) {
Integer codeOffset = entry.getValue().getLineMapping().get(line);
if (codeOffset != null) {
return new SimpleEntry<>(entry.getKey(), codeOffset);
}
}
return null;
}
public List<SmaliRegister> getRegisterList(String mthFullRawID) {
SmaliMethodNode node = insnMap.get(mthFullRawID);
if (node != null) {
return node.getRegList();
}
return Collections.emptyList();
}
/**
* @return null for no result, FieldInfo for field, Integer for register.
*/
@Nullable
public Object getResultRegOrField(String mthFullRawID, long codeOffset) {
SmaliMethodNode info = insnMap.get(mthFullRawID);
if (info != null) {
InsnNode insn = info.getInsnNode(codeOffset);
if (insn != null) {
if (insn.getType() == InsnType.IPUT) {
return ((IndexInsnNode) insn).getIndex();
}
if (insn.getType() == InsnType.INVOKE) {
if (insn instanceof InvokeNode) {
if (insn.getArgsCount() > 0) {
return ((RegisterArg) insn.getArg(0)).getRegNum();
}
}
}
RegisterArg regArg = insn.getResult();
if (regArg != null) {
return regArg.getRegNum();
}
}
}
return null;
}
private void writeClass(SmaliWriter smali, ClassNode cls) {
IClassData clsData = cls.getClsData();
if (clsData == null) {
smali.startLine(String.format("###### Class %s is created by jadx", cls.getFullName()));
return;
}
smali.startLine("Class: " + clsData.getType())
.startLine("AccessFlags: " + AccessFlags.format(clsData.getAccessFlags(), AccessFlagsScope.CLASS))
.startLine("SuperType: " + clsData.getSuperType())
.startLine("Interfaces: " + clsData.getInterfacesTypes())
.startLine("SourceFile: " + clsData.getSourceFile());
List<IAnnotation> annos = clsData.getAnnotations();
if (annos.size() > 0) {
smali.startLine(String.format("# %d annotations", annos.size()));
writeAnnotations(smali, annos);
smali.startLine();
}
List<RawField> fields = new ArrayList<>();
int[] colWidths = new int[] { 0, 0 }; // first is access flag, second is name
int[] mthIndex = new int[] { 0 };
LineInfo line = new LineInfo();
clsData.visitFieldsAndMethods(
f -> {
RawField fld = RawField.make(f);
fields.add(fld);
if (fld.accessFlag.length() > colWidths[0]) {
colWidths[0] = fld.accessFlag.length();
}
if (fld.name.length() > colWidths[1]) {
colWidths[1] = fld.name.length();
}
},
m -> {
if (!fields.isEmpty()) {
writeFields(smali, clsData, fields, colWidths);
fields.clear();
}
writeMethod(smali, cls.getMethods().get(mthIndex[0]++), m, line);
line.reset();
});
if (!fields.isEmpty()) { // in case there's no methods.
writeFields(smali, clsData, fields, colWidths);
}
for (ClassNode innerClass : cls.getInnerClasses()) {
writeClass(smali, innerClass);
}
}
private void writeFields(SmaliWriter smali, IClassData classData, List<RawField> fields, int[] colWidths) {
int staticIdx = 0;
List<EncodedValue> staticFieldInitValues = classData.getStaticFieldInitValues();
smali.startLine().startLine("# fields");
String whites = new String(new byte[Math.max(colWidths[0], colWidths[1])]).replace("\0", " ");
for (RawField fld : fields) {
smali.startLine();
int pad = colWidths[0] - fld.accessFlag.length();
if (pad > 0) {
fld.accessFlag += whites.substring(0, pad);
}
smali.add(".field ").add(fld.accessFlag);
pad = colWidths[1] - fld.name.length();
if (pad > 0) {
fld.name += whites.substring(0, pad);
}
smali.add(fld.name).add(" ");
smali.add(": ").add(fld.type);
if (fld.isStatic) { // static field
if (staticIdx < staticFieldInitValues.size()) {
smali.add(" # init val = ");
writeEncodedValue(smali, staticFieldInitValues.get(staticIdx++), false);
}
}
smali.incIndent();
writeAnnotations(smali, fld.annoList);
smali.decIndent();
}
smali.startLine();
}
private void writeMethod(SmaliWriter smali, MethodNode methodNode, IMethodData mth, LineInfo line) {
if (insnDecoder == null) {
insnDecoder = new SmaliInsnDecoder(methodNode);
}
smali.startLine()
.startLine(mth.isDirect() ? "# direct method" : " # virtual method")
.startLine(".method ");
writeMethodDef(smali, mth, line);
ICodeReader codeReader = mth.getCodeReader();
if (codeReader != null) {
line.smaliMthNode.setParamRegStart(getParamStartRegNum(mth));
line.smaliMthNode.setRegCount(codeReader.getRegistersCount());
Map<Long, InsnNode> nodes = new HashMap<>(codeReader.getInsnsCount() / 2);
line.smaliMthNode.setInsnNodes(nodes, codeReader.getInsnsCount());
line.smaliMthNode.initRegInfoList(codeReader.getRegistersCount(), codeReader.getInsnsCount());
smali.incIndent();
smali.startLine(".registers ")
.add("" + codeReader.getRegistersCount())
.startLine();
writeTries(codeReader, line);
if (formatMthParamInfo(mth, smali, codeReader, line)) {
smali.startLine();
}
smali.startLine();
if (codeReader.getDebugInfo() != null) {
formatDbgInfo(codeReader.getDebugInfo(), line);
}
codeReader.visitInstructions(insn -> {
InsnNode node = decodeInsn(insn, line);
nodes.put((long) insn.getOffset(), node);
});
line.write(smali);
insnMap.put(methodNode.getMethodInfo().getRawFullId(), line.smaliMthNode);
smali.decIndent();
}
smali.startLine(".end method");
}
private void writeTries(ICodeReader codeReader, LineInfo line) {
List<ITry> tries = codeReader.getTries();
for (ITry aTry : tries) {
int end = aTry.getStartAddress() + aTry.getInstructionCount();
String tryEndTip = String.format(FMT_TRY_END_TAG, end);
String tryStartTip = String.format(FMT_TRY_TAG, aTry.getStartAddress());
String tryStartTipExtra = " # :" + tryStartTip.substring(0, tryStartTip.length() - 1);
line.addTip(aTry.getStartAddress(), tryStartTip, " # :" + tryEndTip.substring(0, tryEndTip.length() - 1));
line.addTip(end, tryEndTip, tryStartTipExtra);
ICatch iCatch = aTry.getCatch();
int[] addresses = iCatch.getAddresses();
int addr;
for (int i = 0; i < addresses.length; i++) {
addr = addresses[i];
String catchTip = String.format(FMT_CATCH_TAG, addr);
line.addTip(addr, catchTip, " # " + iCatch.getTypes()[i]);
line.addTip(addr, catchTip, tryStartTipExtra);
line.addTip(aTry.getStartAddress(), tryStartTip, " # :" + catchTip.substring(0, catchTip.length() - 1));
}
addr = iCatch.getCatchAllAddress();
if (addr > -1) {
String catchAllTip = String.format(FMT_CATCH_ALL_TAG, addr);
line.addTip(addr, catchAllTip, tryStartTipExtra);
line.addTip(aTry.getStartAddress(), tryStartTip, " # :" + catchAllTip.substring(0, catchAllTip.length() - 1));
}
}
}
private InsnNode decodeInsn(InsnData insn, LineInfo lineInfo) {
insn.decode();
InsnNode node = insnDecoder.decode(insn);
formatInsn(insn, node, lineInfo);
return node;
}
private void formatInsn(InsnData insn, InsnNode node, LineInfo line) {
line.getLineWriter().delete(0, line.getLineWriter().length());
fmtCols(insn, line);
if (fmtPayloadInsn(insn, line)) {
return;
}
line.getLineWriter()
.append(String.format(FMT_INSN_COL, MNEMONIC.MNEMONICS[getOpenCodeByte(insn)]))
.append(" ");
fmtRegs(insn, node.getType(), line);
if (!tryFormatTargetIns(insn, node.getType(), line)) {
if (hasLiteral(insn)) {
line.getLineWriter().append(", ").append(literal(insn));
} else if (node.getType() == InsnType.INVOKE) {
line.getLineWriter().append(", ").append(method(insn));
} else if (insn.getIndexType() == InsnIndexType.FIELD_REF) {
line.getLineWriter().append(", ").append(field(insn));
} else if (insn.getIndexType() == InsnIndexType.STRING_REF) {
line.getLineWriter().append(", ").append(str(insn));
} else if (insn.getIndexType() == InsnIndexType.TYPE_REF) {
line.getLineWriter().append(", ").append(type(insn));
} else if (insn.getOpcode() == CONST_METHOD_HANDLE) {
line.getLineWriter().append(", ").append(methodHandle(insn));
} else if (insn.getOpcode() == CONST_METHOD_TYPE) {
line.getLineWriter().append(", ").append(proto(insn, insn.getIndex()));
}
}
line.addInsnLine(insn.getOffset(), line.getLineWriter().toString());
}
private boolean tryFormatTargetIns(InsnData insn, InsnType insnType, LineInfo line) {
switch (insnType) {
case IF: {
int target = insn.getTarget();
line.addTip(target, String.format(FMT_COND_TAG, target), "");
line.getLineWriter().append(", ").append(String.format(FMT_COND, target));
return true;
}
case GOTO: {
int target = insn.getTarget();
line.addTip(target, String.format(FMT_GOTO_TAG, target), "");
line.getLineWriter().append(String.format(FMT_GOTO, target));
return true;
}
case FILL_ARRAY: {
int target = insn.getTarget();
line.addTip(target, String.format(FMT_DATA_TAG, target), "");
line.getLineWriter().append(", ").append(String.format(FMT_DATA, target));
return true;
}
case SWITCH: {
int target = insn.getTarget();
if (insn.getOpcode() == Opcode.PACKED_SWITCH) {
line.addTip(target, String.format(FMT_P_SWITCH_TAG, target), "");
line.getLineWriter().append(", ").append(String.format(FMT_P_SWITCH, target));
} else {
line.addTip(target, String.format(FMT_S_SWITCH_TAG, target), "");
line.getLineWriter().append(", ").append(String.format(FMT_S_SWITCH, target));
}
line.addPayloadOffset(insn.getOffset(), target);
return true;
}
}
return false;
}
private static boolean hasStaticFlag(int flag) {
return (flag & AccessFlags.STATIC) != 0;
}
private void writeMethodDef(SmaliWriter smali, IMethodData mth, LineInfo lineInfo) {
smali.add(AccessFlags.format(mth.getAccessFlags(), METHOD));
IMethodRef methodRef = mth.getMethodRef();
methodRef.load();
lineInfo.smaliMthNode.setDefPos(smali.getLength());
smali.add(methodRef.getName())
.add('(');
methodRef.getArgTypes().forEach(smali::add);
smali.add(')');
smali.add(methodRef.getReturnType());
List<IAnnotation> annos = mth.getAnnotations();
if (annos.size() > 0) {
smali.incIndent();
writeAnnotations(smali, annos);
smali.decIndent();
smali.startLine();
}
}
private boolean formatMthParamInfo(IMethodData mth, SmaliWriter smali, ICodeReader codeReader, LineInfo line) {
List<String> types = mth.getMethodRef().getArgTypes();
if (types.size() == 0) {
return false;
}
int paramCount = 0;
int paramStart = 0;
int regNum = line.smaliMthNode.getParamRegStart();
if (!hasStaticFlag(mth.getAccessFlags())) {
line.addRegName(regNum, "p0");
line.smaliMthNode.setParamReg(regNum, "p0");
regNum += 1;
paramStart = 1;
}
IDebugInfo dbgInfo = codeReader.getDebugInfo();
if (dbgInfo != null) {
for (ILocalVar var : dbgInfo.getLocalVars()) {
if (var.getStartOffset() == -1) {
int i = writeParamInfo(smali, line, regNum, paramStart, var.getName(), var.getType());
regNum += i;
paramStart += i;
paramCount++;
}
}
}
for (; paramCount < types.size(); paramCount++) {
int i = writeParamInfo(smali, line, regNum, paramStart, "", types.get(paramCount));
regNum += i;
paramStart += i;
}
return true;
}
private static int writeParamInfo(SmaliWriter smali, LineInfo line,
int regNum, int paramNum, String dbgInfoName, String type) {
smali.startLine(String.format(".param p%d, \"%s\":%s", paramNum, dbgInfoName, type));
String pName = "p" + paramNum;
line.addRegName(regNum, pName);
line.smaliMthNode.setParamReg(regNum, pName);
if (isWideType(type)) {
regNum++;
dbgInfoName = "p" + (paramNum + 1);
line.addRegName(regNum, dbgInfoName);
line.smaliMthNode.setParamReg(regNum, dbgInfoName);
return 2;
}
return 1;
}
private static int getParamStartRegNum(IMethodData mth) {
ICodeReader codeReader = mth.getCodeReader();
if (codeReader != null) {
int startNum = codeReader.getRegistersCount();
if (startNum > 0) {
for (String argType : mth.getMethodRef().getArgTypes()) {
if (isWideType(argType)) {
startNum -= 2;
} else {
startNum -= 1;
}
}
if (!hasStaticFlag(mth.getAccessFlags())) {
startNum--;
}
return startNum;
}
}
return -1;
}
private static boolean isWideType(String type) {
return type.equals("D") || type.equals("J");
}
private void writeAnnotations(SmaliWriter smali, List<IAnnotation> annoList) {
if (annoList.size() > 0) {
for (int i = 0; i < annoList.size(); i++) {
smali.startLine();
writeAnnotation(smali, annoList.get(i));
if (i != annoList.size() - 1) {
smali.startLine();
}
}
}
}
private void writeAnnotation(SmaliWriter smali, IAnnotation anno) {
smali.add(".annotation")
.add(" ");
AnnotationVisibility vby = anno.getVisibility();
if (vby != null) {
smali.add(vby.toString().toLowerCase()).add(" ");
}
smali.add(anno.getAnnotationClass());
anno.getValues().forEach((k, v) -> {
smali.incIndent();
smali.startLine(k).add(" = ");
writeEncodedValue(smali, v, true);
smali.decIndent();
});
smali.startLine(".end annotation");
}
private void formatDbgInfo(IDebugInfo dbgInfo, LineInfo line) {
dbgInfo.getSourceLineMapping().forEach((codeOffset, srcLine) -> {
if (codeOffset > -1) {
line.addDebugLineTip(codeOffset, String.format(".line %d", srcLine), "");
}
});
for (ILocalVar localVar : dbgInfo.getLocalVars()) {
String type = localVar.getSignature();
if (type == null || type.trim().isEmpty()) {
type = localVar.getType();
}
if (localVar.getStartOffset() > -1) {
line.addTip(
localVar.getStartOffset(),
String.format(".local v%d", localVar.getRegNum()),
String.format(", \"%s\":%s", localVar.getName(), type));
}
if (localVar.getEndOffset() > -1) {
line.addTip(
localVar.getEndOffset(),
String.format(".end local v%d", localVar.getRegNum()),
String.format(" # \"%s\":%s", localVar.getName(), type));
}
}
}
private void writeEncodedValue(SmaliWriter smali, EncodedValue value, boolean wrapArray) {
switch (value.getType()) {
case ENCODED_ARRAY:
smali.add("{");
if (wrapArray) {
smali.incIndent();
smali.startLine();
}
List<EncodedValue> values = (List<EncodedValue>) value.getValue();
for (int i = 0; i < values.size(); i++) {
writeEncodedValue(smali, values.get(i), wrapArray);
if (i != values.size() - 1) {
smali.add(",");
if (wrapArray) {
smali.startLine();
} else {
smali.add(" ");
}
}
}
if (wrapArray) {
smali.decIndent();
smali.startLine("}");
}
break;
case ENCODED_NULL:
smali.add("null");
break;
case ENCODED_ANNOTATION:
writeAnnotation(smali, (IAnnotation) value.getValue());
break;
case ENCODED_BYTE:
smali.add(TypeGen.formatByte((Byte) value.getValue(), false));
break;
case ENCODED_SHORT:
smali.add(TypeGen.formatShort((Short) value.getValue(), false));
break;
case ENCODED_CHAR:
smali.add(smali.getClassNode().root().getStringUtils().unescapeChar((Character) value.getValue()));
break;
case ENCODED_INT:
smali.add(TypeGen.formatInteger((Integer) value.getValue(), false));
break;
case ENCODED_LONG:
smali.add(TypeGen.formatLong((Long) value.getValue(), false));
break;
case ENCODED_FLOAT:
smali.add(TypeGen.formatFloat((Float) value.getValue()));
break;
case ENCODED_DOUBLE:
smali.add(TypeGen.formatDouble((Double) value.getValue()));
break;
case ENCODED_STRING:
smali.add(smali.getClassNode().root().getStringUtils().unescapeString((String) value.getValue()));
break;
case ENCODED_TYPE:
smali.add(ArgType.parse((String) value.getValue()) + ".class");
break;
default:
smali.add(String.valueOf(value.getValue()));
}
}
private static final int CODE_OFFSET_COLUMN_WIDTH = 4;
private static final int BYTECODE_COLUMN_WIDTH = 20 + 3; // 3 for ellipses.
private static final String FMT_BYTECODE_COL = "%-" + (BYTECODE_COLUMN_WIDTH - 3) + "s";
private static final int INSN_COL_WIDTH = "const-method-handle".length();
private static final String FMT_INSN_COL = "%-" + INSN_COL_WIDTH + "s";
private static final String FMT_FILE_OFFSET = "%08x:";
private static final String FMT_CODE_OFFSET = "%04x:";
private static final String FMT_TARGET_OFFSET = "%04x";
private static final String FMT_GOTO = ":goto_" + FMT_TARGET_OFFSET;
private static final String FMT_COND = ":cond_" + FMT_TARGET_OFFSET;
private static final String FMT_DATA = ":array_" + FMT_TARGET_OFFSET;
private static final String FMT_P_SWITCH = ":p_switch_" + FMT_TARGET_OFFSET;
private static final String FMT_S_SWITCH = ":s_switch_" + FMT_TARGET_OFFSET;
private static final String FMT_P_SWITCH_CASE = ":p_case_" + FMT_TARGET_OFFSET;
private static final String FMT_S_SWITCH_CASE = ":s_case_" + FMT_TARGET_OFFSET;
private static final String FMT_TRY_TAG = "try_" + FMT_TARGET_OFFSET + ":";
private static final String FMT_TRY_END_TAG = "try_end_" + FMT_TARGET_OFFSET + ":";
private static final String FMT_CATCH_TAG = "catch_" + FMT_TARGET_OFFSET + ":";
private static final String FMT_CATCH_ALL_TAG = "catch_all_" + FMT_TARGET_OFFSET + ":";
private static final String FMT_GOTO_TAG = "goto_" + FMT_TARGET_OFFSET + ":";
private static final String FMT_COND_TAG = "cond_" + FMT_TARGET_OFFSET + ":";
private static final String FMT_DATA_TAG = "array_" + FMT_TARGET_OFFSET + ":";
private static final String FMT_P_SWITCH_TAG = "p_switch_" + FMT_TARGET_OFFSET + ":";
private static final String FMT_S_SWITCH_TAG = "s_switch_" + FMT_TARGET_OFFSET + ":";
private static final String FMT_P_SWITCH_CASE_TAG = "p_case_" + FMT_TARGET_OFFSET + ":";
private static final String FMT_S_SWITCH_CASE_TAG = "s_case_" + FMT_TARGET_OFFSET + ":";
private void fmtRegs(InsnData insn, InsnType insnType, LineInfo line) {
boolean appendBrace = insnType == InsnType.INVOKE || isRegList(insn);
if (appendBrace) {
line.getLineWriter().append("{");
}
if (isRangeRegIns(insn)) {
line.getLineWriter().append(line.getRegName(insn.getReg(0)))
.append(" .. ")
.append(line.getRegName(insn.getReg(insn.getRegsCount() - 1)));
} else if (insn.getRegsCount() > 0) {
for (int i = 0; i < insn.getRegsCount(); i++) {
if (i > 0) {
line.getLineWriter().append(", ");
}
line.getLineWriter().append(line.getRegName(insn.getReg(i)));
}
}
if (appendBrace) {
line.getLineWriter().append("}");
}
}
private int getInsnColStart() {
int start = 0;
if (printFileOffset) {
start += 8 + 1 + 1; // plus 1s for space and the ':'
}
if (printBytecode) {
start += BYTECODE_COLUMN_WIDTH + 1; // plus 1 for space
}
return start;
}
private void fmtCols(InsnData insn, LineInfo line) {
if (printFileOffset) {
line.getLineWriter().append(String.format(FMT_FILE_OFFSET + " ", insn.getFileOffset()));
}
if (printBytecode) {
formatByteCode(line.getLineWriter(), insn.getByteCode());
line.getLineWriter().append(" ");
line.getLineWriter().append(String.format(FMT_CODE_OFFSET + " ", insn.getOffset()));
}
}
private void formatByteCode(StringBuilder smali, byte[] bytes) {
int maxLen = Math.min(bytes.length, 4 * 2); // limit to 4 units
StringBuilder inHex = new StringBuilder();
for (int i = 0; i < maxLen; i++) {
int temp = ((bytes[i++] & 0xff) << 8) | (bytes[i] & 0xff);
inHex.append(String.format("%04x ", temp));
}
smali.append(String.format(FMT_BYTECODE_COL, inHex));
if (maxLen < bytes.length) {
smali.append("...");
} else {
smali.append(" ");
}
}
private boolean fmtPayloadInsn(InsnData insn, LineInfo line) {
Opcode opcode = insn.getOpcode();
if (opcode == PACKED_SWITCH_PAYLOAD) {
line.getLineWriter().append("packed-switch-payload");
line.addInsnLine(insn.getOffset(), line.getLineWriter().toString());
ISwitchPayload payload = (ISwitchPayload) insn.getPayload();
if (payload != null) {
fmtSwitchPayload(insn, FMT_P_SWITCH_CASE, FMT_P_SWITCH_CASE_TAG, line, payload, insn.getOffset());
}
return true;
}
if (opcode == SPARSE_SWITCH_PAYLOAD) {
line.getLineWriter().append("sparse-switch-payload");
line.addInsnLine(insn.getOffset(), line.getLineWriter().toString());
ISwitchPayload payload = (ISwitchPayload) insn.getPayload();
if (payload != null) {
fmtSwitchPayload(insn, FMT_S_SWITCH_CASE, FMT_S_SWITCH_CASE_TAG, line, payload, insn.getOffset());
}
return true;
}
if (opcode == FILL_ARRAY_DATA_PAYLOAD) {
line.getLineWriter().append("fill-array-data-payload");
line.addInsnLine(insn.getOffset(), line.getLineWriter().toString());
return true;
}
return false;
}
private void fmtSwitchPayload(InsnData insn, String fmtTarget, String fmtTag, LineInfo line,
ISwitchPayload payload, int curOffset) {
int lineStart = getInsnColStart();
lineStart += CODE_OFFSET_COLUMN_WIDTH + 1 + 1; // plus 1s for space and the ':'
String basicIndent = new String(new byte[lineStart]).replace("\0", " ");
String indent = SmaliWriter.INDENT_STR + basicIndent;
int[] keys = payload.getKeys();
int[] targets = payload.getTargets();
int opcodeOffset = line.payloadOffsetMap.get(curOffset);
for (int i = 0; i < keys.length; i++) {
int target = opcodeOffset + targets[i];
line.addInsnLine(insn.getOffset(),
String.format("%scase %d: -> " + fmtTarget, indent, keys[i], target));
line.addTip(target,
String.format(fmtTag, target), String.format(" # case %d", keys[i]));
}
line.addInsnLine(insn.getOffset(), basicIndent + ".end payload");
}
private static String literal(InsnData insn) {
long it = insn.getLiteral();
String tip = "";
if (it > Integer.MAX_VALUE) {
if (isWideIns(insn)) {
tip = " # double: " + Double.longBitsToDouble(it);
} else if (getOpenCodeByte(insn) == 0x15) { // CONST_HIGH16 = 0x15;
tip = " # float: " + Float.intBitsToFloat((int) it);
}
} else if (it <= 0) {
return "" + it + tip;
}
return "0x" + Long.toHexString(it) + tip;
}
private static String str(InsnData insn) {
return String.format("\"%s\" # string@%04x",
insn.getIndexAsString()
.replace("\n", "\\n")
.replace("\t", "\\t"),
insn.getIndex());
}
private static String type(InsnData insn) {
return String.format("%s # type@%04x", insn.getIndexAsType(), insn.getIndex());
}
private static String field(InsnData insn) {
return String.format("%s # field@%04x", insn.getIndexAsField().toString(), insn.getIndex());
}
private static String method(InsnData insn) {
Opcode op = insn.getOpcode();
if (op == INVOKE_CUSTOM || op == INVOKE_CUSTOM_RANGE) {
insn.getIndexAsCallSite().load();
return String.format("%s # call_site@%04x", insn.getIndexAsCallSite().toString(), insn.getIndex());
}
IMethodRef mthRef = insn.getIndexAsMethod();
mthRef.load();
if (op == INVOKE_POLYMORPHIC || op == INVOKE_POLYMORPHIC_RANGE) {
return String.format("%s, %s # method@%04x, proto@%04x",
mthRef.toString(), insn.getIndexAsProto(insn.getTarget()).toString(),
insn.getIndex(), insn.getTarget());
}
return String.format("%s # method@%04x", mthRef.toString(), insn.getIndex());
}
private static String proto(InsnData insn, int protoIndex) {
return String.format("%s # proto@%04x", insn.getIndexAsProto(protoIndex).toString(), protoIndex);
}
private static String methodHandle(InsnData insn) {
return String.format("%s # method_handle@%04x",
insn.getIndexAsMethodHandle().toString(), insn.getIndex());
}
protected static boolean isRangeRegIns(InsnData insn) {
switch (insn.getOpcode()) {
case INVOKE_VIRTUAL_RANGE:
case INVOKE_SUPER_RANGE:
case INVOKE_DIRECT_RANGE:
case INVOKE_STATIC_RANGE:
case INVOKE_INTERFACE_RANGE:
case FILLED_NEW_ARRAY_RANGE:
case INVOKE_CUSTOM_RANGE:
case INVOKE_POLYMORPHIC_RANGE:
return true;
}
return false;
}
private static int getOpenCodeByte(InsnData insn) {
return insn.getRawOpcodeUnit() & 0xff;
}
private static boolean isWideIns(InsnData insn) {
return insn.getOpcode() == CONST_WIDE;
}
private static boolean hasLiteral(InsnData insn) {
int opcode = getOpenCodeByte(insn);
return insn.getOpcode() == CONST
|| insn.getOpcode() == CONST_WIDE
|| (opcode >= 0xd0 && opcode <= 0xe2); // add-int/lit16 to ushr-int/lit8
}
private static boolean isRegList(InsnData insn) {
return insn.getOpcode() == FILLED_NEW_ARRAY || insn.getOpcode() == FILLED_NEW_ARRAY_RANGE;
}
private class LineInfo {
private SmaliMethodNode smaliMthNode = new SmaliMethodNode();
private final StringBuilder lineWriter = new StringBuilder(50);
private String lastDebugTip = "";
private final Map<Integer, List<String>> insnOffsetMap = new LinkedHashMap<>();
private final Map<Integer, String> regNameMap = new HashMap<>();
private Map<Integer, Map<String, Object>> tipMap = Collections.emptyMap();
private Map<Integer, Integer> payloadOffsetMap = Collections.emptyMap();
public LineInfo() {
}
public StringBuilder getLineWriter() {
return lineWriter;
}
public void reset() {
lastDebugTip = "";
payloadOffsetMap = Collections.emptyMap();
tipMap = Collections.emptyMap();
insnOffsetMap.clear();
regNameMap.clear();
smaliMthNode = new SmaliMethodNode();
}
public void addRegName(int regNum, String name) {
regNameMap.put(regNum, name);
}
public String getRegName(int regNum) {
String name = regNameMap.get(regNum);
if (name == null) {
name = "v" + regNum;
}
return name;
}
public void addInsnLine(int codeOffset, String insnLine) {
List<String> insnList = insnOffsetMap.computeIfAbsent(codeOffset, k -> new ArrayList<>(1));
insnList.add(insnLine);
}
public void addTip(int offset, String tip, String extra) {
if (tipMap.isEmpty()) {
tipMap = new LinkedHashMap<>();
}
Map<String, Object> innerMap = tipMap.computeIfAbsent(offset, k -> new LinkedHashMap<>());
Object obj = innerMap.get(tip);
if (obj != null) {
if (obj instanceof String) {
if (obj.equals("")) {
innerMap.put(tip, 2);
} else {
List<String> extras = new ArrayList<>(2);
extras.add((String) obj);
extras.add(extra);
innerMap.put(tip, extras);
}
} else if (obj instanceof Integer) {
innerMap.put(tip, ((int) obj) + 1);
} else if (obj instanceof List) {
if (!extra.equals("")) {
List<String> extras = (List<String>) obj;
extras.add(extra);
}
}
} else {
innerMap.put(tip, extra);
}
}
public void addDebugLineTip(int offset, String tip, String extra) {
if (tip.equals(lastDebugTip)) {
return;
}
lastDebugTip = tip;
if (tipMap.isEmpty()) {
tipMap = new LinkedHashMap<>();
}
Map<String, Object> innerMap = tipMap.computeIfAbsent(offset, k -> new LinkedHashMap<>());
innerMap.put(tip, extra);
}
public void addPayloadOffset(int curOffset, int payloadOffset) {
if (payloadOffsetMap.isEmpty()) {
payloadOffsetMap = new HashMap<>();
}
payloadOffsetMap.put(payloadOffset, curOffset);
}
public void write(SmaliWriter smali) {
int lineOffset = getInsnColStart();
for (Entry<Integer, List<String>> entry : insnOffsetMap.entrySet()) {
writeTip(smali, entry.getKey(), lineOffset);
smaliMthNode.setInsnInfo(entry.getKey(), lineOffset + smali.getLength());
smaliMthNode.attachLine(smali.getLine(), entry.getKey());
smali.attachSourceLine(entry.getKey());
for (String s : entry.getValue()) {
smali.add(s).startLine();
}
}
}
private void writeTip(SmaliWriter smali, int codeOffset, int lineOffset) {
Map<String, Object> tip = tipMap.get(codeOffset);
if (tip != null) {
for (Entry<String, Object> entry : tip.entrySet()) {
int start = Math.max(0, lineOffset - entry.getKey().length());
if (start > 0) {
smali.add(new String(new byte[start]).replace("\0", " "));
}
if (entry.getValue() instanceof Integer) {
smali.add(String.format("%s # %d refs", entry.getKey(), entry.getValue()))
.startLine();
} else if (entry.getValue() instanceof String) {
smali.add(String.format("%s%s", entry.getKey(), entry.getValue()))
.startLine();
} else if (entry.getValue() instanceof List) {
List<String> extras = (List<String>) entry.getValue();
smali.add(String.format("%s%s", entry.getKey(), extras.get(0)))
.startLine();
String pad = new String(new byte[lineOffset]).replace("\0", " ");
for (int i = 1; i < extras.size(); i++) {
smali.add(String.format("%s%s", pad, extras.get(i)))
.startLine();
}
} else {
smali.add(String.format("%s%s", entry.getKey(), entry.getValue()))
.startLine();
}
}
}
}
}
private static class SmaliInsnDecoder extends InsnDecoder {
@Override
protected @NotNull InsnNode decode(InsnData insn) {
try {
return super.decode(insn);
} catch (Exception e) {
switch (insn.getOpcode()) {
case INVOKE_CUSTOM:
case INVOKE_CUSTOM_RANGE:
case INVOKE_POLYMORPHIC:
case INVOKE_POLYMORPHIC_RANGE:
case CONST_METHOD_HANDLE:
case CONST_METHOD_TYPE:
return new InsnNode(InsnType.INVOKE, insn.getRegsCount());
default:
throw new RuntimeException(e);
}
}
}
public SmaliInsnDecoder(MethodNode mthNode) {
super(mthNode);
}
@Override
public InsnNode[] process(ICodeReader codeReader) {
return null;
}
}
private static class RawField {
boolean isStatic;
String accessFlag;
String name;
String type;
List<IAnnotation> annoList;
private static RawField make(IFieldData f) {
RawField field = new RawField();
field.isStatic = hasStaticFlag(f.getAccessFlags());
field.accessFlag = AccessFlags.format(f.getAccessFlags(), FIELD);
field.name = f.getName();
field.type = f.getType();
field.annoList = f.getAnnotations();
return field;
}
}
}
@@ -0,0 +1,104 @@
package jadx.gui.device.debugger.smali;
import java.util.*;
import jadx.core.dex.instructions.args.InsnArg;
import jadx.core.dex.instructions.args.RegisterArg;
import jadx.core.dex.nodes.InsnNode;
class SmaliMethodNode {
private Map<Long, InsnNode> nodes; // codeOffset: InsnNode
private List<SmaliRegister> regList;
private int[] insnPos;
private int defPos;
private Map<Integer, Integer> lineMapping = Collections.emptyMap(); // line: codeOffset
private int paramRegStart;
private int regCount;
public int getParamRegStart() {
return this.paramRegStart;
}
public int getRegCount() {
return this.regCount;
}
public Map<Integer, Integer> getLineMapping() {
return lineMapping;
}
public void initRegInfoList(int regCount, int insnCount) {
regList = new ArrayList<>(regCount);
for (int i = 0; i < regCount; i++) {
regList.add(new SmaliRegister(i, insnCount));
}
}
public int getInsnPos(long codeOffset) {
if (insnPos != null && codeOffset < insnPos.length) {
return insnPos[(int) codeOffset];
}
return -1;
}
public int getDefPos() {
return defPos;
}
public InsnNode getInsnNode(long codeOffset) {
return nodes.get(codeOffset);
}
public List<SmaliRegister> getRegList() {
return regList;
}
protected SmaliMethodNode() {
}
protected void setRegCount(int regCount) {
this.regCount = regCount;
}
protected void attachLine(int line, int codeOffset) {
if (lineMapping.isEmpty()) {
lineMapping = new HashMap<>();
}
lineMapping.put(line, codeOffset);
}
protected void setInsnInfo(int codeOffset, int pos) {
if (insnPos != null && codeOffset < insnPos.length) {
insnPos[codeOffset] = pos;
}
InsnNode insn = getInsnNode(codeOffset);
RegisterArg r = insn.getResult();
if (r != null) {
regList.get(r.getRegNum()).setStartOffset(codeOffset);
}
for (InsnArg arg : insn.getArguments()) {
if (arg instanceof RegisterArg) {
regList.get(((RegisterArg) arg).getRegNum()).setStartOffset(codeOffset);
}
}
}
protected void setDefPos(int pos) {
defPos = pos;
}
protected void setParamReg(int regNum, String name) {
SmaliRegister r = regList.get(regNum);
r.setParam(name);
r.setStartOffset(-1);
}
protected void setParamRegStart(int paramRegStart) {
this.paramRegStart = paramRegStart;
}
protected void setInsnNodes(Map<Long, InsnNode> nodes, int insnCount) {
this.nodes = nodes;
insnPos = new int[insnCount];
}
}
@@ -0,0 +1,74 @@
package jadx.gui.device.debugger.smali;
public class SmaliRegister extends RegisterInfo {
private final int num;
private String paramName;
private final int endOffset;
private int startOffset;
private boolean isParam;
private int runtimeNum;
public SmaliRegister(int num, int insnCount) {
this.num = num;
this.endOffset = insnCount;
this.startOffset = insnCount;
}
public int getRuntimeRegNum() {
return runtimeNum;
}
public void setRuntimeRegNum(int runtimeNum) {
this.runtimeNum = runtimeNum;
}
@Override
public boolean isInitialized(long codeOffset) {
return codeOffset > getStartOffset() && codeOffset < getEndOffset();
}
protected void setParam(String name) {
paramName = name;
isParam = true;
}
protected void setStartOffset(int off) {
if (startOffset == -1 && !isParam) {
startOffset = off;
return;
}
if (off < startOffset) {
startOffset = off;
}
}
@Override
public String getName() {
return paramName != null ? paramName : "v" + num;
}
@Override
public int getRegNum() {
return num;
}
@Override
public String getType() {
return "";
}
@Override
public String getSignature() {
return null;
}
@Override
public int getStartOffset() {
return startOffset;
}
@Override
public int getEndOffset() {
return endOffset;
}
}
@@ -0,0 +1,54 @@
package jadx.gui.device.debugger.smali;
import java.util.Collections;
import java.util.Map;
import jadx.api.CodePosition;
import jadx.api.ICodeInfo;
import jadx.api.impl.SimpleCodeWriter;
import jadx.core.dex.nodes.ClassNode;
public class SmaliWriter extends SimpleCodeWriter {
private int line = 0;
private final ClassNode cls;
public SmaliWriter(ClassNode cls) {
this.cls = cls;
}
public ClassNode getClassNode() {
return cls;
}
@Override
protected void addLine() {
super.addLine();
line++;
}
@Override
public int getLine() {
return line;
}
@Override
public ICodeInfo finish() {
return new ICodeInfo() {
@Override
public String getCodeStr() {
return buf.toString();
}
@Override
public Map<Integer, Integer> getLineMapping() {
return Collections.emptyMap();
}
@Override
public Map<CodePosition, Object> getAnnotations() {
return Collections.emptyMap();
}
};
}
}
@@ -0,0 +1,632 @@
package jadx.gui.device.protocol;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
import java.util.*;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import io.reactivex.annotations.NonNull;
import jadx.core.utils.StringUtils;
public class ADB {
private static final int DEFAULT_PORT = 5037;
private static final String DEFAULT_ADDR = "localhost";
private static final String CMD_FEATURES = "000dhost:features";
private static final String CMD_TRACK_JDWP = "000atrack-jdwp";
private static final String CMD_TRACK_DEVICES = "0014host:track-devices-l";
private static final byte[] OKAY = "OKAY".getBytes();
private static boolean isOkay(InputStream stream) throws IOException {
byte[] buf = new byte[4];
stream.read(buf, 0, 4);
return Arrays.equals(buf, OKAY);
}
public static byte[] exec(String cmd, OutputStream outputStream, InputStream inputStream) throws IOException {
return execCommandSync(outputStream, inputStream, cmd);
}
public static byte[] exec(String cmd) throws IOException {
byte[] res;
Socket socket = connect();
res = exec(cmd, socket.getOutputStream(), socket.getInputStream());
socket.close();
return res;
}
public static Socket connect() throws IOException {
return connect(DEFAULT_ADDR, DEFAULT_PORT);
}
public static Socket connect(String host, int port) throws IOException {
return new Socket(host, port);
}
private static boolean execCommandAsync(OutputStream outputStream,
InputStream inputStream, String cmd) throws IOException {
outputStream.write(cmd.getBytes());
return isOkay(inputStream);
}
private static byte[] execCommandSync(OutputStream outputStream,
InputStream inputStream, String cmd) throws IOException {
outputStream.write(cmd.getBytes());
if (isOkay(inputStream)) {
return readServiceProtocol(inputStream);
}
return null;
}
private static byte[] readServiceProtocol(InputStream stream) {
byte[] bytes = null;
byte[] buf = new byte[4];
try {
int len = stream.read(buf, 0, 4);
if (len == 4) {
len = unhex(buf);
if (len == 0) {
return new byte[0];
}
if (len != -1) {
buf = new byte[len];
if (stream.read(buf, 0, len) == len) {
bytes = buf;
}
}
}
} catch (IOException ignore) {
}
return bytes;
}
private static boolean setSerial(String serial, OutputStream outputStream, InputStream inputStream) throws IOException {
String setSerialCmd = String.format("host:tport:serial:%s", serial);
setSerialCmd = String.format("%04x%s", setSerialCmd.length(), setSerialCmd);
outputStream.write(setSerialCmd.getBytes());
boolean ok = isOkay(inputStream);
if (ok) {
// skip the shell-state-id returned by ADB server, it's not important for the following actions.
ok = inputStream.skip(8) == 8;
}
return ok;
}
private static byte[] execShellCommandRaw(String cmd,
OutputStream outputStream, InputStream inputStream) throws IOException {
cmd = String.format("shell,v2,TERM=xterm-256color,raw:%s", cmd);
cmd = String.format("%04x%s", cmd.length(), cmd);
outputStream.write(cmd.getBytes());
if (isOkay(inputStream)) {
return ShellProtocol.readStdout(inputStream);
}
return null;
}
private static byte[] execShellCommandRaw(String serial, String cmd,
OutputStream outputStream, InputStream inputStream) throws IOException {
if (setSerial(serial, outputStream, inputStream)) {
return execShellCommandRaw(cmd, outputStream, inputStream);
}
return null;
}
public static List<String> getFeatures() throws IOException {
byte[] rst = exec(CMD_FEATURES);
if (rst != null) {
return Arrays.asList(new String(rst).trim().split(","));
}
return Collections.emptyList();
}
public static boolean startServer(String adbPath, int port) throws IOException {
String tcpPort = String.format("tcp:%d", port);
java.lang.Process proc = new ProcessBuilder(adbPath, "-L", tcpPort, "start-server")
.redirectErrorStream(true)
.start();
try {
proc.waitFor(3, TimeUnit.SECONDS); // for listening to a port, 3 sec should be more than enough.
proc.exitValue();
} catch (InterruptedException e) {
e.printStackTrace();
proc.destroyForcibly();
return false;
}
InputStream is = proc.getInputStream();
int size = is.available();
byte[] bytes = new byte[size];
is.read(bytes, 0, size);
return new String(bytes).contains(tcpPort);
}
public static boolean isServerRunning(String host, int port) {
try {
Socket sock = new Socket(host, port);
sock.close();
return true;
} catch (IOException e) {
e.printStackTrace();
}
return false;
}
/**
* @return a socket connected to adb server, otherwise null
*/
public static Socket listenForDeviceState(DeviceStateListener listener, String host, int port) throws IOException {
Socket socket = connect(host, port);
InputStream inputStream = socket.getInputStream();
OutputStream outputStream = socket.getOutputStream();
if (!execCommandAsync(outputStream, inputStream, CMD_TRACK_DEVICES)) {
socket.close();
return null;
}
ExecutorService listenThread = Executors.newFixedThreadPool(1);
listenThread.execute(() -> {
for (;;) {
byte[] res = readServiceProtocol(inputStream);
if (res != null) {
if (listener != null) {
String payload = new String(res);
String[] deviceLines = payload.split("\n");
List<DeviceInfo> deviceInfoList = new ArrayList<>(deviceLines.length);
for (String deviceLine : deviceLines) {
if (!deviceLine.trim().isEmpty()) {
deviceInfoList.add(DeviceInfo.make(deviceLine, host, port));
}
}
listener.onDeviceStatusChange(deviceInfoList);
}
} else { // socket disconnected
break;
}
}
if (listener != null) {
listener.adbDisconnected();
}
});
return socket;
}
public static List<String> listForward(String host, int port) throws IOException {
Socket socket = connect(host, port);
String cmd = "0011host:list-forward";
InputStream inputStream = socket.getInputStream();
OutputStream outputStream = socket.getOutputStream();
outputStream.write(cmd.getBytes());
if (isOkay(inputStream)) {
byte[] bytes = readServiceProtocol(inputStream);
if (bytes != null) {
String[] forwards = new String(bytes).split("\n");
List<String> forwardList = new ArrayList<>(forwards.length);
for (String forward : forwards) {
forwardList.add(forward.trim());
}
socket.close();
return forwardList;
}
}
socket.close();
return Collections.emptyList();
}
public static boolean removeForward(String host, int port, String serial, String localPort) throws IOException {
Socket socket = connect(host, port);
String cmd = String.format("host:killforward:tcp:%s", localPort);
cmd = String.format("%04x%s", cmd.length(), cmd);
InputStream inputStream = socket.getInputStream();
OutputStream outputStream = socket.getOutputStream();
boolean ok = false;
if (setSerial(serial, outputStream, inputStream)) {
outputStream.write(cmd.getBytes());
ok = isOkay(inputStream) && isOkay(inputStream);
}
socket.close();
return ok;
}
// Little endian
private static int readInt(byte[] bytes, int start) {
int result = 0;
result = (bytes[start] & 0xff);
result += ((bytes[start + 1] & 0xff) << 8);
result += ((bytes[start + 2] & 0xff) << 16);
result += (bytes[start + 3] & 0xff) << 24;
return result;
}
private static byte[] appendBytes(byte[] dest, byte[] src, int realSrcSize) {
byte[] rst = new byte[dest.length + realSrcSize];
System.arraycopy(dest, 0, rst, 0, dest.length);
System.arraycopy(src, 0, rst, dest.length, realSrcSize);
return rst;
}
private static int unhex(byte[] hex) {
int n = 0;
byte b;
for (int i = 0; i < 4; i++) {
b = hex[i];
switch (b) {
case '0':
case '1':
case '2':
case '3':
case '4':
case '5':
case '6':
case '7':
case '8':
case '9':
b -= '0';
break;
case 'a':
case 'b':
case 'c':
case 'd':
case 'e':
case 'f':
b = (byte) (b - 'a' + 10);
break;
case 'A':
case 'B':
case 'C':
case 'D':
case 'E':
case 'F':
b = (byte) (b - 'A' + 10);
break;
default:
return -1;
}
n = (n << 4) | (b & 0xff);
}
return n;
}
public interface JDWPProcessListener {
void jdwpProcessOccurred(Device device, Set<String> id);
void jdwpListenerClosed(Device device);
}
public interface DeviceStateListener {
void onDeviceStatusChange(List<DeviceInfo> deviceInfoList);
void adbDisconnected();
}
public static class Device {
DeviceInfo info;
String androidReleaseVer;
volatile Socket jdwpListenerSock;
public Device(DeviceInfo info) {
this.info = info;
}
public DeviceInfo getDeviceInfo() {
return info;
}
public boolean updateDeviceInfo(DeviceInfo info) {
boolean matched = this.info.serial.equals(info.serial);
if (matched) {
this.info = info;
}
return matched;
}
public String getSerial() {
return info.serial;
}
public boolean removeForward(String localPort) throws IOException {
return ADB.removeForward(info.adbHost, info.adbPort, info.serial, localPort);
}
public ForwardResult forwardJDWP(String localPort, String jdwpPid) throws IOException {
Socket socket = connect(info.adbHost, info.adbPort);
String cmd = String.format("host:forward:tcp:%s;jdwp:%s", localPort, jdwpPid);
cmd = String.format("%04x%s", cmd.length(), cmd);
InputStream inputStream = socket.getInputStream();
OutputStream outputStream = socket.getOutputStream();
ForwardResult rst;
if (setSerial(info.serial, outputStream, inputStream)) {
outputStream.write(cmd.getBytes());
if (!isOkay(inputStream)) {
rst = new ForwardResult(1, readServiceProtocol(inputStream));
} else if (!isOkay(inputStream)) {
rst = new ForwardResult(2, readServiceProtocol(inputStream));
} else {
rst = new ForwardResult(0, null);
}
} else {
rst = new ForwardResult(1, "Unknown error.".getBytes());
}
socket.close();
return rst;
}
public static class ForwardResult {
/**
* 0 for success, 1 for failed at binding to local tcp, 2 for failed at remote.
*/
public int state;
public String desc;
public ForwardResult(int state, byte[] desc) {
if (desc != null) {
this.desc = new String(desc);
} else {
this.desc = "";
}
this.state = state;
}
}
/**
* @return pid otherwise -1
*/
public int launchApp(String fullAppName) throws IOException, InterruptedException {
Socket socket = connect(info.adbHost, info.adbPort);
String cmd = "am start -D -n " + fullAppName;
byte[] res = execShellCommandRaw(info.serial, cmd, socket.getOutputStream(), socket.getInputStream());
socket.close();
String rst = new String(res).trim();
if (rst.startsWith("Starting: Intent {") && rst.endsWith(fullAppName + " }")) {
Thread.sleep(40);
String pkg = fullAppName.split("/")[0];
for (Process process : getProcessByPkg(pkg)) {
return Integer.parseInt(process.pid);
}
}
return -1;
}
public String getAndroidReleaseVersion() {
if (!StringUtils.isEmpty(androidReleaseVer)) {
return androidReleaseVer;
}
try {
List<String> list = getProp("ro.build.version.release");
if (list.size() != 0) {
androidReleaseVer = list.get(0);
}
} catch (IOException e) {
e.printStackTrace();
androidReleaseVer = "";
}
return androidReleaseVer;
}
public List<String> getProp(String entry) throws IOException {
Socket socket = connect(info.adbHost, info.adbPort);
List<String> props = Collections.emptyList();
String cmd = "getprop";
if (!StringUtils.isEmpty(entry)) {
cmd += " " + entry;
}
byte[] payload = execShellCommandRaw(info.serial, cmd,
socket.getOutputStream(), socket.getInputStream());
if (payload != null) {
props = new ArrayList<>();
String[] lines = new String(payload).split("\n");
for (String line : lines) {
line = line.trim();
if (!line.isEmpty()) {
props.add(line.trim());
}
}
}
socket.close();
return props;
}
public List<Process> getProcessByPkg(String pkg) throws IOException {
return getProcessList("ps | grep " + pkg, 0);
}
@NonNull
public List<Process> getProcessList() throws IOException {
return getProcessList("ps", 1);
}
private List<Process> getProcessList(String cmd, int index) throws IOException {
Socket socket = connect(info.adbHost, info.adbPort);
List<Process> procs = Collections.emptyList();
byte[] payload = execShellCommandRaw(info.serial, cmd,
socket.getOutputStream(), socket.getInputStream());
if (payload != null) {
String ps = new String(payload);
String[] psLines = ps.split("\n");
for (int i = index; i < psLines.length; i++) {
Process proc = Process.make(psLines[i]);
if (proc != null) {
if (procs.isEmpty()) {
procs = new ArrayList<>();
}
procs.add(proc);
}
}
}
socket.close();
return procs;
}
public boolean listenForJDWP(JDWPProcessListener listener) throws IOException {
if (this.jdwpListenerSock != null) {
return false;
}
jdwpListenerSock = connect(this.info.adbHost, this.info.adbPort);
InputStream inputStream = jdwpListenerSock.getInputStream();
OutputStream outputStream = jdwpListenerSock.getOutputStream();
if (setSerial(info.serial, outputStream, inputStream)
&& execCommandAsync(outputStream, inputStream, CMD_TRACK_JDWP)) {
Executors.newFixedThreadPool(1).execute(() -> {
for (;;) {
byte[] res = readServiceProtocol(inputStream);
if (res != null) {
if (listener != null) {
String payload = new String(res);
String[] ids = payload.split("\n");
Set<String> idList = new HashSet<>(ids.length);
for (String id : ids) {
if (!id.trim().isEmpty()) {
idList.add(id);
}
}
listener.jdwpProcessOccurred(this, idList);
}
} else { // socket disconnected
break;
}
}
if (listener != null) {
this.jdwpListenerSock = null;
listener.jdwpListenerClosed(this);
}
});
} else {
jdwpListenerSock.close();
jdwpListenerSock = null;
return false;
}
return true;
}
public void stopListenForJDWP() {
if (jdwpListenerSock != null) {
try {
jdwpListenerSock.close();
} catch (IOException e) {
e.printStackTrace();
}
}
this.jdwpListenerSock = null;
}
@Override
public int hashCode() {
return info.serial.hashCode();
}
@Override
public boolean equals(Object obj) {
if (obj instanceof Device) {
return ((Device) obj).getDeviceInfo().serial.equals(info.serial);
}
return false;
}
@Override
public String toString() {
return info.allInfo;
}
}
public static class DeviceInfo {
public String adbHost;
public int adbPort;
public String serial;
public String state;
public String model;
public String allInfo;
public boolean isOnline() {
return state.equals("device");
}
@Override
public String toString() {
return allInfo;
}
static DeviceInfo make(String info, String host, int port) {
DeviceInfo deviceInfo = new DeviceInfo();
String[] infoFields = info.trim().split("\\s+");
deviceInfo.allInfo = String.join(" ", infoFields);
if (infoFields.length > 2) {
deviceInfo.serial = infoFields[0];
deviceInfo.state = infoFields[1];
}
int pos = info.indexOf("model:");
if (pos != -1) {
int spacePos = info.indexOf(" ", pos);
if (spacePos != -1) {
deviceInfo.model = info.substring(pos + "model:".length(), spacePos);
}
}
if (deviceInfo.model == null || deviceInfo.model.equals("")) {
deviceInfo.model = deviceInfo.serial;
}
deviceInfo.adbHost = host;
deviceInfo.adbPort = port;
return deviceInfo;
}
}
public static class Process {
public String user;
public String pid;
public String ppid;
public String name;
public static Process make(String processLine) {
String[] fields = processLine.split("\\s+");
if (fields.length >= 4) {
// 0 for user, 1 for pid, 2 for ppid, the last one for name
Process proc = new Process();
proc.user = fields[0];
proc.pid = fields[1];
proc.ppid = fields[2];
proc.name = fields[fields.length - 1];
return proc;
}
return null;
}
}
private static class ShellProtocol {
private static final int ID_STD_IN = 0;
private static final int ID_STD_OUT = 1;
private static final int ID_STD_ERR = 2;
private static final int ID_EXIT = 3;
// Close subprocess stdin if possible.
private static final int ID_CLOSE_STDIN = 4;
// Window size change (an ASCII version of struct winsize).
private static final int ID_WINDOW_SIZE_CHANGE = 5;
// Indicates an invalid or unknown packet.
private static final int ID_INVALID = 255;
public static byte[] readStdout(InputStream inputStream) throws IOException {
byte[] header = new byte[5];
byte[] payload = new byte[0];
byte[] tempBuf = new byte[0];
for (boolean exit = false; !exit;) {
if (inputStream.read(header, 0, 5) == 5) {
exit = header[0] == ID_EXIT;
int payloadSize = readInt(header, 1);
if (tempBuf.length < payloadSize) {
tempBuf = new byte[payloadSize];
}
int readSize = inputStream.read(tempBuf, 0, payloadSize);
if (readSize != payloadSize) {
return null; // we don't want corrupted data.
}
payload = appendBytes(payload, tempBuf, readSize);
}
}
return payload;
}
}
}
@@ -71,6 +71,14 @@ public class JadxSettings extends JadxCLIArgs {
private boolean keepCommonDialogOpen = false;
private boolean smaliAreaShowBytecode = false;
private int mainWindowVerticalSplitterLoc = 300;
private int debuggerStackFrameSplitterLoc = 300;
private int debuggerVarTreeSplitterLoc = 700;
private String adbDialogPath = "";
private String adbDialogHost = "localhost";
private String adbDialogPort = "5037";
/**
* UI setting: the width of the tree showing the classes, resources, ...
*/
@@ -464,6 +472,57 @@ public class JadxSettings extends JadxCLIArgs {
return smaliAreaShowBytecode;
}
public void setMainWindowVerticalSplitterLoc(int location) {
mainWindowVerticalSplitterLoc = location;
partialSync(settings -> settings.mainWindowVerticalSplitterLoc = location);
}
public int getMainWindowVerticalSplitterLoc() {
return mainWindowVerticalSplitterLoc;
}
public void setDebuggerStackFrameSplitterLoc(int location) {
debuggerStackFrameSplitterLoc = location;
partialSync(settings -> settings.debuggerStackFrameSplitterLoc = location);
}
public int getDebuggerStackFrameSplitterLoc() {
return debuggerStackFrameSplitterLoc;
}
public void setDebuggerVarTreeSplitterLoc(int location) {
debuggerVarTreeSplitterLoc = location;
partialSync(settings -> debuggerVarTreeSplitterLoc = location);
}
public int getDebuggerVarTreeSplitterLoc() {
return debuggerVarTreeSplitterLoc;
}
public String getAdbDialogPath() {
return adbDialogPath;
}
public void setAdbDialogPath(String path) {
this.adbDialogPath = path;
}
public String getAdbDialogHost() {
return adbDialogHost;
}
public void setAdbDialogHost(String host) {
this.adbDialogHost = host;
}
public String getAdbDialogPort() {
return adbDialogPort;
}
public void setAdbDialogPort(String port) {
this.adbDialogPort = port;
}
private void upgradeSettings(int fromVersion) {
LOG.debug("upgrade settings from version: {} to {}", fromVersion, CURRENT_SETTINGS_VERSION);
if (fromVersion == 0) {
@@ -107,10 +107,6 @@ public class JClass extends JLoadableNode {
return cls.getSmali();
}
public String getSmaliV2() {
return cls.getClassNode().getSmaliV2();
}
@Override
public String getSyntaxName() {
return SyntaxConstants.SYNTAX_STYLE_JAVA;
@@ -0,0 +1,680 @@
package jadx.gui.ui;
import java.awt.*;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.io.File;
import java.io.IOException;
import java.net.Socket;
import java.util.*;
import java.util.List;
import javax.swing.*;
import javax.swing.tree.*;
import jadx.core.utils.StringUtils;
import jadx.core.utils.exceptions.JadxRuntimeException;
import jadx.gui.device.debugger.DbgUtils;
import jadx.gui.device.protocol.ADB;
import jadx.gui.treemodel.JClass;
import jadx.gui.utils.NLS;
import jadx.gui.utils.UiUtils;
import static jadx.gui.device.protocol.ADB.Device.*;
public class ADBDialog extends JDialog implements ADB.DeviceStateListener, ADB.JDWPProcessListener {
private static final long serialVersionUID = -1111111202102181630L;
private static final ImageIcon ICON_DEVICE = UiUtils.openIcon("device");
private static final ImageIcon ICON_PROCESS = UiUtils.openIcon("process");
private static DebugSetting debugSetter = null;
private final transient MainWindow mainWindow;
private transient Label tipLabel;
private transient JTextField pathTextField;
private transient JTextField hostTextField;
private transient JTextField portTextField;
private transient DefaultTreeModel procTreeModel;
private transient DefaultMutableTreeNode procTreeRoot;
private transient JTree procTree;
private Socket deviceSocket;
private transient List<DeviceNode> deviceNodes = new ArrayList<>();
public ADBDialog(MainWindow mainWindow) {
super(mainWindow);
this.mainWindow = mainWindow;
if (debugSetter == null) {
debugSetter = new DebugSetting();
}
initUI();
pathTextField.setText(mainWindow.getSettings().getAdbDialogPath());
hostTextField.setText(mainWindow.getSettings().getAdbDialogHost());
portTextField.setText(mainWindow.getSettings().getAdbDialogPort());
if (pathTextField.getText().equals("")) {
detectADBPath();
} else {
pathTextField.setText("");
}
SwingUtilities.invokeLater(this::connectToADB);
UiUtils.addEscapeShortCutToDispose(this);
}
private void initUI() {
pathTextField = new JTextField();
portTextField = new JTextField();
hostTextField = new JTextField();
JPanel adbPanel = new JPanel(new BorderLayout(5, 5));
adbPanel.add(new JLabel(NLS.str("adb_dialog.path")), BorderLayout.WEST);
adbPanel.add(pathTextField, BorderLayout.CENTER);
JPanel portPanel = new JPanel(new BorderLayout(5, 0));
portPanel.add(new JLabel(NLS.str("adb_dialog.port")), BorderLayout.WEST);
portPanel.add(portTextField, BorderLayout.CENTER);
JPanel hostPanel = new JPanel(new BorderLayout(5, 0));
hostPanel.add(new JLabel(NLS.str("adb_dialog.addr")), BorderLayout.WEST);
hostPanel.add(hostTextField, BorderLayout.CENTER);
JPanel wrapperPanel = new JPanel(new GridLayout(1, 2, 5, 0));
wrapperPanel.add(hostPanel);
wrapperPanel.add(portPanel);
adbPanel.add(wrapperPanel, BorderLayout.SOUTH);
procTree = new JTree();
JScrollPane scrollPane = new JScrollPane(procTree);
scrollPane.setMinimumSize(new Dimension(100, 150));
scrollPane.setBorder(BorderFactory.createLineBorder(Color.black));
procTree.getSelectionModel().setSelectionMode(TreeSelectionModel.SINGLE_TREE_SELECTION);
procTreeRoot = new DefaultMutableTreeNode(NLS.str("adb_dialog.device_node"));
procTreeModel = new DefaultTreeModel(procTreeRoot);
procTree.setModel(procTreeModel);
procTree.setRowHeight(-1);
Font font = mainWindow.getSettings().getFont();
procTree.setFont(font.deriveFont(font.getSize() + 1.f));
procTree.addMouseListener(new MouseAdapter() {
@Override
public void mouseClicked(MouseEvent e) {
if (e.getClickCount() == 2) {
processSelected(e);
}
}
});
procTree.setCellRenderer(new DefaultTreeCellRenderer() {
private static final long serialVersionUID = -1111111202103170735L;
@Override
public Component getTreeCellRendererComponent(JTree tree, Object value, boolean selected, boolean expanded, boolean leaf,
int row, boolean hasFocus) {
Component c = super.getTreeCellRendererComponent(tree, value, selected, expanded, leaf, row, hasFocus);
if (value instanceof DeviceTreeNode || value == procTreeRoot) {
setIcon(ICON_DEVICE);
} else {
setIcon(ICON_PROCESS);
}
return c;
}
});
JPanel btnPane = new JPanel();
BoxLayout boxLayout = new BoxLayout(btnPane, BoxLayout.LINE_AXIS);
btnPane.setLayout(boxLayout);
tipLabel = new Label(NLS.str("adb_dialog.waiting"));
btnPane.add(tipLabel);
JButton refreshBtn = new JButton(NLS.str("adb_dialog.refresh"));
JButton startServerBtn = new JButton(NLS.str("adb_dialog.start_server"));
JButton launchAppBtn = new JButton(NLS.str("adb_dialog.launch_app"));
btnPane.add(launchAppBtn);
btnPane.add(startServerBtn);
btnPane.add(refreshBtn);
refreshBtn.addActionListener(e -> {
clear();
procTreeRoot.removeAllChildren();
procTreeModel.reload(procTreeRoot);
SwingUtilities.invokeLater(this::connectToADB);
});
startServerBtn.addActionListener(e -> startADBServer());
launchAppBtn.addActionListener(e -> launchApp());
JPanel mainPane = new JPanel(new BorderLayout(5, 5));
mainPane.add(adbPanel, BorderLayout.NORTH);
mainPane.add(scrollPane, BorderLayout.CENTER);
mainPane.add(btnPane, BorderLayout.SOUTH);
mainPane.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10));
getContentPane().add(mainPane);
pack();
setSize(800, 500);
setLocationRelativeTo(null);
setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);
setModalityType(ModalityType.MODELESS);
}
private void clear() {
if (deviceSocket != null) {
try {
deviceSocket.close();
} catch (IOException e) {
e.printStackTrace();
}
deviceSocket = null;
}
for (DeviceNode deviceNode : deviceNodes) {
deviceNode.device.stopListenForJDWP();
}
deviceNodes.clear();
}
private void detectADBPath() {
boolean isWinOS;
try {
isWinOS = System.getProperty("os.name").startsWith("Windows");
} catch (Exception e) {
e.printStackTrace();
return;
}
String slash = isWinOS ? "\\" : "/";
String adbName = isWinOS ? "adb.exe" : "adb";
String sdkPath = System.getenv("ANDROID_HOME");
if (!StringUtils.isEmpty(sdkPath)) {
if (!sdkPath.endsWith(slash)) {
sdkPath += slash;
}
sdkPath += "platform-tools" + slash + adbName;
if ((new File(sdkPath)).exists()) {
pathTextField.setText(sdkPath);
return;
}
}
String envPath = System.getenv("PATH");
String[] paths = envPath.split(isWinOS ? ";" : ":");
for (String path : paths) {
if (!path.endsWith(slash)) {
path += slash;
}
path = path + adbName;
if (new File(path).exists()) {
pathTextField.setText(path);
return;
}
}
}
private void startADBServer() {
String path = pathTextField.getText();
if (path.isEmpty()) {
UiUtils.showMessageBox(mainWindow, NLS.str("adb_dialog.missing_path"));
return;
}
String tip;
try {
if (ADB.startServer(path, Integer.parseInt(portTextField.getText()))) {
tip = NLS.str("adb_dialog.start_okay", portTextField.getText());
} else {
tip = NLS.str("adb_dialog.start_fail", portTextField.getText());
}
} catch (Exception except) {
tip = except.getMessage();
except.printStackTrace();
}
UiUtils.showMessageBox(mainWindow, tip);
tipLabel.setText(tip);
}
private void connectToADB() {
String tip;
try {
String host = hostTextField.getText();
String port = portTextField.getText();
tipLabel.setText(NLS.str("adb_dialog.connecting", host, port));
deviceSocket = ADB.listenForDeviceState(this, host, Integer.parseInt(port));
if (deviceSocket != null) {
tip = NLS.str("adb_dialog.connect_okay", host, port);
this.setTitle(tip);
} else {
tip = NLS.str("adb_dialog.connect_fail");
}
} catch (IOException e) {
e.printStackTrace();
tip = e.getMessage();
UiUtils.showMessageBox(mainWindow, tip);
}
tipLabel.setText(tip);
}
@Override
public void onDeviceStatusChange(List<ADB.DeviceInfo> deviceInfoList) {
List<DeviceNode> nodes = new ArrayList<>(deviceInfoList.size());
info_loop: for (ADB.DeviceInfo info : deviceInfoList) {
for (DeviceNode deviceNode : deviceNodes) {
if (deviceNode.device.updateDeviceInfo(info)) {
deviceNode.refresh();
nodes.add(deviceNode);
continue info_loop;
}
}
ADB.Device device = new ADB.Device(info);
device.getAndroidReleaseVersion();
nodes.add(new DeviceNode(device));
listenJDWP(device);
}
deviceNodes = nodes;
SwingUtilities.invokeLater(() -> {
tipLabel.setText(NLS.str("adb_dialog.tip_devices", deviceNodes.size()));
procTreeRoot.removeAllChildren();
deviceNodes.forEach(n -> procTreeRoot.add(n.tNode));
procTreeModel.reload(procTreeRoot);
for (DeviceNode deviceNode : deviceNodes) {
procTree.expandPath(new TreePath(deviceNode.tNode.getPath()));
}
});
}
private void processSelected(MouseEvent e) {
TreePath path = procTree.getPathForLocation(e.getX(), e.getY());
if (path != null) {
DefaultMutableTreeNode node = (DefaultMutableTreeNode) path.getLastPathComponent();
String pid = getPid((String) node.getUserObject());
if (StringUtils.isEmpty(pid)) {
return;
}
if (mainWindow.getDebuggerPanel() != null && mainWindow.getDebuggerPanel().getDbgController().isDebugging()) {
if (JOptionPane.showConfirmDialog(mainWindow,
NLS.str("adb_dialog.restart_while_debugging_msg"),
NLS.str("adb_dialog.restart_while_debugging_title"),
JOptionPane.OK_CANCEL_OPTION) != JOptionPane.CANCEL_OPTION) {
IDebugController ctrl = mainWindow.getDebuggerPanel().getDbgController();
if (launchForDebugging(mainWindow, ctrl.getProcessName(), true)) {
dispose();
}
}
return;
}
DeviceNode deviceNode = getDeviceNode((DefaultMutableTreeNode) node.getParent());
if (deviceNode == null) {
return;
}
if (!setupArgs(deviceNode.device, pid, (String) node.getUserObject())) {
return;
}
if (debugSetter.isBeingDebugged()) {
if (JOptionPane.showConfirmDialog(mainWindow,
NLS.str("adb_dialog.being_debugged_msg"),
NLS.str("adb_dialog.being_debugged_title"),
JOptionPane.OK_CANCEL_OPTION) == JOptionPane.CANCEL_OPTION) {
return;
}
}
tipLabel.setText(NLS.str("adb_dialog.starting_debugger"));
if (!attachProcess(mainWindow)) {
tipLabel.setText(NLS.str("adb_dialog.init_dbg_fail"));
} else {
dispose();
}
}
}
private static boolean attachProcess(MainWindow mainWindow) {
boolean ok = false;
if (debugSetter == null) {
return ok;
}
debugSetter.clearForward();
String rst = debugSetter.forwardJDWP();
if (!rst.isEmpty()) {
UiUtils.showMessageBox(mainWindow, rst);
return ok;
}
try {
ok = mainWindow.getDebuggerPanel().showDebugger(
debugSetter.name,
debugSetter.device.getDeviceInfo().adbHost,
debugSetter.forwardTcpPort,
debugSetter.ver);
} catch (Exception except) {
except.printStackTrace();
}
return ok;
}
public static boolean launchForDebugging(MainWindow mainWindow, String fullAppPath, boolean autoAttach) {
if (debugSetter != null) {
debugSetter.autoAttachPkg = autoAttach;
try {
int pid = debugSetter.device.launchApp(fullAppPath);
if (pid != -1) {
debugSetter.setPid(String.valueOf(pid))
.setName(fullAppPath);
return attachProcess(mainWindow);
}
} catch (Exception e) {
e.printStackTrace();
}
}
return false;
}
private String getPid(String nodeText) {
if (nodeText.startsWith("[pid:")) {
int pos = nodeText.indexOf("]", "[pid:".length());
if (pos != -1) {
return nodeText.substring("[pid:".length(), pos).trim();
}
}
return null;
}
private DeviceNode getDeviceNode(DefaultMutableTreeNode node) {
for (DeviceNode deviceNode : deviceNodes) {
if (deviceNode.tNode == node) {
return deviceNode;
}
}
return null;
}
private DeviceNode getDeviceNode(ADB.Device device) {
for (DeviceNode deviceNode : deviceNodes) {
if (deviceNode.device.equals(device)) {
return deviceNode;
}
}
throw new JadxRuntimeException("Unexpected device: " + device);
}
private void listenJDWP(ADB.Device device) {
try {
device.listenForJDWP(this);
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
public void dispose() {
clear();
super.dispose();
boolean save = mainWindow.getSettings().getAdbDialogPath().equals(pathTextField.getText());
boolean save1 = mainWindow.getSettings().getAdbDialogHost().equals(hostTextField.getText());
boolean save2 = mainWindow.getSettings().getAdbDialogPort().equals(portTextField.getText());
if (save || save1 || save2) {
mainWindow.getSettings().sync();
}
}
@Override
public void adbDisconnected() {
deviceSocket = null;
SwingUtilities.invokeLater(() -> {
tipLabel.setText(NLS.str("adb_dialog.disconnected"));
this.setTitle("");
});
}
@Override
public void jdwpProcessOccurred(ADB.Device device, Set<String> id) {
List<ADB.Process> procs;
try {
Thread.sleep(40); /*
* wait for a moment, let the new processes on remote be fully initialized,
* otherwise we may not get its real name but the <pre-initialized> state text.
*/
procs = device.getProcessList();
} catch (IOException | InterruptedException e) {
e.printStackTrace();
procs = Collections.emptyList();
}
List<String> procList = new ArrayList<>(id.size());
if (procs.size() == 0) {
procList.addAll(id);
} else {
for (ADB.Process proc : procs) {
if (id.contains(proc.pid)) {
procList.add(String.format("[pid: %-6s] %s", proc.pid, proc.name));
}
}
}
Collections.reverse(procList);
DeviceNode node;
try {
node = getDeviceNode(device);
} catch (Exception e) {
e.printStackTrace();
return;
}
node.tNode.removeAllChildren();
DefaultMutableTreeNode tempNode = null;
for (String s : procList) {
DefaultMutableTreeNode pnode = new DefaultMutableTreeNode(s);
node.tNode.add(pnode);
if (!debugSetter.expectPkg.isEmpty() && s.endsWith(debugSetter.expectPkg)) {
if (debugSetter.autoAttachPkg && debugSetter.device.equals(node.device)) {
debugSetter.set(node.device, debugSetter.ver, getPid(s), s);
if (attachProcess(mainWindow)) {
dispose();
return;
}
}
tempNode = pnode;
}
}
DefaultMutableTreeNode theNode = tempNode;
SwingUtilities.invokeLater(() -> {
procTreeModel.reload(node.tNode);
procTree.expandPath(new TreePath(node.tNode.getPath()));
if (theNode != null) {
TreePath thePath = new TreePath(theNode.getPath());
procTree.scrollPathToVisible(thePath);
procTree.setSelectionPath(thePath);
}
});
}
private void launchApp() {
if (deviceNodes.size() == 0) {
UiUtils.showMessageBox(mainWindow, NLS.str("adb_dialog.no_devices"));
return;
}
JClass cls = DbgUtils.searchMainActivity(mainWindow);
String pkg = DbgUtils.searchPackageName(mainWindow);
if (pkg.isEmpty() || cls == null) {
UiUtils.showMessageBox(mainWindow, NLS.str("adb_dialog.msg_read_mani_fail"));
return;
}
if (scrollToProcNode(pkg)) {
return;
}
String fullName = pkg + "/" + cls.getCls().getClassNode().getClassInfo().getFullName();
ADB.Device device = deviceNodes.get(0).device; // TODO: if multiple devices presented should let user select the one they desire.
if (device != null) {
try {
device.launchApp(fullName);
} catch (Exception e) {
e.printStackTrace();
UiUtils.showMessageBox(mainWindow, e.getMessage());
}
}
}
private boolean scrollToProcNode(String pkg) {
if (pkg.isEmpty()) {
return false;
}
debugSetter.expectPkg = " " + pkg;
for (int i = 0; i < procTreeRoot.getChildCount(); i++) {
DefaultMutableTreeNode rn = (DefaultMutableTreeNode) procTreeRoot.getChildAt(i);
for (int j = 0; j < rn.getChildCount(); j++) {
DefaultMutableTreeNode n = (DefaultMutableTreeNode) rn.getChildAt(j);
String pName = (String) n.getUserObject();
if (pName.endsWith(debugSetter.expectPkg)) {
TreePath path = new TreePath(n.getPath());
procTree.scrollPathToVisible(path);
procTree.setSelectionPath(path);
return true;
}
}
}
return false;
}
@Override
public void jdwpListenerClosed(ADB.Device device) {
}
private static class DeviceTreeNode extends DefaultMutableTreeNode {
private static final long serialVersionUID = -1111111202103131112L;
}
private static class DeviceNode {
ADB.Device device;
DeviceTreeNode tNode;
DeviceNode(ADB.Device adbDevice) {
this.device = adbDevice;
tNode = new DeviceTreeNode();
refresh();
}
void refresh() {
ADB.DeviceInfo info = device.getDeviceInfo();
String text = info.model;
if (!text.equals(info.serial)) {
text += String.format(" [serial: %s]", info.serial);
}
text += String.format(" [state: %s]", info.isOnline() ? "online" : "offline");
tNode.setUserObject(text);
}
}
private boolean setupArgs(ADB.Device device, String pid, String name) {
String ver = device.getAndroidReleaseVersion();
if (StringUtils.isEmpty(ver)) {
if (JOptionPane.showConfirmDialog(mainWindow,
NLS.str("adb_dialog.unknown_android_ver"),
"",
JOptionPane.OK_CANCEL_OPTION) == JOptionPane.CANCEL_OPTION) {
return false;
}
ver = "8";
}
ver = getMajorVer(ver);
debugSetter.set(device, ver, pid, name);
return true;
}
private String getMajorVer(String ver) {
int pos = ver.indexOf(".");
if (pos != -1) {
ver = ver.substring(0, pos);
}
return ver;
}
private class DebugSetting {
private static final int FORWARD_TCP_PORT = 33233;
private String ver;
private String pid;
private String name;
private ADB.Device device;
private int forwardTcpPort = FORWARD_TCP_PORT;
private String expectPkg = "";
private boolean autoAttachPkg = false;
private void set(ADB.Device device, String ver, String pid, String name) {
this.ver = ver;
this.pid = pid;
this.name = name;
this.device = device;
this.autoAttachPkg = false;
this.expectPkg = "";
}
private DebugSetting setPid(String pid) {
this.pid = pid;
return this;
}
private DebugSetting setName(String name) {
this.name = name;
return this;
}
private String forwardJDWP() {
int localPort = forwardTcpPort;
String resultDesc = "";
try {
do {
ForwardResult rst = device.forwardJDWP(localPort + "", pid);
if (rst.state == 0) {
forwardTcpPort = localPort;
return "";
}
if (rst.state == 1) {
if (rst.desc.contains("Only one usage of each socket address")) { // port is taken by other process
if (localPort < 65536) {
localPort++; // retry
continue;
}
}
}
resultDesc = rst.desc;
} while (false);
} catch (IOException e) {
e.printStackTrace();
}
if (StringUtils.isEmpty(resultDesc)) {
resultDesc = NLS.str("adb_dialog.forward_fail");
}
return resultDesc;
}
// we have to remove all ports that forwarding the jdwp:pid, otherwise our JDWP handshake may fail.
private void clearForward() {
String jdwpPid = " jdwp:" + pid;
String tcpPort = " tcp:" + forwardTcpPort;
try {
List<String> list = ADB.listForward(device.getDeviceInfo().adbHost,
device.getDeviceInfo().adbPort);
for (String s : list) {
if (s.startsWith(device.getSerial()) && s.endsWith(jdwpPid) && !s.contains(tcpPort)) {
String[] fields = s.split("\\s+");
for (String field : fields) {
if (field.startsWith("tcp:")) {
try {
device.removeForward(field.substring("tcp:".length()));
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
private boolean isBeingDebugged() {
String jdwpPid = " jdwp:" + pid;
String tcpPort = " tcp:" + forwardTcpPort;
try {
List<String> list = ADB.listForward(device.getDeviceInfo().adbHost,
device.getDeviceInfo().adbPort);
for (String s : list) {
if (s.startsWith(device.getSerial()) && s.endsWith(jdwpPid)) {
return !s.contains(tcpPort);
}
}
} catch (IOException e) {
e.printStackTrace();
}
return false;
}
}
}
@@ -0,0 +1,36 @@
package jadx.gui.ui;
import jadx.core.dex.instructions.args.ArgType;
import jadx.gui.ui.JDebuggerPanel.ValueTreeNode;
public interface IDebugController {
boolean startDebugger(JDebuggerPanel panel, String[] args);
boolean run();
boolean stepOver();
boolean stepInto();
boolean stepOut();
boolean pause();
boolean stop();
boolean exit();
boolean isSuspended();
boolean isDebugging();
boolean modifyRegValue(ValueTreeNode node, ArgType type, Object val);
String getProcessName();
void setStateListener(StateListener l);
interface StateListener {
void onStateChanged(boolean suspended, boolean stopped);
}
}
@@ -0,0 +1,550 @@
package jadx.gui.ui;
import java.awt.*;
import java.awt.event.*;
import java.util.List;
import javax.swing.*;
import javax.swing.tree.*;
import io.reactivex.annotations.Nullable;
import jadx.core.utils.StringUtils;
import jadx.gui.device.debugger.DebugController;
import jadx.gui.treemodel.JClass;
import jadx.gui.ui.codearea.SmaliArea;
import jadx.gui.utils.NLS;
import jadx.gui.utils.UiUtils;
public class JDebuggerPanel extends JPanel {
private static final long serialVersionUID = -1111111202102181631L;
private static final ImageIcon ICON_RUN = UiUtils.openIcon("run");
private static final ImageIcon ICON_RERUN = UiUtils.openIcon("rerun");
private static final ImageIcon ICON_PAUSE = UiUtils.openIcon("pause");
private static final ImageIcon ICON_STOP = UiUtils.openIcon("stop");
private static final ImageIcon ICON_STOP_GRAY = UiUtils.openIcon("stop_gray");
private static final ImageIcon ICON_STEP_INTO = UiUtils.openIcon("step_into");
private static final ImageIcon ICON_STEP_OVER = UiUtils.openIcon("step_over");
private static final ImageIcon ICON_STEP_OUT = UiUtils.openIcon("step_out");
private final transient MainWindow mainWindow;
private final transient JList<IListElement> stackFrameList;
private final transient JComboBox<IListElement> threadBox;
private final transient JTextArea logger;
private final transient JTree variableTree;
private final transient DefaultTreeModel variableTreeModel;
private final transient DefaultMutableTreeNode rootTreeNode;
private final transient DefaultMutableTreeNode thisTreeNode;
private final transient DefaultMutableTreeNode regTreeNode;
private final transient JSplitPane rightSplitter;
private final transient JSplitPane leftSplitter;
private final transient IDebugController controller;
private final transient VarTreePopupMenu varTreeMenu;
private transient KeyEventDispatcher controllerShortCutDispatcher;
public JDebuggerPanel(MainWindow mainWindow) {
this.mainWindow = mainWindow;
controller = new DebugController();
this.setLayout(new BorderLayout());
this.setMinimumSize(new Dimension(100, 150));
leftSplitter = new JSplitPane();
rightSplitter = new JSplitPane();
leftSplitter.setDividerLocation(mainWindow.getSettings().getDebuggerStackFrameSplitterLoc());
rightSplitter.setDividerLocation(mainWindow.getSettings().getDebuggerVarTreeSplitterLoc());
JPanel stackFramePanel = new JPanel(new BorderLayout());
threadBox = new JComboBox<>();
stackFrameList = new JList<>();
threadBox.setModel(new DefaultComboBoxModel<>());
stackFrameList.setModel(new DefaultListModel<>());
stackFramePanel.add(threadBox, BorderLayout.NORTH);
stackFramePanel.add(new JScrollPane(stackFrameList), BorderLayout.CENTER);
JPanel variablePanel = new JPanel(new CardLayout());
variableTree = new JTree();
variablePanel.add(new JScrollPane(variableTree));
rootTreeNode = new DefaultMutableTreeNode();
thisTreeNode = new DefaultMutableTreeNode("this");
regTreeNode = new DefaultMutableTreeNode("var");
rootTreeNode.add(thisTreeNode);
rootTreeNode.add(regTreeNode);
variableTreeModel = new DefaultTreeModel(rootTreeNode);
variableTree.setModel(variableTreeModel);
variableTree.expandPath(new TreePath(rootTreeNode.getPath()));
variableTree.setCellRenderer(new DefaultTreeCellRenderer() {
private static final long serialVersionUID = -1111111202103170725L;
@Override
public Component getTreeCellRendererComponent(JTree tree, Object value, boolean sel, boolean expanded, boolean leaf, int row,
boolean hasFocus) {
Component c = super.getTreeCellRendererComponent(tree, value, sel, expanded, leaf, row, hasFocus);
if (value instanceof ValueTreeNode) {
if (sel) {
setForeground(Color.WHITE);
} else if (((ValueTreeNode) value).isUpdated()) {
setForeground(Color.RED);
} else {
setForeground(Color.BLACK);
}
}
return c;
}
});
varTreeMenu = new VarTreePopupMenu(mainWindow);
JPanel loggerPanel = new JPanel(new CardLayout());
logger = new JTextArea();
logger.setEditable(false);
logger.setLineWrap(true);
loggerPanel.add(new JScrollPane(logger));
leftSplitter.setLeftComponent(stackFramePanel);
leftSplitter.setRightComponent(rightSplitter);
leftSplitter.setResizeWeight(MainWindow.SPLIT_PANE_RESIZE_WEIGHT);
rightSplitter.setLeftComponent(variablePanel);
rightSplitter.setRightComponent(loggerPanel);
rightSplitter.setResizeWeight(MainWindow.SPLIT_PANE_RESIZE_WEIGHT);
JPanel headerPanel = new JPanel(new BorderLayout());
headerPanel.add(new Label(), BorderLayout.WEST);
headerPanel.add(initToolBar(), BorderLayout.CENTER);
JButton closeBtn = new JButton(UiUtils.openIcon("cross"));
closeBtn.addMouseListener(new MouseAdapter() {
@Override
public void mouseClicked(MouseEvent e) {
if (controller.isDebugging()) {
int what = JOptionPane.showConfirmDialog(mainWindow,
NLS.str("debugger.cfm_dialog_msg"),
NLS.str("debugger.cfm_dialog_title"),
JOptionPane.OK_CANCEL_OPTION);
if (what == JOptionPane.OK_OPTION) {
controller.exit();
} else {
return;
}
} else {
mainWindow.destroyDebuggerPanel();
}
unregShortcuts();
}
});
headerPanel.add(closeBtn, BorderLayout.EAST);
this.add(headerPanel, BorderLayout.NORTH);
this.add(leftSplitter, BorderLayout.CENTER);
listenUIEvents();
}
public MainWindow getMainWindow() {
return mainWindow;
}
private void listenUIEvents() {
stackFrameList.addMouseListener(new MouseAdapter() {
@Override
public void mouseClicked(MouseEvent e) {
if (e.getClickCount() % 2 == 0) {
stackFrameSelected(e.getPoint());
}
}
});
variableTree.addMouseListener(new MouseAdapter() {
@Override
public void mouseClicked(MouseEvent e) {
if (SwingUtilities.isRightMouseButton(e)) {
treeNodeRightClicked(e);
}
}
});
}
private JToolBar initToolBar() {
AbstractAction stepOver = new AbstractAction(NLS.str("debugger.step_over"), ICON_STEP_OVER) {
private static final long serialVersionUID = -1111111202103170726L;
@Override
public void actionPerformed(ActionEvent e) {
controller.stepOver();
}
};
stepOver.putValue(Action.SHORT_DESCRIPTION, NLS.str("debugger.step_over"));
AbstractAction stepInto = new AbstractAction(NLS.str("debugger.step_into"), ICON_STEP_INTO) {
private static final long serialVersionUID = -1111111202103170727L;
@Override
public void actionPerformed(ActionEvent e) {
controller.stepInto();
}
};
stepInto.putValue(Action.SHORT_DESCRIPTION, NLS.str("debugger.step_into"));
AbstractAction stepOut = new AbstractAction(NLS.str("debugger.step_out"), ICON_STEP_OUT) {
private static final long serialVersionUID = -1111111202103170728L;
@Override
public void actionPerformed(ActionEvent e) {
controller.stepOut();
}
};
stepOut.putValue(Action.SHORT_DESCRIPTION, NLS.str("debugger.step_out"));
AbstractAction stop = new AbstractAction(NLS.str("debugger.stop"), ICON_STOP_GRAY) {
private static final long serialVersionUID = -1111111202103170728L;
@Override
public void actionPerformed(ActionEvent e) {
controller.stop();
}
};
stop.putValue(Action.SHORT_DESCRIPTION, NLS.str("debugger.stop"));
AbstractAction run = new AbstractAction(NLS.str("debugger.run"), ICON_RUN) {
private static final long serialVersionUID = -1111111202103170728L;
@Override
public void actionPerformed(ActionEvent e) {
if (controller.isDebugging()) {
if (controller.isSuspended()) {
controller.run();
} else {
controller.pause();
}
}
}
};
run.putValue(Action.SHORT_DESCRIPTION, NLS.str("debugger.run"));
AbstractAction rerun = new AbstractAction(NLS.str("debugger.rerun"), ICON_RERUN) {
private static final long serialVersionUID = -1111111202103210433L;
@Override
public void actionPerformed(ActionEvent e) {
if (controller.isDebugging()) {
controller.stop();
}
String pkgName = controller.getProcessName();
if (pkgName.isEmpty() || !ADBDialog.launchForDebugging(mainWindow, pkgName, true)) {
(new ADBDialog(mainWindow)).setVisible(true);
}
}
};
rerun.putValue(Action.SHORT_DESCRIPTION, NLS.str("debugger.rerun"));
controller.setStateListener(new DebugController.StateListener() {
boolean isGray = true;
@Override
public void onStateChanged(boolean suspended, boolean stopped) {
if (!stopped) {
if (isGray) {
stop.putValue(Action.SMALL_ICON, ICON_STOP);
}
} else {
stop.putValue(Action.SMALL_ICON, ICON_STOP_GRAY);
run.putValue(Action.SMALL_ICON, ICON_RUN);
run.putValue(Action.SHORT_DESCRIPTION, NLS.str("debugger.run"));
isGray = true;
return;
}
if (suspended) {
run.putValue(Action.SMALL_ICON, ICON_RUN);
run.putValue(Action.SHORT_DESCRIPTION, NLS.str("debugger.run"));
} else {
run.putValue(Action.SMALL_ICON, ICON_PAUSE);
run.putValue(Action.SHORT_DESCRIPTION, NLS.str("debugger.pause"));
}
}
});
JToolBar toolBar = new JToolBar();
toolBar.add(new Label());
toolBar.add(Box.createHorizontalGlue());
toolBar.add(rerun);
toolBar.add(Box.createRigidArea(new Dimension(5, 0)));
toolBar.add(stop);
toolBar.add(Box.createRigidArea(new Dimension(5, 0)));
toolBar.add(run);
toolBar.add(Box.createRigidArea(new Dimension(5, 0)));
toolBar.add(stepOver);
toolBar.add(Box.createRigidArea(new Dimension(5, 0)));
toolBar.add(stepInto);
toolBar.add(Box.createRigidArea(new Dimension(5, 0)));
toolBar.add(stepOut);
toolBar.add(Box.createHorizontalGlue());
toolBar.add(new Label());
regShortcuts();
return toolBar;
}
private void unregShortcuts() {
KeyboardFocusManager
.getCurrentKeyboardFocusManager()
.removeKeyEventDispatcher(controllerShortCutDispatcher);
}
private void regShortcuts() {
controllerShortCutDispatcher = new KeyEventDispatcher() {
@Override
public boolean dispatchKeyEvent(KeyEvent e) {
if (e.getID() == KeyEvent.KEY_PRESSED
&& mainWindow.getTabbedPane().getFocusedComp() instanceof SmaliArea) {
if (e.getModifiersEx() == KeyEvent.SHIFT_DOWN_MASK
&& e.getKeyCode() == KeyEvent.VK_F8) {
controller.stepOut();
return true;
}
switch (e.getKeyCode()) {
case KeyEvent.VK_F7:
controller.stepInto();
return true;
case KeyEvent.VK_F8:
controller.stepOver();
return true;
case KeyEvent.VK_F9:
controller.run();
return true;
}
}
return false;
}
};
KeyboardFocusManager.getCurrentKeyboardFocusManager()
.addKeyEventDispatcher(controllerShortCutDispatcher);
}
private void treeNodeRightClicked(MouseEvent e) {
TreePath path = variableTree.getPathForLocation(e.getX(), e.getY());
if (path != null) {
Object node = path.getLastPathComponent();
if (node instanceof ValueTreeNode) {
varTreeMenu.show((ValueTreeNode) node, e.getComponent(), e.getX(), e.getY());
}
}
}
private void stackFrameSelected(Point p) {
int loc = stackFrameList.locationToIndex(p);
if (loc > -1) {
IListElement ele = stackFrameList.getModel().getElementAt(loc);
if (ele != null) {
ele.onSelected();
}
}
}
public boolean showDebugger(String procName, String host, int port, String androidVer) {
boolean ok = controller.startDebugger(this, new String[] { host, String.valueOf(port), androidVer });
if (ok) {
log(String.format("Attached %s %s:%d", procName, host, port));
leftSplitter.setDividerLocation(mainWindow.getSettings().getDebuggerStackFrameSplitterLoc());
rightSplitter.setDividerLocation(mainWindow.getSettings().getDebuggerVarTreeSplitterLoc());
mainWindow.showDebuggerPanel();
}
return ok;
}
public IDebugController getDbgController() {
return controller;
}
public int getLeftSplitterLocation() {
return leftSplitter.getDividerLocation();
}
public int getRightSplitterLocation() {
return rightSplitter.getDividerLocation();
}
public void loadSettings() {
Font font = mainWindow.getSettings().getFont();
variableTree.setFont(font.deriveFont(font.getSize() + 1.f));
variableTree.setRowHeight(-1);
stackFrameList.setFont(font);
threadBox.setFont(font);
logger.setFont(font);
}
public void resetUI() {
thisTreeNode.removeAllChildren();
regTreeNode.removeAllChildren();
clearFrameAndThreadList();
threadBox.updateUI();
stackFrameList.updateUI();
variableTreeModel.reload(rootTreeNode);
variableTree.expandPath(new TreePath(rootTreeNode.getPath()));
logger.setText("");
}
public void scrollToSmaliLine(JClass cls, int pos, boolean debugMode) {
SwingUtilities.invokeLater(() -> getMainWindow().getTabbedPane().smaliJump(cls, pos, debugMode));
}
public void resetAllDebuggingInfo() {
clearFrameAndThreadList();
resetRegTreeNodes();
resetThisTreeNodes();
}
public void resetThisTreeNodes() {
thisTreeNode.removeAllChildren();
SwingUtilities.invokeLater(() -> variableTreeModel.reload(thisTreeNode));
}
public void resetRegTreeNodes() {
regTreeNode.removeAllChildren();
SwingUtilities.invokeLater(() -> variableTreeModel.reload(regTreeNode));
}
public void updateRegTreeNodes(List<? extends ValueTreeNode> nodes) {
nodes.forEach(regTreeNode::add);
}
public void updateThisFieldNodes(List<? extends ValueTreeNode> nodes) {
nodes.forEach(thisTreeNode::add);
}
public void refreshThreadBox(List<? extends IListElement> elements) {
if (elements.size() > 0) {
DefaultComboBoxModel<IListElement> model =
(DefaultComboBoxModel<IListElement>) threadBox.getModel();
elements.forEach(model::addElement);
}
SwingUtilities.invokeLater(() -> {
threadBox.updateUI();
stackFrameList.setFont(mainWindow.getSettings().getFont());
});
}
public void refreshStackFrameList(List<? extends IListElement> elements) {
if (elements.size() > 0) {
DefaultListModel<IListElement> model =
(DefaultListModel<IListElement>) stackFrameList.getModel();
elements.forEach(model::addElement);
stackFrameList.setFont(mainWindow.getSettings().getFont());
}
SwingUtilities.invokeLater(stackFrameList::repaint);
}
public void refreshRegisterTree() {
SwingUtilities.invokeLater(() -> {
variableTreeModel.reload(regTreeNode);
variableTree.expandPath(new TreePath(regTreeNode.getPath()));
});
}
public void refreshThisFieldTree() {
SwingUtilities.invokeLater(() -> {
boolean expanded = variableTree.isExpanded(new TreePath(thisTreeNode.getPath()));
variableTreeModel.reload(thisTreeNode);
if (expanded) {
variableTree.expandPath(new TreePath(regTreeNode.getPath()));
}
});
}
public void clearFrameAndThreadList() {
((DefaultListModel<IListElement>) stackFrameList.getModel()).removeAllElements();
((DefaultComboBoxModel<IListElement>) threadBox.getModel()).removeAllElements();
}
public void log(String msg) {
StringBuilder sb = new StringBuilder();
sb.append(" > ")
.append(StringUtils.getDateText())
.append(" ")
.append(msg)
.append("\n");
SwingUtilities.invokeLater(() -> {
logger.append(sb.toString());
});
}
public void updateRegTree(ValueTreeNode node) {
SwingUtilities.invokeLater(() -> {
variableTreeModel.reload(regTreeNode);
scrollToUpdatedNode(node);
});
}
public void updateThisTree(ValueTreeNode node) {
SwingUtilities.invokeLater(() -> {
variableTreeModel.reload(thisTreeNode);
scrollToUpdatedNode(node);
});
}
public void scrollToUpdatedNode(ValueTreeNode node) {
SwingUtilities.invokeLater(() -> {
TreeNode[] path = node.getPath();
variableTree.scrollPathToVisible(new TreePath(path));
});
}
public abstract static class ValueTreeNode extends DefaultMutableTreeNode {
private static final long serialVersionUID = -1111111202103122236L;
private boolean updated;
public void setUpdated(boolean updated) {
this.updated = updated;
}
public boolean isUpdated() {
return updated;
}
public abstract String getName();
@Nullable
public abstract String getValue();
@Nullable
public abstract String getType();
public abstract long getTypeID();
public abstract ValueTreeNode updateValue(String val);
public abstract ValueTreeNode updateType(String val);
public abstract ValueTreeNode updateTypeID(long id);
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append(getName());
String val = getValue();
if (val != null) {
sb.append(" val: ").append(val).append(",");
}
String type = getType();
if (type != null) {
sb.append(" type: ").append(getType());
long id = getTypeID();
if (id > 0) {
sb.append("@").append(id);
}
}
if (val == null && type == null) {
sb.append(" undefined");
}
return sb.toString();
}
}
public interface IListElement {
void onSelected();
}
}
@@ -85,6 +85,7 @@ import jadx.core.utils.StringUtils;
import jadx.core.utils.Utils;
import jadx.core.utils.files.FileUtils;
import jadx.gui.JadxWrapper;
import jadx.gui.device.debugger.BreakpointManager;
import jadx.gui.jobs.BackgroundExecutor;
import jadx.gui.jobs.BackgroundWorker;
import jadx.gui.jobs.DecompileJob;
@@ -129,7 +130,7 @@ public class MainWindow extends JFrame {
private static final double BORDER_RATIO = 0.15;
private static final double WINDOW_RATIO = 1 - BORDER_RATIO * 2;
private static final double SPLIT_PANE_RESIZE_WEIGHT = 0.15;
public static final double SPLIT_PANE_RESIZE_WEIGHT = 0.15;
private static final ImageIcon ICON_OPEN = UiUtils.openIcon("folder");
private static final ImageIcon ICON_ADD_FILES = UiUtils.openIcon("folder_add");
@@ -148,6 +149,7 @@ public class MainWindow extends JFrame {
private static final ImageIcon ICON_DEOBF = UiUtils.openIcon("lock_edit");
private static final ImageIcon ICON_LOG = UiUtils.openIcon("report");
private static final ImageIcon ICON_JADX = UiUtils.openIcon("jadx-logo");
private static final ImageIcon ICON_DEBUGGER = UiUtils.openIcon("debugger");
private final transient JadxWrapper wrapper;
private final transient JadxSettings settings;
@@ -179,6 +181,9 @@ public class MainWindow extends JFrame {
private transient BackgroundExecutor backgroundExecutor;
private transient Theme editorTheme;
private JDebuggerPanel debuggerPanel;
private JSplitPane verticalSplitter;
public MainWindow(JadxSettings settings) {
this.wrapper = new JadxWrapper(settings);
this.settings = settings;
@@ -378,6 +383,7 @@ public class MainWindow extends JFrame {
} else {
project.setFilePath(paths);
clearTree();
BreakpointManager.saveAndExit();
if (paths.isEmpty()) {
return;
}
@@ -388,6 +394,7 @@ public class MainWindow extends JFrame {
initTree();
update();
runBackgroundJobs();
BreakpointManager.init(paths.get(0).getParent());
onFinish.run();
});
}
@@ -925,6 +932,15 @@ public class MainWindow extends JFrame {
};
quarkAction.putValue(Action.SHORT_DESCRIPTION, "Quark Engine");
Action openDeviceAction = new AbstractAction(NLS.str("debugger.process_selector"), ICON_DEBUGGER) {
@Override
public void actionPerformed(ActionEvent e) {
ADBDialog dialog = new ADBDialog(MainWindow.this);
dialog.setVisible(true);
}
};
openDeviceAction.putValue(Action.SHORT_DESCRIPTION, NLS.str("debugger.process_selector"));
JMenu file = new JMenu(NLS.str("menu.file"));
file.setMnemonic(KeyEvent.VK_F);
file.add(openAction);
@@ -1011,6 +1027,8 @@ public class MainWindow extends JFrame {
toolbar.addSeparator();
toolbar.add(quarkAction);
toolbar.addSeparator();
toolbar.add(openDeviceAction);
toolbar.addSeparator();
toolbar.add(Box.createHorizontalGlue());
toolbar.add(updateLink);
@@ -1103,6 +1121,11 @@ public class MainWindow extends JFrame {
heapUsageBar = new HeapUsageBar();
mainPanel.add(heapUsageBar, BorderLayout.SOUTH);
verticalSplitter = new JSplitPane(JSplitPane.VERTICAL_SPLIT);
verticalSplitter.setTopComponent(splitPane);
verticalSplitter.setResizeWeight(SPLIT_PANE_RESIZE_WEIGHT);
mainPanel.add(verticalSplitter, BorderLayout.CENTER);
setContentPane(mainPanel);
setTitle(DEFAULT_TITLE);
}
@@ -1221,15 +1244,25 @@ public class MainWindow extends JFrame {
settings.setTreeWidth(splitPane.getDividerLocation());
settings.saveWindowPos(this);
settings.setMainWindowExtendedState(getExtendedState());
if (debuggerPanel != null) {
saveSplittersInfo();
}
cancelBackgroundJobs();
wrapper.close();
heapUsageBar.reset();
dispose();
BreakpointManager.saveAndExit();
FileUtils.deleteTempRootDir();
System.exit(0);
}
private void saveSplittersInfo() {
settings.setMainWindowVerticalSplitterLoc(verticalSplitter.getDividerLocation());
settings.setDebuggerStackFrameSplitterLoc(debuggerPanel.getLeftSplitterLocation());
settings.setDebuggerVarTreeSplitterLoc(debuggerPanel.getRightSplitterLocation());
}
public JadxWrapper getWrapper() {
return wrapper;
}
@@ -1266,6 +1299,34 @@ public class MainWindow extends JFrame {
return treeRoot;
}
public JDebuggerPanel getDebuggerPanel() {
initDebuggerPanel();
return debuggerPanel;
}
public void showDebuggerPanel() {
initDebuggerPanel();
}
public void destroyDebuggerPanel() {
saveSplittersInfo();
debuggerPanel.setVisible(false);
debuggerPanel = null;
}
private void initDebuggerPanel() {
if (debuggerPanel == null) {
debuggerPanel = new JDebuggerPanel(this);
debuggerPanel.loadSettings();
verticalSplitter.setBottomComponent(debuggerPanel);
int loc = settings.getMainWindowVerticalSplitterLoc();
if (loc == 0) {
loc = 300;
}
verticalSplitter.setDividerLocation(loc);
}
}
private class RecentProjectsMenuListener implements MenuListener {
private final JMenu menu;
@@ -0,0 +1,140 @@
package jadx.gui.ui;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.KeyEvent;
import java.util.AbstractMap.SimpleEntry;
import java.util.ArrayList;
import java.util.Map.Entry;
import javax.swing.*;
import jadx.core.dex.instructions.args.ArgType;
import jadx.core.utils.exceptions.JadxRuntimeException;
import jadx.gui.ui.JDebuggerPanel.ValueTreeNode;
import jadx.gui.utils.NLS;
import jadx.gui.utils.TextStandardActions;
import jadx.gui.utils.UiUtils;
public class SetValueDialog extends JDialog {
private static final long serialVersionUID = -1111111202103121002L;
private final transient MainWindow mainWindow;
private final transient ValueTreeNode valNode;
public SetValueDialog(MainWindow mainWindow, ValueTreeNode valNode) {
super(mainWindow);
this.mainWindow = mainWindow;
this.valNode = valNode;
initUI();
UiUtils.addEscapeShortCutToDispose(this);
setTitle(valNode.toString());
}
private void initUI() {
JTextField valField = new JTextField();
TextStandardActions.attach(valField);
JPanel valPane = new JPanel(new BorderLayout(5, 5));
valPane.add(new JLabel(NLS.str("set_value_dialog.label_value")), BorderLayout.WEST);
valPane.add(valField, BorderLayout.CENTER);
JPanel btnPane = new JPanel();
btnPane.setLayout(new BoxLayout(btnPane, BoxLayout.LINE_AXIS));
JButton setValueBtn = new JButton(NLS.str("set_value_dialog.btn_set"));
btnPane.add(new Label());
btnPane.add(setValueBtn);
UiUtils.addKeyBinding(valField, KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0), "set value",
new AbstractAction() {
@Override
public void actionPerformed(ActionEvent e) {
setValueBtn.doClick();
}
});
JPanel typePane = new JPanel();
typePane.setLayout(new BoxLayout(typePane, BoxLayout.LINE_AXIS));
java.util.List<JRadioButton> rbs = new ArrayList<>(6);
rbs.add(new JRadioButton("int"));
rbs.add(new JRadioButton("String"));
rbs.add(new JRadioButton("long"));
rbs.add(new JRadioButton("float"));
rbs.add(new JRadioButton("double"));
rbs.add(new JRadioButton("Object id"));
rbs.get(0).setSelected(true); // select int radio
ButtonGroup rbGroup = new ButtonGroup();
rbs.forEach(rbGroup::add);
rbs.forEach(typePane::add);
JPanel mainPane = new JPanel(new BorderLayout(5, 5));
mainPane.add(typePane, BorderLayout.NORTH);
mainPane.add(valPane, BorderLayout.CENTER);
mainPane.add(btnPane, BorderLayout.SOUTH);
mainPane.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10));
getContentPane().add(mainPane);
this.setTitle(NLS.str("set_value_dialog.title"));
pack();
setSize(480, 160);
setLocationRelativeTo(null);
setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);
setModalityType(ModalityType.MODELESS);
UiUtils.addEscapeShortCutToDispose(this);
setValueBtn.addActionListener(new AbstractAction() {
private static final long serialVersionUID = -1111111202103260220L;
@Override
public void actionPerformed(ActionEvent e) {
boolean ok;
try {
Entry<ArgType, Object> type = getType();
if (type != null) {
ok = mainWindow
.getDebuggerPanel()
.getDbgController()
.modifyRegValue(valNode, type.getKey(), type.getValue());
} else {
UiUtils.showMessageBox(mainWindow, NLS.str("set_value_dialog.sel_type"));
return;
}
} catch (JadxRuntimeException except) {
UiUtils.showMessageBox(mainWindow, except.getMessage());
return;
}
if (ok) {
dispose();
} else {
UiUtils.showMessageBox(mainWindow, NLS.str("set_value_dialog.neg_msg"));
}
}
private Entry<ArgType, Object> getType() {
String val = valField.getText();
for (JRadioButton rb : rbs) {
if (rb.isSelected()) {
switch (rb.getText()) {
case "int":
return new SimpleEntry<>(ArgType.INT, Integer.valueOf(val));
case "String":
return new SimpleEntry<>(ArgType.STRING, val);
case "long":
return new SimpleEntry<>(ArgType.LONG, Long.valueOf(val));
case "float":
return new SimpleEntry<>(ArgType.FLOAT, Float.valueOf(val));
case "double":
return new SimpleEntry<>(ArgType.DOUBLE, Double.valueOf(val));
case "Object id":
return new SimpleEntry<>(ArgType.OBJECT, Long.valueOf(val));
default:
throw new JadxRuntimeException("Unexpected type: " + rb.getText());
}
}
}
return null;
}
});
}
}
@@ -25,8 +25,10 @@ import jadx.api.ResourceType;
import jadx.core.utils.StringUtils;
import jadx.core.utils.exceptions.JadxRuntimeException;
import jadx.gui.treemodel.ApkSignature;
import jadx.gui.treemodel.JClass;
import jadx.gui.treemodel.JNode;
import jadx.gui.treemodel.JResource;
import jadx.gui.ui.codearea.*;
import jadx.gui.ui.codearea.AbstractCodeArea;
import jadx.gui.ui.codearea.AbstractCodeContentPanel;
import jadx.gui.ui.codearea.ClassCodeContentPanel;
@@ -226,6 +228,27 @@ public class TabbedPane extends JTabbedPane {
showCode(pos);
}
public void smaliJump(JClass cls, int pos, boolean debugMode) {
ContentPanel panel = getOpenTabs().get(cls);
if (panel == null) {
showCode(new JumpPosition(cls, 0, 1));
panel = getOpenTabs().get(cls);
if (panel == null) {
throw new JadxRuntimeException("Failed to open panel for JClass: " + cls);
}
} else {
setSelectedComponent(panel);
}
ClassCodeContentPanel codePane = ((ClassCodeContentPanel) panel);
codePane.showSmaliPane();
SmaliArea smaliArea = (SmaliArea) codePane.getSmaliCodeArea();
if (debugMode) {
smaliArea.scrollToDebugPos(pos);
}
smaliArea.scrollToPos(pos);
smaliArea.requestFocus();
}
@Nullable
public JumpPosition getCurrentPosition() {
ContentPanel selectedCodePanel = getSelectedCodePanel();
@@ -352,9 +375,15 @@ public class TabbedPane extends JTabbedPane {
lastTab = null;
}
@Nullable
public Component getFocusedComp() {
return FocusManager.isActive() ? FocusManager.focusedComp : null;
}
private static class FocusManager implements FocusListener {
static boolean active = false;
static FocusManager listener = new FocusManager();
static Component focusedComp;
static boolean isActive() {
return active;
@@ -363,6 +392,7 @@ public class TabbedPane extends JTabbedPane {
@Override
public void focusGained(FocusEvent e) {
active = true;
focusedComp = (Component) e.getSource();
}
@Override
@@ -0,0 +1,95 @@
package jadx.gui.ui;
import java.awt.*;
import java.awt.datatransfer.Clipboard;
import java.awt.datatransfer.StringSelection;
import java.awt.event.ActionEvent;
import javax.swing.*;
import jadx.core.dex.instructions.args.ArgType;
import jadx.gui.ui.JDebuggerPanel.ValueTreeNode;
import jadx.gui.utils.NLS;
import jadx.gui.utils.UiUtils;
public class VarTreePopupMenu extends JPopupMenu {
private static final long serialVersionUID = -1111111202103170724L;
private final MainWindow mainWindow;
private ValueTreeNode valNode;
public VarTreePopupMenu(MainWindow mainWindow) {
this.mainWindow = mainWindow;
addItems();
}
public void show(ValueTreeNode treeNode, Component invoker, int x, int y) {
valNode = treeNode;
super.show(invoker, x, y);
}
private void addItems() {
JMenuItem copyValItem = new JMenuItem(new AbstractAction(NLS.str("debugger.popup_copy_value")) {
private static final long serialVersionUID = -1111111202103171118L;
@Override
public void actionPerformed(ActionEvent e) {
String val = valNode.getValue();
if (val != null) {
if (val.startsWith("\"") && val.endsWith("\"")) {
val = val.substring(1, val.length() - 1);
}
StringSelection stringSelection = new StringSelection(val);
Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard();
clipboard.setContents(stringSelection, null);
}
}
});
JMenuItem setValItem = new JMenuItem(new AbstractAction(NLS.str("debugger.popup_set_value")) {
private static final long serialVersionUID = -1111111202103171119L;
@Override
public void actionPerformed(ActionEvent e) {
(new SetValueDialog(mainWindow, valNode)).setVisible(true);
}
});
JMenuItem zeroItem = new JMenuItem(new AbstractAction(NLS.str("debugger.popup_change_to_zero")) {
private static final long serialVersionUID = -1111111202103171120L;
@Override
public void actionPerformed(ActionEvent e) {
try {
mainWindow.getDebuggerPanel()
.getDbgController()
.modifyRegValue(valNode, ArgType.INT, 0);
} catch (Exception except) {
except.printStackTrace();
UiUtils.showMessageBox(mainWindow, except.getMessage());
}
}
});
JMenuItem oneItem = new JMenuItem(new AbstractAction(NLS.str("debugger.popup_change_to_one")) {
private static final long serialVersionUID = -1111111202103171121L;
@Override
public void actionPerformed(ActionEvent e) {
try {
mainWindow.getDebuggerPanel()
.getDbgController()
.modifyRegValue(valNode, ArgType.INT, 1);
} catch (Exception except) {
except.printStackTrace();
UiUtils.showMessageBox(mainWindow, except.getMessage());
}
}
});
this.add(copyValItem);
this.add(new Separator());
this.add(setValItem);
this.add(zeroItem);
this.add(oneItem);
this.add(zeroItem);
}
}
@@ -86,4 +86,8 @@ public final class ClassCodeContentPanel extends AbstractCodeContentPanel {
public AbstractCodeArea getSmaliCodeArea() {
return smaliCodePanel.getCodeArea();
}
public void showSmaliPane() {
areaTabbedPane.setSelectedComponent(smaliCodePanel);
}
}
@@ -21,6 +21,7 @@ import javax.swing.SwingUtilities;
import javax.swing.border.EmptyBorder;
import javax.swing.event.PopupMenuEvent;
import org.fife.ui.rtextarea.RTextScrollPane;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -48,7 +49,7 @@ public class CodePanel extends JPanel {
public CodePanel(AbstractCodeArea codeArea) {
this.codeArea = codeArea;
searchBar = new SearchBar(codeArea);
codeScrollPane = new JScrollPane(codeArea);
codeScrollPane = codeArea instanceof SmaliArea ? new RTextScrollPane(codeArea) : new JScrollPane(codeArea);
setLayout(new BorderLayout());
setBorder(new EmptyBorder(0, 0, 0, 0));
@@ -116,6 +117,12 @@ public class CodePanel extends JPanel {
}
private void initLineNumbers() {
if (codeArea instanceof SmaliArea) {
return;
}
LineNumbers numbers = new LineNumbers(codeArea);
numbers.setUseSourceLines(isUseSourceLines());
codeScrollPane.setRowHeaderView(numbers);
initLineNumbers(isUseSourceLines());
}
@@ -2,41 +2,60 @@ package jadx.gui.ui.codearea;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.KeyEvent;
import java.awt.event.MouseEvent;
import java.beans.PropertyChangeListener;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.swing.*;
import javax.swing.text.BadLocationException;
import javax.swing.text.EditorKit;
import javax.swing.text.JTextComponent;
import org.fife.ui.rsyntaxtextarea.*;
import org.fife.ui.rtextarea.*;
import jadx.gui.device.debugger.BreakpointManager;
import jadx.gui.device.debugger.DbgUtils;
import jadx.gui.settings.JadxSettings;
import jadx.gui.treemodel.JClass;
import jadx.gui.treemodel.JNode;
import jadx.gui.treemodel.TextNode;
import jadx.gui.ui.ContentPanel;
import jadx.gui.utils.NLS;
import jadx.gui.utils.UiUtils;
public final class SmaliArea extends AbstractCodeArea {
private static final long serialVersionUID = 1334485631870306494L;
private final JNode textNode;
private static final Icon ICON_BREAKPOINT = UiUtils.openIcon("breakpoint");
private static final Icon ICON_BREAKPOINT_DISABLED = UiUtils.openIcon("breakpoint_disabled");
private static final Color BREAKPOINT_LINE_COLOR = Color.decode("#FF986E");
private static final Color DEBUG_LINE_COLOR = Color.decode("#80B4FF");
private SmaliV2Style smaliV2Style;
private boolean curVersion = false;
private final JNode textNode;
private final JCheckBoxMenuItem cbUseSmaliV2;
private boolean curVersion = false;
private SmaliModel model;
SmaliArea(ContentPanel contentPanel) {
super(contentPanel);
this.textNode = new TextNode(node.getName());
cbUseSmaliV2 = new JCheckBoxMenuItem(NLS.str("popup.bytecode_col"), shouldUseSmaliPrinterV2());
cbUseSmaliV2 = new JCheckBoxMenuItem(NLS.str("popup.bytecode_col"),
shouldUseSmaliPrinterV2());
cbUseSmaliV2.setAction(new AbstractAction(NLS.str("popup.bytecode_col")) {
private static final long serialVersionUID = -1111111202103170737L;
@Override
public void actionPerformed(ActionEvent e) {
boolean usingV2 = shouldUseSmaliPrinterV2();
JadxSettings settings = getContentPanel().getTabbedPane().getMainWindow().getSettings();
settings.setSmaliAreaShowBytecode(!usingV2);
settings.setSmaliAreaShowBytecode(!settings.getSmaliAreaShowBytecode());
contentPanel.getTabbedPane().getOpenTabs().values().forEach(v -> {
if (v instanceof ClassCodeContentPanel) {
switchModel();
((ClassCodeContentPanel) v).getSmaliCodeArea().refresh();
}
});
@@ -44,63 +63,14 @@ public final class SmaliArea extends AbstractCodeArea {
}
});
getPopupMenu().add(cbUseSmaliV2);
if (shouldUseSmaliPrinterV2()) {
loadV2Style();
}
}
@Override
public Font getFont() {
if (smaliV2Style != null && shouldUseSmaliPrinterV2()) {
return smaliV2Style.getFont();
}
return super.getFont();
}
@Override
public Font getFontForTokenType(int type) {
if (shouldUseSmaliPrinterV2()) {
return smaliV2Style.getFont();
}
return super.getFontForTokenType(type);
}
private boolean shouldUseSmaliPrinterV2() {
return getContentPanel().getTabbedPane().getMainWindow().getSettings().getSmaliAreaShowBytecode();
}
private void loadV2Style() {
if (smaliV2Style == null) {
smaliV2Style = new SmaliV2Style(this);
addPropertyChangeListener(SYNTAX_SCHEME_PROPERTY, evt -> {
if (smaliV2Style.refreshTheme() && shouldUseSmaliPrinterV2()) {
setSyntaxScheme(smaliV2Style);
}
});
}
setSyntaxScheme(smaliV2Style);
switchModel();
}
@Override
public void load() {
boolean useSmaliV2 = shouldUseSmaliPrinterV2();
if (useSmaliV2 != cbUseSmaliV2.getState()) {
cbUseSmaliV2.setState(useSmaliV2);
}
if (getText().isEmpty() || curVersion != useSmaliV2) {
curVersion = useSmaliV2;
if (!useSmaliV2) {
if (getSyntaxScheme() == smaliV2Style) {
Theme theme = getContentPanel().getTabbedPane().getMainWindow().getEditorTheme();
setSyntaxScheme(theme.scheme);
}
setSyntaxEditingStyle(SyntaxConstants.SYNTAX_STYLE_JAVA);
setText(node.getSmali());
} else {
loadV2Style();
setSyntaxEditingStyle(SyntaxConstants.SYNTAX_STYLE_ASSEMBLER_6502);
setText(((JClass) node).getSmaliV2());
}
if (getText().isEmpty() || curVersion != shouldUseSmaliPrinterV2()) {
curVersion = shouldUseSmaliPrinterV2();
model.load();
setCaretPosition(0);
}
}
@@ -116,56 +86,326 @@ public final class SmaliArea extends AbstractCodeArea {
return textNode;
}
private static class SmaliV2Style extends SyntaxScheme {
private void switchModel() {
if (model != null) {
model.unload();
}
model = shouldUseSmaliPrinterV2() ? new DebugModel() : new NormalModel();
}
SmaliArea smaliArea;
Theme curTheme;
public void scrollToDebugPos(int pos) {
getContentPanel().getTabbedPane().getMainWindow()
.getSettings().setSmaliAreaShowBytecode(true); // don't sync when it's set programmatically.
cbUseSmaliV2.setState(shouldUseSmaliPrinterV2());
if (!(model instanceof DebugModel)) {
switchModel();
refresh();
}
model.togglePosHighlight(pos);
}
public SmaliV2Style(SmaliArea smaliArea) {
super(true);
this.smaliArea = smaliArea;
curTheme = smaliArea.getContentPanel().getTabbedPane().getMainWindow().getEditorTheme();
updateTheme();
@Override
public Font getFont() {
if (model == null) {
return super.getFont();
}
return model.getFont();
}
@Override
public Font getFontForTokenType(int type) {
return model.getFont();
}
private boolean shouldUseSmaliPrinterV2() {
return getContentPanel().getTabbedPane().getMainWindow().getSettings().getSmaliAreaShowBytecode();
}
private abstract class SmaliModel {
abstract void load();
abstract void unload();
Font getFont() {
return SmaliArea.super.getFont();
}
public Font getFont() {
return smaliArea.getContentPanel().getTabbedPane().getMainWindow().getSettings().getSmaliFont();
Font getFontForTokenType(int type) {
return SmaliArea.super.getFontForTokenType(type);
}
public boolean refreshTheme() {
Theme theme = smaliArea.getContentPanel().getTabbedPane().getMainWindow().getEditorTheme();
boolean refresh = theme != curTheme;
if (refresh) {
curTheme = theme;
updateTheme();
}
return refresh;
void setBreakpoint(int off) {
}
private void updateTheme() {
Style[] mainStyles = curTheme.scheme.getStyles();
Style[] styles = new Style[mainStyles.length];
for (int i = 0; i < mainStyles.length; i++) {
Style mainStyle = mainStyles[i];
if (mainStyle == null) {
styles[i] = new Style();
} else {
// font will be hijacked by getFont & getFontForTokenType,
// so it doesn't need to be set here.
styles[i] = new Style(mainStyle.foreground, mainStyle.background, null);
}
}
setStyles(styles);
}
@Override
public void restoreDefaults(Font baseFont) {
restoreDefaults(baseFont, true);
}
@Override
public void restoreDefaults(Font baseFont, boolean fontStyles) {
// Note: it's a hook for continue using the editor theme, better don't remove it.
void togglePosHighlight(int pos) {
}
}
private class NormalModel extends SmaliModel {
public NormalModel() {
Theme theme = getContentPanel().getTabbedPane().getMainWindow().getEditorTheme();
setSyntaxScheme(theme.scheme);
setSyntaxEditingStyle(SyntaxConstants.SYNTAX_STYLE_JAVA);
}
@Override
public void load() {
setText(node.getSmali());
}
@Override
public void unload() {
}
}
private class DebugModel extends SmaliModel {
private KeyStroke bpShortcut;
private final String keyID = "set a break point";
private Gutter gutter;
private Object runningHighlightTag = null; // running line
private final SmaliV2Style smaliV2Style = new SmaliV2Style(SmaliArea.this);
private final Map<Integer, BreakpointLine> bpMap = new HashMap<>();
private final PropertyChangeListener listener = evt -> {
if (smaliV2Style.refreshTheme()) {
setSyntaxScheme(smaliV2Style);
}
};
public DebugModel() {
loadV2Style();
setSyntaxEditingStyle(SyntaxConstants.SYNTAX_STYLE_ASSEMBLER_6502);
addPropertyChangeListener(SYNTAX_SCHEME_PROPERTY, listener);
regBreakpointEvents();
}
@Override
public void load() {
if (gutter == null) {
gutter = RSyntaxUtilities.getGutter(SmaliArea.this);
gutter.setBookmarkingEnabled(true);
gutter.setIconRowHeaderInheritsGutterBackground(true);
Font baseFont = SmaliArea.super.getFont();
gutter.setLineNumberFont(baseFont.deriveFont(baseFont.getSize2D() - 1.0f));
}
setText(DbgUtils.getSmaliCode(((JClass) node).getCls().getClassNode()));
loadV2Style();
loadBreakpoints();
}
@Override
public void unload() {
removePropertyChangeListener(listener);
removeLineHighlight(runningHighlightTag);
UiUtils.removeKeyBinding(SmaliArea.this, bpShortcut, keyID);
BreakpointManager.removeListener((JClass) node);
bpMap.forEach((k, v) -> {
v.remove();
});
}
@Override
public Font getFont() {
return smaliV2Style.getFont();
}
@Override
public Font getFontForTokenType(int type) {
return smaliV2Style.getFont();
}
private void loadV2Style() {
setSyntaxScheme(smaliV2Style);
}
private void regBreakpointEvents() {
bpShortcut = KeyStroke.getKeyStroke(KeyEvent.VK_F2, 0);
UiUtils.addKeyBinding(SmaliArea.this, bpShortcut, "set break point", new AbstractAction() {
private static final long serialVersionUID = -1111111202103170738L;
@Override
public void actionPerformed(ActionEvent e) {
setBreakpoint(getCaretPosition());
}
});
BreakpointManager.addListener((JClass) node, this::setBreakpointDisabled);
}
private void loadBreakpoints() {
List<Integer> posList = BreakpointManager.getPositions((JClass) node);
for (Integer integer : posList) {
setBreakpoint(integer);
}
}
@Override
public void setBreakpoint(int pos) {
int line;
try {
line = getLineOfOffset(pos);
} catch (BadLocationException badLocationException) {
badLocationException.printStackTrace();
return;
}
BreakpointLine bpLine = bpMap.remove(line);
if (bpLine == null) {
bpLine = new BreakpointLine(line);
bpLine.setDisabled(false);
bpMap.put(line, bpLine);
if (!BreakpointManager.set((JClass) node, line)) {
bpLine.setDisabled(true);
}
} else {
BreakpointManager.remove((JClass) node, line);
bpLine.remove();
}
}
@Override
public void togglePosHighlight(int pos) {
if (runningHighlightTag != null) {
removeLineHighlight(runningHighlightTag);
}
try {
int line = getLineOfOffset(pos);
runningHighlightTag = addLineHighlight(line, DEBUG_LINE_COLOR);
} catch (BadLocationException e) {
e.printStackTrace();
}
}
private void setBreakpointDisabled(int pos) {
try {
int line = getLineOfOffset(pos);
bpMap.computeIfAbsent(line, k -> new BreakpointLine(line)).setDisabled(true);
} catch (BadLocationException e) {
e.printStackTrace();
}
}
private class SmaliV2Style extends SyntaxScheme {
Theme curTheme;
public SmaliV2Style(SmaliArea smaliArea) {
super(true);
curTheme = smaliArea.getContentPanel().getTabbedPane().getMainWindow().getEditorTheme();
updateTheme();
}
public Font getFont() {
return getContentPanel().getTabbedPane().getMainWindow().getSettings().getSmaliFont();
}
public boolean refreshTheme() {
Theme theme = getContentPanel().getTabbedPane().getMainWindow().getEditorTheme();
boolean refresh = theme != curTheme;
if (refresh) {
curTheme = theme;
updateTheme();
}
return refresh;
}
private void updateTheme() {
Style[] mainStyles = curTheme.scheme.getStyles();
Style[] styles = new Style[mainStyles.length];
for (int i = 0; i < mainStyles.length; i++) {
Style mainStyle = mainStyles[i];
if (mainStyle == null) {
styles[i] = new Style();
} else {
// font will be hijacked by getFont & getFontForTokenType,
// so it doesn't need to be set here.
styles[i] = new Style(mainStyle.foreground, mainStyle.background, null);
}
}
setStyles(styles);
}
@Override
public void restoreDefaults(Font baseFont) {
restoreDefaults(baseFont, true);
}
@Override
public void restoreDefaults(Font baseFont, boolean fontStyles) {
// Note: it's a hook for continue using the editor theme, better don't remove it.
}
}
private class BreakpointLine {
Object highlightTag;
GutterIconInfo iconInfo;
boolean disabled;
final int line;
BreakpointLine(int line) {
this.line = line;
this.disabled = true;
}
void remove() {
gutter.removeTrackingIcon(iconInfo);
if (!this.disabled) {
removeLineHighlight(highlightTag);
}
}
void setDisabled(boolean disabled) {
if (disabled) {
if (!this.disabled) {
gutter.removeTrackingIcon(iconInfo);
removeLineHighlight(highlightTag);
try {
iconInfo = gutter.addLineTrackingIcon(line, ICON_BREAKPOINT_DISABLED);
} catch (BadLocationException e) {
e.printStackTrace();
}
}
} else {
if (this.disabled) {
gutter.removeTrackingIcon(this.iconInfo);
try {
iconInfo = gutter.addLineTrackingIcon(line, ICON_BREAKPOINT);
highlightTag = addLineHighlight(line, BREAKPOINT_LINE_COLOR);
} catch (BadLocationException e) {
e.printStackTrace();
}
}
}
this.disabled = disabled;
}
}
}
@Override
protected RTextAreaUI createRTextAreaUI() {
// IconRowHeader won't fire an event when people click on it for adding/removing icons,
// so our poor breakpoints won't be set if we don't hijack IconRowHeader.
return new RSyntaxTextAreaUI(this) {
@Override
public EditorKit getEditorKit(JTextComponent tc) {
return new RSyntaxTextAreaEditorKit() {
private static final long serialVersionUID = -1111111202103170740L;
@Override
public IconRowHeader createIconRowHeader(RTextArea textArea) {
return new FoldingAwareIconRowHeader((RSyntaxTextArea) textArea) {
private static final long serialVersionUID = -1111111202103170739L;
@Override
public void mousePressed(MouseEvent e) {
int offs = textArea.viewToModel(e.getPoint());
if (offs > -1) {
model.setBreakpoint(offs);
}
}
};
}
};
}
};
}
}
@@ -0,0 +1,34 @@
package jadx.gui.utils;
import java.lang.ref.WeakReference;
import java.util.concurrent.ConcurrentLinkedQueue;
public class ObjectPool<T> {
private final ConcurrentLinkedQueue<WeakReference<T>> pool = new ConcurrentLinkedQueue<>();
private final Creator<T> creator;
public interface Creator<T> {
T create();
}
public ObjectPool(Creator<T> creator) {
this.creator = creator;
}
public T get() {
T node;
do {
WeakReference<T> wNode = pool.poll();
if (wNode == null) {
return creator.create();
}
node = wNode.get();
} while (node == null);
return node;
}
public void put(T node) {
pool.add(new WeakReference<>(node));
}
}
@@ -78,6 +78,11 @@ public class UiUtils {
comp.getActionMap().put(id, action);
}
public static void removeKeyBinding(JComponent comp, KeyStroke key, String id) {
comp.getInputMap().remove(key);
comp.getActionMap().remove(id);
}
public static String typeFormat(String name, ArgType type) {
return name + " " + typeStr(type);
}
@@ -220,3 +220,52 @@ apkSignature.errors=Fehler
apkSignature.warnings=Warnhinweise
apkSignature.exception=APK-Verifizierung fehlgeschlagen
apkSignature.unprotectedEntry=Dateien, die nicht durch eine Signatur geschützt sind. Unbefugte Änderungen an diesem JAR-Eintrag werden nicht erkannt.
#debugger.process_selector=Select a process to debug
#debugger.step_into=Step Into (F7)
#debugger.step_over=Step Over (F8)
#debugger.step_out=Step Out (Shift + F8)
#debugger.run=Run (F9)
#debugger.stop=Stop debugger and kill app
#debugger.pause=Pause
#debugger.rerun=Rerun
#debugger.cfm_dialog_title=Exit while debugging
#debugger.cfm_dialog_msg=Are you sure to terminate debugger?
#debugger.popup_set_value=Set Value
#debugger.popup_change_to_zero=Change to 0
#debugger.popup_change_to_one=Change to 1
#debugger.popup_copy_value=Copy Value
#set_value_dialog.label_value=Value
#set_value_dialog.btn_set=Set Value
#set_value_dialog.title=Set Value
#set_value_dialog.neg_msg=Failed to set value.
#set_value_dialog.sel_type=Select a type to set value.
#adb_dialog.addr=ADB Addr
#adb_dialog.port=ADB Port
#adb_dialog.path=ADB Path
#adb_dialog.launch_app=Launch App
#adb_dialog.start_server=Start ADB Server
#adb_dialog.refresh=Refresh
#adb_dialog.tip_devices=%d devices
#adb_dialog.device_node=Device
#adb_dialog.missing_path=Must provide the ADB path to start an ADB server.
#adb_dialog.waiting=Waiting to connect to ADB server...
#adb_dialog.connecting=Connecting to ADB server, addr: %s:%s...
#adb_dialog.connect_okay=ADB server connected, addr: %s:%s
#adb_dialog.connect_fail=Failed to connect to ADB server.
#adb_dialog.disconnected=ADB server disconnected.
#adb_dialog.start_okay=ADB server started on port: %s.
#adb_dialog.start_fail=Failed to start ADB server on port: %s!
#adb_dialog.forward_fail=Failed to forward for some reasons.
#adb_dialog.being_debugged_msg=This process seems like it's being debugged, should we proceed?"
#adb_dialog.unknown_android_ver=Failed to get Android release version, use Android 8 as default?
#adb_dialog.being_debugged_title=It's Debugging by other.
#adb_dialog.init_dbg_fail=Failed to init debugger.
#adb_dialog.msg_read_mani_fail=Failed to decode AndroidManifest.xml
#adb_dialog.no_devices=Can't found any device to start app.
#adb_dialog.restart_while_debugging_title=Restart while debugging
#adb_dialog.restart_while_debugging_msg=You're debugging an app, are you sure to restart a session?
#adb_dialog.starting_debugger=Starting debugger...
@@ -171,7 +171,7 @@ msg.rename_node_disabled=Can't rename this node
msg.rename_node_failed=Can't rename %s
msg.cant_add_comment=Can't add comment here
popup.bytecode_col=Show Bytecode
popup.bytecode_col=Show Dalvik Bytecode
popup.line_wrap=Line Wrap
popup.undo=Undo
popup.redo=Redo
@@ -220,3 +220,52 @@ apkSignature.errors=Errors
apkSignature.warnings=Warnings
apkSignature.exception=APK verification failed
apkSignature.unprotectedEntry=Files that are not protected by signature. Unauthorized modifications to this JAR entry will not be detected.
debugger.process_selector=Select a process to debug
debugger.step_into=Step Into (F7)
debugger.step_over=Step Over (F8)
debugger.step_out=Step Out (Shift + F8)
debugger.run=Run (F9)
debugger.stop=Stop debugger and kill app
debugger.pause=Pause
debugger.rerun=Rerun
debugger.cfm_dialog_title=Exit while debugging
debugger.cfm_dialog_msg=Are you sure to terminate debugger?
debugger.popup_set_value=Set Value
debugger.popup_change_to_zero=Change to 0
debugger.popup_change_to_one=Change to 1
debugger.popup_copy_value=Copy Value
set_value_dialog.label_value=Value
set_value_dialog.btn_set=Set Value
set_value_dialog.title=Set Value
set_value_dialog.neg_msg=Failed to set value.
set_value_dialog.sel_type=Select a type to set value.
adb_dialog.addr=ADB Addr
adb_dialog.port=ADB Port
adb_dialog.path=ADB Path
adb_dialog.launch_app=Launch App
adb_dialog.start_server=Start ADB Server
adb_dialog.refresh=Refresh
adb_dialog.tip_devices=%d devices
adb_dialog.device_node=Device
adb_dialog.missing_path=Must provide the ADB path to start an ADB server.
adb_dialog.waiting=Waiting to connect to ADB server...
adb_dialog.connecting=Connecting to ADB server, addr: %s:%s...
adb_dialog.connect_okay=ADB server connected, addr: %s:%s
adb_dialog.connect_fail=Failed to connect to ADB server.
adb_dialog.disconnected=ADB server disconnected.
adb_dialog.start_okay=ADB server started on port: %s.
adb_dialog.start_fail=Failed to start ADB server on port: %s!
adb_dialog.forward_fail=Failed to forward for some reasons.
adb_dialog.being_debugged_msg=This process seems like it's being debugged, should we proceed?"
adb_dialog.unknown_android_ver=Failed to get Android release version, use Android 8 as default?
adb_dialog.being_debugged_title=It's Debugging by other.
adb_dialog.init_dbg_fail=Failed to init debugger.
adb_dialog.msg_read_mani_fail=Failed to decode AndroidManifest.xml
adb_dialog.no_devices=Can't found any device to start app.
adb_dialog.restart_while_debugging_title=Restart while debugging
adb_dialog.restart_while_debugging_msg=You're debugging an app, are you sure to restart a session?
adb_dialog.starting_debugger=Starting debugger...
@@ -220,3 +220,52 @@ certificate.serialPubKeyY=Y
#apkSignature.warnings=
#apkSignature.exception=
#apkSignature.unprotectedEntry=
#debugger.process_selector=Select a process to debug
#debugger.step_into=Step Into (F7)
#debugger.step_over=Step Over (F8)
#debugger.step_out=Step Out (Shift + F8)
#debugger.run=Run (F9)
#debugger.stop=Stop debugger and kill app
#debugger.pause=Pause
#debugger.rerun=Rerun
#debugger.cfm_dialog_title=Exit while debugging
#debugger.cfm_dialog_msg=Are you sure to terminate debugger?
#debugger.popup_set_value=Set Value
#debugger.popup_change_to_zero=Change to 0
#debugger.popup_change_to_one=Change to 1
#debugger.popup_copy_value=Copy Value
#set_value_dialog.label_value=Value
#set_value_dialog.btn_set=Set Value
#set_value_dialog.title=Set Value
#set_value_dialog.neg_msg=Failed to set value.
#set_value_dialog.sel_type=Select a type to set value.
#adb_dialog.addr=ADB Addr
#adb_dialog.port=ADB Port
#adb_dialog.path=ADB Path
#adb_dialog.launch_app=Launch App
#adb_dialog.start_server=Start ADB Server
#adb_dialog.refresh=Refresh
#adb_dialog.tip_devices=%d devices
#adb_dialog.device_node=Device
#adb_dialog.missing_path=Must provide the ADB path to start an ADB server.
#adb_dialog.waiting=Waiting to connect to ADB server...
#adb_dialog.connecting=Connecting to ADB server, addr: %s:%s...
#adb_dialog.connect_okay=ADB server connected, addr: %s:%s
#adb_dialog.connect_fail=Failed to connect to ADB server.
#adb_dialog.disconnected=ADB server disconnected.
#adb_dialog.start_okay=ADB server started on port: %s.
#adb_dialog.start_fail=Failed to start ADB server on port: %s!
#adb_dialog.forward_fail=Failed to forward for some reasons.
#adb_dialog.being_debugged_msg=This process seems like it's being debugged, should we proceed?"
#adb_dialog.unknown_android_ver=Failed to get Android release version, use Android 8 as default?
#adb_dialog.being_debugged_title=It's Debugging by other.
#adb_dialog.init_dbg_fail=Failed to init debugger.
#adb_dialog.msg_read_mani_fail=Failed to decode AndroidManifest.xml
#adb_dialog.no_devices=Can't found any device to start app.
#adb_dialog.restart_while_debugging_title=Restart while debugging
#adb_dialog.restart_while_debugging_msg=You're debugging an app, are you sure to restart a session?
#adb_dialog.starting_debugger=Starting debugger...
@@ -220,3 +220,52 @@ apkSignature.errors=오류
apkSignature.warnings=경고
apkSignature.exception=APK 검증 실패
apkSignature.unprotectedEntry=서명으로 보호되지 않는 파일. 이 JAR 항목에 대한 승인되지 않은 수정은 감지되지 않습니다.
#debugger.process_selector=Select a process to debug
#debugger.step_into=Step Into (F7)
#debugger.step_over=Step Over (F8)
#debugger.step_out=Step Out (Shift + F8)
#debugger.run=Run (F9)
#debugger.stop=Stop debugger and kill app
#debugger.pause=Pause
#debugger.rerun=Rerun
#debugger.cfm_dialog_title=Exit while debugging
#debugger.cfm_dialog_msg=Are you sure to terminate debugger?
#debugger.popup_set_value=Set Value
#debugger.popup_change_to_zero=Change to 0
#debugger.popup_change_to_one=Change to 1
#debugger.popup_copy_value=Copy Value
#set_value_dialog.label_value=Value
#set_value_dialog.btn_set=Set Value
#set_value_dialog.title=Set Value
#set_value_dialog.neg_msg=Failed to set value.
#set_value_dialog.sel_type=Select a type to set value.
#adb_dialog.addr=ADB Addr
#adb_dialog.port=ADB Port
#adb_dialog.path=ADB Path
#adb_dialog.launch_app=Launch App
#adb_dialog.start_server=Start ADB Server
#adb_dialog.refresh=Refresh
#adb_dialog.tip_devices=%d devices
#adb_dialog.device_node=Device
#adb_dialog.missing_path=Must provide the ADB path to start an ADB server.
#adb_dialog.waiting=Waiting to connect to ADB server...
#adb_dialog.connecting=Connecting to ADB server, addr: %s:%s...
#adb_dialog.connect_okay=ADB server connected, addr: %s:%s
#adb_dialog.connect_fail=Failed to connect to ADB server.
#adb_dialog.disconnected=ADB server disconnected.
#adb_dialog.start_okay=ADB server started on port: %s.
#adb_dialog.start_fail=Failed to start ADB server on port: %s!
#adb_dialog.forward_fail=Failed to forward for some reasons.
#adb_dialog.being_debugged_msg=This process seems like it's being debugged, should we proceed?"
#adb_dialog.unknown_android_ver=Failed to get Android release version, use Android 8 as default?
#adb_dialog.being_debugged_title=It's Debugging by other.
#adb_dialog.init_dbg_fail=Failed to init debugger.
#adb_dialog.msg_read_mani_fail=Failed to decode AndroidManifest.xml
#adb_dialog.no_devices=Can't found any device to start app.
#adb_dialog.restart_while_debugging_title=Restart while debugging
#adb_dialog.restart_while_debugging_msg=You're debugging an app, are you sure to restart a session?
#adb_dialog.starting_debugger=Starting debugger...
@@ -220,3 +220,52 @@ apkSignature.errors=错误
apkSignature.warnings=警告
apkSignature.exception=APK 验证失败
apkSignature.unprotectedEntry=不受签名保护的文件。不会检测对此 JAR 条目的未经授权的修改。
#debugger.process_selector=Select a process to debug
#debugger.step_into=Step Into (F7)
#debugger.step_over=Step Over (F8)
#debugger.step_out=Step Out (Shift + F8)
#debugger.run=Run (F9)
#debugger.stop=Stop debugger and kill app
#debugger.pause=Pause
#debugger.rerun=Rerun
#debugger.cfm_dialog_title=Exit while debugging
#debugger.cfm_dialog_msg=Are you sure to terminate debugger?
#debugger.popup_set_value=Set Value
#debugger.popup_change_to_zero=Change to 0
#debugger.popup_change_to_one=Change to 1
#debugger.popup_copy_value=Copy Value
#set_value_dialog.label_value=Value
#set_value_dialog.btn_set=Set Value
#set_value_dialog.title=Set Value
#set_value_dialog.neg_msg=Failed to set value.
#set_value_dialog.sel_type=Select a type to set value.
#adb_dialog.addr=ADB Addr
#adb_dialog.port=ADB Port
#adb_dialog.path=ADB Path
#adb_dialog.launch_app=Launch App
#adb_dialog.start_server=Start ADB Server
#adb_dialog.refresh=Refresh
#adb_dialog.tip_devices=%d devices
#adb_dialog.device_node=Device
#adb_dialog.missing_path=Must provide the ADB path to start an ADB server.
#adb_dialog.waiting=Waiting to connect to ADB server...
#adb_dialog.connecting=Connecting to ADB server, addr: %s:%s...
#adb_dialog.connect_okay=ADB server connected, addr: %s:%s
#adb_dialog.connect_fail=Failed to connect to ADB server.
#adb_dialog.disconnected=ADB server disconnected.
#adb_dialog.start_okay=ADB server started on port: %s.
#adb_dialog.start_fail=Failed to start ADB server on port: %s!
#adb_dialog.forward_fail=Failed to forward for some reasons.
#adb_dialog.being_debugged_msg=This process seems like it's being debugged, should we proceed?"
#adb_dialog.unknown_android_ver=Failed to get Android release version, use Android 8 as default?
#adb_dialog.being_debugged_title=It's Debugging by other.
#adb_dialog.init_dbg_fail=Failed to init debugger.
#adb_dialog.msg_read_mani_fail=Failed to decode AndroidManifest.xml
#adb_dialog.no_devices=Can't found any device to start app.
#adb_dialog.restart_while_debugging_title=Restart while debugging
#adb_dialog.restart_while_debugging_msg=You're debugging an app, are you sure to restart a session?
#adb_dialog.starting_debugger=Starting debugger...
Binary file not shown.

After

Width:  |  Height:  |  Size: 700 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 504 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 774 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 813 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 582 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 653 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 685 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 395 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 349 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 516 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 379 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 700 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 504 B

@@ -230,60 +230,4 @@ public class DexOpcodes {
public static final int PACKED_SWITCH_PAYLOAD = 0x0100;
public static final int SPARSE_SWITCH_PAYLOAD = 0x0200;
public static final int FILL_ARRAY_DATA_PAYLOAD = 0x0300;
public static final String MNE_UNUSED = "(unused)";
public static final String[] MNEMONICS = new String[] {
"nop", "move", "move/from16", "move/16", "move-wide",
"move-wide/from16", "move-wide/16", "move-object", "move-object/from16", "move-object/16",
"move-result", "move-result-wide", "move-result-object", "move-exception", "return-void",
"return", "return-wide", "return-object", "const/4", "const/16",
"const", "const/high16", "const-wide/16", "const-wide/32", "const-wide",
"const-wide/high16", "const-string", "const-string/jumbo", "const-class", "monitor-enter",
"monitor-exit", "check-cast", "instance-of", "array-length", "new-instance",
"new-array", "filled-new-array", "filled-new-array/range", "fill-array-data", "throw",
"goto", "goto/16", "goto/32", "packed-switch", "sparse-switch",
"cmpl-float", "cmpg-float", "cmpl-double", "cmpg-double", "cmp-long",
"if-eq", "if-ne", "if-lt", "if-ge", "if-gt",
"if-le", "if-eqz", "if-nez", "if-ltz", "if-gez",
"if-gtz", "if-lez", "(unused)", "(unused)", "(unused)",
"(unused)", "(unused)", "(unused)", "aget", "aget-wide",
"aget-object", "aget-boolean", "aget-byte", "aget-char", "aget-short",
"aput", "aput-wide", "aput-object", "aput-boolean", "aput-byte",
"aput-char", "aput-short", "iget", "iget-wide", "iget-object",
"iget-boolean", "iget-byte", "iget-char", "iget-short", "iput",
"iput-wide", "iput-object", "iput-boolean", "iput-byte", "iput-char",
"iput-short", "sget", "sget-wide", "sget-object", "sget-boolean",
"sget-byte", "sget-char", "sget-short", "sput", "sput-wide",
"sput-object", "sput-boolean", "sput-byte", "sput-char", "sput-short",
"invoke-virtual", "invoke-super", "invoke-direct", "invoke-static", "invoke-interface",
"(unused)", "invoke-virtual/range", "invoke-super/range", "invoke-direct/range", "invoke-static/range",
"invoke-interface/range", "(unused)", "(unused)", "neg-int", "not-int",
"neg-long", "not-long", "neg-float", "neg-double", "int-to-long",
"int-to-float", "int-to-double", "long-to-int", "long-to-float", "long-to-double",
"float-to-int", "float-to-long", "float-to-double", "double-to-int", "double-to-long",
"double-to-float", "int-to-byte", "int-to-char", "int-to-short", "add-int",
"sub-int", "mul-int", "div-int", "rem-int", "and-int",
"or-int", "xor-int", "shl-int", "shr-int", "ushr-int",
"add-long", "sub-long", "mul-long", "div-long", "rem-long",
"and-long", "or-long", "xor-long", "shl-long", "shr-long",
"ushr-long", "add-float", "sub-float", "mul-float", "div-float",
"rem-float", "add-double", "sub-double", "mul-double", "div-double",
"rem-double", "add-int/2addr", "sub-int/2addr", "mul-int/2addr", "div-int/2addr",
"rem-int/2addr", "and-int/2addr", "or-int/2addr", "xor-int/2addr", "shl-int/2addr",
"shr-int/2addr", "ushr-int/2addr", "add-long/2addr", "sub-long/2addr", "mul-long/2addr",
"div-long/2addr", "rem-long/2addr", "and-long/2addr", "or-long/2addr", "xor-long/2addr",
"shl-long/2addr", "shr-long/2addr", "ushr-long/2addr", "add-float/2addr", "sub-float/2addr",
"mul-float/2addr", "div-float/2addr", "rem-float/2addr", "add-double/2addr", "sub-double/2addr",
"mul-double/2addr", "div-double/2addr", "rem-double/2addr", "add-int/lit16", "rsub-int",
"mul-int/lit16", "div-int/lit16", "rem-int/lit16", "and-int/lit16", "or-int/lit16",
"xor-int/lit16", "add-int/lit8", "rsub-int/lit8", "mul-int/lit8", "div-int/lit8",
"rem-int/lit8", "and-int/lit8", "or-int/lit8", "xor-int/lit8", "shl-int/lit8",
"shr-int/lit8", "ushr-int/lit8", "(unused)", "(unused)", "(unused)",
"(unused)", "(unused)", "(unused)", "(unused)", "(unused)",
"(unused)", "(unused)", "(unused)", "(unused)", "(unused)",
"(unused)", "(unused)", "(unused)", "(unused)", "(unused)",
"(unused)", "(unused)", "(unused)", "(unused)", "(unused)",
"invoke-polymorphic", "invoke-polymorphic/range", "invoke-custom", "invoke-custom/range", "const-method-handle",
"const-method-type" };
}
@@ -13,7 +13,6 @@ import jadx.api.plugins.input.data.IMethodData;
import jadx.api.plugins.input.data.annotations.EncodedValue;
import jadx.api.plugins.input.data.annotations.IAnnotation;
import jadx.plugins.input.dex.sections.annotations.AnnotationsParser;
import jadx.plugins.input.dex.smali.SmaliPrinter;
import jadx.plugins.input.dex.utils.SmaliUtils;
public class DexClassData implements IClassData {
@@ -194,11 +193,6 @@ public class DexClassData implements IClassData {
return SmaliUtils.getSmaliCode(dexBuf, getClassDefOffset());
}
@Override
public String getDisassembledCodeV2() {
return SmaliPrinter.printClass(this);
}
@Override
public String toString() {
return getType();
@@ -1,21 +1,11 @@
package jadx.plugins.input.dex.smali;
import java.util.*;
import java.util.Map.Entry;
import jadx.api.plugins.input.data.*;
import jadx.api.plugins.input.data.annotations.AnnotationVisibility;
import jadx.api.plugins.input.data.annotations.EncodedValue;
import jadx.api.plugins.input.data.annotations.IAnnotation;
import jadx.api.plugins.input.insns.InsnData;
import jadx.api.plugins.input.insns.InsnIndexType;
import jadx.plugins.input.dex.insns.DexOpcodes;
import jadx.plugins.input.dex.insns.payloads.DexSwitchPayload;
import jadx.plugins.input.dex.sections.DexFieldData;
import jadx.plugins.input.dex.sections.DexMethodData;
import jadx.plugins.input.dex.sections.DexMethodRef;
import static jadx.api.plugins.input.data.AccessFlagsScope.FIELD;
import static jadx.api.plugins.input.data.AccessFlagsScope.METHOD;
// TODO: not finished
@@ -48,762 +38,4 @@ public class SmaliPrinter {
codeWriter.startLine(".end method");
return codeWriter.getCode();
}
public static String printClass(IClassData cls) {
SmaliCodeWriter smali = new SmaliCodeWriter();
smali.startLine("Class: " + cls.getType())
.startLine("AccessFlags: " + AccessFlags.format(cls.getAccessFlags(), AccessFlagsScope.CLASS))
.startLine("SuperType: " + cls.getSuperType())
.startLine("Interfaces: " + cls.getInterfacesTypes())
.startLine("SourceFile: " + cls.getSourceFile());
if (cls.getAnnotations().size() > 0) {
smali.startLine().startLine("# annotations");
printAnnotations(smali, cls.getAnnotations());
}
List<Entry<DexFieldData, List<IAnnotation>>> flds = new ArrayList<>();
cls.visitFieldsAndMethods(
f -> {
DexFieldData fld = new DexFieldData(null);
fld.setParentClassType(f.getParentClassType());
fld.setAccessFlags(f.getAccessFlags());
fld.setName(f.getName());
fld.setType(f.getType());
flds.add(new AbstractMap.SimpleEntry<>(fld, f.getAnnotations()));
},
m -> {
if (!flds.isEmpty()) {
printField(smali, flds, cls.getStaticFieldInitValues());
flds.clear();
smali.startLine("# methods");
}
printMethod(smali, m);
});
if (!flds.isEmpty()) { // in case there are no methods.
printField(smali, flds, cls.getStaticFieldInitValues());
flds.clear();
}
return smali.getCode();
}
private static void printField(SmaliCodeWriter smali,
List<Entry<DexFieldData, List<IAnnotation>>> flds,
List<EncodedValue> staticFieldInitValues) {
int staticIdx = 0;
int accessColWidth = 0;
int nameColWidth = 0;
List<String> accesses = new ArrayList<>(flds.size());
for (Entry<DexFieldData, List<IAnnotation>> fld : flds) { // calc width of cols
String temp = fld.getKey().getName();
if (temp.length() > nameColWidth) {
nameColWidth = temp.length();
}
temp = AccessFlags.format(fld.getKey().getAccessFlags(), FIELD);
accesses.add(temp);
if (temp.length() > accessColWidth) {
accessColWidth = temp.length();
}
}
smali.startLine().startLine("# fields");
String whites = new String(new byte[Math.max(accessColWidth, nameColWidth)]).replace("\0", " ");
for (int i = 0; i < flds.size(); i++) {
smali.startLine();
Entry<DexFieldData, List<IAnnotation>> fld = flds.get(i);
String access = accesses.get(i);
int pad = accessColWidth - access.length();
if (pad > 0) {
access += whites.substring(0, pad);
}
smali.add(".field ").add(access);
String name = fld.getKey().getName();
pad = nameColWidth - name.length();
if (pad > 0) {
name += whites.substring(0, pad);
}
smali.add(name).add(" ");
smali.add(": ").add(fld.getKey().getType());
if ((fld.getKey().getAccessFlags() & AccessFlags.STATIC) != 0) { // static field
if (staticIdx < staticFieldInitValues.size()) {
smali.add(" # init val = ");
printEncodedValue(smali, staticFieldInitValues.get(staticIdx++), false);
}
}
smali.incIndent();
printAnnotations(smali, fld.getValue());
smali.decIndent();
}
smali.startLine();
}
private static void printMethod(SmaliCodeWriter smali, IMethodData mth) {
smali.startLine()
.startLine(mth.isDirect() ? "# direct method" : " # virtual method")
.startLine(".method ");
printMethodDef(smali, mth);
smali.incIndent();
ICodeReader codeReader = mth.getCodeReader();
if (codeReader != null) {
smali.startLine(".registers ")
.add(codeReader.getRegistersCount())
.startLine();
Map<Integer, String> paramMap = formatMthParamInfo(mth, smali, codeReader);
if (paramMap.size() > 0) {
smali.startLine();
}
SmaliGen smaliGen = new SmaliGen(paramMap, codeReader.getDebugInfo(), true, true);
codeReader.visitInstructions(insn -> {
insn.decode();
smaliGen.format(insn);
});
smaliGen.gen(smali);
}
smali.decIndent();
smali.startLine(".end method");
}
private static void printMethodDef(SmaliCodeWriter smali, IMethodData mth) {
smali.add(AccessFlags.format(mth.getAccessFlags(), METHOD));
IMethodRef methodRef = mth.getMethodRef();
methodRef.load();
smali.add(methodRef.getName());
smali.add('(').addArgs(methodRef.getArgTypes()).add(')');
smali.add(methodRef.getReturnType());
if (mth.getAnnotations().size() > 0) {
smali.incIndent();
printAnnotations(smali, mth.getAnnotations());
smali.decIndent();
smali.startLine();
}
}
private static Map<Integer, String> formatMthParamInfo(IMethodData mth, SmaliCodeWriter smali, ICodeReader codeReader) {
List<String> types = mth.getMethodRef().getArgTypes();
if (types.size() == 0) {
return Collections.emptyMap();
}
int i = 0;
int paramCount = 0;
int paramStart = isStaticMethod(mth) ? 0 : 1;
int regNum = getParamStartRegNum(mth);
Map<Integer, String> paramMap = new HashMap<>(types.size());
IDebugInfo dbgInfo = codeReader.getDebugInfo();
if (dbgInfo != null) {
for (ILocalVar var : dbgInfo.getLocalVars()) {
if (var.getStartOffset() == -1) {
smali.startLine(String.format(".param p%d, \"%s\":%s",
paramStart + i, var.getName(), var.getType()));
paramMap.put(regNum + i, "p" + (paramStart + i));
paramCount++;
i += 1;
if (isWideType(var.getType())) {
paramMap.put(regNum + i, "p" + (paramStart + i));
i += 1;
}
}
}
if (paramCount + 1 == types.size()) {
return paramMap;
}
}
for (; paramCount < types.size(); paramCount++) {
String type = types.get(paramCount);
smali.startLine(String.format(".param p%d, \"\":%s", paramStart + i, type));
paramMap.put(regNum + i, "p" + (paramStart + i));
i += 1;
if (isWideType(type)) {
paramMap.put(regNum + i, "p" + (paramStart + i));
i += 1;
}
}
return paramMap;
}
private static int getParamStartRegNum(IMethodData mth) {
ICodeReader codeReader = mth.getCodeReader();
if (codeReader != null) {
int startNum = codeReader.getRegistersCount();
if (startNum > 0) {
for (String argType : mth.getMethodRef().getArgTypes()) {
if (isWideType(argType)) {
startNum -= 2;
} else {
startNum -= 1;
}
}
if (!isStaticMethod(mth)) {
startNum--;
}
return startNum;
}
}
return -1;
}
private static boolean isWideType(String type) {
return type.equals("D") || type.equals("J");
}
private static boolean isStaticMethod(IMethodData mth) {
return (mth.getAccessFlags() & AccessFlags.STATIC) != 0;
}
private static void printAnnotations(SmaliCodeWriter smali, List<IAnnotation> annoList) {
if (annoList.size() > 0) {
for (int i = 0; i < annoList.size(); i++) {
smali.startLine();
printAnnotation(smali, annoList.get(i));
if (i != annoList.size() - 1) {
smali.startLine();
}
}
}
}
private static void printAnnotation(SmaliCodeWriter smali, IAnnotation anno) {
smali.add(".annotation")
.add(" ");
AnnotationVisibility vby = anno.getVisibility();
if (vby != null) {
smali.add(vby.toString().toLowerCase()).add(" ");
}
smali.add(anno.getAnnotationClass());
anno.getValues().forEach((k, v) -> {
smali.incIndent();
smali.startLine(k).add(" = ");
printEncodedValue(smali, v, true);
smali.decIndent();
});
smali.startLine(".end annotation");
}
private static void printEncodedValue(SmaliCodeWriter smali, EncodedValue value, boolean wrapArray) {
switch (value.getType()) {
case ENCODED_ARRAY:
smali.add("{");
if (wrapArray) {
smali.incIndent();
smali.startLine();
}
List<EncodedValue> values = (List<EncodedValue>) value.getValue();
for (int i = 0; i < values.size(); i++) {
printEncodedValue(smali, values.get(i), wrapArray);
if (i != values.size() - 1) {
smali.add(",");
if (wrapArray) {
smali.startLine();
} else {
smali.add(" ");
}
}
}
if (wrapArray) {
smali.decIndent();
smali.startLine("}");
}
break;
case ENCODED_STRING:
smali.add("\"").add(value.getValue()).add("\"");
break;
case ENCODED_NULL:
smali.add("null");
break;
case ENCODED_ANNOTATION:
printAnnotation(smali, (IAnnotation) value.getValue());
break;
default:
smali.add(value.getValue());
}
}
private static final int CODE_OFFSET_COLUMN_WIDTH = 4;
private static final int BYTECODE_COLUMN_WIDTH = 20 + 3; // 3 for ellipses.
private static final String FMT_BYTECODE_COL = "%-" + (BYTECODE_COLUMN_WIDTH - 3) + "s";
private static final int INSN_COL_WIDTH = "const-method-handle".length();
private static final String FMT_INSN_COL = "%-" + INSN_COL_WIDTH + "s";
private static final String FMT_FILE_OFFSET = "%08x:";
private static final String FMT_CODE_OFFSET = "%04x:";
private static final String FMT_TARGET_OFFSET = "%04x";
private static final String FMT_GOTO = ":goto_" + FMT_TARGET_OFFSET;
private static final String FMT_COND = ":cond_" + FMT_TARGET_OFFSET;
private static final String FMT_DATA = ":data_" + FMT_TARGET_OFFSET;
private static final String FMT_P_SWITCH = ":p_switch_" + FMT_TARGET_OFFSET;
private static final String FMT_S_SWITCH = ":s_switch_" + FMT_TARGET_OFFSET;
private static final String FMT_P_SWITCH_CASE = ":p_case_" + FMT_TARGET_OFFSET;
private static final String FMT_S_SWITCH_CASE = ":s_case_" + FMT_TARGET_OFFSET;
private static final String FMT_GOTO_TAG = "goto_" + FMT_TARGET_OFFSET + ":";
private static final String FMT_COND_TAG = "cond_" + FMT_TARGET_OFFSET + ":";
private static final String FMT_DATA_TAG = "data_" + FMT_TARGET_OFFSET + ":";
private static final String FMT_P_SWITCH_TAG = "p_switch_" + FMT_TARGET_OFFSET + ":";
private static final String FMT_S_SWITCH_TAG = "s_switch_" + FMT_TARGET_OFFSET + ":";
private static final String FMT_P_SWITCH_CASE_TAG = "p_case_" + FMT_TARGET_OFFSET + ":";
private static final String FMT_S_SWITCH_CASE_TAG = "s_case_" + FMT_TARGET_OFFSET + ":";
public static class SmaliGen {
static class SmaliLine {
Object line;
List<Entry<String, String>> tips = Collections.emptyList();
void setLine(String str) {
line = str;
}
void addLine(String str) {
if (!(line instanceof List)) {
line = new ArrayList<String>();
}
((ArrayList<String>) this.line).add(str);
}
void addLineTip(String tip, String extra) {
if (tips.isEmpty()) {
tips = new ArrayList<>();
}
tips.add(new AbstractMap.SimpleEntry<>(tip, extra));
}
private void fmtLineTip(int lineOffset, SmaliCodeWriter smali) {
for (Entry<String, String> tip : tips) {
int start = Math.max(0, lineOffset - tip.getKey().length());
if (start > 0) {
smali.add(new String(new byte[start]).replace("\0", " "));
}
smali.add(tip.getKey() + tip.getValue()).startLine();
}
}
private void gen(int lineOffset, SmaliCodeWriter smali) {
fmtLineTip(lineOffset, smali);
if (line instanceof List) {
int size = ((List<String>) line).size();
for (int i = 0; i < size; i++) {
smali.add(((List<String>) line).get(i));
if (i != size - 1) {
smali.startLine();
}
}
} else {
smali.add(line);
}
}
}
StringBuilder lineWriter = new StringBuilder(50);
Map<Integer, SmaliLine> targetMap = new HashMap<>();
Map<Integer, Integer> payloadOffsetMap = new HashMap<>();
Map<Integer, String> paramMap;
List<SmaliLine> smaliList = new ArrayList<>();
boolean fileOffset;
boolean bytecode;
boolean hasDbgInfo;
/**
* @param fileOffset adds file offset column to smali output
* @param bytecode adds bytecode column to smali output
*/
public SmaliGen(Map<Integer, String> paramMap, IDebugInfo dbgInfo,
boolean fileOffset, boolean bytecode) {
this.fileOffset = fileOffset;
this.bytecode = bytecode;
this.paramMap = paramMap;
this.hasDbgInfo = dbgInfo != null;
if (hasDbgInfo) {
fmtDbgInfo(dbgInfo);
}
}
private boolean isParamReg(int regNum) {
return paramMap.containsKey(regNum);
}
private String getRegName(int regNum) {
String text = paramMap.get(regNum);
if (text == null || text.isEmpty()) {
return "v" + regNum;
}
return text;
}
public void gen(SmaliCodeWriter smali) {
removeDupTips();
int lineOffset = getInsnColStart();
for (SmaliLine smaliLine : smaliList) {
smali.startLine();
smaliLine.gen(lineOffset, smali);
}
}
public void format(InsnData insnData) {
SmaliLine line = targetMap.computeIfAbsent(insnData.getOffset(), k -> new SmaliLine());
smaliList.add(line);
fmt(insnData, line);
}
private void fmt(InsnData insn, SmaliLine line) {
fmtCols(insn);
if (!fmtPayloadInsn(insn, line)) {
fmtInsn(insn);
line.line = lineWriter.toString();
}
lineWriter.delete(0, lineWriter.length());
}
private void fmtDbgInfo(IDebugInfo dbgInfo) {
dbgInfo.getSourceLineMapping().forEach((codeOffset, srcLine) -> {
if (codeOffset > -1) {
SmaliLine line = targetMap.computeIfAbsent(codeOffset, k -> new SmaliLine());
line.addLineTip(String.format(".line %d", srcLine), "");
}
});
for (ILocalVar localVar : dbgInfo.getLocalVars()) {
if (localVar.getStartOffset() > -1) {
SmaliLine line = targetMap.computeIfAbsent(localVar.getStartOffset(), k -> new SmaliLine());
line.addLineTip(String.format(".local v%d", localVar.getRegNum()),
String.format(", \"%s\":%s", localVar.getName(), localVar.getType()));
}
if (localVar.getEndOffset() > -1) {
if (isParamReg(localVar.getRegNum())) {
return; // no need to add .end local for parameters.
}
SmaliLine line = targetMap.computeIfAbsent(localVar.getEndOffset(), k -> new SmaliLine());
line.addLineTip(String.format(".end local v%d", localVar.getRegNum()),
String.format(" # \"%s\":%s", localVar.getName(), localVar.getType()));
}
}
}
private void fmtInsn(InsnData insn) {
int opcode = insn.getRawOpcodeUnit();
opcode = opcode & 0xff;
String mne = DexOpcodes.MNEMONICS[opcode];
lineWriter.append(String.format(FMT_INSN_COL, mne)).append(" ");
fmtRegs(opcode, insn, lineWriter);
if (hasTarget(opcode)) {
if (isGotoIns(opcode)) {
lineWriter.append(String.format(FMT_GOTO, insn.getTarget()));
addTarget(FMT_GOTO_TAG, insn.getTarget());
return;
}
lineWriter.append(", ");
if (isConditionIns(opcode)) {
lineWriter.append(String.format(FMT_COND, insn.getTarget()));
addTarget(FMT_COND_TAG, insn.getTarget());
} else if (opcode == DexOpcodes.PACKED_SWITCH) {
payloadOffsetMap.put(insn.getTarget(), insn.getOffset());
lineWriter.append(String.format(FMT_P_SWITCH, insn.getTarget()));
addTarget(FMT_P_SWITCH_TAG, insn.getTarget());
} else if (opcode == DexOpcodes.SPARSE_SWITCH) {
payloadOffsetMap.put(insn.getTarget(), insn.getOffset());
lineWriter.append(String.format(FMT_S_SWITCH, insn.getTarget()));
addTarget(FMT_S_SWITCH_TAG, insn.getTarget());
} else {
lineWriter.append(String.format(FMT_DATA, insn.getTarget()));
addTarget(FMT_DATA_TAG, insn.getTarget());
}
return;
}
if (isInvokeIns(opcode)) {
lineWriter.append(", ").append(method(insn));
return;
}
if (insn.getIndexType() == InsnIndexType.TYPE_REF) {
lineWriter.append(", ").append(type(insn));
return;
}
if (insn.getIndexType() == InsnIndexType.FIELD_REF) {
lineWriter.append(", ").append(field(insn));
return;
}
if (insn.getIndexType() == InsnIndexType.STRING_REF) {
lineWriter.append(", ").append(str(insn));
return;
}
if (hasLiteral(opcode)) {
lineWriter.append(", ").append(literal(insn, opcode));
return;
}
if (opcode == DexOpcodes.CONST_METHOD_HANDLE) {
lineWriter.append(", ").append(methodHandle(insn));
return;
}
if (opcode == DexOpcodes.CONST_METHOD_TYPE) {
lineWriter.append(", ").append(proto(insn, insn.getIndex()));
return;
}
}
private void addTarget(String fmtTag, int target) {
addTarget(fmtTag, target, "");
}
private void addTarget(String fmtTag, int target, String extraTip) {
targetMap.computeIfAbsent(target, k -> new SmaliLine())
.addLineTip(String.format(fmtTag, target), extraTip);
}
private void fmtRegs(int opcode, InsnData insn, StringBuilder smali) {
boolean appendBrace = isRegList(opcode);
if (appendBrace) {
smali.append("{");
}
if (isRangeRegIns(opcode)) {
smali.append(getRegName(insn.getReg(0)))
.append(" .. ")
.append(getRegName(insn.getReg(insn.getRegsCount() - 1)));
} else if (insn.getRegsCount() > 0) {
for (int i = 0; i < insn.getRegsCount(); i++) {
if (i > 0) {
smali.append(", ");
}
smali.append(getRegName(insn.getReg(i)));
}
}
if (appendBrace) {
smali.append("}");
}
}
private boolean fmtPayloadInsn(InsnData insn, SmaliLine line) {
int opcode = insn.getRawOpcodeUnit();
if (opcode == DexOpcodes.PACKED_SWITCH_PAYLOAD) {
lineWriter.append("packed-switch-payload");
line.addLine(lineWriter.toString());
DexSwitchPayload payload = (DexSwitchPayload) insn.getPayload();
if (payload != null) {
fmtSwitchPayload(FMT_P_SWITCH_CASE, FMT_P_SWITCH_CASE_TAG, line, payload, insn.getOffset());
}
return true;
}
if (opcode == DexOpcodes.SPARSE_SWITCH_PAYLOAD) {
lineWriter.append("sparse-switch-payload");
line.addLine(lineWriter.toString());
DexSwitchPayload payload = (DexSwitchPayload) insn.getPayload();
if (payload != null) {
fmtSwitchPayload(FMT_S_SWITCH_CASE, FMT_S_SWITCH_CASE_TAG, line, payload, insn.getOffset());
}
return true;
}
if (opcode == DexOpcodes.FILL_ARRAY_DATA_PAYLOAD) {
lineWriter.append("fill-array-data-payload");
line.setLine(lineWriter.toString());
return true;
}
return false;
}
private void fmtSwitchPayload(String fmtTarget, String fmtTag, SmaliLine line,
DexSwitchPayload payload, int curOffset) {
int lineStart = getInsnColStart();
lineStart += CODE_OFFSET_COLUMN_WIDTH + 1 + 1; // plus 1s for space and the ':'
String basicIndent = new String(new byte[lineStart]).replace("\0", " ");
String indent = SmaliCodeWriter.INDENT_STR + basicIndent;
int[] keys = payload.getKeys();
int[] targets = payload.getTargets();
int opcodeOffset = payloadOffsetMap.get(curOffset);
for (int i = 0; i < keys.length; i++) {
int target = opcodeOffset + targets[i];
line.addLine(String.format("%scase %d: -> " + fmtTarget, indent, keys[i], target));
addTarget(fmtTag, target, String.format(" # case %d", keys[i]));
}
line.addLine(basicIndent + ".end payload");
}
private void removeDupTips() {
List<Entry<Integer, Entry<String, String>>> dbgLines = null; // line num: tip
if (hasDbgInfo) {
dbgLines = new ArrayList<>();
}
for (int i = 0; i < smaliList.size(); i++) {
SmaliLine line = smaliList.get(i);
Map<String, Integer> tipSet = Collections.emptyMap(); // tip: reference count
for (Iterator<Entry<String, String>> it = line.tips.iterator(); it.hasNext();) {
Entry<String, String> tip = it.next();
if (hasDbgInfo && removeDupSourceLine(tip, i, dbgLines)) { // debug info source line.
it.remove();
continue;
}
if (tipSet.containsKey(tip.getKey())) { // remove dup tips like cond_:/goto_:.
it.remove();
tipSet.computeIfPresent(tip.getKey(), (k, v) -> v + 1);
} else {
if (tipSet.isEmpty()) {
tipSet = new HashMap<>();
}
tipSet.computeIfAbsent(tip.getKey(), k -> 1);
}
}
tipSet.forEach((k, v) -> {
if (v > 1) {
for (int j = 0; j < line.tips.size(); j++) {
if (line.tips.get(j).getKey().equals(k)) {
line.tips.set(j, new AbstractMap.SimpleEntry<>(k, " # " + v + " refs"));
}
}
}
});
}
}
private boolean removeDupSourceLine(Entry<String, String> tip, int i,
List<Entry<Integer, Entry<String, String>>> dbgLines) {
boolean removeIt = false;
if (tip.getKey().startsWith(".line ")) { // debug info source line.
if (dbgLines.size() > 0) {
Entry<Integer, Entry<String, String>> entry = dbgLines.get(dbgLines.size() - 1);
if (i - entry.getKey() == 1 && entry.getValue().getKey().equals(tip.getKey())) {
removeIt = true; // duplicated.
}
}
dbgLines.add(new AbstractMap.SimpleEntry<>(i, tip));
}
return removeIt;
}
private int getInsnColStart() {
int start = 0;
if (fileOffset) {
start += 8 + 1 + 1; // plus 1s for space and the ':'
}
if (bytecode) {
start += BYTECODE_COLUMN_WIDTH + 1; // plus 1 for space
}
return start;
}
private void fmtCols(InsnData insn) {
if (fileOffset) {
lineWriter.append(String.format(FMT_FILE_OFFSET + " ", insn.getFileOffset()));
}
if (bytecode) {
formatByteCode(lineWriter, insn.getByteCode());
lineWriter.append(" ");
lineWriter.append(String.format(FMT_CODE_OFFSET + " ", insn.getOffset()));
}
}
private static void formatByteCode(StringBuilder smali, byte[] bytes) {
int maxLen = Math.min(bytes.length, 4 * 2); // limit to 4 units
StringBuilder inHex = new StringBuilder();
for (int i = 0; i < maxLen; i++) {
int temp = ((bytes[i++] & 0xff) << 8) | (bytes[i] & 0xff);
inHex.append(String.format("%04x ", temp));
}
smali.append(String.format(FMT_BYTECODE_COL, inHex));
if (maxLen < bytes.length) {
smali.append("...");
} else {
smali.append(" ");
}
}
private static String literal(InsnData insn, int opcode) {
long it = insn.getLiteral();
String tip = "";
if (it > Integer.MAX_VALUE) {
if (isWideIns(opcode)) {
tip = " # double: " + Double.longBitsToDouble(it);
} else if (opcode == DexOpcodes.CONST_HIGH16) {
tip = " # float: " + Float.intBitsToFloat((int) it);
}
} else if (it <= 0) {
return "" + it + tip;
}
return "0x" + Long.toHexString(it) + tip;
}
private static String str(InsnData insn) {
return String.format("\"%s\" # string@%04x",
insn.getIndexAsString()
.replace("\n", "\\n")
.replace("\t", "\\t"),
insn.getIndex());
}
private static String type(InsnData insn) {
return String.format("%s # type@%04x", insn.getIndexAsType(), insn.getIndex());
}
private static String field(InsnData insn) {
return String.format("%s # field@%04x", insn.getIndexAsField().toString(), insn.getIndex());
}
private static String method(InsnData insn) {
int rawOpcodeUnit = insn.getRawOpcodeUnit();
int opcode = rawOpcodeUnit & 0xFF;
if (opcode == DexOpcodes.INVOKE_CUSTOM || opcode == DexOpcodes.INVOKE_CUSTOM_RANGE) {
insn.getIndexAsCallSite().load();
return String.format("%s # call_site@%04x", insn.getIndexAsCallSite().toString(), insn.getIndex());
}
IMethodRef mthRef = insn.getIndexAsMethod();
mthRef.load();
if (opcode == DexOpcodes.INVOKE_POLYMORPHIC || opcode == DexOpcodes.INVOKE_POLYMORPHIC_RANGE) {
return String.format("%s, %s # method@%04x, proto@%04x",
mthRef.toString(), insn.getIndexAsProto(insn.getTarget()).toString(),
insn.getIndex(), insn.getTarget());
}
return String.format("%s # method@%04x", mthRef.toString(), insn.getIndex());
}
private static String proto(InsnData insn, int protoIndex) {
return String.format("%s # proto@%04x", insn.getIndexAsProto(protoIndex).toString(), protoIndex);
}
private static String methodHandle(InsnData insn) {
return String.format("%s # method_handle@%04x",
insn.getIndexAsMethodHandle().toString(), insn.getIndex());
}
private static boolean isGotoIns(int opcode) {
return opcode >= DexOpcodes.GOTO && opcode <= DexOpcodes.GOTO_32;
}
private static boolean isInvokeIns(int opcode) {
return (opcode >= DexOpcodes.INVOKE_VIRTUAL && opcode <= DexOpcodes.INVOKE_INTERFACE)
|| (opcode >= DexOpcodes.INVOKE_VIRTUAL_RANGE && opcode <= DexOpcodes.INVOKE_INTERFACE_RANGE)
|| (opcode >= DexOpcodes.INVOKE_POLYMORPHIC && opcode <= DexOpcodes.INVOKE_CUSTOM_RANGE);
}
private static boolean isRangeRegIns(int opcode) {
if (opcode >= DexOpcodes.INVOKE_VIRTUAL_RANGE && opcode <= DexOpcodes.INVOKE_INTERFACE_RANGE) {
return true;
}
switch (opcode) {
case DexOpcodes.FILLED_NEW_ARRAY_RANGE:
case DexOpcodes.INVOKE_CUSTOM_RANGE:
case DexOpcodes.INVOKE_POLYMORPHIC_RANGE:
return true;
}
return false;
}
private static boolean isWideIns(int opcode) {
return (opcode >= DexOpcodes.CONST_WIDE_16 && opcode <= DexOpcodes.CONST_WIDE_HIGH16);
}
private static boolean hasLiteral(int opcode) {
return (opcode >= DexOpcodes.CONST_4 && opcode <= DexOpcodes.CONST_WIDE_HIGH16)
|| (opcode >= DexOpcodes.ADD_INT_LIT16 && opcode <= DexOpcodes.USHR_INT_LIT8);
}
private static boolean isConditionIns(int opcode) {
return opcode >= DexOpcodes.IF_EQ && opcode <= DexOpcodes.IF_LEZ;
}
private static boolean hasTarget(int opcode) {
return (opcode >= DexOpcodes.IF_EQ && opcode <= DexOpcodes.IF_LEZ)
|| (opcode >= DexOpcodes.GOTO && opcode <= DexOpcodes.SPARSE_SWITCH)
|| (opcode == DexOpcodes.FILL_ARRAY_DATA);
}
private static boolean isRegList(int opcode) {
return isInvokeIns(opcode)
|| (opcode >= DexOpcodes.FILLED_NEW_ARRAY && opcode <= DexOpcodes.FILLED_NEW_ARRAY_RANGE);
}
}
}
@@ -31,6 +31,4 @@ public interface IClassData {
List<IAnnotation> getAnnotations();
String getDisassembledCode();
String getDisassembledCodeV2();
}