From 4705194a1d96c54f584d013ff4fb57e530323680 Mon Sep 17 00:00:00 2001 From: LBJ-the-GOAT <66319139+LBJ-the-GOAT@users.noreply.github.com> Date: Sun, 28 Mar 2021 18:23:07 +0800 Subject: [PATCH] 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 --- .../main/java/jadx/api/JadxDecompiler.java | 34 + .../core/dex/instructions/InsnDecoder.java | 2 +- .../java/jadx/core/dex/nodes/ClassNode.java | 23 +- .../java/jadx/core/utils/StringUtils.java | 7 + jadx-gui/build.gradle | 3 +- .../jadx/gui/device/debugger/ArtAdapter.java | 55 + .../device/debugger/BreakpointManager.java | 206 +++ .../jadx/gui/device/debugger/DbgUtils.java | 167 ++ .../gui/device/debugger/DebugController.java | 1475 +++++++++++++++ .../gui/device/debugger/RegisterObserver.java | 121 ++ .../gui/device/debugger/SmaliDebugger.java | 1592 +++++++++++++++++ .../gui/device/debugger/smali/MNEMONIC.java | 57 + .../device/debugger/smali/RegisterInfo.java | 14 + .../jadx/gui/device/debugger/smali/Smali.java | 998 +++++++++++ .../debugger/smali/SmaliMethodNode.java | 104 ++ .../device/debugger/smali/SmaliRegister.java | 74 + .../device/debugger/smali/SmaliWriter.java | 54 + .../java/jadx/gui/device/protocol/ADB.java | 632 +++++++ .../java/jadx/gui/settings/JadxSettings.java | 59 + .../main/java/jadx/gui/treemodel/JClass.java | 4 - .../src/main/java/jadx/gui/ui/ADBDialog.java | 680 +++++++ .../java/jadx/gui/ui/IDebugController.java | 36 + .../main/java/jadx/gui/ui/JDebuggerPanel.java | 550 ++++++ .../src/main/java/jadx/gui/ui/MainWindow.java | 63 +- .../main/java/jadx/gui/ui/SetValueDialog.java | 140 ++ .../src/main/java/jadx/gui/ui/TabbedPane.java | 30 + .../java/jadx/gui/ui/VarTreePopupMenu.java | 95 + .../ui/codearea/ClassCodeContentPanel.java | 4 + .../java/jadx/gui/ui/codearea/CodePanel.java | 9 +- .../java/jadx/gui/ui/codearea/SmaliArea.java | 444 +++-- .../main/java/jadx/gui/utils/ObjectPool.java | 34 + .../src/main/java/jadx/gui/utils/UiUtils.java | 5 + .../resources/i18n/Messages_de_DE.properties | 49 + .../resources/i18n/Messages_en_US.properties | 51 +- .../resources/i18n/Messages_es_ES.properties | 49 + .../resources/i18n/Messages_ko_KR.properties | 49 + .../resources/i18n/Messages_zh_CN.properties | 49 + .../main/resources/icons-16/breakpoint.png | Bin 0 -> 700 bytes .../icons-16/breakpoint_disabled.png | Bin 0 -> 504 bytes .../src/main/resources/icons-16/debugger.png | Bin 0 -> 774 bytes .../src/main/resources/icons-16/device.png | Bin 0 -> 813 bytes .../src/main/resources/icons-16/pause.png | Bin 0 -> 582 bytes .../src/main/resources/icons-16/process.png | Bin 0 -> 653 bytes .../src/main/resources/icons-16/rerun.png | Bin 0 -> 685 bytes jadx-gui/src/main/resources/icons-16/run.png | Bin 0 -> 395 bytes .../src/main/resources/icons-16/step_into.png | Bin 0 -> 349 bytes .../src/main/resources/icons-16/step_out.png | Bin 0 -> 516 bytes .../src/main/resources/icons-16/step_over.png | Bin 0 -> 379 bytes jadx-gui/src/main/resources/icons-16/stop.png | Bin 0 -> 700 bytes .../src/main/resources/icons-16/stop_gray.png | Bin 0 -> 504 bytes .../plugins/input/dex/insns/DexOpcodes.java | 56 - .../input/dex/sections/DexClassData.java | 6 - .../plugins/input/dex/smali/SmaliPrinter.java | 768 -------- .../api/plugins/input/data/IClassData.java | 2 - 54 files changed, 7886 insertions(+), 964 deletions(-) create mode 100644 jadx-gui/src/main/java/jadx/gui/device/debugger/ArtAdapter.java create mode 100644 jadx-gui/src/main/java/jadx/gui/device/debugger/BreakpointManager.java create mode 100644 jadx-gui/src/main/java/jadx/gui/device/debugger/DbgUtils.java create mode 100644 jadx-gui/src/main/java/jadx/gui/device/debugger/DebugController.java create mode 100644 jadx-gui/src/main/java/jadx/gui/device/debugger/RegisterObserver.java create mode 100644 jadx-gui/src/main/java/jadx/gui/device/debugger/SmaliDebugger.java create mode 100644 jadx-gui/src/main/java/jadx/gui/device/debugger/smali/MNEMONIC.java create mode 100644 jadx-gui/src/main/java/jadx/gui/device/debugger/smali/RegisterInfo.java create mode 100644 jadx-gui/src/main/java/jadx/gui/device/debugger/smali/Smali.java create mode 100644 jadx-gui/src/main/java/jadx/gui/device/debugger/smali/SmaliMethodNode.java create mode 100644 jadx-gui/src/main/java/jadx/gui/device/debugger/smali/SmaliRegister.java create mode 100644 jadx-gui/src/main/java/jadx/gui/device/debugger/smali/SmaliWriter.java create mode 100644 jadx-gui/src/main/java/jadx/gui/device/protocol/ADB.java create mode 100644 jadx-gui/src/main/java/jadx/gui/ui/ADBDialog.java create mode 100644 jadx-gui/src/main/java/jadx/gui/ui/IDebugController.java create mode 100644 jadx-gui/src/main/java/jadx/gui/ui/JDebuggerPanel.java create mode 100644 jadx-gui/src/main/java/jadx/gui/ui/SetValueDialog.java create mode 100644 jadx-gui/src/main/java/jadx/gui/ui/VarTreePopupMenu.java create mode 100644 jadx-gui/src/main/java/jadx/gui/utils/ObjectPool.java create mode 100644 jadx-gui/src/main/resources/icons-16/breakpoint.png create mode 100644 jadx-gui/src/main/resources/icons-16/breakpoint_disabled.png create mode 100644 jadx-gui/src/main/resources/icons-16/debugger.png create mode 100644 jadx-gui/src/main/resources/icons-16/device.png create mode 100644 jadx-gui/src/main/resources/icons-16/pause.png create mode 100644 jadx-gui/src/main/resources/icons-16/process.png create mode 100644 jadx-gui/src/main/resources/icons-16/rerun.png create mode 100644 jadx-gui/src/main/resources/icons-16/run.png create mode 100644 jadx-gui/src/main/resources/icons-16/step_into.png create mode 100644 jadx-gui/src/main/resources/icons-16/step_out.png create mode 100644 jadx-gui/src/main/resources/icons-16/step_over.png create mode 100644 jadx-gui/src/main/resources/icons-16/stop.png create mode 100644 jadx-gui/src/main/resources/icons-16/stop_gray.png diff --git a/jadx-core/src/main/java/jadx/api/JadxDecompiler.java b/jadx-core/src/main/java/jadx/api/JadxDecompiler.java index bab7a9964..b0cba377e 100644 --- a/jadx-core/src/main/java/jadx/api/JadxDecompiler.java +++ b/jadx-core/src/main/java/jadx/api/JadxDecompiler.java @@ -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)) { diff --git a/jadx-core/src/main/java/jadx/core/dex/instructions/InsnDecoder.java b/jadx-core/src/main/java/jadx/core/dex/instructions/InsnDecoder.java index c9dd3d93c..bed5046dd 100644 --- a/jadx-core/src/main/java/jadx/core/dex/instructions/InsnDecoder.java +++ b/jadx-core/src/main/java/jadx/core/dex/instructions/InsnDecoder.java @@ -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); diff --git a/jadx-core/src/main/java/jadx/core/dex/nodes/ClassNode.java b/jadx-core/src/main/java/jadx/core/dex/nodes/ClassNode.java index aad6a7b13..30c9c8457 100644 --- a/jadx-core/src/main/java/jadx/core/dex/nodes/ClassNode.java +++ b/jadx-core/src/main/java/jadx/core/dex/nodes/ClassNode.java @@ -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 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() { diff --git a/jadx-core/src/main/java/jadx/core/utils/StringUtils.java b/jadx-core/src/main/java/jadx/core/utils/StringUtils.java index 73311a3f0..57d08503c 100644 --- a/jadx-core/src/main/java/jadx/core/utils/StringUtils.java +++ b/jadx-core/src/main/java/jadx/core/utils/StringUtils.java @@ -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()); + } } diff --git a/jadx-gui/build.gradle b/jadx-gui/build.gradle index 703073f7e..1e7d1ec92 100644 --- a/jadx-gui/build.gradle +++ b/jadx-gui/build.gradle @@ -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 { diff --git a/jadx-gui/src/main/java/jadx/gui/device/debugger/ArtAdapter.java b/jadx-gui/src/main/java/jadx/gui/device/debugger/ArtAdapter.java new file mode 100644 index 000000000..b1552ab21 --- /dev/null +++ b/jadx-gui/src/main/java/jadx/gui/device/debugger/ArtAdapter.java @@ -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"; + } + } +} diff --git a/jadx-gui/src/main/java/jadx/gui/device/debugger/BreakpointManager.java b/jadx-gui/src/main/java/jadx/gui/device/debugger/BreakpointManager.java new file mode 100644 index 000000000..23f172b07 --- /dev/null +++ b/jadx-gui/src/main/java/jadx/gui/device/debugger/BreakpointManager.java @@ -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>>() { + }.getType(); + + private static Map> bpm; + private static Path savePath; + private static DebugController debugController; + private static Map> 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 getPositions(JClass topCls) { + List bps = bpm.get(DbgUtils.getRawFullName(topCls)); + if (bps != null && bps.size() > 0) { + Smali smali = DbgUtils.getSmali(topCls.getCls().getClassNode()); + if (smali != null) { + List 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 lineInfo = DbgUtils.getCodeOffsetInfoByLine(topCls, line); + if (lineInfo != null) { + if (bpm.isEmpty()) { + bpm = new HashMap<>(); + } + String name = DbgUtils.getRawFullName(topCls); + List 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 lineInfo = DbgUtils.getCodeOffsetInfoByLine(topCls, line); + if (lineInfo != null) { + List bps = bpm.get(DbgUtils.getRawFullName(topCls)); + for (Iterator 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 getAllBreakpoints() { + List bpList = new ArrayList<>(); + for (Entry> entry : bpm.entrySet()) { + bpList.addAll(entry.getValue()); + } + return bpList; + } + + protected static void failBreakpoint(FileBreakpoint bp) { + Entry 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; + } +} diff --git a/jadx-gui/src/main/java/jadx/gui/device/debugger/DbgUtils.java b/jadx-gui/src/main/java/jadx/gui/device/debugger/DbgUtils.java new file mode 100644 index 000000000..982b0f6ad --- /dev/null +++ b/jadx-gui/src/main/java/jadx/gui/device/debugger/DbgUtils.java @@ -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 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 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(" -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(" -1) { + pos = content.lastIndexOf(" -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; + } +} diff --git a/jadx-gui/src/main/java/jadx/gui/device/debugger/DebugController.java b/jadx-gui/src/main/java/jadx/gui/device/debugger/DebugController.java new file mode 100644 index 000000000..8d187cd04 --- /dev/null +++ b/jadx-gui/src/main/java/jadx/gui/device/debugger/DebugController.java @@ -0,0 +1,1475 @@ +package jadx.gui.device.debugger; + +import java.util.*; +import java.util.Map.Entry; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +import javax.swing.tree.DefaultMutableTreeNode; + +import io.reactivex.annotations.NonNull; +import io.reactivex.annotations.Nullable; + +import jadx.core.dex.info.FieldInfo; +import jadx.core.dex.instructions.args.ArgType; +import jadx.core.dex.nodes.ClassNode; +import jadx.core.dex.nodes.FieldNode; +import jadx.core.utils.StringUtils; +import jadx.core.utils.exceptions.JadxRuntimeException; +import jadx.gui.device.debugger.BreakpointManager.FileBreakpoint; +import jadx.gui.device.debugger.SmaliDebugger.*; +import jadx.gui.device.debugger.smali.Smali; +import jadx.gui.device.debugger.smali.SmaliRegister; +import jadx.gui.treemodel.JClass; +import jadx.gui.ui.IDebugController; +import jadx.gui.ui.JDebuggerPanel; +import jadx.gui.ui.JDebuggerPanel.*; + +import static jadx.gui.device.debugger.SmaliDebugger.RuntimeType; + +public final class DebugController implements SmaliDebugger.SuspendListener, IDebugController { + private static final String ONCREATE_SIGNATURE = "onCreate(Landroid/os/Bundle;)V"; + private static final Map TYPE_MAP = new HashMap<>(); + private static final RuntimeType[] POSSIBLE_TYPES = { RuntimeType.OBJECT, RuntimeType.INT, RuntimeType.LONG }; + + private JDebuggerPanel debuggerPanel; + private SmaliDebugger debugger; + private ArtAdapter.Debugger art; + private final CurrentInfo cur = new CurrentInfo(); + + private BreakpointStore bpStore; + private boolean updateAllFldAndReg = false; // update all fields and registers + private ValueTreeNode toBeUpdatedTreeNode; // a field or register number. + private volatile boolean isSuspended = true; + private boolean hasResumed; + private ResumeCmd run; + private ResumeCmd stepOver; + private ResumeCmd stepInto; + private ResumeCmd stepOut; + private StateListener stateListener; + + private final Map regAdaMap = new ConcurrentHashMap<>(); + + private final ExecutorService updateQueue = Executors.newSingleThreadExecutor(); + private final ExecutorService lazyQueue = Executors.newSingleThreadExecutor(); + + /** + * @param args at least 3 elements, host, port and android release version respectively. + */ + @Override + public boolean startDebugger(JDebuggerPanel debuggerPanel, String[] args) { + if (TYPE_MAP.isEmpty()) { + initTypeMap(); + } + this.debuggerPanel = debuggerPanel; + debuggerPanel.resetUI(); + try { + debugger = SmaliDebugger.attach(args[0], Integer.parseInt(args[1]), this); + } catch (SmaliDebuggerException e) { + logErr(e); + return false; + } + art = ArtAdapter.getAdapter(Integer.parseInt(args[2])); + resetAllInfo(); + hasResumed = false; + run = debugger::resume; + stepOver = debugger::stepOver; + stepInto = debugger::stepInto; + stepOut = debugger::stepOut; + stopAtOnCreate(); + if (bpStore == null) { + bpStore = new BreakpointStore(); + } else { + bpStore.reset(); + } + BreakpointManager.setDebugController(this); + initBreakpoints(BreakpointManager.getAllBreakpoints()); + return true; + } + + private void openMainActivityTab(JClass mainActivity) { + String fullID = DbgUtils.getRawFullName(mainActivity) + "." + ONCREATE_SIGNATURE; + Smali smali = DbgUtils.getSmali(mainActivity.getCls().getClassNode()); + int pos = smali.getMethodDefPos(fullID); + int finalPos = Math.max(1, pos); + debuggerPanel.scrollToSmaliLine(mainActivity, finalPos, true); + } + + private void stopAtOnCreate() { + JClass mainActivity = DbgUtils.searchMainActivity(debuggerPanel.getMainWindow()); + if (mainActivity == null) { + debuggerPanel.log("Failed to set breakpoint at onCreate, you have to do it yourself."); + return; + } + lazyQueue.execute(() -> openMainActivityTab(mainActivity)); + String clsSig = DbgUtils.getRawFullName(mainActivity); + try { + long id = debugger.getClassID(clsSig, true); + if (id != -1) { + return; // this app is running, we can't stop at onCreate anymore. + } + debuggerPanel.log(String.format("Breakpoint will set at %s.%s", clsSig, ONCREATE_SIGNATURE)); + debugger.regMethodEntryEventSync(clsSig, ONCREATE_SIGNATURE::equals); + } catch (SmaliDebuggerException e) { + logErr(e, String.format("Failed set breakpoint at %s.%s", clsSig, ONCREATE_SIGNATURE)); + } + } + + @Override + public boolean isSuspended() { + return isSuspended; + } + + @Override + public boolean isDebugging() { + return debugger != null; + } + + @Override + public boolean run() { + return execResumeCmd(run); + } + + @Override + public boolean stepInto() { + return execResumeCmd(stepInto); + } + + @Override + public boolean stepOver() { + return execResumeCmd(stepOver); + } + + @Override + public boolean stepOut() { + return execResumeCmd(stepOut); + } + + @Override + public boolean pause() { + if (isDebugging()) { + try { + debugger.suspend(); + } catch (SmaliDebuggerException e) { + logErr(e); + return false; + } + setDebuggerState(true, false); + resetAllInfo(); + } + return true; + } + + @Override + public boolean stop() { + if (isDebugging()) { + try { + debugger.exit(); + } catch (SmaliDebuggerException e) { + logErr(e); + return false; + } + } + return true; + } + + @Override + public boolean exit() { + if (isDebugging()) { + setDebuggerState(true, true); + stop(); + debugger = null; + } + BreakpointManager.setDebugController(null); + debuggerPanel.getMainWindow().destroyDebuggerPanel(); + debuggerPanel = null; + return true; + } + + /** + * @param type must be one of int, long, float, double, string or object. + */ + @Override + public boolean modifyRegValue(ValueTreeNode valNode, ArgType type, Object value) { + checkType(type, value); + if (isDebugging() && isSuspended()) { + return modifyValueInternal(valNode, castType(type), value); + } + return false; + } + + @Override + public String getProcessName() { + String pkg = DbgUtils.searchPackageName(debuggerPanel.getMainWindow()); + if (pkg.isEmpty()) { + return ""; + } + JClass cls = DbgUtils.searchMainActivity(debuggerPanel.getMainWindow()); + if (cls == null) { + return ""; + } + return pkg + "/" + cls.getCls().getClassNode().getClassInfo().getFullName(); + } + + private RuntimeType castType(ArgType type) { + if (type == ArgType.INT) { + return RuntimeType.INT; + } + if (type == ArgType.STRING) { + return RuntimeType.STRING; + } + if (type == ArgType.LONG) { + return RuntimeType.LONG; + } + if (type == ArgType.FLOAT) { + return RuntimeType.FLOAT; + } + if (type == ArgType.DOUBLE) { + return RuntimeType.DOUBLE; + } + if (type == ArgType.OBJECT) { + return RuntimeType.OBJECT; + } + throw new JadxRuntimeException("Unexpected type: " + type); + } + + @NonNull + protected static RuntimeType castType(String type) { + RuntimeType rt = null; + if (!StringUtils.isEmpty(type)) { + rt = TYPE_MAP.get(type); + } + if (rt == null) { + rt = POSSIBLE_TYPES[0]; + } + return rt; + } + + private void checkType(ArgType type, Object value) { + if (!(type == ArgType.INT && value instanceof Integer) + && !(type == ArgType.STRING && value instanceof String) + && !(type == ArgType.LONG && value instanceof Long) + && !(type == ArgType.FLOAT && value instanceof Float) + && !(type == ArgType.DOUBLE && value instanceof Double) + && !(type == ArgType.OBJECT && value instanceof Long)) { + throw new JadxRuntimeException("Type must be one of int, long, float, double, String or Object."); + } + } + + private boolean modifyValueInternal(ValueTreeNode valNode, RuntimeType type, Object value) { + if (valNode instanceof RegTreeNode) { + try { + RegTreeNode regNode = (RegTreeNode) valNode; + debugger.setValueSync( + regNode.getRuntimeRegNum(), + type, + value, + cur.frame.getThreadID(), + cur.frame.getFrame().getID()); + lazyQueue.execute(() -> { + setRegsNotUpdated(); + updateRegister((RegTreeNode) valNode, type, true); + }); + } catch (SmaliDebuggerException e) { + logErr(e); + return false; + } + } else if (valNode instanceof FieldTreeNode) { + // TODO: check type. + FieldTreeNode fldNode = (FieldTreeNode) valNode; + try { + debugger.setValueSync( + fldNode.getObjectID(), + ((RuntimeField) fldNode.getRuntimeValue()).getFieldID(), + fldNode.getRuntimeField().getType(), + value); + lazyQueue.execute(() -> { + updateField((FieldTreeNode) valNode); + }); + } catch (SmaliDebuggerException e) { + logErr(e); + return false; + } + } + return true; + } + + private interface ResumeCmd { + void exec() throws SmaliDebuggerException; + } + + private boolean execResumeCmd(ResumeCmd cmd) { + if (!hasResumed) { + if (cmd != run) { + return false; + } + hasResumed = true; + } + if (isDebugging() && isSuspended()) { + updateAllFldAndReg = cmd == run; + setDebuggerState(false, false); + try { + cmd.exec(); + return true; + } catch (SmaliDebuggerException e) { + logErr(e); + setDebuggerState(true, false); + } + } + return false; + } + + /** + * @param suspended suspended by step, breakpoint, etc.. + * @param stopped remote app had been terminated, it's used to + * change icons only, to check if it's running use isDebugging() instead. + */ + private void setDebuggerState(boolean suspended, boolean stopped) { + isSuspended = suspended; + if (stopped) { + hasResumed = false; + } + if (stateListener != null) { + stateListener.onStateChanged(suspended, stopped); + } + } + + @Override + public void setStateListener(StateListener listener) { + stateListener = listener; + } + + @Override + public void onSuspendEvent(SmaliDebugger.SuspendInfo info) { + if (!isDebugging()) { + return; + } + if (info.isTerminated()) { + debuggerPanel.log("Debugger exited."); + setDebuggerState(true, true); + debugger = null; + return; + } + setDebuggerState(true, false); + long threadID = info.getThreadID(); + int refreshLevel = 2; // update all threads, stack frames, registers and fields. + if (cur.frame != null) { + if (threadID == cur.frame.getThreadID() + && info.getClassID() == cur.frame.getClsID() + && info.getMethodID() == cur.frame.getMthID()) { + + refreshLevel = 1; // relevant registers or fields. + } else { + cur.frame.getClsID(); + } + setRegsNotUpdated(); + } + if (refreshLevel == 2) { + updateAllInfo(threadID, info.getOffset()); + + } else { + if (cur.smali != null && cur.frame != null) { + refreshRegInfo(info.getOffset()); + refreshCurFrame(threadID, info.getOffset()); + if (updateAllFldAndReg) { + debuggerPanel.resetRegTreeNodes(); + updateAllRegisters(cur.frame); + } else if (toBeUpdatedTreeNode != null) { + lazyQueue.execute(() -> updateRegOrField(toBeUpdatedTreeNode)); + } + markCodeOffset(info.getOffset()); + } else { + debuggerPanel.resetRegTreeNodes(); + } + if (cur.frame != null) { + // update current code offset in stack frame. + cur.frame.updateCodeOffset(info.getOffset()); + debuggerPanel.refreshStackFrameList(Collections.emptyList()); + } + } + } + + private void refreshRegInfo(long codeOffset) { + List list = cur.regAdapter.getInfoAt(codeOffset); + for (RegisterObserver.Info info : list) { + RegTreeNode reg = cur.frame.getRegNodes().get(info.getSmaliRegNum()); + if (info.isLoad()) { + applyDbgInfo(reg, info.getInfo()); + } else { + reg.setAlias(""); + reg.setAbsoluteType(false); + } + } + if (list.size() > 0) { + debuggerPanel.refreshRegisterTree(); + } + } + + private void updateRegOrField(ValueTreeNode valTreeNode) { + if (valTreeNode instanceof RegTreeNode) { + updateRegister((RegTreeNode) valTreeNode, null, true); + return; + } + if (valTreeNode instanceof FieldTreeNode) { + updateField((FieldTreeNode) valTreeNode); + return; + } + } + + public void updateField(FieldTreeNode node) { + try { + setFieldsNotUpdated(); + debugger.getValueSync(node.getObjectID(), node.getRuntimeField()); + decodeRuntimeValue(node); + debuggerPanel.updateThisTree(node); + } catch (SmaliDebuggerException e) { + logErr(e); + } + } + + public boolean updateRegister(RegTreeNode regNode, RuntimeType type, boolean retry) { + if (type == null) { + if (regNode.isAbsoluteType()) { + type = castType(regNode.getType()); + } else { + type = POSSIBLE_TYPES[0]; + } + } + boolean ok = false; + RuntimeRegister register = null; + try { + register = debugger.getRegisterSync( + cur.frame.getThreadID(), + cur.frame.getFrame().getID(), + regNode.getRuntimeRegNum(), + type); + } catch (SmaliDebuggerException e) { + if (retry) { + if (debugger.errIsTypeMismatched(e.getErrCode())) { + RuntimeType[] types = getPossibleTypes(type); + for (RuntimeType nextType : types) { + ok = updateRegister(regNode, nextType, false); + if (ok) { + regNode.updateType(nextType.getDesc()); + break; + } + } + } else { + logErr(e.getMessage() + " for " + regNode.getName()); + regNode.updateType(null); + regNode.updateValue(null); + } + } + } + if (register != null) { + regNode.updateReg(register); + decodeRuntimeValue(regNode); + } + debuggerPanel.updateRegTree(regNode); + return ok; + } + + private RuntimeType[] getPossibleTypes(RuntimeType cur) { + RuntimeType[] types = new RuntimeType[2]; + for (int i = 0, j = 0; i < POSSIBLE_TYPES.length; i++) { + if (cur != POSSIBLE_TYPES[i]) { + types[j++] = POSSIBLE_TYPES[i]; + } + } + return types; + } + + // when single stepping we can detect which reg need to be updated. + private void markNextToBeUpdated(long codeOffset) { + if (codeOffset != -1) { + Object rst = cur.smali.getResultRegOrField(cur.mthFullID, codeOffset); + toBeUpdatedTreeNode = null; + if (cur.frame != null) { + if (rst instanceof Integer) { + int regNum = (int) rst; + if (cur.frame.getRegNodes().size() > regNum) { + toBeUpdatedTreeNode = cur.frame.getRegNodes().get(regNum); + } + return; + } + if (rst instanceof FieldInfo) { + FieldInfo info = (FieldInfo) rst; + toBeUpdatedTreeNode = cur.frame.getFieldNodes() + .stream() + .filter(f -> f.getName().equals(info.getName())) + .findFirst() + .orElse(null); + } + } + } + } + + private void updateAllThreads() { + List threads; + try { + threads = debugger.getAllThreadsSync(); + } catch (SmaliDebuggerException e) { + logErr(e); + return; + } + List threadEleList = new ArrayList<>(threads.size()); + for (Long thread : threads) { + ThreadBoxElement ele = new ThreadBoxElement(thread); + threadEleList.add(ele); + } + debuggerPanel.refreshThreadBox(threadEleList); + lazyQueue.execute(() -> { + for (ThreadBoxElement ele : threadEleList) { // get thread names + try { + ele.setName(debugger.getThreadNameSync(ele.getThreadID())); + } catch (SmaliDebuggerException e) { + logErr(e); + } + } + debuggerPanel.refreshThreadBox(Collections.emptyList()); + }); + } + + private FrameNode updateAllStackFrames(long threadID) { + List frames = Collections.emptyList(); + try { + frames = debugger.getFramesSync(threadID); + } catch (SmaliDebuggerException e) { + logErr(e); + } + if (frames.size() == 0) { + return null; + } + List frameEleList = new ArrayList<>(frames.size()); + for (SmaliDebugger.Frame frame : frames) { + FrameNode ele = new FrameNode(threadID, frame); + frameEleList.add(ele); + } + FrameNode curEle = frameEleList.get(0); + fetchStackFrameNames(curEle); + + debuggerPanel.refreshStackFrameList(frameEleList); + lazyQueue.execute(() -> { // get class & method names for frames + for (int i = 1; i < frameEleList.size(); i++) { + fetchStackFrameNames(frameEleList.get(i)); + } + debuggerPanel.refreshStackFrameList(Collections.emptyList()); + }); + return frameEleList.get(0); + } + + private void fetchStackFrameNames(FrameNode ele) { + try { + long clsID = ele.getFrame().getClassID(); + String clsSig = debugger.getClassSignatureSync(clsID); + String mthSig = debugger.getMethodSignatureSync(clsID, ele.getFrame().getMethodID()); + ele.setSignatures(clsSig, mthSig); + } catch (SmaliDebuggerException e) { + logErr(e); + } + } + + private Smali decodeSmali(FrameNode frame) { + if (cur.frame.getClsSig() != null) { + JClass jClass = DbgUtils.getTopClassBySig(frame.getClsSig(), debuggerPanel.getMainWindow()); + if (jClass != null) { + ClassNode cNode = jClass.getCls().getClassNode(); + cur.clsNode = jClass; + cur.mthFullID = DbgUtils.classSigToRawFullName(frame.getClsSig()) + "." + frame.getMthSig(); + return DbgUtils.getSmali(cNode); + } + } + return null; + } + + private void refreshCurFrame(long threadID, long codeOffset) { + try { + Frame frame = debugger.getCurrentFrame(threadID); + cur.frame.setFrame(frame); + cur.frame.updateCodeOffset(codeOffset); + } catch (SmaliDebuggerException e) { + logErr(e); + } + } + + private void updateAllFields(FrameNode frame) { + List fldNodes = Collections.emptyList(); + String clsSig = frame.getClsSig(); + if (clsSig != null) { + ClassNode clsNode = DbgUtils.getClassNodeBySig(clsSig, debuggerPanel.getMainWindow()); + if (clsNode != null) { + fldNodes = clsNode.getFields(); + } + } + try { + long thisID = debugger.getThisID(frame.getThreadID(), frame.getFrame().getID()); + List flds = debugger.getAllFieldsSync(frame.getClsID()); + List nodes = new ArrayList<>(flds.size()); + for (RuntimeField fld : flds) { + FieldTreeNode fldNode = new FieldTreeNode(fld, thisID); + fldNodes.stream() + .filter(f -> f.getName().equals(fldNode.getName())) + .findFirst() + .ifPresent(smaliFld -> fldNode.setAlias(smaliFld.getAlias())); + nodes.add(fldNode); + } + debuggerPanel.updateThisFieldNodes(nodes); + frame.setFieldNodes(nodes); + if (thisID > 0 && nodes.size() > 0) { + lazyQueue.execute(() -> updateAllFieldValues(thisID, frame)); + } + } catch (SmaliDebuggerException e) { + logErr(e); + } + } + + private void updateAllFieldValues(long thisID, FrameNode frame) { + List nodes = frame.getFieldNodes(); + if (nodes.size() > 0) { + List flds = new ArrayList<>(nodes.size()); + List rts = new ArrayList<>(nodes.size()); + nodes.forEach(n -> { + RuntimeField f = n.getRuntimeField(); + if (f.isBelongToThis()) { + flds.add(n); + rts.add(f); + } + }); + try { + debugger.getAllFieldValuesSync(thisID, rts); + flds.forEach(n -> decodeRuntimeValue(n)); + debuggerPanel.refreshThisFieldTree(); + } catch (SmaliDebuggerException e) { + logErr(e); + } + } + } + + private void updateAllRegisters(FrameNode frame) { + if (buildRegTreeNodes(frame).size() > 0) { + fetchAllRegisters(frame); + } + } + + private void fetchAllRegisters(FrameNode frame) { + List regs = cur.regAdapter.getInitializedList(frame.getCodeOffset()); + for (SmaliRegister reg : regs) { + lazyQueue.execute(() -> { + Entry info = cur.regAdapter.getInfo(reg.getRuntimeRegNum(), frame.getCodeOffset()); + RegTreeNode regNode = frame.getRegNodes().get(reg.getRegNum()); + if (info != null) { + applyDbgInfo(regNode, info); + } + updateRegister(regNode, null, true); + }); + } + } + + private void applyDbgInfo(RegTreeNode rn, Entry info) { + rn.setAlias(info.getKey()); + rn.updateType(info.getValue()); + rn.setAbsoluteType(true); + } + + private void setRegsNotUpdated() { + if (cur.frame != null) { + for (RegTreeNode regNode : cur.frame.getRegNodes()) { + regNode.setUpdated(false); + } + } + } + + private void setFieldsNotUpdated() { + if (cur.frame != null) { + for (FieldTreeNode node : cur.frame.getFieldNodes()) { + node.setUpdated(false); + } + } + } + + private List buildRegTreeNodes(FrameNode frame) { + List regs = cur.smali.getRegisterList(cur.mthFullID); + List regNodes = new ArrayList<>(regs.size()); + List inRtOrder = new ArrayList<>(regs.size()); + + regs.forEach(r -> { + RegTreeNode rn = new RegTreeNode(r); + regNodes.add(rn); + inRtOrder.add(rn); + }); + inRtOrder.sort(Comparator.comparingInt(RegTreeNode::getRuntimeRegNum)); + frame.setRegNodes(regNodes); + debuggerPanel.updateRegTreeNodes(inRtOrder); + debuggerPanel.refreshRegisterTree(); + return regNodes; + } + + private boolean decodeRuntimeValue(RuntimeValueTreeNode valNode) { + RuntimeValue rValue = valNode.getRuntimeValue(); + RuntimeType type = rValue.getType(); + if (!valNode.isAbsoluteType()) { + valNode.updateType(null); + } + try { + switch (type) { + case OBJECT: + return decodeObject(valNode); + case STRING: + String str = "\"" + debugger.readStringSync(rValue) + "\""; + valNode.updateType("java.lang.String") + .updateTypeID(debugger.readID(rValue)) + .updateValue(str); + break; + case INT: + valNode.updateValue(Integer.toString(debugger.readInt(rValue))); + break; + case LONG: + valNode.updateValue(Long.toString(debugger.readAll(rValue))); + break; + case ARRAY: + decodeArrayVal(valNode); + break; + case BOOLEAN: { + int b = debugger.readByte(rValue); + valNode.updateValue(b == 1 ? "true" : "false"); + break; + } + case SHORT: + valNode.updateValue(Short.toString(debugger.readShort(rValue))); + break; + case CHAR: + case BYTE: { + int b = (int) debugger.readAll(rValue); + if (DbgUtils.isPrintableChar(b)) { + valNode.updateValue(type == RuntimeType.CHAR ? String.valueOf((char) b) : String.valueOf((byte) b)); + } else { + valNode.updateValue(String.valueOf(b)); + } + break; + } + case DOUBLE: + double d = debugger.readDouble(rValue); + valNode.updateValue(Double.toString(d)); + break; + case FLOAT: + float f = debugger.readFloat(rValue); + valNode.updateValue(Float.toString(f)); + break; + case VOID: + valNode.updateType("void"); + break; + case THREAD: + valNode.updateType("thread").updateTypeID(debugger.readID(rValue)); + break; + case THREAD_GROUP: + valNode.updateType("thread_group").updateTypeID(debugger.readID(rValue)); + break; + case CLASS_LOADER: + valNode.updateType("class_loader").updateTypeID(debugger.readID(rValue)); + break; + case CLASS_OBJECT: + valNode.updateType("class_object").updateTypeID(debugger.readID(rValue)); + break; + } + + } catch (SmaliDebuggerException e) { + logErr(e); + return false; + } + return true; + } + + private boolean decodeObject(RuntimeValueTreeNode valNode) { + RuntimeValue rValue = valNode.getRuntimeValue(); + boolean ok = true; + if (debugger.readID(rValue) == 0) { + if (valNode.isAbsoluteType()) { + valNode.updateValue("null"); + return ok; + } else if (!art.readNullObject()) { + valNode.updateType(art.typeForNull()); + valNode.updateValue("0"); + return ok; + } + } + String sig; + try { + sig = debugger.readObjectSignatureSync(rValue); + valNode.updateType(String.format("%s@%d", DbgUtils.classSigToRawFullName(sig), + debugger.readID(rValue))); + } catch (SmaliDebuggerException e) { + ok = debugger.errIsInvalidObject(e.getErrCode()) && valNode instanceof RegTreeNode; + if (ok) { + try { + RegTreeNode reg = (RegTreeNode) valNode; + RuntimeRegister rr = debugger.getRegisterSync( + cur.frame.getThreadID(), + cur.frame.getFrame().getID(), + reg.getRuntimeRegNum(), RuntimeType.INT); + reg.updateReg(rr); + rValue = rr; + valNode.updateType(RuntimeType.INT.getDesc()); + valNode.updateValue(Long.toString((int) debugger.readAll(rValue))); + } catch (SmaliDebuggerException except) { + logErr(except, String.format("Update %s failed, %s", valNode.getName(), except.getMessage())); + valNode.updateValue(except.getMessage()); + ok = false; + } + } else { + logErr(e); + } + } + return ok; + } + + private void decodeArrayVal(RuntimeValueTreeNode valNode) throws SmaliDebuggerException { + String type = debugger.readObjectSignatureSync(valNode.getRuntimeValue()); + ArgType argType = ArgType.parse(type); + String javaType = argType.toString(); + Entry> ret = debugger.readArray(valNode.getRuntimeValue(), 0, 0); + javaType = javaType.substring(0, javaType.length() - 1) + ret.getKey() + "]"; + valNode.updateType(javaType + "@" + debugger.readID(valNode.getRuntimeValue())); + + if (argType.getArrayElement().isPrimitive()) { + for (Long aLong : ret.getValue()) { + valNode.add(new DefaultMutableTreeNode(Long.toString(aLong))); + } + return; + } + String typeSig = type.substring(1); + if (DbgUtils.isStringObjectSig(typeSig)) { + for (Long aLong : ret.getValue()) { + valNode.add(new DefaultMutableTreeNode(debugger.readStringSync(aLong))); + } + return; + } + typeSig = DbgUtils.classSigToRawFullName(typeSig); + for (Long aLong : ret.getValue()) { + valNode.add(new DefaultMutableTreeNode(String.format("%s@%d", typeSig, aLong))); + } + } + + private void updateAllInfo(long threadID, long codeOffset) { + updateQueue.execute(() -> { + resetAllInfo(); + cur.frame = updateAllStackFrames(threadID); + if (cur.frame != null) { + lazyQueue.execute(() -> updateAllFields(cur.frame)); + if (cur.frame.getClsSig() == null || cur.frame.getMthSig() == null) { + fetchStackFrameNames(cur.frame); + } + cur.smali = decodeSmali(cur.frame); + if (cur.smali != null) { + cur.regAdapter = regAdaMap.computeIfAbsent(cur.mthFullID, + k -> RegisterObserver.merge( + getRuntimeDebugInfo(cur.frame), + getRegisterList())); + + if (cur.smali.getRegCount(cur.mthFullID) > 0) { + updateAllRegisters(cur.frame); + } + markCodeOffset(codeOffset); + } + } + updateAllThreads(); + }); + } + + private List getRegisterList() { + int regCount = cur.smali.getRegCount(cur.mthFullID); + int paramStart = cur.smali.getParamRegStart(cur.mthFullID); + List srs = cur.smali.getRegisterList(cur.mthFullID); + for (SmaliRegister sr : srs) { + sr.setRuntimeRegNum(art.getRuntimeRegNum(sr.getRegNum(), regCount, paramStart)); + } + return srs; + } + + private void resetAllInfo() { + isSuspended = true; + toBeUpdatedTreeNode = null; + debuggerPanel.resetAllDebuggingInfo(); + cur.reset(); + } + + private List getRuntimeDebugInfo(FrameNode frame) { + try { + RuntimeDebugInfo dbgInfo = debugger.getRuntimeDebugInfo(frame.getClsID(), frame.getMthID()); + if (dbgInfo != null) { + return dbgInfo.getInfoList(); + } + } catch (SmaliDebuggerException ignore) { + // logErr(e); + } + return Collections.emptyList(); + } + + private void markCodeOffset(long codeOffset) { + scrollToPos(codeOffset); + markNextToBeUpdated(codeOffset); + } + + private void logErr(Exception e, String extra) { + debuggerPanel.log(e.getMessage()); + debuggerPanel.log(extra); + e.printStackTrace(); + } + + private void logErr(Exception e) { + debuggerPanel.log(e.getMessage()); + e.printStackTrace(); + } + + private void logErr(String e) { + debuggerPanel.log(e); + } + + private void scrollToPos(long codeOffset) { + int pos = -1; + if (codeOffset > -1) { + pos = cur.smali.getInsnPosByCodeOffset(cur.mthFullID, codeOffset); + } + if (pos == -1) { + pos = cur.smali.getMethodDefPos(cur.mthFullID); + if (pos == -1) { + debuggerPanel.log("Can't scroll to " + cur.mthFullID); + return; + } + } + debuggerPanel.scrollToSmaliLine(cur.clsNode, pos, true); + } + + private void initBreakpoints(List fbps) { + if (fbps.size() == 0) { + return; + } + boolean fetch = true; + for (FileBreakpoint fbp : fbps) { + try { + long id = debugger.getClassID(fbp.cls, fetch); + // only fetch classes from JVM once, + // if this time this class hasn't been loaded then it won't load next time, cuz JVM is freezed. + fetch = false; + if (id > -1) { + setBreakpoint(id, fbp); + } else { + setDelayBreakpoint(fbp); + } + } catch (SmaliDebuggerException e) { + logErr(e); + failBreakpoint(fbp, e.getMessage()); + } + } + } + + protected boolean setBreakpoint(FileBreakpoint bp) { + if (!isDebugging()) { + return true; + } + try { + long cid = debugger.getClassID(bp.cls, true); + if (cid > -1) { + setBreakpoint(cid, bp); + } else { + setDelayBreakpoint(bp); + } + } catch (SmaliDebuggerException e) { + logErr(e); + BreakpointManager.failBreakpoint(bp); + return false; + } + return true; + } + + private void setDelayBreakpoint(FileBreakpoint bp) { + boolean hasSet = bpStore.hasSetDelaied(bp.cls); + bpStore.add(bp, null); + if (!hasSet) { + updateQueue.execute(() -> { + try { + debugger.regClassPrepareEventForBreakpoint(bp.cls, id -> { + List list = bpStore.get(bp.cls); + for (FileBreakpoint fbp : list) { + setBreakpoint(id, fbp); + } + }); + } catch (SmaliDebuggerException e) { + logErr(e); + failBreakpoint(bp, ""); + } + }); + } + } + + protected void setBreakpoint(long cid, FileBreakpoint fbp) { + try { + long mid = debugger.getMethodID(cid, fbp.mth); + if (mid > -1) { + RuntimeBreakpoint rbp = debugger.makeBreakpoint(cid, mid, fbp.codeOffset); + debugger.setBreakpoint(rbp); + bpStore.add(fbp, rbp); + return; + } + } catch (SmaliDebuggerException e) { + logErr(e); + } + failBreakpoint(fbp, "Failed to get method for breakpoint, " + fbp.mth + ":" + fbp.codeOffset); + } + + private void failBreakpoint(FileBreakpoint fbp, String msg) { + if (!msg.isEmpty()) { + debuggerPanel.log(msg); + } + bpStore.removeBreakpoint(fbp); + BreakpointManager.failBreakpoint(fbp); + } + + protected boolean removeBreakpoint(FileBreakpoint fbp) { + if (!isDebugging()) { + return true; + } + RuntimeBreakpoint rbp = bpStore.removeBreakpoint(fbp); + if (rbp != null) { + try { + debugger.removeBreakpoint(rbp); + } catch (SmaliDebuggerException e) { + logErr(e); + return false; + } + } + return true; + } + + private static RuntimeBreakpoint delayBP = null; + + private class BreakpointStore { + Map bpm = Collections.emptyMap(); + + BreakpointStore() { + if (delayBP == null) { + delayBP = debugger.makeBreakpoint(-1, -1, -1); + } + } + + void reset() { + bpm.clear(); + } + + boolean hasSetDelaied(String cls) { + for (Entry entry : bpm.entrySet()) { + if (entry.getValue() == delayBP && entry.getKey().cls.equals(cls)) { + return true; + } + } + return false; + } + + List get(String cls) { + List fbps = new ArrayList<>(); + bpm.forEach((k, v) -> { + if (v == delayBP && k.cls.equals(cls)) { + fbps.add(k); + bpm.remove(k); + } + }); + return fbps; + } + + void add(FileBreakpoint fbp, RuntimeBreakpoint rbp) { + if (bpm == Collections.EMPTY_MAP) { + bpm = new ConcurrentHashMap<>(); + } + bpm.put(fbp, rbp == null ? delayBP : rbp); + } + + RuntimeBreakpoint removeBreakpoint(FileBreakpoint fbp) { + return bpm.remove(fbp); + } + } + + public class FrameNode implements IListElement { + private SmaliDebugger.Frame frame; + private final long threadID; + private String clsSig; + private String mthSig; + private StringBuilder cache; + private long codeOffset = -1; + private List regNodes; + private List thisNodes; + private long thisID; + + public FrameNode(long threadID, SmaliDebugger.Frame frame) { + cache = new StringBuilder(16); + this.frame = frame; + this.threadID = threadID; + regNodes = Collections.emptyList(); + thisNodes = Collections.emptyList(); + } + + public SmaliDebugger.Frame getFrame() { + return frame; + } + + public void setFrame(SmaliDebugger.Frame frame) { + this.frame = frame; + } + + public long getClsID() { + return frame.getClassID(); + } + + public long getMthID() { + return frame.getMethodID(); + } + + public long getThreadID() { + return threadID; + } + + public long getThisID() { + return thisID; + } + + public void setThisID(long thisID) { + this.thisID = thisID; + } + + public void setSignatures(String clsSig, String mthSig) { + this.clsSig = clsSig; + this.mthSig = mthSig; + this.cache.delete(0, this.cache.length()); + } + + public String getClsSig() { + return clsSig; + } + + public String getMthSig() { + return mthSig; + } + + public void updateCodeOffset(long codeOffset) { + this.codeOffset = codeOffset; + if (this.codeOffset > -1) { + this.cache.delete(0, this.cache.length()); + } + } + + public long getCodeOffset() { + return codeOffset == -1 ? frame.getCodeIndex() : codeOffset; + } + + public void setRegNodes(List regNodes) { + this.regNodes = regNodes; + } + + public List getRegNodes() { + return regNodes; + } + + public List getFieldNodes() { + return thisNodes; + } + + public void setFieldNodes(List thisNodes) { + this.thisNodes = thisNodes; + } + + @Override + public void onSelected() { + if (clsSig != null) { + JClass cls = DbgUtils.getTopClassBySig(clsSig, debuggerPanel.getMainWindow()); + if (cls != null) { + Smali smali = DbgUtils.getSmali(cls.getCls().getClassNode()); + if (smali != null) { + int pos = smali.getInsnPosByCodeOffset( + DbgUtils.classSigToRawFullName(clsSig) + "." + mthSig, + getCodeOffset()); + debuggerPanel.scrollToSmaliLine(cls, Math.max(0, pos), true); + return; + } + } + debuggerPanel.log("Can't open smali panel for " + clsSig + "->" + mthSig); + } + } + + @Override + public String toString() { + if (cache.length() == 0) { + long off = getCodeOffset(); + if (off < 0) { + cache.append(String.format("index: %-4d ", off)); + } else { + cache.append(String.format("index: %04x ", off)); + } + if (clsSig == null) { + cache.append("clsID: ").append(frame.getClassID()); + } else { + cache.append(clsSig).append("->"); + } + if (mthSig == null) { + cache.append(" mthID: ").append(frame.getMethodID()); + } else { + cache.append(mthSig); + } + } + return cache.toString(); + } + } + + private static class ThreadBoxElement implements IListElement { + private long threadID; + private String name; + + public ThreadBoxElement(long threadID) { + this.threadID = threadID; + } + + public void setName(String name) { + this.name = name; + } + + public long getThreadID() { + return threadID; + } + + @Override + public String toString() { + if (name == null) { + return "thread id: " + threadID; + } + return "thread id: " + threadID + " name:" + name; + } + + @Override + public void onSelected() { + + } + } + + private static class RegTreeNode extends RuntimeValueTreeNode { + private static final long serialVersionUID = -1111111202103122234L; + + private final SmaliRegister smaliReg; + private RuntimeRegister runtimeReg; + private String value; + private String type; + private String alias; + private boolean absType; + + public RegTreeNode(SmaliRegister smaliReg) { + this.smaliReg = smaliReg; + } + + public void updateReg(RuntimeRegister reg) { + runtimeReg = reg; + } + + public void setAlias(String alias) { + this.alias = alias; + } + + @Override + public RegTreeNode updateValue(String value) { + setUpdated(true); + this.value = value; + removeAllChildren(); + return this; + } + + @Override + public RegTreeNode updateType(String type) { + if (this.type == null || !this.type.equals(type)) { + this.type = type; + reset(); + } + return this; + } + + private void reset() { + value = null; + removeAllChildren(); + setUpdated(true); + this.absType = false; + updateTypeID(0); + } + + @Override + public String getName() { + if (!StringUtils.isEmpty(alias)) { + return String.format("%s (%s)", smaliReg.getName(), alias); + } + return String.format("%-3s", smaliReg.getName()); + } + + @Override + @Nullable + public String getValue() { + return value; + } + + public RuntimeRegister getRuntimeReg() { + return runtimeReg; + } + + public int getRuntimeRegNum() { + return smaliReg.getRuntimeRegNum(); + } + + @Override + public String getType() { + if (type != null) { + return type; + } + if (runtimeReg != null) { + return runtimeReg.getType().getDesc(); + } + return null; + } + + @Override + public RuntimeValue getRuntimeValue() { + return getRuntimeReg(); + } + + @Override + public boolean isAbsoluteType() { + return absType; + } + + public void setAbsoluteType(boolean abs) { + absType = abs; + } + } + + private static class FieldTreeNode extends RuntimeValueTreeNode { + private static final long serialVersionUID = -1111111202103122235L; + + private final RuntimeField field; + private String value; + private String alias; + private long objectID; + + private FieldTreeNode(RuntimeField field, long id) { + this.field = field; + objectID = id; + } + + public long getObjectID() { + return objectID; + } + + public void setObjectID(long id) { + this.objectID = id; + } + + public RuntimeField getRuntimeField() { + return this.field; + } + + public void setAlias(String alias) { + this.alias = alias; + } + + @Override + public FieldTreeNode updateValue(String val) { + setUpdated(true); + value = val; + removeAllChildren(); + return this; + } + + @Override + public FieldTreeNode updateType(String val) { + return this; + } + + @Override + public String getName() { + if (StringUtils.isEmpty(alias) || alias.equals(field.getName())) { + return field.getName(); + } + return field.getName() + " (" + alias + ")"; + } + + @Override + public String getValue() { + return value; + } + + @Override + public String getType() { + return ArgType.parse(field.getFieldType()).toString(); + } + + @Override + public RuntimeValue getRuntimeValue() { + return field; + } + + @Override + public boolean isAbsoluteType() { + return true; + } + } + + private abstract static class RuntimeValueTreeNode extends ValueTreeNode { + private static final long serialVersionUID = -1111111202103260222L; + private long typeID; + + @Override + public ValueTreeNode updateTypeID(long id) { + this.typeID = id; + return this; + } + + @Override + public long getTypeID() { + return this.typeID; + } + + public abstract RuntimeValue getRuntimeValue(); + + public abstract boolean isAbsoluteType(); + } + + private class CurrentInfo { + JClass clsNode; + String mthFullID; + Smali smali; + FrameNode frame; + RegisterObserver regAdapter; + + public void reset() { + frame = null; + smali = null; + clsNode = null; + regAdapter = null; + mthFullID = ""; + } + } + + private static void initTypeMap() { + TYPE_MAP.put("I", RuntimeType.INT); + TYPE_MAP.put("Z", RuntimeType.INT); + TYPE_MAP.put("B", RuntimeType.INT); + TYPE_MAP.put("C", RuntimeType.INT); + TYPE_MAP.put("F", RuntimeType.INT); + TYPE_MAP.put("S", RuntimeType.INT); + TYPE_MAP.put("V", RuntimeType.INT); + TYPE_MAP.put("int", RuntimeType.INT); + TYPE_MAP.put("boolean", RuntimeType.INT); + TYPE_MAP.put("byte", RuntimeType.INT); + TYPE_MAP.put("short", RuntimeType.INT); + TYPE_MAP.put("char", RuntimeType.INT); + TYPE_MAP.put("float", RuntimeType.INT); + TYPE_MAP.put("void", RuntimeType.INT); + TYPE_MAP.put("L", RuntimeType.LONG); + TYPE_MAP.put("D", RuntimeType.LONG); + TYPE_MAP.put("long", RuntimeType.LONG); + TYPE_MAP.put("double", RuntimeType.LONG); + TYPE_MAP.put("java.lang.String", RuntimeType.STRING); + TYPE_MAP.put("Ljava/lang/String;", RuntimeType.STRING); + } +} diff --git a/jadx-gui/src/main/java/jadx/gui/device/debugger/RegisterObserver.java b/jadx-gui/src/main/java/jadx/gui/device/debugger/RegisterObserver.java new file mode 100644 index 000000000..b86b1e3a9 --- /dev/null +++ b/jadx-gui/src/main/java/jadx/gui/device/debugger/RegisterObserver.java @@ -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> infoMap; + private final List>> regList; + private boolean hasDbgInfo = false; + + private RegisterObserver() { + regList = new ArrayList<>(); + infoMap = Collections.emptyMap(); + } + + public static RegisterObserver merge(List rtRegs, List 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> 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 getInitializedList(long codeOffset) { + List ret = Collections.emptyList(); + for (Entry> info : regList) { + if (info.getKey().isInitialized(codeOffset)) { + if (ret.isEmpty()) { + ret = new ArrayList<>(); + } + ret.add(info.getKey()); + } + } + return ret; + } + + @Nullable + public Entry getInfo(int runtimeNum, long codeOffset) { + Entry> 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 getInfoAt(long codeOffset) { + if (hasDbgInfo) { + List 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 info; + + private Info(int smaliRegNum, boolean load, Entry info) { + this.smaliRegNum = smaliRegNum; + this.load = load; + this.info = info; + } + + public int getSmaliRegNum() { + return smaliRegNum; + } + + public boolean isLoad() { + return load; + } + + public Entry getInfo() { + return info; + } + } +} diff --git a/jadx-gui/src/main/java/jadx/gui/device/debugger/SmaliDebugger.java b/jadx-gui/src/main/java/jadx/gui/device/debugger/SmaliDebugger.java new file mode 100644 index 000000000..717275476 --- /dev/null +++ b/jadx-gui/src/main/java/jadx/gui/device/debugger/SmaliDebugger.java @@ -0,0 +1,1592 @@ +package jadx.gui.device.debugger; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.Socket; +import java.util.*; +import java.util.AbstractMap.SimpleEntry; +import java.util.Map.Entry; +import java.util.concurrent.*; +import java.util.concurrent.atomic.AtomicInteger; + +import io.github.hqktech.JDWP; +import io.github.hqktech.JDWP.ArrayReference.Length.LengthReplyData; +import io.github.hqktech.JDWP.ByteBuffer; +import io.github.hqktech.JDWP.Event.Composite.*; +import io.github.hqktech.JDWP.EventRequest.Set.ClassMatchRequest; +import io.github.hqktech.JDWP.EventRequest.Set.CountRequest; +import io.github.hqktech.JDWP.EventRequest.Set.LocationOnlyRequest; +import io.github.hqktech.JDWP.EventRequest.Set.StepRequest; +import io.github.hqktech.JDWP.Method.VariableTableWithGeneric.VarTableWithGenericData; +import io.github.hqktech.JDWP.Method.VariableTableWithGeneric.VarWithGenericSlot; +import io.github.hqktech.JDWP.ObjectReference; +import io.github.hqktech.JDWP.ObjectReference.ReferenceType.ReferenceTypeReplyData; +import io.github.hqktech.JDWP.ObjectReference.SetValues.FieldValueSetter; +import io.github.hqktech.JDWP.Packet; +import io.github.hqktech.JDWP.ReferenceType.FieldsWithGeneric.FieldsWithGenericData; +import io.github.hqktech.JDWP.ReferenceType.FieldsWithGeneric.FieldsWithGenericReplyData; +import io.github.hqktech.JDWP.ReferenceType.MethodsWithGeneric.MethodsWithGenericData; +import io.github.hqktech.JDWP.ReferenceType.MethodsWithGeneric.MethodsWithGenericReplyData; +import io.github.hqktech.JDWP.ReferenceType.Signature.SignatureReplyData; +import io.github.hqktech.JDWP.StackFrame.GetValues.GetValuesReplyData; +import io.github.hqktech.JDWP.StackFrame.GetValues.GetValuesSlots; +import io.github.hqktech.JDWP.StackFrame.SetValues.SlotValueSetter; +import io.github.hqktech.JDWP.StackFrame.ThisObject.ThisObjectReplyData; +import io.github.hqktech.JDWP.StringReference.Value.ValueReplyData; +import io.github.hqktech.JDWP.ThreadReference.Frames.FramesReplyData; +import io.github.hqktech.JDWP.ThreadReference.Frames.FramesReplyDataFrames; +import io.github.hqktech.JDWP.ThreadReference.Name.NameReplyData; +import io.github.hqktech.JDWP.VirtualMachine.AllClassesWithGeneric.AllClassesWithGenericData; +import io.github.hqktech.JDWP.VirtualMachine.AllClassesWithGeneric.AllClassesWithGenericReplyData; +import io.github.hqktech.JDWP.VirtualMachine.AllThreads.AllThreadsReplyData; +import io.github.hqktech.JDWP.VirtualMachine.AllThreads.AllThreadsReplyDataThreads; +import io.github.hqktech.JDWP.VirtualMachine.CreateString.CreateStringReplyData; +import io.reactivex.annotations.NonNull; +import io.reactivex.annotations.Nullable; + +import jadx.api.plugins.input.data.AccessFlags; +import jadx.gui.device.debugger.smali.RegisterInfo; +import jadx.gui.utils.ObjectPool; + +// TODO: Finish error notification, inner errors should be logged let user notice. + +public class SmaliDebugger { + + private final JDWP jdwp; + private int localTcpPort; + private InputStream inputStream; + private OutputStream outputStream; + + // All event callbacks will be called in this queue, e.g. class prepare/unload + private static final Executor EVENT_LISTENER_QUEUE = Executors.newSingleThreadExecutor(); + + // Handle callbacks of single step, breakpoint and watchpoint + private static final Executor SUSPEND_LISTENER_QUEUE = Executors.newSingleThreadExecutor(); + + private final Map callbackMap = new ConcurrentHashMap<>(); + private final Map eventListenerMap = new ConcurrentHashMap<>(); + + private final Map classMap = new ConcurrentHashMap<>(); + private final Map classIDMap = new ConcurrentHashMap<>(); + private final Map> clsMethodMap = new ConcurrentHashMap<>(); + private final Map> clsFieldMap = new ConcurrentHashMap<>(); + private Map> varMap = Collections.emptyMap(); // cls id: + + private final CountRequest oneOffEventReq; + private final AtomicInteger idGenerator = new AtomicInteger(1); + + private final SuspendInfo suspendInfo = new SuspendInfo(); + private final SuspendListener suspendListener; + + private ObjectPool> slotsPool; + private ObjectPool> stepReqPool; + private ObjectPool> syncQueuePool; + private ObjectPool> fieldIdPool; + private final Map syncQueueMap = new ConcurrentHashMap<>(); + private final AtomicInteger syncQueueID = new AtomicInteger(0); + + private static final ICommandResult SKIP_RESULT = res -> { + }; + + private SmaliDebugger(SuspendListener suspendListener, int localTcpPort, JDWP jdwp) { + this.jdwp = jdwp; + this.localTcpPort = localTcpPort; + this.suspendListener = suspendListener; + + oneOffEventReq = jdwp.eventRequest().cmdSet().newCountRequest(); + oneOffEventReq.count = 1; + } + + /** + * After a successful attach the remote app will be suspended, so we have times to + * set breakpoints or do any other things, after that call resume() to activate the app. + */ + public static SmaliDebugger attach(String host, int port, SuspendListener suspendListener) throws SmaliDebuggerException { + try { + byte[] bytes = JDWP.IDSizes.encode().getBytes(); + JDWP.setPacketID(bytes, 1); + Socket socket = new Socket(host, port); + InputStream inputStream = socket.getInputStream(); + OutputStream outputStream = socket.getOutputStream(); + + socket.setSoTimeout(5000); + JDWP jdwp = initJDWP(outputStream, inputStream); + socket.setSoTimeout(0); // set back to 0 so the decodingLoop won't break for timeout. + + SmaliDebugger debugger = new SmaliDebugger(suspendListener, port, jdwp); + debugger.inputStream = inputStream; + debugger.outputStream = outputStream; + + debugger.decodingLoop(); + debugger.listenClassUnloadEvent(); + debugger.initPools(); + return debugger; + } catch (IOException e) { + throw new SmaliDebuggerException(e); + } + } + + private void onSuspended(long thread, long clazz, long mth, long offset) { + suspendInfo.update() + .updateThread(thread) + .updateClass(clazz) + .updateMethod(mth) + .updateOffset(offset); + if (suspendInfo.isAnythingChanged()) { + SUSPEND_LISTENER_QUEUE.execute(() -> suspendListener.onSuspendEvent(suspendInfo)); + } + } + + public void stepInto() throws SmaliDebuggerException { + sendStepRequest(suspendInfo.getThreadID(), JDWP.StepDepth.INTO); + } + + public void stepOver() throws SmaliDebuggerException { + sendStepRequest(suspendInfo.getThreadID(), JDWP.StepDepth.OVER); + } + + public void stepOut() throws SmaliDebuggerException { + sendStepRequest(suspendInfo.getThreadID(), JDWP.StepDepth.OUT); + } + + public void exit() throws SmaliDebuggerException { + Packet res = sendCommandSync(jdwp.virtualMachine().cmdExit().encode(-1)); + tryThrowError(res); + } + + public void detach() throws SmaliDebuggerException { + Packet res = sendCommandSync(jdwp.virtualMachine().cmdDispose().encode()); + tryThrowError(res); + } + + private void initPools() { + slotsPool = new ObjectPool<>(() -> { + List slots = new ArrayList<>(1); + GetValuesSlots slot = jdwp.stackFrame().cmdGetValues().newValuesSlots(); + slot.slot = 0; + slot.sigbyte = JDWP.Tag.OBJECT; + slots.add(slot); + return slots; + }); + stepReqPool = new ObjectPool<>(() -> { + List eventEncoders = new ArrayList<>(2); + eventEncoders.add(jdwp.eventRequest().cmdSet().newStepRequest()); + eventEncoders.add(oneOffEventReq); + return eventEncoders; + }); + syncQueuePool = new ObjectPool<>(SynchronousQueue::new); + fieldIdPool = new ObjectPool<>(() -> { + List ids = new ArrayList<>(1); + ids.add((long) -1); + return ids; + }); + } + + /** + * @param regNum If it's an argument, just pass its index, non-static method should be index + 1. + * e.g. void a(int b, int c), you want the value of b, then should pass 1 (0 + 1), + * this is a virtual method, so 0 is for the this object and 1 is the real index of b. + *

+ * If it's a variable then should be the reg number + number of arguments and + 1 + * if it's in a non-static method. + * e.g. to get the value of v3 in a virtual method with 2 arguments, should pass + * 6 (3 + 2 + 1 = 6). + */ + public RuntimeRegister getRegisterSync(long threadID, long frameID, int regNum, RuntimeType type) throws SmaliDebuggerException { + List slots = slotsPool.get(); + GetValuesSlots slot = slots.get(0); + slot.slot = regNum; + slot.sigbyte = (byte) type.getTag(); + Packet res = sendCommandSync(jdwp.stackFrame().cmdGetValues().encode(threadID, frameID, slots)); + tryThrowError(res); + slotsPool.put(slots); + GetValuesReplyData val = jdwp.stackFrame().cmdGetValues().decode(res.getBuf(), JDWP.PACKET_HEADER_SIZE); + return buildRegister(regNum, val.values.get(0).slotValue.tag, val.values.get(0).slotValue.idOrValue); + } + + public long getThisID(long threadID, long frameID) throws SmaliDebuggerException { + Packet res = sendCommandSync(jdwp.stackFrame().cmdThisObject().encode(threadID, frameID)); + tryThrowError(res); + ThisObjectReplyData data = jdwp.stackFrame().cmdThisObject().decode(res.getBuf(), JDWP.PACKET_HEADER_SIZE); + return data.objectThis.objectID; + } + + public List getAllFieldsSync(long clsID) throws SmaliDebuggerException { + return getAllFields(clsID); + } + + public void getFieldValueSync(long clsID, RuntimeField fld) throws SmaliDebuggerException { + List list = new ArrayList<>(1); + list.add(fld); + getAllFieldValuesSync(clsID, list); + } + + public void getAllFieldValuesSync(long thisID, List flds) throws SmaliDebuggerException { + List ids = new ArrayList<>(flds.size()); + flds.forEach(f -> ids.add(f.getFieldID())); + Packet res = sendCommandSync(jdwp.objectReference().cmdGetValues().encode(thisID, ids)); + tryThrowError(res); + ObjectReference.GetValues.GetValuesReplyData data = + jdwp.objectReference().cmdGetValues().decode(res.getBuf(), JDWP.PACKET_HEADER_SIZE); + List values = data.values; + for (int i = 0; i < values.size(); i++) { + ObjectReference.GetValues.GetValuesReplyDataValues value = values.get(i); + flds.get(i).setValue(value.value.idOrValue) + .setType(getType(value.value.tag)); + } + } + + public Frame getCurrentFrame(long threadID) throws SmaliDebuggerException { + return getCurrentFrameInternal(threadID); + } + + public List getFramesSync(long threadID) throws SmaliDebuggerException { + return getAllFrames(threadID); + } + + public List getAllThreadsSync() throws SmaliDebuggerException { + return getAllThreads(); + } + + @Nullable + public String getThreadNameSync(long threadID) throws SmaliDebuggerException { + return sendThreadNameReq(threadID); + } + + @Nullable + public String getClassSignatureSync(long classID) throws SmaliDebuggerException { + return getClassSignatureInternal(classID); + } + + @Nullable + public String getMethodSignatureSync(long classID, long methodID) throws SmaliDebuggerException { + return getMethodSignatureInternal(classID, methodID); + } + + public boolean errIsTypeMismatched(int errCode) { + return errCode == JDWP.Error.TYPE_MISMATCH; + } + + public boolean errIsInvalidSlot(int errCode) { + return errCode == JDWP.Error.INVALID_SLOT; + } + + public boolean errIsInvalidObject(int errCode) { + return errCode == JDWP.Error.INVALID_OBJECT; + } + + private static class ClassListenerInfo { + int prepareReqID; + int unloadReqID; + ClassListener listener; + + void reset(ClassListener l) { + this.listener = l; + this.prepareReqID = -1; + this.unloadReqID = -1; + } + } + + private ClassListenerInfo clsListener; + + /** + * Listens for class preparation and unload events. + */ + public void setClassListener(ClassListener listener) throws SmaliDebuggerException { + if (clsListener != null) { + if (listener != clsListener.listener) { + unregisterEventSync(JDWP.EventKind.CLASS_PREPARE, clsListener.prepareReqID); + unregisterEventSync(JDWP.EventKind.CLASS_UNLOAD, clsListener.unloadReqID); + } + } else { + clsListener = new ClassListenerInfo(); + } + clsListener.reset(listener); + regClassPrepareEvent(clsListener); + regClassUnloadEvent(clsListener); + } + + private void regClassUnloadEvent(ClassListenerInfo info) throws SmaliDebuggerException { + Packet res = sendCommandSync( + jdwp.eventRequest().cmdSet().newClassExcludeRequest((byte) JDWP.EventKind.CLASS_UNLOAD, + (byte) JDWP.SuspendPolicy.NONE, "java.*")); + tryThrowError(res); + info.unloadReqID = jdwp.eventRequest().cmdSet().decodeRequestID(res.getBuf(), JDWP.PACKET_HEADER_SIZE); + eventListenerMap.put(info.unloadReqID, new EventListenerAdapter() { + @Override + void onClassUnload(ClassUnloadEvent event) { + info.listener.onUnloaded(DbgUtils.classSigToRawFullName(event.signature)); + } + }); + } + + private void regClassPrepareEvent(ClassListenerInfo info) throws SmaliDebuggerException { + Packet res = sendCommandSync( + jdwp.eventRequest().cmdSet().newClassExcludeRequest((byte) JDWP.EventKind.CLASS_PREPARE, + (byte) JDWP.SuspendPolicy.NONE, "java.*")); + tryThrowError(res); + info.prepareReqID = jdwp.eventRequest().cmdSet().decodeRequestID(res.getBuf(), JDWP.PACKET_HEADER_SIZE); + eventListenerMap.put(info.prepareReqID, new EventListenerAdapter() { + @Override + void onClassPrepare(ClassPrepareEvent event) { + info.listener.onPrepared(DbgUtils.classSigToRawFullName(event.signature), event.typeID); + } + }); + } + + public void regClassPrepareEventForBreakpoint(String clsSig, ClassPrepareListener l) throws SmaliDebuggerException { + Packet res = sendCommandSync(buildClassMatchReqForBreakpoint(clsSig, JDWP.EventKind.CLASS_PREPARE)); + tryThrowError(res); + int reqID = jdwp.eventRequest().cmdSet().decodeRequestID(res.getBuf(), JDWP.PACKET_HEADER_SIZE); + eventListenerMap.put(reqID, new EventListenerAdapter() { + @Override + void onClassPrepare(ClassPrepareEvent event) { + EVENT_LISTENER_QUEUE.execute(() -> { + try { + l.onPrepared(event.typeID); + } finally { + eventListenerMap.remove(reqID); + try { + resume(); + } catch (SmaliDebuggerException e) { + e.printStackTrace(); + } + } + }); + } + }); + } + + public interface MethodEntryListener { + /** + * return ture to remove + */ + boolean entry(String mthSig); + } + + public void regMethodEntryEventSync(String clsSig, MethodEntryListener l) throws SmaliDebuggerException { + Packet res = sendCommandSync( + jdwp.eventRequest().cmdSet().newClassMatchRequest((byte) JDWP.EventKind.METHOD_ENTRY, + (byte) JDWP.SuspendPolicy.ALL, clsSig)); + tryThrowError(res); + int reqID = jdwp.eventRequest().cmdSet().decodeRequestID(res.getBuf(), JDWP.PACKET_HEADER_SIZE); + eventListenerMap.put(reqID, new EventListenerAdapter() { + @Override + void onMethodEntry(MethodEntryEvent event) { + EVENT_LISTENER_QUEUE.execute(() -> { + boolean removeListener = false; + try { + String sig = getMethodSignatureInternal(event.location.classID, event.location.methodID); + removeListener = l.entry(sig); + if (removeListener) { + sendCommand(jdwp.eventRequest().cmdClear().encode((byte) JDWP.EventKind.METHOD_ENTRY, reqID), SKIP_RESULT); + onSuspended(event.thread, event.location.classID, event.location.methodID, -1); + eventListenerMap.remove(reqID); + } + } catch (SmaliDebuggerException e) { + e.printStackTrace(); + } finally { + if (!removeListener) { + try { + resume(); + } catch (SmaliDebuggerException e) { + e.printStackTrace(); + } + } + } + }); + } + }); + } + + private void unregisterEventSync(int eventKind, int reqID) throws SmaliDebuggerException { + eventListenerMap.remove(reqID); + Packet rst = sendCommandSync(jdwp.eventRequest().cmdClear().encode((byte) eventKind, reqID)); + tryThrowError(rst); + } + + public String readObjectSignatureSync(RuntimeValue val) throws SmaliDebuggerException { + long objID = readID(val); + // get type reference by object id. + Packet res = sendCommandSync(jdwp.objectReference().cmdReferenceType().encode(objID)); + tryThrowError(res); + ReferenceTypeReplyData data = jdwp.objectReference().cmdReferenceType().decode(res.getBuf(), JDWP.PACKET_HEADER_SIZE); + + // get signature by type reference id. + res = sendCommandSync(jdwp.referenceType().cmdSignature().encode(data.typeID)); + tryThrowError(res); + SignatureReplyData sigData = jdwp.referenceType().cmdSignature().decode(res.getBuf(), JDWP.PACKET_HEADER_SIZE); + return sigData.signature; + } + + public String readStringSync(RuntimeValue val) throws SmaliDebuggerException { + return readStringSync(readID(val)); + } + + public String readStringSync(long id) throws SmaliDebuggerException { + Packet res = sendCommandSync(jdwp.stringReference().cmdValue().encode(id)); + tryThrowError(res); + ValueReplyData strData = jdwp.stringReference().cmdValue().decode(res.getBuf(), JDWP.PACKET_HEADER_SIZE); + return strData.stringValue; + } + + public boolean setValueSync(int runtimeRegNum, RuntimeType type, Object val, long threadID, long frameID) + throws SmaliDebuggerException { + if (type == RuntimeType.STRING) { + long newID = createString((String) val); + if (newID == -1) { + return false; + } + val = newID; + type = RuntimeType.OBJECT; + } + List setters = buildRegValueSetter(type.getTag(), runtimeRegNum); + JDWP.encodeAny(setters.get(0).slotValue.idOrValue, val); + Packet res = sendCommandSync(jdwp.stackFrame().cmdSetValues().encode(threadID, frameID, setters)); + tryThrowError(res); + return jdwp.stackFrame().cmdSetValues().decode(res.getBuf(), JDWP.PACKET_HEADER_SIZE); + } + + public boolean setValueSync(long objID, long fldID, RuntimeType type, Object val) throws SmaliDebuggerException { + if (type == RuntimeType.STRING) { + long newID = createString((String) val); + if (newID == -1) { + return false; + } + val = newID; + } + List setters = buildFieldValueSetter(); + FieldValueSetter setter = setters.get(0); + setter.fieldID = fldID; + JDWP.encodeAny(setter.value.idOrValue, val); + Packet res = sendCommandSync(jdwp.objectReference().cmdSetValues().encode(objID, setters)); + tryThrowError(res); + return jdwp.objectReference().cmdSetValues().decode(res.getBuf(), JDWP.PACKET_HEADER_SIZE); + } + + public void getValueSync(long objID, RuntimeField fld) throws SmaliDebuggerException { + List ids = fieldIdPool.get(); + ids.set(0, fld.getFieldID()); + Packet res = sendCommandSync(jdwp.objectReference().cmdGetValues().encode(objID, ids)); + tryThrowError(res); + ObjectReference.GetValues.GetValuesReplyData data = + jdwp.objectReference().cmdGetValues().decode(res.getBuf(), JDWP.PACKET_HEADER_SIZE); + fld.setValue(data.values.get(0).value.idOrValue) + .setType(getType(data.values.get(0).value.tag)); + } + + private long createString(String localStr) throws SmaliDebuggerException { + Packet res = sendCommandSync(jdwp.virtualMachine().cmdCreateString().encode(localStr)); + tryThrowError(res); + CreateStringReplyData id = jdwp.virtualMachine().cmdCreateString().decode(res.getBuf(), JDWP.PACKET_HEADER_SIZE); + return id.stringObject; + } + + public long readID(RuntimeValue val) { + return JDWP.decodeBySize(val.getRawVal().getBytes(), 0, val.getRawVal().size()); + } + + public String readArraySignature(RuntimeValue val) throws SmaliDebuggerException { + return readObjectSignatureSync(val); + } + + public int readArrayLength(RuntimeValue val) throws SmaliDebuggerException { + Packet res = sendCommandSync(jdwp.arrayReference().cmdLength().encode(readID(val))); + tryThrowError(res); + LengthReplyData data = jdwp.arrayReference().cmdLength().decode(res.getBuf(), JDWP.PACKET_HEADER_SIZE); + return data.arrayLength; + } + + /** + * @param startIndex less than 0 means 0 + * @param len less than or equals 0 means the maximum value 99 or the rest of the elements. + * @return An entry, The key is the total length of this array when len is <= 0, otherwise 0, + * the value, if this array is an object array then it's object ids. + */ + public Entry> readArray(RuntimeValue reg, int startIndex, int len) throws SmaliDebuggerException { + long id = readID(reg); + Entry> ret; + if (len <= 0) { + Packet res = sendCommandSync(jdwp.arrayReference().cmdLength().encode(id)); + tryThrowError(res); + LengthReplyData data = jdwp.arrayReference().cmdLength().decode(res.getBuf(), JDWP.PACKET_HEADER_SIZE); + len = Math.min(99, data.arrayLength); + ret = new SimpleEntry<>(data.arrayLength, null); + } else { + ret = new SimpleEntry<>(0, null); + } + startIndex = Math.max(0, startIndex); + Packet res = sendCommandSync(jdwp.arrayReference().cmdGetValues().encode(id, startIndex, len)); + tryThrowError(res); + JDWP.ArrayReference.GetValues.GetValuesReplyData valData = + jdwp.arrayReference().cmdGetValues().decode(res.getBuf(), JDWP.PACKET_HEADER_SIZE); + ret.setValue(valData.values.idOrValues); + return ret; + } + + public byte readByte(RuntimeValue val) { + return JDWP.decodeByte(val.getRawVal().getBytes(), 0); + } + + public char readChar(RuntimeValue val) { + return JDWP.decodeChar(val.getRawVal().getBytes(), 0); + } + + public short readShort(RuntimeValue val) { + return JDWP.decodeShort(val.getRawVal().getBytes(), 0); + } + + public int readInt(RuntimeValue val) { + return JDWP.decodeInt(val.getRawVal().getBytes(), 0); + } + + public float readFloat(RuntimeValue val) { + return JDWP.decodeFloat(val.getRawVal().getBytes(), 0); + } + + /** + * @param val has 8 bytes mostly + */ + public long readAll(RuntimeValue val) { + return JDWP.decodeBySize(val.getRawVal().getBytes(), 0, Math.min(val.getRawVal().size(), 8)); + } + + public double readDouble(RuntimeValue val) { + return JDWP.decodeDouble(val.getRawVal().getBytes(), 0); + } + + @Nullable + public RuntimeDebugInfo getRuntimeDebugInfo(long clsID, long mthID) throws SmaliDebuggerException { + Map secMap = varMap.get(clsID); + RuntimeDebugInfo info = null; + if (secMap != null) { + info = secMap.get(mthID); + } + if (info == null) { + info = initDebugInfo(clsID, mthID); + } + return info; + } + + private RuntimeDebugInfo initDebugInfo(long clsID, long mthID) throws SmaliDebuggerException { + Packet res = sendCommandSync(jdwp.method().cmdVariableTableWithGeneric.encode(clsID, mthID)); + tryThrowError(res); + VarTableWithGenericData data = jdwp.method().cmdVariableTableWithGeneric.decode(res.getBuf(), JDWP.PACKET_HEADER_SIZE); + if (varMap == Collections.EMPTY_MAP) { + varMap = new ConcurrentHashMap<>(); + } + RuntimeDebugInfo info = new RuntimeDebugInfo(data); + varMap.computeIfAbsent(clsID, k -> new HashMap<>()).put(mthID, info); + return info; + } + + private static JDWP initJDWP(OutputStream outputStream, InputStream inputStream) throws SmaliDebuggerException { + try { + handShake(outputStream, inputStream); + outputStream.write(JDWP.Suspend.encode().setPacketID(1).getBytes()); // suspend all threads + Packet res = readPacket(inputStream); + tryThrowError(res); + if (res.isReplyPacket() && res.getID() == 1) { + outputStream.write(JDWP.IDSizes.encode().setPacketID(1).getBytes()); // get id sizes for decoding & encoding of jdwp + // packets. + res = readPacket(inputStream); + tryThrowError(res); + if (res.isReplyPacket() && res.getID() == 1) { + JDWP.IDSizes.IDSizesReplyData sizes = JDWP.IDSizes.decode(res.getBuf(), JDWP.PACKET_HEADER_SIZE); + return new JDWP(sizes); + } + } + } catch (IOException e) { + throw new SmaliDebuggerException(e); + } + throw new SmaliDebuggerException("Failed to init JDWP."); + } + + private static void handShake(OutputStream outputStream, InputStream inputStream) throws SmaliDebuggerException { + byte[] buf = new byte[14]; + try { + outputStream.write(JDWP.encodeHandShakePacket()); + inputStream.read(buf, 0, 14); + } catch (Exception e) { + throw new SmaliDebuggerException("jdwp handshake failed, " + e.getMessage()); + } + if (!JDWP.decodeHandShakePacket(buf)) { + throw new SmaliDebuggerException("jdwp handshake failed."); + } + } + + private MethodsWithGenericData getMethodBySig(long classID, String sig) { + List methods = clsMethodMap.get(classID); + if (methods != null) { + for (MethodsWithGenericData method : methods) { + if (sig.startsWith(method.name + "(") && sig.endsWith(method.signature)) { + return method; + } + } + } + return null; + } + + private int genID() { + return idGenerator.getAndAdd(1); + } + + private static byte[] appendBytes(byte[] buf1, byte[] buf2) { + byte[] tempBuf = new byte[buf1.length + buf2.length]; + System.arraycopy(buf1, 0, tempBuf, 0, buf1.length); + System.arraycopy(buf2, 0, tempBuf, buf1.length, buf2.length); + return tempBuf; + } + + /** + * Read & decode packets from Socket connection + */ + private void decodingLoop() { + Executors.newSingleThreadExecutor().execute(() -> { + boolean errFromCallback; + for (;;) { + errFromCallback = false; + try { + Packet res = readPacket(inputStream); + suspendInfo.nextRound(); + ICommandResult callback = callbackMap.remove(res.getID()); + if (callback != null) { + if (callback != SKIP_RESULT) { + errFromCallback = true; + callback.onCommandReply(res); + } + continue; + } + if (res.getCommandSetID() == 64 && res.getCommandID() == 100) { // command from JVM + errFromCallback = true; + decodeCompositeEvents(res); + } else { + printUnexpectedID(res.getID()); + } + } catch (SmaliDebuggerException e) { + e.printStackTrace(); + if (!errFromCallback) { // fatal error + break; + } + } + } + suspendInfo.setTerminated(); + clearWaitingSyncQueue(); + suspendListener.onSuspendEvent(suspendInfo); + }); + } + + private void sendCommand(ByteBuffer buf, ICommandResult callback) throws SmaliDebuggerException { + int id = genID(); + callbackMap.put(id, callback); + try { + outputStream.write(buf.setPacketID(id).getBytes()); + } catch (IOException e) { + throw new SmaliDebuggerException(e); + } + } + + /** + * Do not use this method inside a ICommandResult callback, it will cause deadlock. + * It should be used in a thread. + */ + private Packet sendCommandSync(ByteBuffer buf) throws SmaliDebuggerException { + SynchronousQueue store = syncQueuePool.get(); + sendCommand(buf, res -> { + try { + store.put(res); + } catch (InterruptedException e) { + e.printStackTrace(); + } + }); + Integer id = syncQueueID.getAndAdd(1); + try { + syncQueueMap.put(id, Thread.currentThread()); + return store.take(); + } catch (InterruptedException e) { + throw new SmaliDebuggerException(e); + } finally { + syncQueueMap.remove(id); + syncQueuePool.put(store); + } + } + + // called by decodingLoop() when fatal error occurred, + // if don't do so the store.take() may block forever. + private void clearWaitingSyncQueue() { + syncQueueMap.keySet().forEach(k -> { + Thread t = syncQueueMap.remove(k); + if (t != null) { + t.interrupt(); + } + }); + } + + private void printUnexpectedID(int id) throws SmaliDebuggerException { + throw new SmaliDebuggerException("Missing handler for this id: " + id); + } + + private void decodeCompositeEvents(Packet res) throws SmaliDebuggerException { + EventData data = jdwp.event().cmdComposite().decode(res.getBuf(), JDWP.PACKET_HEADER_SIZE); + for (JDWP.EventRequestDecoder event : data.events) { + EventListenerAdapter listener = eventListenerMap.get(event.getRequestID()); + if (listener == null) { + try { + printUnexpectedID(event.getRequestID()); + } catch (Exception e) { + e.printStackTrace(); + } + continue; + } + if (event instanceof VMStartEvent) { + listener.onVMStart((VMStartEvent) event); + return; + } + if (event instanceof VMDeathEvent) { + listener.onVMDeath((VMDeathEvent) event); + return; + } + if (event instanceof SingleStepEvent) { + listener.onSingleStep((SingleStepEvent) event); + return; + } + if (event instanceof BreakpointEvent) { + listener.onBreakpoint((BreakpointEvent) event); + return; + } + if (event instanceof MethodEntryEvent) { + listener.onMethodEntry((MethodEntryEvent) event); + return; + } + if (event instanceof MethodExitEvent) { + listener.onMethodExit((MethodExitEvent) event); + return; + } + if (event instanceof MethodExitWithReturnValueEvent) { + listener.onMethodExitWithReturnValue((MethodExitWithReturnValueEvent) event); + return; + } + if (event instanceof MonitorContendedEnterEvent) { + listener.onMonitorContendedEnter((MonitorContendedEnterEvent) event); + return; + } + if (event instanceof MonitorContendedEnteredEvent) { + listener.onMonitorContendedEntered((MonitorContendedEnteredEvent) event); + return; + } + if (event instanceof MonitorWaitEvent) { + listener.onMonitorWait((MonitorWaitEvent) event); + return; + } + if (event instanceof MonitorWaitedEvent) { + listener.onMonitorWaited((MonitorWaitedEvent) event); + return; + } + if (event instanceof ExceptionEvent) { + listener.onException((ExceptionEvent) event); + return; + } + if (event instanceof ThreadStartEvent) { + listener.onThreadStart((ThreadStartEvent) event); + return; + } + if (event instanceof ThreadDeathEvent) { + listener.onThreadDeath((ThreadDeathEvent) event); + return; + } + if (event instanceof ClassPrepareEvent) { + listener.onClassPrepare((ClassPrepareEvent) event); + return; + } + if (event instanceof ClassUnloadEvent) { + listener.onClassUnload((ClassUnloadEvent) event); + return; + } + if (event instanceof FieldAccessEvent) { + listener.onFieldAccess((FieldAccessEvent) event); + return; + } + if (event instanceof FieldModificationEvent) { + listener.onFieldModification((FieldModificationEvent) event); + return; + } + throw new SmaliDebuggerException("Unexpected event: " + event); + } + } + + private final EventListenerAdapter stepListener = new EventListenerAdapter() { + @Override + void onSingleStep(SingleStepEvent event) { + onSuspended(event.thread, + event.location.classID, + event.location.methodID, + event.location.index); + } + }; + + private void sendStepRequest(long threadID, int depth) throws SmaliDebuggerException { + List stepReq = buildStepRequest(threadID, JDWP.StepSize.MIN, depth); + ByteBuffer stepEncodedBuf = jdwp.eventRequest().cmdSet().encode( + (byte) JDWP.EventKind.SINGLE_STEP, + (byte) JDWP.SuspendPolicy.ALL, + stepReq); + stepReqPool.put(stepReq); + sendCommand(stepEncodedBuf, res -> { + tryThrowError(res); + int reqID = jdwp.eventRequest().cmdSet().decodeRequestID(res.getBuf(), JDWP.PACKET_HEADER_SIZE); + eventListenerMap.put(reqID, stepListener); + }); + resume(); + } + + public void resume() throws SmaliDebuggerException { + sendCommand(JDWP.Resume.encode(), SKIP_RESULT); + } + + public void suspend() throws SmaliDebuggerException { + sendCommand(JDWP.Suspend.encode(), SKIP_RESULT); + } + + public void setBreakpoint(RuntimeBreakpoint bp) throws SmaliDebuggerException { + sendCommand(buildBreakpointRequest(bp), res -> { + tryThrowError(res); + bp.reqID = jdwp.eventRequest().cmdSet().decodeRequestID(res.getBuf(), JDWP.PACKET_HEADER_SIZE); + eventListenerMap.put(bp.reqID, new EventListenerAdapter() { + @Override + void onBreakpoint(BreakpointEvent event) { + onSuspended(event.thread, + event.location.classID, + event.location.methodID, + event.location.index); + } + }); + }); + } + + public long getClassID(String clsSig, boolean fetch) throws SmaliDebuggerException { + do { + AllClassesWithGenericData data = classMap.get(clsSig); + if (data == null) { + if (fetch) { + getAllClasses(); + fetch = false; + continue; + } + break; + } else { + return data.typeID; + } + } while (true); + return -1; + } + + public long getMethodID(long cid, String mthSig) throws SmaliDebuggerException { + initClassCache(cid); + MethodsWithGenericData data = getMethodBySig(cid, mthSig); + if (data != null) { + return data.methodID; + } + return -1; + } + + public void initClassCache(long clsID) throws SmaliDebuggerException { + initFields(clsID); + initMethods(clsID); + } + + public void removeBreakpoint(RuntimeBreakpoint bp) throws SmaliDebuggerException { + sendCommand(jdwp.eventRequest().cmdClear().encode((byte) JDWP.EventKind.BREAKPOINT, bp.reqID), SKIP_RESULT); + } + + private ByteBuffer buildBreakpointRequest(RuntimeBreakpoint bp) { + LocationOnlyRequest req = jdwp.eventRequest().cmdSet().newLocationOnlyRequest(); + req.loc.classID = bp.clsID; + req.loc.methodID = bp.mthID; + req.loc.index = bp.offset; + req.loc.tag = JDWP.TypeTag.CLASS; + List list = new ArrayList<>(1); + list.add(req); + return jdwp.eventRequest().cmdSet().encode((byte) JDWP.EventKind.BREAKPOINT, + (byte) JDWP.SuspendPolicy.ALL, list); + } + + /** + * Builds a one-off class prepare event for setting up breakpoints. + */ + private ByteBuffer buildClassMatchReqForBreakpoint(String cls, int eventKind) { + List encoders = new ArrayList<>(2); + ClassMatchRequest match = jdwp.eventRequest().cmdSet().newClassMatchRequest(); + encoders.add(match); + encoders.add(oneOffEventReq); + match.classPattern = cls; + return jdwp.eventRequest().cmdSet().encode((byte) eventKind, + (byte) JDWP.SuspendPolicy.ALL, encoders); + } + + private List buildStepRequest(long threadID, int stepSize, int stepDepth) { + List eventEncoders = stepReqPool.get(); + StepRequest req = (StepRequest) eventEncoders.get(0); + req.size = stepSize; + req.depth = stepDepth; + req.thread = threadID; + return eventEncoders; + } + + private List buildFieldValueSetter() { + FieldValueSetter setter = jdwp.objectReference().cmdSetValues().new FieldValueSetter(); + setter.value = jdwp.new UntaggedValuePacket(); + setter.value.idOrValue = new ByteBuffer(); + List setters = new ArrayList<>(1); + setters.add(setter); + return setters; + } + + private List buildRegValueSetter(int tag, int regNum) { + List setters = new ArrayList<>(1); + SlotValueSetter setter = jdwp.stackFrame().cmdSetValues().new SlotValueSetter(); + setters.add(setter); + setter.slot = regNum; + setter.slotValue = jdwp.new ValuePacket(); + setter.slotValue.tag = tag; + setter.slotValue.idOrValue = new ByteBuffer(); + return setters; + } + + private String getClassSignatureInternal(long id) throws SmaliDebuggerException { + AllClassesWithGenericData data = classIDMap.get(id); + if (data == null) { + getAllClasses(); + } + data = classIDMap.get(id); + if (data != null) { + return data.signature; + } + return null; + } + + private String getMethodSignatureInternal(long clsID, long mthID) throws SmaliDebuggerException { + List mthData = clsMethodMap.get(clsID); + if (mthData == null) { + Packet res = sendCommandSync(jdwp.referenceType().cmdMethodsWithGeneric().encode(clsID)); + tryThrowError(res); + MethodsWithGenericReplyData data = + jdwp.referenceType().cmdMethodsWithGeneric().decode(res.getBuf(), JDWP.PACKET_HEADER_SIZE); + mthData = data.declared; + clsMethodMap.put(clsID, mthData); + } + if (mthData != null) { + for (MethodsWithGenericData data : mthData) { + if (data.methodID == mthID) { + return data.name + data.signature; + } + } + } + return null; + } + + private String sendThreadNameReq(long id) throws SmaliDebuggerException { + Packet res = sendCommandSync(jdwp.threadReference().cmdName().encode(id)); + tryThrowError(res); + NameReplyData nameData = jdwp.threadReference().cmdName().decode(res.getBuf(), JDWP.PACKET_HEADER_SIZE); + return nameData.threadName; + } + + private List getAllFields(long clsID) throws SmaliDebuggerException { + initFields(clsID); + List flds = clsFieldMap.get(clsID); + if (flds != null && flds.size() > 0) { + List rfs = new ArrayList<>(flds.size()); + for (FieldsWithGenericData fld : flds) { + String type = fld.signature; + if (fld.genericSignature != null && !fld.genericSignature.trim().isEmpty()) { + type += "<" + fld.genericSignature + ">"; + } + rfs.add(new RuntimeField(fld.name, type, fld.fieldID, fld.modBits)); + } + return rfs; + } + return Collections.emptyList(); + } + + public Frame getCurrentFrameInternal(long threadID) throws SmaliDebuggerException { + Packet res = sendCommandSync(jdwp.threadReference().cmdFrames().encode(threadID, 0, 1)); + tryThrowError(res); + FramesReplyData frameData = jdwp.threadReference().cmdFrames().decode(res.getBuf(), JDWP.PACKET_HEADER_SIZE); + FramesReplyDataFrames frame = frameData.frames.get(0); + return new Frame(frame.frameID, frame.location.classID, frame.location.methodID, + frame.location.index); + } + + private List getAllFrames(long threadID) throws SmaliDebuggerException { + Packet res = sendCommandSync(jdwp.threadReference().cmdFrames().encode(threadID, 0, -1)); + tryThrowError(res); + FramesReplyData frameData = jdwp.threadReference().cmdFrames().decode(res.getBuf(), JDWP.PACKET_HEADER_SIZE); + List frames = new ArrayList<>(); + for (FramesReplyDataFrames frame : frameData.frames) { + frames.add(new Frame(frame.frameID, frame.location.classID, + frame.location.methodID, frame.location.index)); + } + return frames; + } + + private List getAllThreads() throws SmaliDebuggerException { + Packet res = sendCommandSync(jdwp.virtualMachine().cmdAllThreads().encode()); + tryThrowError(res); + AllThreadsReplyData data; + data = jdwp.virtualMachine().cmdAllThreads().decode(res.getBuf(), + JDWP.PACKET_HEADER_SIZE); + List threads = new ArrayList<>(data.threads.size()); + for (AllThreadsReplyDataThreads thread : data.threads) { + threads.add(thread.thread); + } + return threads; + } + + private void getAllClasses() throws SmaliDebuggerException { + Packet res = sendCommandSync(jdwp.virtualMachine().cmdAllClassesWithGeneric().encode()); + tryThrowError(res); + AllClassesWithGenericReplyData classData = + jdwp.virtualMachine().cmdAllClassesWithGeneric().decode(res.getBuf(), JDWP.PACKET_HEADER_SIZE); + for (AllClassesWithGenericData aClass : classData.classes) { + classMap.put(DbgUtils.classSigToRawFullName(aClass.signature), aClass); + classIDMap.put(aClass.typeID, aClass); + } + } + + private void initFields(long clsID) throws SmaliDebuggerException { + if (clsFieldMap.get(clsID) == null) { + Packet res = sendCommandSync(jdwp.referenceType().cmdFieldsWithGeneric().encode(clsID)); + tryThrowError(res); + FieldsWithGenericReplyData data = + jdwp.referenceType().cmdFieldsWithGeneric().decode(res.getBuf(), JDWP.PACKET_HEADER_SIZE); + clsFieldMap.put(clsID, data.declared); + } + } + + private void initMethods(long clsID) throws SmaliDebuggerException { + if (clsMethodMap.get(clsID) == null) { + Packet res = sendCommandSync(jdwp.referenceType().cmdMethodsWithGeneric().encode(clsID)); + tryThrowError(res); + MethodsWithGenericReplyData data = + jdwp.referenceType().cmdMethodsWithGeneric().decode(res.getBuf(), JDWP.PACKET_HEADER_SIZE); + clsMethodMap.put(clsID, data.declared); + } + } + + /** + * Removes class cache when it's unloaded from JVM. + */ + private void listenClassUnloadEvent() throws SmaliDebuggerException { + sendCommand( + jdwp.eventRequest().cmdSet().encode((byte) JDWP.EventKind.CLASS_UNLOAD, + (byte) JDWP.SuspendPolicy.NONE, Collections.emptyList()), + res -> { + int reqID = jdwp.eventRequest().cmdSet().decodeRequestID(res.getBuf(), JDWP.PACKET_HEADER_SIZE); + eventListenerMap.put(reqID, new EventListenerAdapter() { + @Override + void onClassUnload(ClassUnloadEvent event) { + EVENT_LISTENER_QUEUE.execute(() -> { + System.out.printf("ClassUnloaded: %s%n", event.signature); + AllClassesWithGenericData clsData = classMap.remove(event.signature); + if (clsData != null) { + classIDMap.remove(clsData.typeID); + clsFieldMap.remove(clsData.typeID); + clsMethodMap.remove(clsData.typeID); + varMap.remove(clsData.typeID); + } + }); + } + }); + }); + } + + /** + * Reads a JDWP packet. + */ + private static Packet readPacket(InputStream inputStream) throws SmaliDebuggerException { + byte[] bytes = new byte[JDWP.PACKET_HEADER_SIZE]; + try { + if (inputStream.read(bytes, 0, bytes.length) == bytes.length) { + int len = JDWP.getPacketLength(bytes, 0) - JDWP.PACKET_HEADER_SIZE; + if (len > 0) { + byte[] payload = new byte[len]; + int readSize = 0; + do { + readSize += inputStream.read(payload, readSize, len - readSize); + if (readSize == len) { + bytes = appendBytes(bytes, payload); + break; + } + } while (true); + } + return Packet.make(bytes); + } + } catch (IOException e) { + throw new SmaliDebuggerException(e); + } + throw new SmaliDebuggerException("read packet failed."); + } + + private static void tryThrowError(Packet res) throws SmaliDebuggerException { + if (res.isError()) { + throw new SmaliDebuggerException("(JDWP Error Code:" + res.getErrorCode() + ") " + + res.getErrorText(), res.getErrorCode()); + } + } + + private interface ICommandResult { + void onCommandReply(Packet res) throws SmaliDebuggerException; + } + + private abstract class EventListenerAdapter { + void onVMStart(VMStartEvent event) { + } + + void onVMDeath(VMDeathEvent event) { + } + + void onSingleStep(SingleStepEvent event) { + } + + void onBreakpoint(BreakpointEvent event) { + } + + void onMethodEntry(MethodEntryEvent event) { + } + + void onMethodExit(MethodExitEvent event) { + } + + void onMethodExitWithReturnValue(MethodExitWithReturnValueEvent event) { + } + + void onMonitorContendedEnter(MonitorContendedEnterEvent event) { + } + + void onMonitorContendedEntered(MonitorContendedEnteredEvent event) { + } + + void onMonitorWait(MonitorWaitEvent event) { + } + + void onMonitorWaited(MonitorWaitedEvent event) { + } + + void onException(ExceptionEvent event) { + } + + void onThreadStart(ThreadStartEvent event) { + } + + void onThreadDeath(ThreadDeathEvent event) { + } + + void onClassPrepare(ClassPrepareEvent event) { + } + + void onClassUnload(ClassUnloadEvent event) { + } + + void onFieldAccess(FieldAccessEvent event) { + } + + void onFieldModification(FieldModificationEvent event) { + } + } + + public static class RuntimeField extends RuntimeValue { + private final String name; + private final String fldType; + private final long fieldID; + private final int modBits; + + private RuntimeField(String name, String type, long fieldID, int modBits) { + super(null, null); + this.name = name; + this.fldType = type; + this.fieldID = fieldID; + this.modBits = modBits; + } + + public String getFieldType() { + return fldType; + } + + public String getName() { + return name; + } + + public long getFieldID() { + return fieldID; + } + + private RuntimeField setValue(ByteBuffer rawVal) { + super.rawVal = rawVal; + return this; + } + + public boolean isBelongToThis() { + return !AccessFlags.hasFlag(modBits, AccessFlags.STATIC) + && !AccessFlags.hasFlag(modBits, AccessFlags.SYNTHETIC); + } + } + + public static class RuntimeBreakpoint { + private long clsID; + private long mthID; + private long offset; + private int reqID; + + public long getCodeOffset() { + return offset; + } + } + + public RuntimeBreakpoint makeBreakpoint(long cid, long mid, long offset) { + RuntimeBreakpoint bp = new RuntimeBreakpoint(); + bp.clsID = cid; + bp.mthID = mid; + bp.offset = offset; + return bp; + } + + private RuntimeRegister buildRegister(int num, int tag, ByteBuffer buf) throws SmaliDebuggerException { + return new RuntimeRegister(num, getType(tag), buf); + } + + private RuntimeType getType(int tag) throws SmaliDebuggerException { + switch (tag) { + case JDWP.Tag.ARRAY: + return RuntimeType.ARRAY; + case JDWP.Tag.BYTE: + return RuntimeType.BYTE; + case JDWP.Tag.CHAR: + return RuntimeType.CHAR; + case JDWP.Tag.OBJECT: + return RuntimeType.OBJECT; + case JDWP.Tag.FLOAT: + return RuntimeType.FLOAT; + case JDWP.Tag.DOUBLE: + return RuntimeType.DOUBLE; + case JDWP.Tag.INT: + return RuntimeType.INT; + case JDWP.Tag.LONG: + return RuntimeType.LONG; + case JDWP.Tag.SHORT: + return RuntimeType.SHORT; + case JDWP.Tag.VOID: + return RuntimeType.VOID; + case JDWP.Tag.BOOLEAN: + return RuntimeType.BOOLEAN; + case JDWP.Tag.STRING: + return RuntimeType.STRING; + case JDWP.Tag.THREAD: + return RuntimeType.THREAD; + case JDWP.Tag.THREAD_GROUP: + return RuntimeType.THREAD_GROUP; + case JDWP.Tag.CLASS_LOADER: + return RuntimeType.CLASS_LOADER; + case JDWP.Tag.CLASS_OBJECT: + return RuntimeType.CLASS_OBJECT; + default: + throw new SmaliDebuggerException("Unexpected value: " + tag); + } + } + + public static class RuntimeValue { + protected ByteBuffer rawVal; + protected RuntimeType type; + + RuntimeValue(RuntimeType type, ByteBuffer rawVal) { + this.rawVal = rawVal; + this.type = type; + } + + public RuntimeType getType() { + return type; + } + + public void setType(RuntimeType type) { + this.type = type; + } + + private ByteBuffer getRawVal() { + return rawVal; + } + } + + public static class RuntimeRegister extends RuntimeValue { + private final int num; + + private RuntimeRegister(int num, RuntimeType type, ByteBuffer rawVal) { + super(type, rawVal); + this.num = num; + } + + public int getRegNum() { + return num; + } + } + + public static class RuntimeVarInfo extends RegisterInfo { + private final VarWithGenericSlot slot; + + private RuntimeVarInfo(VarWithGenericSlot slot) { + this.slot = slot; + } + + @Override + public String getName() { + return slot.name; + } + + @Override + public int getRegNum() { + return slot.slot; + } + + @Override + public String getType() { + String gen = getSignature(); + return gen.isEmpty() ? this.slot.signature : gen; + } + + @NonNull + @Override + public String getSignature() { + return this.slot.genericSignature.trim(); + } + + @Override + public int getStartOffset() { + return (int) slot.codeIndex; + } + + @Override + public int getEndOffset() { + return (int) (slot.codeIndex + slot.length); + } + } + + public static class RuntimeDebugInfo { + private final List infoList; + + private RuntimeDebugInfo(VarTableWithGenericData data) { + infoList = new ArrayList<>(data.slots.size()); + for (VarWithGenericSlot slot : data.slots) { + infoList.add(new RuntimeVarInfo(slot)); + } + } + + public List getInfoList() { + return infoList; + } + } + + public enum RuntimeType { + ARRAY(91, "[]"), + BYTE(66, "byte"), + CHAR(67, "char"), + OBJECT(76, "object"), + FLOAT(70, "float"), + DOUBLE(68, "double"), + INT(73, "int"), + LONG(74, "long"), + SHORT(83, "short"), + VOID(86, "void"), + BOOLEAN(90, "boolean"), + STRING(115, "string"), + THREAD(116, "thread"), + THREAD_GROUP(103, "thread_group"), + CLASS_LOADER(108, "class_loader"), + CLASS_OBJECT(99, "class_object"); + + private final int jdwpTag; + private final String desc; + + RuntimeType(int tag, String desc) { + this.jdwpTag = tag; + this.desc = desc; + } + + private int getTag() { + return jdwpTag; + } + + public String getDesc() { + return this.desc; + } + } + + public static class Frame { + private final long id; + private final long clsID; + private final long mthID; + private final long index; + + private Frame(long id, long clsID, long mthID, long index) { + this.id = id; + this.clsID = clsID; + this.mthID = mthID; + this.index = index; + } + + public long getID() { + return id; + } + + public long getClassID() { + return clsID; + } + + public long getMethodID() { + return mthID; + } + + public long getCodeIndex() { + return index; + } + } + + public interface ClassPrepareListener { + void onPrepared(long id); + } + + public interface ClassListener { + void onPrepared(String cls, long id); + + void onUnloaded(String cls); + } + + public static class SmaliDebuggerException extends Exception { + private final int errCode; + private static final long serialVersionUID = -1111111202102191403L; + + public SmaliDebuggerException(Exception e) { + super(e); + errCode = -1; + } + + public SmaliDebuggerException(String msg) { + super(msg); + this.errCode = -1; + } + + public SmaliDebuggerException(String msg, int errCode) { + super(msg); + this.errCode = errCode; + } + + public int getErrCode() { + return errCode; + } + } + + /** + * Listener for breakpoint, watch, step, etc. + */ + public interface SuspendListener { + /** + * For step, breakpoint, watchpoint, and any other events that suspend the JVM. + * This method will be called in stateListenQueue. + */ + void onSuspendEvent(SuspendInfo current); + } + + public static class SuspendInfo { + private boolean terminated; + private boolean newRound; + private final InfoSetter updater = new InfoSetter(); + + public long getThreadID() { + return updater.thread; + } + + public long getClassID() { + return updater.clazz; + } + + public long getMethodID() { + return updater.method; + } + + public long getOffset() { + return updater.offset; + } + + private InfoSetter update() { + updater.changed = false; + updater.nextRound(newRound); + this.newRound = false; + return updater; + } + + // called by decodingLoop, to tell the updater even though the values are the same, + // they are decoded from another packet, they should be treated as new. + private void nextRound() { + newRound = true; + } + + // according to JDWP document it's legal to fire two or more events on a same location, + // e.g. one for single step and the other for breakpoint, so when this happened we only + // want one of them. + private boolean isAnythingChanged() { + return updater.changed; + } + + public boolean isTerminated() { + return terminated; + } + + private void setTerminated() { + terminated = true; + } + + private static class InfoSetter { + private long thread; + private long clazz; + private long method; + private long offset; // code offset; + private boolean changed; + + private void nextRound(boolean newRound) { + if (!changed) { + changed = newRound; + } + } + + private InfoSetter updateThread(long thread) { + if (!changed) { + changed = this.thread != thread; + } + this.thread = thread; + return this; + } + + private InfoSetter updateClass(long clazz) { + if (!changed) { + changed = this.clazz != clazz; + } + this.clazz = clazz; + return this; + } + + private InfoSetter updateMethod(long method) { + if (!changed) { + changed = this.method != method; + } + this.method = method; + return this; + } + + private InfoSetter updateOffset(long offset) { + if (!changed) { + changed = this.offset != offset; + } + this.offset = offset; + return this; + } + } + } +} diff --git a/jadx-gui/src/main/java/jadx/gui/device/debugger/smali/MNEMONIC.java b/jadx-gui/src/main/java/jadx/gui/device/debugger/smali/MNEMONIC.java new file mode 100644 index 000000000..c58a01b52 --- /dev/null +++ b/jadx-gui/src/main/java/jadx/gui/device/debugger/smali/MNEMONIC.java @@ -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" }; +} diff --git a/jadx-gui/src/main/java/jadx/gui/device/debugger/smali/RegisterInfo.java b/jadx-gui/src/main/java/jadx/gui/device/debugger/smali/RegisterInfo.java new file mode 100644 index 000000000..29913a180 --- /dev/null +++ b/jadx-gui/src/main/java/jadx/gui/device/debugger/smali/RegisterInfo.java @@ -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(); + } +} diff --git a/jadx-gui/src/main/java/jadx/gui/device/debugger/smali/Smali.java b/jadx-gui/src/main/java/jadx/gui/device/debugger/smali/Smali.java new file mode 100644 index 000000000..a153437d2 --- /dev/null +++ b/jadx-gui/src/main/java/jadx/gui/device/debugger/smali/Smali.java @@ -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 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 getMthFullIDAndCodeOffsetByLine(int line) { + for (Entry entry : insnMap.entrySet()) { + Integer codeOffset = entry.getValue().getLineMapping().get(line); + if (codeOffset != null) { + return new SimpleEntry<>(entry.getKey(), codeOffset); + } + } + return null; + } + + public List 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 annos = clsData.getAnnotations(); + if (annos.size() > 0) { + smali.startLine(String.format("# %d annotations", annos.size())); + writeAnnotations(smali, annos); + smali.startLine(); + } + + List 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 fields, int[] colWidths) { + int staticIdx = 0; + List 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 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 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 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 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 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 values = (List) 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> insnOffsetMap = new LinkedHashMap<>(); + private final Map regNameMap = new HashMap<>(); + private Map> tipMap = Collections.emptyMap(); + private Map 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 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 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 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 extras = (List) 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 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> 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 tip = tipMap.get(codeOffset); + if (tip != null) { + for (Entry 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 extras = (List) 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 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; + } + } + +} diff --git a/jadx-gui/src/main/java/jadx/gui/device/debugger/smali/SmaliMethodNode.java b/jadx-gui/src/main/java/jadx/gui/device/debugger/smali/SmaliMethodNode.java new file mode 100644 index 000000000..508e548b0 --- /dev/null +++ b/jadx-gui/src/main/java/jadx/gui/device/debugger/smali/SmaliMethodNode.java @@ -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 nodes; // codeOffset: InsnNode + private List regList; + private int[] insnPos; + private int defPos; + private Map 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 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 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 nodes, int insnCount) { + this.nodes = nodes; + insnPos = new int[insnCount]; + } +} diff --git a/jadx-gui/src/main/java/jadx/gui/device/debugger/smali/SmaliRegister.java b/jadx-gui/src/main/java/jadx/gui/device/debugger/smali/SmaliRegister.java new file mode 100644 index 000000000..59097938b --- /dev/null +++ b/jadx-gui/src/main/java/jadx/gui/device/debugger/smali/SmaliRegister.java @@ -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; + } +} diff --git a/jadx-gui/src/main/java/jadx/gui/device/debugger/smali/SmaliWriter.java b/jadx-gui/src/main/java/jadx/gui/device/debugger/smali/SmaliWriter.java new file mode 100644 index 000000000..c5d80ba5a --- /dev/null +++ b/jadx-gui/src/main/java/jadx/gui/device/debugger/smali/SmaliWriter.java @@ -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 getLineMapping() { + return Collections.emptyMap(); + } + + @Override + public Map getAnnotations() { + return Collections.emptyMap(); + } + }; + } +} diff --git a/jadx-gui/src/main/java/jadx/gui/device/protocol/ADB.java b/jadx-gui/src/main/java/jadx/gui/device/protocol/ADB.java new file mode 100644 index 000000000..eb0a174c8 --- /dev/null +++ b/jadx-gui/src/main/java/jadx/gui/device/protocol/ADB.java @@ -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 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 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 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 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 id); + + void jdwpListenerClosed(Device device); + } + + public interface DeviceStateListener { + void onDeviceStatusChange(List 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 list = getProp("ro.build.version.release"); + if (list.size() != 0) { + androidReleaseVer = list.get(0); + } + } catch (IOException e) { + e.printStackTrace(); + androidReleaseVer = ""; + } + return androidReleaseVer; + } + + public List getProp(String entry) throws IOException { + Socket socket = connect(info.adbHost, info.adbPort); + List 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 getProcessByPkg(String pkg) throws IOException { + return getProcessList("ps | grep " + pkg, 0); + } + + @NonNull + public List getProcessList() throws IOException { + return getProcessList("ps", 1); + } + + private List getProcessList(String cmd, int index) throws IOException { + Socket socket = connect(info.adbHost, info.adbPort); + List 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 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; + } + } +} diff --git a/jadx-gui/src/main/java/jadx/gui/settings/JadxSettings.java b/jadx-gui/src/main/java/jadx/gui/settings/JadxSettings.java index ad8a9bc10..f658f0e52 100644 --- a/jadx-gui/src/main/java/jadx/gui/settings/JadxSettings.java +++ b/jadx-gui/src/main/java/jadx/gui/settings/JadxSettings.java @@ -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) { diff --git a/jadx-gui/src/main/java/jadx/gui/treemodel/JClass.java b/jadx-gui/src/main/java/jadx/gui/treemodel/JClass.java index 47af50069..441358a99 100644 --- a/jadx-gui/src/main/java/jadx/gui/treemodel/JClass.java +++ b/jadx-gui/src/main/java/jadx/gui/treemodel/JClass.java @@ -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; diff --git a/jadx-gui/src/main/java/jadx/gui/ui/ADBDialog.java b/jadx-gui/src/main/java/jadx/gui/ui/ADBDialog.java new file mode 100644 index 000000000..e9bf89e7b --- /dev/null +++ b/jadx-gui/src/main/java/jadx/gui/ui/ADBDialog.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 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 deviceInfoList) { + List 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 id) { + List 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 state text. + */ + procs = device.getProcessList(); + } catch (IOException | InterruptedException e) { + e.printStackTrace(); + procs = Collections.emptyList(); + } + List 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 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 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; + } + } +} diff --git a/jadx-gui/src/main/java/jadx/gui/ui/IDebugController.java b/jadx-gui/src/main/java/jadx/gui/ui/IDebugController.java new file mode 100644 index 000000000..9166e873e --- /dev/null +++ b/jadx-gui/src/main/java/jadx/gui/ui/IDebugController.java @@ -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); + } +} diff --git a/jadx-gui/src/main/java/jadx/gui/ui/JDebuggerPanel.java b/jadx-gui/src/main/java/jadx/gui/ui/JDebuggerPanel.java new file mode 100644 index 000000000..48400daaa --- /dev/null +++ b/jadx-gui/src/main/java/jadx/gui/ui/JDebuggerPanel.java @@ -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 stackFrameList; + private final transient JComboBox 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 nodes) { + nodes.forEach(regTreeNode::add); + } + + public void updateThisFieldNodes(List nodes) { + nodes.forEach(thisTreeNode::add); + } + + public void refreshThreadBox(List elements) { + if (elements.size() > 0) { + DefaultComboBoxModel model = + (DefaultComboBoxModel) threadBox.getModel(); + elements.forEach(model::addElement); + } + SwingUtilities.invokeLater(() -> { + threadBox.updateUI(); + stackFrameList.setFont(mainWindow.getSettings().getFont()); + }); + } + + public void refreshStackFrameList(List elements) { + if (elements.size() > 0) { + DefaultListModel model = + (DefaultListModel) 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) stackFrameList.getModel()).removeAllElements(); + ((DefaultComboBoxModel) 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(); + } +} diff --git a/jadx-gui/src/main/java/jadx/gui/ui/MainWindow.java b/jadx-gui/src/main/java/jadx/gui/ui/MainWindow.java index 12998f847..a1d9b4e6d 100644 --- a/jadx-gui/src/main/java/jadx/gui/ui/MainWindow.java +++ b/jadx-gui/src/main/java/jadx/gui/ui/MainWindow.java @@ -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; diff --git a/jadx-gui/src/main/java/jadx/gui/ui/SetValueDialog.java b/jadx-gui/src/main/java/jadx/gui/ui/SetValueDialog.java new file mode 100644 index 000000000..12047b5ce --- /dev/null +++ b/jadx-gui/src/main/java/jadx/gui/ui/SetValueDialog.java @@ -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 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 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 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; + } + }); + } +} diff --git a/jadx-gui/src/main/java/jadx/gui/ui/TabbedPane.java b/jadx-gui/src/main/java/jadx/gui/ui/TabbedPane.java index 18104e516..a6edb6948 100644 --- a/jadx-gui/src/main/java/jadx/gui/ui/TabbedPane.java +++ b/jadx-gui/src/main/java/jadx/gui/ui/TabbedPane.java @@ -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 diff --git a/jadx-gui/src/main/java/jadx/gui/ui/VarTreePopupMenu.java b/jadx-gui/src/main/java/jadx/gui/ui/VarTreePopupMenu.java new file mode 100644 index 000000000..43ad266b7 --- /dev/null +++ b/jadx-gui/src/main/java/jadx/gui/ui/VarTreePopupMenu.java @@ -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); + } +} diff --git a/jadx-gui/src/main/java/jadx/gui/ui/codearea/ClassCodeContentPanel.java b/jadx-gui/src/main/java/jadx/gui/ui/codearea/ClassCodeContentPanel.java index cf465c4c9..b58b2580e 100644 --- a/jadx-gui/src/main/java/jadx/gui/ui/codearea/ClassCodeContentPanel.java +++ b/jadx-gui/src/main/java/jadx/gui/ui/codearea/ClassCodeContentPanel.java @@ -86,4 +86,8 @@ public final class ClassCodeContentPanel extends AbstractCodeContentPanel { public AbstractCodeArea getSmaliCodeArea() { return smaliCodePanel.getCodeArea(); } + + public void showSmaliPane() { + areaTabbedPane.setSelectedComponent(smaliCodePanel); + } } diff --git a/jadx-gui/src/main/java/jadx/gui/ui/codearea/CodePanel.java b/jadx-gui/src/main/java/jadx/gui/ui/codearea/CodePanel.java index e0b729bd7..684737e1b 100644 --- a/jadx-gui/src/main/java/jadx/gui/ui/codearea/CodePanel.java +++ b/jadx-gui/src/main/java/jadx/gui/ui/codearea/CodePanel.java @@ -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()); } diff --git a/jadx-gui/src/main/java/jadx/gui/ui/codearea/SmaliArea.java b/jadx-gui/src/main/java/jadx/gui/ui/codearea/SmaliArea.java index bcdedd32e..85d451336 100644 --- a/jadx-gui/src/main/java/jadx/gui/ui/codearea/SmaliArea.java +++ b/jadx-gui/src/main/java/jadx/gui/ui/codearea/SmaliArea.java @@ -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 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 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); + } + } + }; + } + }; + } + }; + } } diff --git a/jadx-gui/src/main/java/jadx/gui/utils/ObjectPool.java b/jadx-gui/src/main/java/jadx/gui/utils/ObjectPool.java new file mode 100644 index 000000000..0c4166cb0 --- /dev/null +++ b/jadx-gui/src/main/java/jadx/gui/utils/ObjectPool.java @@ -0,0 +1,34 @@ +package jadx.gui.utils; + +import java.lang.ref.WeakReference; +import java.util.concurrent.ConcurrentLinkedQueue; + +public class ObjectPool { + + private final ConcurrentLinkedQueue> pool = new ConcurrentLinkedQueue<>(); + private final Creator creator; + + public interface Creator { + T create(); + } + + public ObjectPool(Creator creator) { + this.creator = creator; + } + + public T get() { + T node; + do { + WeakReference 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)); + } +} diff --git a/jadx-gui/src/main/java/jadx/gui/utils/UiUtils.java b/jadx-gui/src/main/java/jadx/gui/utils/UiUtils.java index 0b01e4d94..1088e4013 100644 --- a/jadx-gui/src/main/java/jadx/gui/utils/UiUtils.java +++ b/jadx-gui/src/main/java/jadx/gui/utils/UiUtils.java @@ -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); } diff --git a/jadx-gui/src/main/resources/i18n/Messages_de_DE.properties b/jadx-gui/src/main/resources/i18n/Messages_de_DE.properties index 409caf344..e2bd282a4 100644 --- a/jadx-gui/src/main/resources/i18n/Messages_de_DE.properties +++ b/jadx-gui/src/main/resources/i18n/Messages_de_DE.properties @@ -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... diff --git a/jadx-gui/src/main/resources/i18n/Messages_en_US.properties b/jadx-gui/src/main/resources/i18n/Messages_en_US.properties index 28c314f15..1e5071e05 100644 --- a/jadx-gui/src/main/resources/i18n/Messages_en_US.properties +++ b/jadx-gui/src/main/resources/i18n/Messages_en_US.properties @@ -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... diff --git a/jadx-gui/src/main/resources/i18n/Messages_es_ES.properties b/jadx-gui/src/main/resources/i18n/Messages_es_ES.properties index 653ff2138..9ba2120e1 100644 --- a/jadx-gui/src/main/resources/i18n/Messages_es_ES.properties +++ b/jadx-gui/src/main/resources/i18n/Messages_es_ES.properties @@ -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... diff --git a/jadx-gui/src/main/resources/i18n/Messages_ko_KR.properties b/jadx-gui/src/main/resources/i18n/Messages_ko_KR.properties index 2142aaacb..6b9bd6f0a 100644 --- a/jadx-gui/src/main/resources/i18n/Messages_ko_KR.properties +++ b/jadx-gui/src/main/resources/i18n/Messages_ko_KR.properties @@ -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... diff --git a/jadx-gui/src/main/resources/i18n/Messages_zh_CN.properties b/jadx-gui/src/main/resources/i18n/Messages_zh_CN.properties index e1aa47abc..4b001ceb1 100644 --- a/jadx-gui/src/main/resources/i18n/Messages_zh_CN.properties +++ b/jadx-gui/src/main/resources/i18n/Messages_zh_CN.properties @@ -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... diff --git a/jadx-gui/src/main/resources/icons-16/breakpoint.png b/jadx-gui/src/main/resources/icons-16/breakpoint.png new file mode 100644 index 0000000000000000000000000000000000000000..0cfd585963d255190b8855a7689e8da1c4d7cf6b GIT binary patch literal 700 zcmV;t0z>_YP)*?Fk0YVb%?UEFajs1S?+YtYiPrjx0+ z+4YbyJXwz!SX#yqTlhtNQ%Ku9=RNm$j)&+(}lZ!UGGp|@|O z09YA#-dR#rIaGe;MBLe!ht*}!c?U}6YT!dfHDO%~>xtx&Klk-^WB==sC_vP4ddg4L z#GN10u$+QGf$!(i3&8VpF6O6+ef~&gQ#>AVqCJH_utvKMAuOeG%3%mn<<%9)yb~#4 zHc70e5sYyQ03$?zFUko7D1Bg1=6jXvg#bUm1b(pVKuC*}koEKGdj<=zdM#RWsl+kfRf;OU^G_BQh+Fc$z&F_AHuQYu(b)aq=H_Fx idDl8IBmWBc*Z2i=4uSP&;Q8VJ0000p(d#PxV(4ao^#JV?>+&chcHA(W9zrcuzIeZN_*SKBfUBi{Yn_Ku88)CIy(5o z14=m5T;5${pO7EA36uz_sNoww%td4ohv#73M1nBfuf4Ii#Zb`Xkx?c^#Wj|EB;1+9 zTAO`3)Mn^#lUbsMOIsNead(bTs~hdEii`#-c%_#Z93Byp8u;Rfb|2RBC;KZL62xLk z1l1Nr<$n(?o+_Scw@RxHl?15^Rr)FwKC;G~!x?W|M;u9M&TQbFkgoE9M-rMS(BuHC5 zlgUq0Q544c8(ae&UR$8ps&snq6^bPY3v3xAmMW74Di$h~GCH6E3TaYs2#6A<7K*gC z777H71_Wa;(dfp+g-drPCSWu)#PInZi72LJ;o?i~$-U=y&UbQ89Dul3%3P+Axkzc* zbH-y;QF=hR{qLItf%ci2_&e5wNo0gnVatG?ul6Zw=o$I9Ljfn*ic3`U?>IfEim3g{ zujU&$-hy6wn;w(xme|zJm;lWJxtTFfM)q0`kX!Vu0+d${$}LCddK1<^htTe-fUYL3 zB`SdNsZD>RgvLj1<^@h6_+cDRK2Brcr2~>%$*5S)hyV33PV^teac3%|4lz@8p4?)5 z?t5o^?q+%^%)Yygo~I^U4VR!bTnWuE35hcWrfCDR3q+sxJ79e7Fg`&)RCqLA^2^y^ z0laVfadW90_Fz8Brm|r47sB^u1VgI>kanj)Z4`zMSfHlm8>CwXa$JVM`$2RrmZB-3 zN10m-!;BvH*Br3V8t`DH7m`jf#2upVDXl{5ff18_pzCPK1Zu$$CKKvd8FGeFf)+K<|x33pc7P&S#3GZT4mEw;nr(Ze*F z3&*?-4U-lm*#tber5 z%S_ceqB`b3ko6r~BbvDwdohTvP(3a(pq{x#T$yQsu#OKwEe}KuH^Mh@nxg_(Nw136 zq#a^3xNBke)In+!?qk3%4wB69{pF`Tzg`07*qoM6N<$ Eg55P&8UO$Q literal 0 HcmV?d00001 diff --git a/jadx-gui/src/main/resources/icons-16/device.png b/jadx-gui/src/main/resources/icons-16/device.png new file mode 100644 index 0000000000000000000000000000000000000000..dd0168ea92e50f4c56768a8a4307774445448f4a GIT binary patch literal 813 zcmV+|1JeA7P)s+KnkY`Eo6h&V=bWA+7NR@wyLq4U|DET3pYsaU)z#lsRm~+9=1NbpotRP@QR?BSgMY-(!CUtKd&Fi%mGr^Lm|%1SMffWkU;#IdzCBL&UGNwUk% z3w3;hXa?gNmU=Q(-80<)DkYZ9q5-ko%lN?eIUdbj+ zNQ|)`L?h9?t^*3D5`Kc)lD^?%tP?)NJ&4LSybYf~_iP~;JICqkrwCgjflLIcSm-hX zC7Z06WmSkScoB}2V{qQ7?_VYKcURCGd(G=TlEDO~ z(CBCi0p}ZRwi-}gR@J|x$~&{u^2NTfu7%?dax5>aMwTT;I$0A5atT96>=BBk3YfJF zh@+)=F+%g#I1b!O-j!j`Lw@>R%*SKs{{1;xde?4ixgL({f}|CJkYXH0d-x8DlCQz4 zG8`#53q@0*s0vhQ7>|siFmpemY*G9g`VjHn7VY{LBE)mzioa$e3H2M{(9cMCAqXOR#lX@1Tgnr->YYTBeqS6v!3>T6kRG3WqCo3x$(B3 zLi6Ce2?`$`uhseluh-j80yoh!G&JN(G)7612HZE7ww8Df;r(Fq7~K;+Jh1#9Xj;Mx rxbAV+2r)%9SQb^qJz1*-|E>Q8ss6iyye%@B00000NkvXXu0mjfIy`F+ literal 0 HcmV?d00001 diff --git a/jadx-gui/src/main/resources/icons-16/pause.png b/jadx-gui/src/main/resources/icons-16/pause.png new file mode 100644 index 0000000000000000000000000000000000000000..96185ff0831139dea464623323df0644c0d8d0cb GIT binary patch literal 582 zcmV-M0=fN(P)Px#1ZP1_K>z@;j|==^1poj532;bRa{vGi!vFvd!vV){sAK>D0ozGLK~y+TO;Sy3 z6hRbx{V_W;i4cU95CS3WIX68jf;az zs<-1N1;b3gdR_CXx}EeG9~{3?k>@@HDXOx&d+*)e_fOBrT_284)%zEMdd3s$dimh$ z>kc-nx7~oM*0E_Mmm=bvsFS9ru$Gw?Y|hy@?_BeWRbVUNy%$Z%3Jy(ON`nm}LQS@Z zs_GY*wa4uQrDWEf^Wrcj8b>_^6bbkN>tQf}*4FtQ0g-RPJBcyc1R=()g_26kH}i3r zz=dda;%){Z#0nW;5C|}^|A4(?dSJK-Jm~3bjz!Kofat?x{y8Cs3SM(czk7-bPj7vj z_|JZ^5x5>69U%Ra0s&57$mN``5Av|Wo_&fJj*qufxv(C<1WFZ7GzOQoNCkhiP`B+{ za-;i`9R%xF_;wY%gNxPR3pv6B(mJ(~N0gB^Ql|f=Za!KEK;hU=K&&+H+Xr9*YfVcW zqokZHgWPp8pHGM6u9`)MfQubnR=Y1HWO3(`_cf+b25@DJ#zdQm}8GzWtq2-QnZ8W6mB^kfeK5f%S{ zUW%tGMCwrwic~ZrQcG=4f?5bkV+3dRk8hw6bk~y$KX#b!y*J4EJ~>;dRASqrSu;ZpM>?P}K~6AT zWv6Dmq?v&9LdXC(m%WCO6ma_di$R(v$@ad_>@R41N3N5lSJq9@6CGhX84-$%Xrd_6 z;){?{E|Ytt5$S-&Au>t4wDlIxdkfe-a22LMj``McG};r8@{GsRPm*+8fFey6C)@ifDBXVyTw(N@Xd41b45OFg6x_QA zpwLiigyy~cVoPxW^r~C7ZQpr%>1$*HKmv~AY-qJw4;gUecS--wnqslISSS=^KA&Ic n@BK|Onfz#3R%n{$a)0j^sqv5F(1NTL00000NkvXXu0mjf3S}fX literal 0 HcmV?d00001 diff --git a/jadx-gui/src/main/resources/icons-16/rerun.png b/jadx-gui/src/main/resources/icons-16/rerun.png new file mode 100644 index 0000000000000000000000000000000000000000..0de26566d4102eec080253c2d08985ec58b14838 GIT binary patch literal 685 zcmV;e0#f~nP)`!iy8(2_#ButL^3%VaH2WCpD^U)OZxp@C)2#hU)y+@T%ZNzJigNk%37 zz-WYJwT%teVfiEI+B*@v4ey@58(ld4VY_&5-ox`e@AKg+0U-I`y79bmuw_~y6+4rZ zBG5EdFDS+@M0OSE`>d7SUDOzKZ&h*4eB1iX7tOd9RiYtW2mQ--bUahxr1`i{RG@dM zL#}_X=DDO1{;UI$pFu=dLYT_=5d8WC-sLfjr7UO-HKMAwa=!>)kEhvuwre zuW3yF@ZxFCkI*+ad|5kOX%5zu8IQjhan)UqgSrFGA_0nQFn@Z08DSEUToCSz4Z1ls z&fDbq$T&7|6iq$_uDI$@q1_kQ@dfqk*0>{SDL6V)94@)ete)j++*>bIc9sj}Y;R1o z#OpH+Yt-^4wfv{nern^iVag8IO8de(|Ml<%@O-40!dwX61{2C5s-llVw2V@@N0oo_PPieZ!0Y2~+R( zk!(QTf=B;X9DnzJ@u9c>OP4(U@7{849!UlyO@H`*;lVfmCvAW6f9CF&{}ZR*{jXDW zb_vl21ozzrYJBy-Vb$aRjjJF3@7nm}zjw#A|58cE9uZ}LbIY~=6ShA8U$XeY|MDdd zfQCH!?_7WRzhvaG%|sbsT7Kz&`}!yUix%Do#>T_{_Ei`DO9UTSBkH=Hg(w4*^UnUS zTk-IJ<+2C=ZObqG7Z2FGlB7VCN;>(!bn*TFHYMl(i+Sx`L~=ArL>~EXU3lidsO!!J pWF;gqzXSh89JkLNxXeT<1_12n>%V}Y6R`jQ002ovPDHLkV1iLCz99er literal 0 HcmV?d00001 diff --git a/jadx-gui/src/main/resources/icons-16/step_into.png b/jadx-gui/src/main/resources/icons-16/step_into.png new file mode 100644 index 0000000000000000000000000000000000000000..b1a1819238c6de8f9e50988f4151261fa6ba64ea GIT binary patch literal 349 zcmV-j0iyniP)o>#A+qW*AYQLZl(!&BX$x7Ik;qO170ssEM z@$bKXf%rGW?|(r27bf-TSv zD}TdX0CM*JhkLO)8|Y^+n~Q^sK~hqR;q|N647YFGy>NTZJsWr!5CaSfwJm@a><8NX v2&h?|8-a>7*bay4g1#tO$)YSGB-# z09u8rA}2Xye`S%*{zU3D^_fPdmiyPg-%=noKoSatC_xfp1wO}z@<18F!7w5ZBTtQy zk>Q?#Vu_u^$z|>m7x@d+R@cH9d2;6$#K=oAGXHn~_+m9`^0Ksl zybr&x6m>QAki2vjRQTUKo@})pZ5^O0;;V6NP3$@V;4k*yCND4)s8xvJbzU?;j6jsI zmJlOe00i|<1M%2^mY^!=O@UVms-Tw!6e7N`0#c#W>-;y{_WlDAEgOIU00000fhdEP)RB*?~^j!LKVQ>(O&A{Xr%)RXLn#U zs4LtZ6rCMFY5|B2$)yG$6aaIF_YP)*?Fk0YVb%?UEFajs1S?+YtYiPrjx0+ z+4YbyJXwz!SX#yqTlhtNQ%Ku9=RNm$j)&+(}lZ!UGGp|@|O z09YA#-dR#rIaGe;MBLe!ht*}!c?U}6YT!dfHDO%~>xtx&Klk-^WB==sC_vP4ddg4L z#GN10u$+QGf$!(i3&8VpF6O6+ef~&gQ#>AVqCJH_utvKMAuOeG%3%mn<<%9)yb~#4 zHc70e5sYyQ03$?zFUko7D1Bg1=6jXvg#bUm1b(pVKuC*}koEKGdj<=zdM#RWsl+kfRf;OU^G_BQh+Fc$z&F_AHuQYu(b)aq=H_Fx idDl8IBmWBc*Z2i=4uSP&;Q8VJ0000p(d#PxV(4ao^#JV?>+&chcHA(W9zrcuzIeZN_*SKBfUBi{Yn_Ku88)CIy(5o z14=m5T;5${pO7EA36uz_sNoww%td4ohv#73M1nBfuf4Ii#Zb`Xkx?c^#Wj|EB;1+9 zTAO`3)Mn^#lUbsMOIsNead(bTs~hdEii`#-c%_#Z93Byp8u;Rfb|2RBC;KZL62xLk z1l1Nr<$n(?o+_Scw@RxHl?15^Rr)FwKC;G~!x?W|M;u9M&TQbFkgoE9M-rMS(BuHC5 0) { - smali.startLine().startLine("# annotations"); - printAnnotations(smali, cls.getAnnotations()); - } - List>> 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>> flds, - List staticFieldInitValues) { - int staticIdx = 0; - int accessColWidth = 0; - int nameColWidth = 0; - List accesses = new ArrayList<>(flds.size()); - for (Entry> 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> 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 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 formatMthParamInfo(IMethodData mth, SmaliCodeWriter smali, ICodeReader codeReader) { - List 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 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 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 values = (List) 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> tips = Collections.emptyList(); - - void setLine(String str) { - line = str; - } - - void addLine(String str) { - if (!(line instanceof List)) { - line = new ArrayList(); - } - ((ArrayList) 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 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) line).size(); - for (int i = 0; i < size; i++) { - smali.add(((List) line).get(i)); - if (i != size - 1) { - smali.startLine(); - } - } - } else { - smali.add(line); - } - } - } - - StringBuilder lineWriter = new StringBuilder(50); - Map targetMap = new HashMap<>(); - Map payloadOffsetMap = new HashMap<>(); - Map paramMap; - List 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 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>> dbgLines = null; // line num: tip - if (hasDbgInfo) { - dbgLines = new ArrayList<>(); - } - for (int i = 0; i < smaliList.size(); i++) { - SmaliLine line = smaliList.get(i); - Map tipSet = Collections.emptyMap(); // tip: reference count - for (Iterator> it = line.tips.iterator(); it.hasNext();) { - Entry 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 tip, int i, - List>> dbgLines) { - boolean removeIt = false; - if (tip.getKey().startsWith(".line ")) { // debug info source line. - if (dbgLines.size() > 0) { - Entry> 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); - } - } } diff --git a/jadx-plugins/jadx-plugins-api/src/main/java/jadx/api/plugins/input/data/IClassData.java b/jadx-plugins/jadx-plugins-api/src/main/java/jadx/api/plugins/input/data/IClassData.java index c7c03c554..863e18843 100644 --- a/jadx-plugins/jadx-plugins-api/src/main/java/jadx/api/plugins/input/data/IClassData.java +++ b/jadx-plugins/jadx-plugins-api/src/main/java/jadx/api/plugins/input/data/IClassData.java @@ -31,6 +31,4 @@ public interface IClassData { List getAnnotations(); String getDisassembledCode(); - - String getDisassembledCodeV2(); }