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