diff --git a/jadx-core/src/main/java/jadx/core/dex/nodes/ClassNode.java b/jadx-core/src/main/java/jadx/core/dex/nodes/ClassNode.java index cb47f3b8e..37db7ef34 100644 --- a/jadx-core/src/main/java/jadx/core/dex/nodes/ClassNode.java +++ b/jadx-core/src/main/java/jadx/core/dex/nodes/ClassNode.java @@ -576,6 +576,29 @@ public class ClassNode extends NotificationAttrNode implements ILoadable, ICodeN sb.append(this.clsData.getDisassembledCode()); } + public String getSmaliV2() { + StringBuilder sb = new StringBuilder(); + getSmaliV2(sb); + sb.append(System.lineSeparator()); + Set allInlinedClasses = new LinkedHashSet<>(); + getInnerAndInlinedClassesRecursive(allInlinedClasses); + for (ClassNode innerClass : allInlinedClasses) { + innerClass.getSmaliV2(sb); + sb.append(System.lineSeparator()); + } + return sb.toString(); + } + + private void getSmaliV2(StringBuilder sb) { + if (this.clsData == null) { + sb.append(String.format("###### Class %s is created by jadx", getFullName())); + return; + } + sb.append(String.format("###### Class %s (%s)", getFullName(), getRawName())); + sb.append(System.lineSeparator()); + sb.append(this.clsData.getDisassembledCodeV2()); + } + public ProcessState getState() { return state; } diff --git a/jadx-gui/src/main/java/jadx/gui/settings/JadxSettings.java b/jadx-gui/src/main/java/jadx/gui/settings/JadxSettings.java index d9b3386b8..e1702cf76 100644 --- a/jadx-gui/src/main/java/jadx/gui/settings/JadxSettings.java +++ b/jadx-gui/src/main/java/jadx/gui/settings/JadxSettings.java @@ -54,6 +54,7 @@ public class JadxSettings extends JadxCLIArgs { private boolean checkForUpdates = false; private List recentProjects = new ArrayList<>(); private String fontStr = ""; + private String smaliFontStr = ""; private String editorThemePath = ""; private LangLocale langLocale = NLS.defaultLocale(); private boolean autoStartJobs = false; @@ -68,6 +69,7 @@ public class JadxSettings extends JadxCLIArgs { private int srhResourceSkipSize = 1000; private String srhResourceFileExt = ".xml|.html|.js|.json|.txt"; private boolean keepCommonDialogOpen = false; + private boolean smaliAreaShowBytecode = false; /** * UI setting: the width of the tree showing the classes, resources, ... @@ -377,6 +379,27 @@ public class JadxSettings extends JadxCLIArgs { } } + public Font getSmaliFont() { + if (smaliFontStr.isEmpty()) { + return DEFAULT_FONT; + } + try { + return FontUtils.loadByStr(smaliFontStr); + } catch (Exception e) { + LOG.warn("Failed to load font: {} for smali, reset to default", smaliFontStr, e); + setSmaliFont(DEFAULT_FONT); + return DEFAULT_FONT; + } + } + + public void setSmaliFont(@Nullable Font font) { + if (font == null) { + this.smaliFontStr = ""; + } else { + this.smaliFontStr = FontUtils.convertToStr(font); + } + } + public void setLogLevel(LogHelper.LogLevelEnum level) { this.logLevel = level; } @@ -430,6 +453,14 @@ public class JadxSettings extends JadxCLIArgs { return keepCommonDialogOpen; } + public void setSmaliAreaShowBytecode(boolean yes) { + smaliAreaShowBytecode = yes; + } + + public boolean getSmaliAreaShowBytecode() { + return smaliAreaShowBytecode; + } + private void upgradeSettings(int fromVersion) { LOG.debug("upgrade settings from version: {} to {}", fromVersion, CURRENT_SETTINGS_VERSION); if (fromVersion == 0) { diff --git a/jadx-gui/src/main/java/jadx/gui/settings/JadxSettingsWindow.java b/jadx-gui/src/main/java/jadx/gui/settings/JadxSettingsWindow.java index 540470242..1bfec9ac9 100644 --- a/jadx-gui/src/main/java/jadx/gui/settings/JadxSettingsWindow.java +++ b/jadx-gui/src/main/java/jadx/gui/settings/JadxSettingsWindow.java @@ -7,8 +7,7 @@ import java.awt.event.ActionEvent; import java.awt.event.ItemEvent; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; -import java.util.Arrays; -import java.util.Collection; +import java.util.*; import javax.swing.*; import javax.swing.event.ChangeEvent; @@ -292,6 +291,7 @@ public class JadxSettingsWindow extends JDialog { private SettingsGroup makeEditorGroup() { JButton fontBtn = new JButton(NLS.str("preferences.select_font")); + JButton smaliFontBtn = new JButton(NLS.str("preferences.select_smali_font")); EditorTheme[] editorThemes = EditorTheme.getAllThemes(); JComboBox themesCbx = new JComboBox<>(editorThemes); @@ -311,6 +311,7 @@ public class JadxSettingsWindow extends JDialog { SettingsGroup group = new SettingsGroup(NLS.str("preferences.editor")); JLabel fontLabel = group.addRow(getFontLabelStr(), fontBtn); group.addRow(NLS.str("preferences.theme"), themesCbx); + JLabel smaliFontLabel = group.addRow(getSmaliFontLabelStr(), smaliFontBtn); fontBtn.addMouseListener(new MouseAdapter() { @Override @@ -327,6 +328,22 @@ public class JadxSettingsWindow extends JDialog { } } }); + + smaliFontBtn.addMouseListener(new MouseAdapter() { + @Override + public void mouseClicked(MouseEvent e) { + JFontChooser fontChooser = new JPreferredFontChooser(); + fontChooser.setSelectedFont(settings.getSmaliFont()); + int result = fontChooser.showDialog(JadxSettingsWindow.this); + if (result == JFontChooser.OK_OPTION) { + Font font = fontChooser.getSelectedFont(); + LOG.debug("Selected Font: {} for smali", font); + settings.setSmaliFont(font); + mainWindow.loadSettings(); + smaliFontLabel.setText(getSmaliFontLabelStr()); + } + } + }); return group; } @@ -336,6 +353,12 @@ public class JadxSettingsWindow extends JDialog { return NLS.str("preferences.font") + ": " + font.getFontName() + ' ' + fontStyleName + ' ' + font.getSize(); } + private String getSmaliFontLabelStr() { + Font font = settings.getSmaliFont(); + String fontStyleName = FontUtils.convertFontStyleToString(font.getStyle()); + return NLS.str("preferences.smali_font") + ": " + font.getFontName() + ' ' + fontStyleName + ' ' + font.getSize(); + } + private SettingsGroup makeDecompilationGroup() { JCheckBox fallback = new JCheckBox(); fallback.setSelected(settings.isFallbackMode()); @@ -578,4 +601,42 @@ public class JadxSettingsWindow extends JDialog { add(Box.createVerticalGlue()); } } + + private static class JPreferredFontChooser extends JFontChooser { + private static final String[] PREFERRED_FONTS = new String[] { + "Monospaced", "Consolas", "Courier", "Courier New", + "Lucida Sans Typewriter", "Lucida Console", + "SimSun", "SimHei", + }; + + private String[] filteredFonts; + + @Override + protected String[] getFontFamilies() { + if (filteredFonts == null) { + GraphicsEnvironment env = GraphicsEnvironment.getLocalGraphicsEnvironment(); + Set fontSet = new HashSet<>(); + Collections.addAll(fontSet, env.getAvailableFontFamilyNames()); + ArrayList found = new ArrayList<>(PREFERRED_FONTS.length); + for (String font : PREFERRED_FONTS) { + if (fontSet.contains(font)) { + found.add(font); + } + } + if (found.size() == PREFERRED_FONTS.length) { + filteredFonts = PREFERRED_FONTS; + } else if (found.size() > 0) { + filteredFonts = new String[found.size()]; + for (int i = 0; i < found.size(); i++) { + filteredFonts[i] = found.get(i); + } + } else { + // this machine is crazy. + LOG.warn("Can't found any preferred fonts for smali, use all available."); + filteredFonts = env.getAvailableFontFamilyNames(); + } + } + return filteredFonts; + } + } } diff --git a/jadx-gui/src/main/java/jadx/gui/treemodel/JClass.java b/jadx-gui/src/main/java/jadx/gui/treemodel/JClass.java index 441358a99..47af50069 100644 --- a/jadx-gui/src/main/java/jadx/gui/treemodel/JClass.java +++ b/jadx-gui/src/main/java/jadx/gui/treemodel/JClass.java @@ -107,6 +107,10 @@ public class JClass extends JLoadableNode { return cls.getSmali(); } + public String getSmaliV2() { + return cls.getClassNode().getSmaliV2(); + } + @Override public String getSyntaxName() { return SyntaxConstants.SYNTAX_STYLE_JAVA; diff --git a/jadx-gui/src/main/java/jadx/gui/ui/codearea/AbstractCodeArea.java b/jadx-gui/src/main/java/jadx/gui/ui/codearea/AbstractCodeArea.java index 02fcd5b74..0d3e88f1d 100644 --- a/jadx-gui/src/main/java/jadx/gui/ui/codearea/AbstractCodeArea.java +++ b/jadx-gui/src/main/java/jadx/gui/ui/codearea/AbstractCodeArea.java @@ -55,9 +55,10 @@ public abstract class AbstractCodeArea extends RSyntaxTextArea { contentPanel.getTabbedPane().getOpenTabs().values().forEach(v -> { if (v instanceof AbstractCodeContentPanel) { AbstractCodeArea codeArea = ((AbstractCodeContentPanel) v).getCodeArea(); - codeArea.setLineWrap(wrap); - if (codeArea.isVisible()) { - codeArea.repaint(); + setCodeAreaLineWrap(codeArea, wrap); + if (v instanceof ClassCodeContentPanel) { + codeArea = ((ClassCodeContentPanel) v).getSmaliCodeArea(); + setCodeAreaLineWrap(codeArea, wrap); } } }); @@ -116,6 +117,13 @@ public abstract class AbstractCodeArea extends RSyntaxTextArea { }); } + private void setCodeAreaLineWrap(AbstractCodeArea codeArea, boolean wrap) { + codeArea.setLineWrap(wrap); + if (codeArea.isVisible()) { + codeArea.repaint(); + } + } + private String highlightCaretWord(String lastText, int pos) { String text = getWordByPosition(pos); if (StringUtils.isEmpty(text)) { diff --git a/jadx-gui/src/main/java/jadx/gui/ui/codearea/SmaliArea.java b/jadx-gui/src/main/java/jadx/gui/ui/codearea/SmaliArea.java index c2bacad60..bcdedd32e 100644 --- a/jadx-gui/src/main/java/jadx/gui/ui/codearea/SmaliArea.java +++ b/jadx-gui/src/main/java/jadx/gui/ui/codearea/SmaliArea.java @@ -1,26 +1,106 @@ package jadx.gui.ui.codearea; -import org.fife.ui.rsyntaxtextarea.SyntaxConstants; +import java.awt.*; +import java.awt.event.ActionEvent; +import javax.swing.*; + +import org.fife.ui.rsyntaxtextarea.*; + +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; public final class SmaliArea extends AbstractCodeArea { private static final long serialVersionUID = 1334485631870306494L; private final JNode textNode; + private SmaliV2Style smaliV2Style; + private boolean curVersion = false; + private final JCheckBoxMenuItem cbUseSmaliV2; + SmaliArea(ContentPanel contentPanel) { super(contentPanel); this.textNode = new TextNode(node.getName()); - setSyntaxEditingStyle(SyntaxConstants.SYNTAX_STYLE_JAVA); + + cbUseSmaliV2 = new JCheckBoxMenuItem(NLS.str("popup.bytecode_col"), shouldUseSmaliPrinterV2()); + cbUseSmaliV2.setAction(new AbstractAction(NLS.str("popup.bytecode_col")) { + @Override + public void actionPerformed(ActionEvent e) { + + boolean usingV2 = shouldUseSmaliPrinterV2(); + JadxSettings settings = getContentPanel().getTabbedPane().getMainWindow().getSettings(); + settings.setSmaliAreaShowBytecode(!usingV2); + contentPanel.getTabbedPane().getOpenTabs().values().forEach(v -> { + if (v instanceof ClassCodeContentPanel) { + ((ClassCodeContentPanel) v).getSmaliCodeArea().refresh(); + } + }); + settings.sync(); + } + }); + 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); } @Override public void load() { - if (getText().isEmpty()) { - setText(node.getSmali()); + 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()); + } setCaretPosition(0); } } @@ -35,4 +115,57 @@ public final class SmaliArea extends AbstractCodeArea { // this area contains only smali without other node attributes return textNode; } + + private static class SmaliV2Style extends SyntaxScheme { + + SmaliArea smaliArea; + Theme curTheme; + + public SmaliV2Style(SmaliArea smaliArea) { + super(true); + this.smaliArea = smaliArea; + curTheme = smaliArea.getContentPanel().getTabbedPane().getMainWindow().getEditorTheme(); + updateTheme(); + } + + public Font getFont() { + return smaliArea.getContentPanel().getTabbedPane().getMainWindow().getSettings().getSmaliFont(); + } + + public boolean refreshTheme() { + Theme theme = smaliArea.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. + } + } } diff --git a/jadx-gui/src/main/resources/i18n/Messages_de_DE.properties b/jadx-gui/src/main/resources/i18n/Messages_de_DE.properties index a204562a4..5fc6c43db 100644 --- a/jadx-gui/src/main/resources/i18n/Messages_de_DE.properties +++ b/jadx-gui/src/main/resources/i18n/Messages_de_DE.properties @@ -115,9 +115,11 @@ preferences.excludedPackages.editDialog=Liste der durch Leerzeichen getren preferences.cfg=Methoden generieren CFG-Grafiken (im 'Punkt'-Format) preferences.raw_cfg=RAW CFG-Grafiken generieren preferences.font=Schrift ändern +#preferences.smali_font= preferences.theme=Thema ändern preferences.start_jobs=Autom. Hintergrunddekompilierung starten preferences.select_font=Ändern +#preferences.select_smali_font= preferences.deobfuscation_on=Deobfuscation aktivieren preferences.deobfuscation_force=Deobfuscationskartendatei umschreiben erzwingen preferences.deobfuscation_min_len=Minimale Namenlänge @@ -154,6 +156,7 @@ msg.cmd_select_class_error=Klasse\n%s auswählen nicht möglich\nSie existiert n #msg.rename_node_disabled= #msg.rename_node_failed= +#popup.bytecode_col= #popup.line_wrap= popup.undo=Rückgängig popup.redo=Wiederholen diff --git a/jadx-gui/src/main/resources/i18n/Messages_en_US.properties b/jadx-gui/src/main/resources/i18n/Messages_en_US.properties index 6ea544068..da9c06cb3 100644 --- a/jadx-gui/src/main/resources/i18n/Messages_en_US.properties +++ b/jadx-gui/src/main/resources/i18n/Messages_en_US.properties @@ -115,9 +115,11 @@ preferences.excludedPackages.editDialog=List of space separated package na preferences.cfg=Generate methods CFG graphs (in 'dot' format) preferences.raw_cfg=Generate RAW CFG graphs preferences.font=Editor font +preferences.smali_font=Smali Editor font preferences.theme=Editor theme preferences.start_jobs=Auto start background decompilation preferences.select_font=Change +preferences.select_smali_font=Change preferences.deobfuscation_on=Enable deobfuscation preferences.deobfuscation_force=Force rewrite deobfuscation map file preferences.deobfuscation_min_len=Minimum name length @@ -154,6 +156,7 @@ msg.cmd_select_class_error=Failed to select the class\n%s\nThe class does not ex msg.rename_node_disabled=Can't rename this node msg.rename_node_failed=Can't rename %s +popup.bytecode_col=Show Bytecode popup.line_wrap=Line Wrap popup.undo=Undo popup.redo=Redo diff --git a/jadx-gui/src/main/resources/i18n/Messages_es_ES.properties b/jadx-gui/src/main/resources/i18n/Messages_es_ES.properties index 9a279a343..1e2e2ca5d 100644 --- a/jadx-gui/src/main/resources/i18n/Messages_es_ES.properties +++ b/jadx-gui/src/main/resources/i18n/Messages_es_ES.properties @@ -115,9 +115,11 @@ preferences.threads=Número de hilos a procesar preferences.cfg=Generar methods CFG graphs (in 'dot' format) preferences.raw_cfg=Generate RAW CFG graphs preferences.font=Fuente del editor +#preferences.smali_font= preferences.theme=Tema del editor preferences.start_jobs=Inicio autom. descompilación de fondo preferences.select_font=Seleccionar +#preferences.select_smali_font= preferences.deobfuscation_on=Activar desobfuscación preferences.deobfuscation_force=Forzar reescritura del fichero de ofuscación preferences.deobfuscation_min_len=Longitud mínima del nombre @@ -154,6 +156,7 @@ msg.index_not_initialized=Índice no inicializado, ¡la bósqueda se desactivar #msg.rename_node_disabled= #msg.rename_node_failed= +#popup.bytecode_col= #popup.line_wrap= popup.undo=Deshacer popup.redo=Rehacer diff --git a/jadx-gui/src/main/resources/i18n/Messages_ko_KR.properties b/jadx-gui/src/main/resources/i18n/Messages_ko_KR.properties index ced8600be..7ae289aa7 100644 --- a/jadx-gui/src/main/resources/i18n/Messages_ko_KR.properties +++ b/jadx-gui/src/main/resources/i18n/Messages_ko_KR.properties @@ -115,9 +115,11 @@ preferences.excludedPackages.editDialog=RAM 절약을 위해 디컴파일 preferences.cfg=메소드 CFG 그래프 생성 ('dot' 포맷) preferences.raw_cfg=RAW CFG 그래프 생성 preferences.font=에디터 글씨체 +#preferences.smali_font= preferences.theme=에디터 테마 preferences.start_jobs=백그라운드에서 디컴파일 자동 시작 preferences.select_font=변경 +#preferences.select_smali_font= preferences.deobfuscation_on=난독 해제 활성화 preferences.deobfuscation_force=난독 해제 맵 파일 다시 쓰기 preferences.deobfuscation_min_len=최소 이름 길이 @@ -154,6 +156,7 @@ msg.cmd_select_class_error=클래스를 선택하지 못했습니다.\n%s\n클 msg.rename_node_disabled=이 노드의 이름을 바꿀 수 없습니다. msg.rename_node_failed=%s의 이름을 바꿀 수 없습니다. +#popup.bytecode_col= popup.line_wrap=줄 바꿈 popup.undo=실행 취소 popup.redo=다시 실행 diff --git a/jadx-gui/src/main/resources/i18n/Messages_zh_CN.properties b/jadx-gui/src/main/resources/i18n/Messages_zh_CN.properties index 16eac3fc8..fd17184fc 100644 --- a/jadx-gui/src/main/resources/i18n/Messages_zh_CN.properties +++ b/jadx-gui/src/main/resources/i18n/Messages_zh_CN.properties @@ -115,9 +115,11 @@ preferences.excludedPackages.editDialog=将不被解压或索引的以空 preferences.cfg=生成方法的 CFG 图(以 .dot 格式保存) preferences.raw_cfg=生成原始的 CFG 图 preferences.font=编辑器字体 +#preferences.smali_font= preferences.theme=编辑器主题 preferences.start_jobs=自动进行后台反编译 preferences.select_font=更改 +#preferences.select_smali_font= preferences.deobfuscation_on=启用反混淆 preferences.deobfuscation_force=强制覆盖反混淆映射文件 preferences.deobfuscation_min_len=最小命名长度 @@ -154,6 +156,7 @@ msg.cmd_select_class_error=无法选择类\n%s\n该类不存在。 #msg.rename_node_disabled= #msg.rename_node_failed= +#popup.bytecode_col= #popup.line_wrap= popup.undo=撤销 popup.redo=重做 diff --git a/jadx-plugins/jadx-dex-input/src/main/java/jadx/plugins/input/dex/insns/DexInsnData.java b/jadx-plugins/jadx-dex-input/src/main/java/jadx/plugins/input/dex/insns/DexInsnData.java index 52d231932..b1b8aaaea 100644 --- a/jadx-plugins/jadx-dex-input/src/main/java/jadx/plugins/input/dex/insns/DexInsnData.java +++ b/jadx-plugins/jadx-dex-input/src/main/java/jadx/plugins/input/dex/insns/DexInsnData.java @@ -2,9 +2,7 @@ package jadx.plugins.input.dex.insns; import org.jetbrains.annotations.Nullable; -import jadx.api.plugins.input.data.ICallSite; -import jadx.api.plugins.input.data.IFieldData; -import jadx.api.plugins.input.data.IMethodRef; +import jadx.api.plugins.input.data.*; import jadx.api.plugins.input.insns.InsnData; import jadx.api.plugins.input.insns.InsnIndexType; import jadx.api.plugins.input.insns.Opcode; @@ -21,6 +19,7 @@ public class DexInsnData implements InsnData { private boolean decoded; private int opcodeUnit; private int length; + private int insnStart; private int offset; private int[] argsReg = new int[5]; @@ -49,6 +48,11 @@ public class DexInsnData implements InsnData { return offset; } + @Override + public int getFileOffset() { + return insnStart; + } + @Override public Opcode getOpcode() { DexInsnInfo info = this.insnInfo; @@ -58,6 +62,11 @@ public class DexInsnData implements InsnData { return info.getApiOpcode(); } + @Override + public byte[] getByteCode() { + return externalReader.getByteCode(insnStart, length * 2); // a unit is 2 bytes + } + @Override public int getRawOpcodeUnit() { return opcodeUnit; @@ -118,6 +127,20 @@ public class DexInsnData implements InsnData { return externalReader.getCallSite(index, secondExtReader); } + /** + * Currently, protoIndex is either being stored at index or target, index for const-method-type, + * target for invoke-polymorphic(/range) + */ + @Override + public IMethodProto getIndexAsProto(int protoIndex) { + return externalReader.getMethodProto(protoIndex); + } + + @Override + public IMethodHandle getIndexAsMethodHandle() { + return externalReader.getMethodHandle(index); + } + @Nullable @Override public ICustomPayload getPayload() { @@ -144,6 +167,10 @@ public class DexInsnData implements InsnData { this.length = length; } + public void setInsnStart(int start) { + this.insnStart = start; + } + public void setLiteral(long literal) { this.literal = literal; } diff --git a/jadx-plugins/jadx-dex-input/src/main/java/jadx/plugins/input/dex/insns/DexOpcodes.java b/jadx-plugins/jadx-dex-input/src/main/java/jadx/plugins/input/dex/insns/DexOpcodes.java index 4333df9cf..654aeaa6d 100644 --- a/jadx-plugins/jadx-dex-input/src/main/java/jadx/plugins/input/dex/insns/DexOpcodes.java +++ b/jadx-plugins/jadx-dex-input/src/main/java/jadx/plugins/input/dex/insns/DexOpcodes.java @@ -230,4 +230,60 @@ 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" }; } diff --git a/jadx-plugins/jadx-dex-input/src/main/java/jadx/plugins/input/dex/sections/DexClassData.java b/jadx-plugins/jadx-dex-input/src/main/java/jadx/plugins/input/dex/sections/DexClassData.java index 93b90926d..719c0a231 100644 --- a/jadx-plugins/jadx-dex-input/src/main/java/jadx/plugins/input/dex/sections/DexClassData.java +++ b/jadx-plugins/jadx-dex-input/src/main/java/jadx/plugins/input/dex/sections/DexClassData.java @@ -13,6 +13,7 @@ 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 { @@ -193,6 +194,11 @@ public class DexClassData implements IClassData { return SmaliUtils.getSmaliCode(dexBuf, getClassDefOffset()); } + @Override + public String getDisassembledCodeV2() { + return SmaliPrinter.printClass(this); + } + @Override public String toString() { return getType(); diff --git a/jadx-plugins/jadx-dex-input/src/main/java/jadx/plugins/input/dex/sections/DexCodeReader.java b/jadx-plugins/jadx-dex-input/src/main/java/jadx/plugins/input/dex/sections/DexCodeReader.java index b6bcda91e..e0d04629d 100644 --- a/jadx-plugins/jadx-dex-input/src/main/java/jadx/plugins/input/dex/sections/DexCodeReader.java +++ b/jadx-plugins/jadx-dex-input/src/main/java/jadx/plugins/input/dex/sections/DexCodeReader.java @@ -59,8 +59,10 @@ public class DexCodeReader implements ICodeReader { int size = in.readInt(); int offset = 0; // in code units (2 byte) while (offset < size) { + int insnStart = in.getAbsPos(); int opcodeUnit = in.readUShort(); DexInsnInfo insnInfo = DexInsnInfo.get(opcodeUnit); + insnData.setInsnStart(insnStart); insnData.setOffset(offset); insnData.setInsnInfo(insnInfo); insnData.setOpcodeUnit(opcodeUnit); diff --git a/jadx-plugins/jadx-dex-input/src/main/java/jadx/plugins/input/dex/sections/SectionReader.java b/jadx-plugins/jadx-dex-input/src/main/java/jadx/plugins/input/dex/sections/SectionReader.java index 6fe3bcebf..aecf782e4 100644 --- a/jadx-plugins/jadx-dex-input/src/main/java/jadx/plugins/input/dex/sections/SectionReader.java +++ b/jadx-plugins/jadx-dex-input/src/main/java/jadx/plugins/input/dex/sections/SectionReader.java @@ -45,6 +45,14 @@ public class SectionReader { return new SectionReader(this, off); } + public byte[] getByteCode(int start, int len) { + int pos = buf.position(); + buf.position(start); + byte[] bytes = readByteArray(len); + buf.position(pos); + return bytes; + } + private static ByteBuffer duplicate(ByteBuffer baseBuffer, int off) { ByteBuffer dupBuf = baseBuffer.duplicate(); dupBuf.order(ByteOrder.LITTLE_ENDIAN); diff --git a/jadx-plugins/jadx-dex-input/src/main/java/jadx/plugins/input/dex/smali/SmaliPrinter.java b/jadx-plugins/jadx-dex-input/src/main/java/jadx/plugins/input/dex/smali/SmaliPrinter.java index b70aeb1c3..64fb08148 100644 --- a/jadx-plugins/jadx-dex-input/src/main/java/jadx/plugins/input/dex/smali/SmaliPrinter.java +++ b/jadx-plugins/jadx-dex-input/src/main/java/jadx/plugins/input/dex/smali/SmaliPrinter.java @@ -1,10 +1,21 @@ package jadx.plugins.input.dex.smali; -import jadx.api.plugins.input.data.AccessFlags; -import jadx.api.plugins.input.data.ICodeReader; +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 @@ -37,4 +48,762 @@ 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>> flds = new ArrayList<>(); + cls.visitFieldsAndMethods( + f -> { + DexFieldData fld = new DexFieldData(null); + fld.setParentClassType(f.getParentClassType()); + fld.setAccessFlags(f.getAccessFlags()); + fld.setName(f.getName()); + fld.setType(f.getType()); + flds.add(new AbstractMap.SimpleEntry<>(fld, f.getAnnotations())); + }, + m -> { + if (!flds.isEmpty()) { + printField(smali, flds, cls.getStaticFieldInitValues()); + flds.clear(); + smali.startLine("# methods"); + } + printMethod(smali, m); + }); + if (!flds.isEmpty()) { // in case there are no methods. + printField(smali, flds, cls.getStaticFieldInitValues()); + flds.clear(); + } + return smali.getCode(); + } + + private static void printField(SmaliCodeWriter smali, + List>> flds, + List staticFieldInitValues) { + int staticIdx = 0; + int accessColWidth = 0; + int nameColWidth = 0; + List accesses = new ArrayList<>(flds.size()); + for (Entry> fld : flds) { // calc width of cols + String temp = fld.getKey().getName(); + if (temp.length() > nameColWidth) { + nameColWidth = temp.length(); + } + temp = AccessFlags.format(fld.getKey().getAccessFlags(), FIELD); + accesses.add(temp); + if (temp.length() > accessColWidth) { + accessColWidth = temp.length(); + } + } + smali.startLine().startLine("# fields"); + String whites = new String(new byte[Math.max(accessColWidth, nameColWidth)]).replace("\0", " "); + for (int i = 0; i < flds.size(); i++) { + smali.startLine(); + Entry> fld = flds.get(i); + String access = accesses.get(i); + int pad = accessColWidth - access.length(); + if (pad > 0) { + access += whites.substring(0, pad); + } + smali.add(".field ").add(access); + String name = fld.getKey().getName(); + pad = nameColWidth - name.length(); + if (pad > 0) { + name += whites.substring(0, pad); + } + smali.add(name).add(" "); + smali.add(": ").add(fld.getKey().getType()); + if ((fld.getKey().getAccessFlags() & AccessFlags.STATIC) != 0) { // static field + if (staticIdx < staticFieldInitValues.size()) { + smali.add(" # init val = "); + printEncodedValue(smali, staticFieldInitValues.get(staticIdx++), false); + } + } + smali.incIndent(); + printAnnotations(smali, fld.getValue()); + smali.decIndent(); + } + smali.startLine(); + } + + private static void printMethod(SmaliCodeWriter smali, IMethodData mth) { + smali.startLine() + .startLine(mth.isDirect() ? "# direct method" : " # virtual method") + .startLine(".method "); + printMethodDef(smali, mth); + smali.incIndent(); + ICodeReader codeReader = mth.getCodeReader(); + if (codeReader != null) { + smali.startLine(".registers ") + .add(codeReader.getRegistersCount()) + .startLine(); + Map paramMap = formatMthParamInfo(mth, smali, codeReader); + if (paramMap.size() > 0) { + smali.startLine(); + } + SmaliGen smaliGen = new SmaliGen(paramMap, codeReader.getDebugInfo(), true, true); + codeReader.visitInstructions(insn -> { + insn.decode(); + smaliGen.format(insn); + }); + smaliGen.gen(smali); + } + smali.decIndent(); + smali.startLine(".end method"); + } + + private static void printMethodDef(SmaliCodeWriter smali, IMethodData mth) { + smali.add(AccessFlags.format(mth.getAccessFlags(), METHOD)); + + IMethodRef methodRef = mth.getMethodRef(); + methodRef.load(); + smali.add(methodRef.getName()); + smali.add('(').addArgs(methodRef.getArgTypes()).add(')'); + smali.add(methodRef.getReturnType()); + if (mth.getAnnotations().size() > 0) { + smali.incIndent(); + printAnnotations(smali, mth.getAnnotations()); + smali.decIndent(); + smali.startLine(); + } + } + + private static Map formatMthParamInfo(IMethodData mth, SmaliCodeWriter smali, ICodeReader codeReader) { + List types = mth.getMethodRef().getArgTypes(); + if (types.size() == 0) { + return Collections.emptyMap(); + } + int i = 0; + int paramCount = 0; + int paramStart = isStaticMethod(mth) ? 0 : 1; + int regNum = getParamStartRegNum(mth); + Map paramMap = new HashMap<>(types.size()); + IDebugInfo dbgInfo = codeReader.getDebugInfo(); + if (dbgInfo != null) { + for (ILocalVar var : dbgInfo.getLocalVars()) { + if (var.getStartOffset() == -1) { + smali.startLine(String.format(".param p%d, \"%s\":%s", + paramStart + i, var.getName(), var.getType())); + paramMap.put(regNum + i, "p" + (paramStart + i)); + paramCount++; + i += 1; + if (isWideType(var.getType())) { + paramMap.put(regNum + i, "p" + (paramStart + i)); + i += 1; + } + } + } + if (paramCount + 1 == types.size()) { + return paramMap; + } + } + for (; paramCount < types.size(); paramCount++) { + String type = types.get(paramCount); + smali.startLine(String.format(".param p%d, \"\":%s", paramStart + i, type)); + paramMap.put(regNum + i, "p" + (paramStart + i)); + i += 1; + if (isWideType(type)) { + paramMap.put(regNum + i, "p" + (paramStart + i)); + i += 1; + } + } + return paramMap; + } + + private static int getParamStartRegNum(IMethodData mth) { + ICodeReader codeReader = mth.getCodeReader(); + if (codeReader != null) { + int startNum = codeReader.getRegistersCount(); + if (startNum > 0) { + for (String argType : mth.getMethodRef().getArgTypes()) { + if (isWideType(argType)) { + startNum -= 2; + } else { + startNum -= 1; + } + } + if (!isStaticMethod(mth)) { + startNum--; + } + return startNum; + } + } + return -1; + } + + private static boolean isWideType(String type) { + return type.equals("D") || type.equals("J"); + } + + private static boolean isStaticMethod(IMethodData mth) { + return (mth.getAccessFlags() & AccessFlags.STATIC) != 0; + } + + private static void printAnnotations(SmaliCodeWriter smali, List annoList) { + if (annoList.size() > 0) { + for (int i = 0; i < annoList.size(); i++) { + smali.startLine(); + printAnnotation(smali, annoList.get(i)); + if (i != annoList.size() - 1) { + smali.startLine(); + } + } + } + } + + private static void printAnnotation(SmaliCodeWriter smali, IAnnotation anno) { + smali.add(".annotation") + .add(" "); + AnnotationVisibility vby = anno.getVisibility(); + if (vby != null) { + smali.add(vby.toString().toLowerCase()).add(" "); + } + smali.add(anno.getAnnotationClass()); + anno.getValues().forEach((k, v) -> { + smali.incIndent(); + smali.startLine(k).add(" = "); + printEncodedValue(smali, v, true); + smali.decIndent(); + }); + smali.startLine(".end annotation"); + } + + private static void printEncodedValue(SmaliCodeWriter smali, EncodedValue value, boolean wrapArray) { + switch (value.getType()) { + case ENCODED_ARRAY: + smali.add("{"); + if (wrapArray) { + smali.incIndent(); + smali.startLine(); + } + List values = (List) value.getValue(); + for (int i = 0; i < values.size(); i++) { + printEncodedValue(smali, values.get(i), wrapArray); + if (i != values.size() - 1) { + smali.add(","); + if (wrapArray) { + smali.startLine(); + } else { + smali.add(" "); + } + } + } + if (wrapArray) { + smali.decIndent(); + smali.startLine("}"); + } + break; + case ENCODED_STRING: + smali.add("\"").add(value.getValue()).add("\""); + break; + case ENCODED_NULL: + smali.add("null"); + break; + case ENCODED_ANNOTATION: + printAnnotation(smali, (IAnnotation) value.getValue()); + break; + default: + smali.add(value.getValue()); + } + } + + private static final int CODE_OFFSET_COLUMN_WIDTH = 4; + private static final int BYTECODE_COLUMN_WIDTH = 20 + 3; // 3 for ellipses. + private static final String FMT_BYTECODE_COL = "%-" + (BYTECODE_COLUMN_WIDTH - 3) + "s"; + + private static final int INSN_COL_WIDTH = "const-method-handle".length(); + private static final String FMT_INSN_COL = "%-" + INSN_COL_WIDTH + "s"; + private static final String FMT_FILE_OFFSET = "%08x:"; + private static final String FMT_CODE_OFFSET = "%04x:"; + private static final String FMT_TARGET_OFFSET = "%04x"; + private static final String FMT_GOTO = ":goto_" + FMT_TARGET_OFFSET; + private static final String FMT_COND = ":cond_" + FMT_TARGET_OFFSET; + private static final String FMT_DATA = ":data_" + FMT_TARGET_OFFSET; + private static final String FMT_P_SWITCH = ":p_switch_" + FMT_TARGET_OFFSET; + private static final String FMT_S_SWITCH = ":s_switch_" + FMT_TARGET_OFFSET; + private static final String FMT_P_SWITCH_CASE = ":p_case_" + FMT_TARGET_OFFSET; + private static final String FMT_S_SWITCH_CASE = ":s_case_" + FMT_TARGET_OFFSET; + + private static final String FMT_GOTO_TAG = "goto_" + FMT_TARGET_OFFSET + ":"; + private static final String FMT_COND_TAG = "cond_" + FMT_TARGET_OFFSET + ":"; + private static final String FMT_DATA_TAG = "data_" + FMT_TARGET_OFFSET + ":"; + private static final String FMT_P_SWITCH_TAG = "p_switch_" + FMT_TARGET_OFFSET + ":"; + private static final String FMT_S_SWITCH_TAG = "s_switch_" + FMT_TARGET_OFFSET + ":"; + private static final String FMT_P_SWITCH_CASE_TAG = "p_case_" + FMT_TARGET_OFFSET + ":"; + private static final String FMT_S_SWITCH_CASE_TAG = "s_case_" + FMT_TARGET_OFFSET + ":"; + + public static class SmaliGen { + static class SmaliLine { + Object line; + List> tips = Collections.emptyList(); + + void setLine(String str) { + line = str; + } + + void addLine(String str) { + if (!(line instanceof List)) { + line = new ArrayList(); + } + ((ArrayList) this.line).add(str); + } + + void addLineTip(String tip, String extra) { + if (tips.isEmpty()) { + tips = new ArrayList<>(); + } + tips.add(new AbstractMap.SimpleEntry<>(tip, extra)); + } + + private void fmtLineTip(int lineOffset, SmaliCodeWriter smali) { + for (Entry tip : tips) { + int start = Math.max(0, lineOffset - tip.getKey().length()); + if (start > 0) { + smali.add(new String(new byte[start]).replace("\0", " ")); + } + smali.add(tip.getKey() + tip.getValue()).startLine(); + } + } + + private void gen(int lineOffset, SmaliCodeWriter smali) { + fmtLineTip(lineOffset, smali); + if (line instanceof List) { + int size = ((List) line).size(); + for (int i = 0; i < size; i++) { + smali.add(((List) line).get(i)); + if (i != size - 1) { + smali.startLine(); + } + } + } else { + smali.add(line); + } + } + } + + StringBuilder lineWriter = new StringBuilder(50); + Map targetMap = new HashMap<>(); + Map payloadOffsetMap = new HashMap<>(); + Map paramMap; + List smaliList = new ArrayList<>(); + boolean fileOffset; + boolean bytecode; + boolean hasDbgInfo; + + /** + * @param fileOffset adds file offset column to smali output + * @param bytecode adds bytecode column to smali output + */ + public SmaliGen(Map paramMap, IDebugInfo dbgInfo, + boolean fileOffset, boolean bytecode) { + this.fileOffset = fileOffset; + this.bytecode = bytecode; + this.paramMap = paramMap; + this.hasDbgInfo = dbgInfo != null; + if (hasDbgInfo) { + fmtDbgInfo(dbgInfo); + } + } + + private boolean isParamReg(int regNum) { + return paramMap.containsKey(regNum); + } + + private String getRegName(int regNum) { + String text = paramMap.get(regNum); + if (text == null || text.isEmpty()) { + return "v" + regNum; + } + return text; + } + + public void gen(SmaliCodeWriter smali) { + removeDupTips(); + int lineOffset = getInsnColStart(); + for (SmaliLine smaliLine : smaliList) { + smali.startLine(); + smaliLine.gen(lineOffset, smali); + } + } + + public void format(InsnData insnData) { + SmaliLine line = targetMap.computeIfAbsent(insnData.getOffset(), k -> new SmaliLine()); + smaliList.add(line); + fmt(insnData, line); + } + + private void fmt(InsnData insn, SmaliLine line) { + fmtCols(insn); + if (!fmtPayloadInsn(insn, line)) { + fmtInsn(insn); + line.line = lineWriter.toString(); + } + lineWriter.delete(0, lineWriter.length()); + } + + private void fmtDbgInfo(IDebugInfo dbgInfo) { + dbgInfo.getSourceLineMapping().forEach((codeOffset, srcLine) -> { + if (codeOffset > -1) { + SmaliLine line = targetMap.computeIfAbsent(codeOffset, k -> new SmaliLine()); + line.addLineTip(String.format(".line %d", srcLine), ""); + } + }); + for (ILocalVar localVar : dbgInfo.getLocalVars()) { + if (localVar.getStartOffset() > -1) { + SmaliLine line = targetMap.computeIfAbsent(localVar.getStartOffset(), k -> new SmaliLine()); + line.addLineTip(String.format(".local v%d", localVar.getRegNum()), + String.format(", \"%s\":%s", localVar.getName(), localVar.getType())); + } + if (localVar.getEndOffset() > -1) { + if (isParamReg(localVar.getRegNum())) { + return; // no need to add .end local for parameters. + } + SmaliLine line = targetMap.computeIfAbsent(localVar.getEndOffset(), k -> new SmaliLine()); + line.addLineTip(String.format(".end local v%d", localVar.getRegNum()), + String.format(" # \"%s\":%s", localVar.getName(), localVar.getType())); + } + } + } + + private void fmtInsn(InsnData insn) { + int opcode = insn.getRawOpcodeUnit(); + opcode = opcode & 0xff; + String mne = DexOpcodes.MNEMONICS[opcode]; + lineWriter.append(String.format(FMT_INSN_COL, mne)).append(" "); + fmtRegs(opcode, insn, lineWriter); + if (hasTarget(opcode)) { + if (isGotoIns(opcode)) { + lineWriter.append(String.format(FMT_GOTO, insn.getTarget())); + addTarget(FMT_GOTO_TAG, insn.getTarget()); + return; + } + lineWriter.append(", "); + if (isConditionIns(opcode)) { + lineWriter.append(String.format(FMT_COND, insn.getTarget())); + addTarget(FMT_COND_TAG, insn.getTarget()); + + } else if (opcode == DexOpcodes.PACKED_SWITCH) { + payloadOffsetMap.put(insn.getTarget(), insn.getOffset()); + lineWriter.append(String.format(FMT_P_SWITCH, insn.getTarget())); + addTarget(FMT_P_SWITCH_TAG, insn.getTarget()); + + } else if (opcode == DexOpcodes.SPARSE_SWITCH) { + payloadOffsetMap.put(insn.getTarget(), insn.getOffset()); + lineWriter.append(String.format(FMT_S_SWITCH, insn.getTarget())); + addTarget(FMT_S_SWITCH_TAG, insn.getTarget()); + + } else { + lineWriter.append(String.format(FMT_DATA, insn.getTarget())); + addTarget(FMT_DATA_TAG, insn.getTarget()); + } + return; + } + if (isInvokeIns(opcode)) { + lineWriter.append(", ").append(method(insn)); + return; + } + if (insn.getIndexType() == InsnIndexType.TYPE_REF) { + lineWriter.append(", ").append(type(insn)); + return; + } + if (insn.getIndexType() == InsnIndexType.FIELD_REF) { + lineWriter.append(", ").append(field(insn)); + return; + } + if (insn.getIndexType() == InsnIndexType.STRING_REF) { + lineWriter.append(", ").append(str(insn)); + return; + } + if (hasLiteral(opcode)) { + lineWriter.append(", ").append(literal(insn, opcode)); + return; + } + if (opcode == DexOpcodes.CONST_METHOD_HANDLE) { + lineWriter.append(", ").append(methodHandle(insn)); + return; + } + if (opcode == DexOpcodes.CONST_METHOD_TYPE) { + lineWriter.append(", ").append(proto(insn, insn.getIndex())); + return; + } + } + + private void addTarget(String fmtTag, int target) { + addTarget(fmtTag, target, ""); + } + + private void addTarget(String fmtTag, int target, String extraTip) { + targetMap.computeIfAbsent(target, k -> new SmaliLine()) + .addLineTip(String.format(fmtTag, target), extraTip); + } + + private void fmtRegs(int opcode, InsnData insn, StringBuilder smali) { + boolean appendBrace = isRegList(opcode); + if (appendBrace) { + smali.append("{"); + } + if (isRangeRegIns(opcode)) { + smali.append(getRegName(insn.getReg(0))) + .append(" .. ") + .append(getRegName(insn.getReg(insn.getRegsCount() - 1))); + + } else if (insn.getRegsCount() > 0) { + for (int i = 0; i < insn.getRegsCount(); i++) { + if (i > 0) { + smali.append(", "); + } + smali.append(getRegName(insn.getReg(i))); + } + } + if (appendBrace) { + smali.append("}"); + } + } + + private boolean fmtPayloadInsn(InsnData insn, SmaliLine line) { + int opcode = insn.getRawOpcodeUnit(); + if (opcode == DexOpcodes.PACKED_SWITCH_PAYLOAD) { + lineWriter.append("packed-switch-payload"); + line.addLine(lineWriter.toString()); + DexSwitchPayload payload = (DexSwitchPayload) insn.getPayload(); + if (payload != null) { + fmtSwitchPayload(FMT_P_SWITCH_CASE, FMT_P_SWITCH_CASE_TAG, line, payload, insn.getOffset()); + } + return true; + } + if (opcode == DexOpcodes.SPARSE_SWITCH_PAYLOAD) { + lineWriter.append("sparse-switch-payload"); + line.addLine(lineWriter.toString()); + DexSwitchPayload payload = (DexSwitchPayload) insn.getPayload(); + if (payload != null) { + fmtSwitchPayload(FMT_S_SWITCH_CASE, FMT_S_SWITCH_CASE_TAG, line, payload, insn.getOffset()); + } + return true; + } + if (opcode == DexOpcodes.FILL_ARRAY_DATA_PAYLOAD) { + lineWriter.append("fill-array-data-payload"); + line.setLine(lineWriter.toString()); + return true; + } + return false; + } + + private void fmtSwitchPayload(String fmtTarget, String fmtTag, SmaliLine line, + DexSwitchPayload payload, int curOffset) { + int lineStart = getInsnColStart(); + lineStart += CODE_OFFSET_COLUMN_WIDTH + 1 + 1; // plus 1s for space and the ':' + String basicIndent = new String(new byte[lineStart]).replace("\0", " "); + String indent = SmaliCodeWriter.INDENT_STR + basicIndent; + int[] keys = payload.getKeys(); + int[] targets = payload.getTargets(); + int opcodeOffset = payloadOffsetMap.get(curOffset); + for (int i = 0; i < keys.length; i++) { + int target = opcodeOffset + targets[i]; + line.addLine(String.format("%scase %d: -> " + fmtTarget, indent, keys[i], target)); + addTarget(fmtTag, target, String.format(" # case %d", keys[i])); + } + line.addLine(basicIndent + ".end payload"); + } + + private void removeDupTips() { + List>> dbgLines = null; // line num: tip + if (hasDbgInfo) { + dbgLines = new ArrayList<>(); + } + for (int i = 0; i < smaliList.size(); i++) { + SmaliLine line = smaliList.get(i); + Map tipSet = Collections.emptyMap(); // tip: reference count + for (Iterator> it = line.tips.iterator(); it.hasNext();) { + Entry tip = it.next(); + if (hasDbgInfo && removeDupSourceLine(tip, i, dbgLines)) { // debug info source line. + it.remove(); + continue; + } + if (tipSet.containsKey(tip.getKey())) { // remove dup tips like cond_:/goto_:. + it.remove(); + tipSet.computeIfPresent(tip.getKey(), (k, v) -> v + 1); + } else { + if (tipSet.isEmpty()) { + tipSet = new HashMap<>(); + } + tipSet.computeIfAbsent(tip.getKey(), k -> 1); + } + } + tipSet.forEach((k, v) -> { + if (v > 1) { + for (int j = 0; j < line.tips.size(); j++) { + if (line.tips.get(j).getKey().equals(k)) { + line.tips.set(j, new AbstractMap.SimpleEntry<>(k, " # " + v + " refs")); + } + } + } + }); + } + } + + private boolean removeDupSourceLine(Entry tip, int i, + List>> dbgLines) { + boolean removeIt = false; + if (tip.getKey().startsWith(".line ")) { // debug info source line. + if (dbgLines.size() > 0) { + Entry> entry = dbgLines.get(dbgLines.size() - 1); + if (i - entry.getKey() == 1 && entry.getValue().getKey().equals(tip.getKey())) { + removeIt = true; // duplicated. + } + } + dbgLines.add(new AbstractMap.SimpleEntry<>(i, tip)); + } + return removeIt; + } + + private int getInsnColStart() { + int start = 0; + if (fileOffset) { + start += 8 + 1 + 1; // plus 1s for space and the ':' + } + if (bytecode) { + start += BYTECODE_COLUMN_WIDTH + 1; // plus 1 for space + } + return start; + } + + private void fmtCols(InsnData insn) { + if (fileOffset) { + lineWriter.append(String.format(FMT_FILE_OFFSET + " ", insn.getFileOffset())); + } + if (bytecode) { + formatByteCode(lineWriter, insn.getByteCode()); + lineWriter.append(" "); + lineWriter.append(String.format(FMT_CODE_OFFSET + " ", insn.getOffset())); + } + } + + private static void formatByteCode(StringBuilder smali, byte[] bytes) { + int maxLen = Math.min(bytes.length, 4 * 2); // limit to 4 units + StringBuilder inHex = new StringBuilder(); + for (int i = 0; i < maxLen; i++) { + int temp = ((bytes[i++] & 0xff) << 8) | (bytes[i] & 0xff); + inHex.append(String.format("%04x ", temp)); + } + smali.append(String.format(FMT_BYTECODE_COL, inHex)); + if (maxLen < bytes.length) { + smali.append("..."); + } else { + smali.append(" "); + } + } + + private static String literal(InsnData insn, int opcode) { + long it = insn.getLiteral(); + String tip = ""; + if (it > Integer.MAX_VALUE) { + if (isWideIns(opcode)) { + tip = " # double: " + Double.longBitsToDouble(it); + } else if (opcode == DexOpcodes.CONST_HIGH16) { + tip = " # float: " + Float.intBitsToFloat((int) it); + } + } else if (it <= 0) { + return "" + it + tip; + } + return "0x" + Long.toHexString(it) + tip; + } + + private static String str(InsnData insn) { + return String.format("\"%s\" # string@%04x", + insn.getIndexAsString() + .replace("\n", "\\n") + .replace("\t", "\\t"), + insn.getIndex()); + } + + private static String type(InsnData insn) { + return String.format("%s # type@%04x", insn.getIndexAsType(), insn.getIndex()); + } + + private static String field(InsnData insn) { + return String.format("%s # field@%04x", insn.getIndexAsField().toString(), insn.getIndex()); + } + + private static String method(InsnData insn) { + int rawOpcodeUnit = insn.getRawOpcodeUnit(); + int opcode = rawOpcodeUnit & 0xFF; + if (opcode == DexOpcodes.INVOKE_CUSTOM || opcode == DexOpcodes.INVOKE_CUSTOM_RANGE) { + insn.getIndexAsCallSite().load(); + return String.format("%s # call_site@%04x", insn.getIndexAsCallSite().toString(), insn.getIndex()); + } + IMethodRef mthRef = insn.getIndexAsMethod(); + mthRef.load(); + if (opcode == DexOpcodes.INVOKE_POLYMORPHIC || opcode == DexOpcodes.INVOKE_POLYMORPHIC_RANGE) { + return String.format("%s, %s # method@%04x, proto@%04x", + mthRef.toString(), insn.getIndexAsProto(insn.getTarget()).toString(), + insn.getIndex(), insn.getTarget()); + } + return String.format("%s # method@%04x", mthRef.toString(), insn.getIndex()); + } + + private static String proto(InsnData insn, int protoIndex) { + return String.format("%s # proto@%04x", insn.getIndexAsProto(protoIndex).toString(), protoIndex); + } + + private static String methodHandle(InsnData insn) { + return String.format("%s # method_handle@%04x", + insn.getIndexAsMethodHandle().toString(), insn.getIndex()); + } + + private static boolean isGotoIns(int opcode) { + return opcode >= DexOpcodes.GOTO && opcode <= DexOpcodes.GOTO_32; + } + + private static boolean isInvokeIns(int opcode) { + return (opcode >= DexOpcodes.INVOKE_VIRTUAL && opcode <= DexOpcodes.INVOKE_INTERFACE) + || (opcode >= DexOpcodes.INVOKE_VIRTUAL_RANGE && opcode <= DexOpcodes.INVOKE_INTERFACE_RANGE) + || (opcode >= DexOpcodes.INVOKE_POLYMORPHIC && opcode <= DexOpcodes.INVOKE_CUSTOM_RANGE); + } + + private static boolean isRangeRegIns(int opcode) { + if (opcode >= DexOpcodes.INVOKE_VIRTUAL_RANGE && opcode <= DexOpcodes.INVOKE_INTERFACE_RANGE) { + return true; + } + switch (opcode) { + case DexOpcodes.FILLED_NEW_ARRAY_RANGE: + case DexOpcodes.INVOKE_CUSTOM_RANGE: + case DexOpcodes.INVOKE_POLYMORPHIC_RANGE: + return true; + } + return false; + } + + private static boolean isWideIns(int opcode) { + return (opcode >= DexOpcodes.CONST_WIDE_16 && opcode <= DexOpcodes.CONST_WIDE_HIGH16); + } + + private static boolean hasLiteral(int opcode) { + return (opcode >= DexOpcodes.CONST_4 && opcode <= DexOpcodes.CONST_WIDE_HIGH16) + || (opcode >= DexOpcodes.ADD_INT_LIT16 && opcode <= DexOpcodes.USHR_INT_LIT8); + } + + private static boolean isConditionIns(int opcode) { + return opcode >= DexOpcodes.IF_EQ && opcode <= DexOpcodes.IF_LEZ; + } + + private static boolean hasTarget(int opcode) { + return (opcode >= DexOpcodes.IF_EQ && opcode <= DexOpcodes.IF_LEZ) + || (opcode >= DexOpcodes.GOTO && opcode <= DexOpcodes.SPARSE_SWITCH) + || (opcode == DexOpcodes.FILL_ARRAY_DATA); + } + + private static boolean isRegList(int opcode) { + return isInvokeIns(opcode) + || (opcode >= DexOpcodes.FILLED_NEW_ARRAY && opcode <= DexOpcodes.FILLED_NEW_ARRAY_RANGE); + } + } } diff --git a/jadx-plugins/jadx-plugins-api/src/main/java/jadx/api/plugins/input/data/IClassData.java b/jadx-plugins/jadx-plugins-api/src/main/java/jadx/api/plugins/input/data/IClassData.java index 863e18843..c7c03c554 100644 --- a/jadx-plugins/jadx-plugins-api/src/main/java/jadx/api/plugins/input/data/IClassData.java +++ b/jadx-plugins/jadx-plugins-api/src/main/java/jadx/api/plugins/input/data/IClassData.java @@ -31,4 +31,6 @@ public interface IClassData { List getAnnotations(); String getDisassembledCode(); + + String getDisassembledCodeV2(); } diff --git a/jadx-plugins/jadx-plugins-api/src/main/java/jadx/api/plugins/input/data/impl/FieldRefHandle.java b/jadx-plugins/jadx-plugins-api/src/main/java/jadx/api/plugins/input/data/impl/FieldRefHandle.java index fa16eb636..4a2c98203 100644 --- a/jadx-plugins/jadx-plugins-api/src/main/java/jadx/api/plugins/input/data/impl/FieldRefHandle.java +++ b/jadx-plugins/jadx-plugins-api/src/main/java/jadx/api/plugins/input/data/impl/FieldRefHandle.java @@ -34,4 +34,9 @@ public class FieldRefHandle implements IMethodHandle { public void load() { // already loaded } + + @Override + public String toString() { + return type + ": " + fieldRef; + } } diff --git a/jadx-plugins/jadx-plugins-api/src/main/java/jadx/api/plugins/input/insns/InsnData.java b/jadx-plugins/jadx-plugins-api/src/main/java/jadx/api/plugins/input/insns/InsnData.java index 20fe106a8..6a9e6c061 100644 --- a/jadx-plugins/jadx-plugins-api/src/main/java/jadx/api/plugins/input/insns/InsnData.java +++ b/jadx-plugins/jadx-plugins-api/src/main/java/jadx/api/plugins/input/insns/InsnData.java @@ -2,19 +2,21 @@ package jadx.api.plugins.input.insns; import org.jetbrains.annotations.Nullable; -import jadx.api.plugins.input.data.ICallSite; -import jadx.api.plugins.input.data.IFieldData; -import jadx.api.plugins.input.data.IMethodRef; +import jadx.api.plugins.input.data.*; import jadx.api.plugins.input.insns.custom.ICustomPayload; public interface InsnData { void decode(); - int getOffset(); + int getOffset(); // offset within method + + int getFileOffset(); // offset within dex file Opcode getOpcode(); + byte[] getByteCode(); + InsnIndexType getIndexType(); int getRawOpcodeUnit(); @@ -39,6 +41,10 @@ public interface InsnData { ICallSite getIndexAsCallSite(); + IMethodProto getIndexAsProto(int protoIndex); + + IMethodHandle getIndexAsMethodHandle(); + @Nullable ICustomPayload getPayload(); }