package jadx.gui.device.debugger.smali; import java.util.AbstractMap.SimpleEntry; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; import java.util.Locale; import java.util.Map; 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.AccessFlags; import jadx.api.plugins.input.data.AccessFlagsScope; import jadx.api.plugins.input.data.ICatch; import jadx.api.plugins.input.data.IClassData; import jadx.api.plugins.input.data.ICodeReader; import jadx.api.plugins.input.data.IDebugInfo; import jadx.api.plugins.input.data.IFieldData; import jadx.api.plugins.input.data.ILocalVar; import jadx.api.plugins.input.data.IMethodData; import jadx.api.plugins.input.data.IMethodRef; import jadx.api.plugins.input.data.ITry; 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.data.attributes.JadxAttrType; import jadx.api.plugins.input.data.attributes.types.AnnotationsAttr; 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.attributes.AttributeStorage; 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 jadx.core.utils.exceptions.JadxRuntimeException; 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.CONST; import static jadx.api.plugins.input.insns.Opcode.CONST_METHOD_HANDLE; import static jadx.api.plugins.input.insns.Opcode.CONST_METHOD_TYPE; import static jadx.api.plugins.input.insns.Opcode.CONST_WIDE; import static jadx.api.plugins.input.insns.Opcode.FILLED_NEW_ARRAY; import static jadx.api.plugins.input.insns.Opcode.FILLED_NEW_ARRAY_RANGE; import static jadx.api.plugins.input.insns.Opcode.FILL_ARRAY_DATA_PAYLOAD; import static jadx.api.plugins.input.insns.Opcode.INVOKE_CUSTOM; import static jadx.api.plugins.input.insns.Opcode.INVOKE_CUSTOM_RANGE; import static jadx.api.plugins.input.insns.Opcode.INVOKE_POLYMORPHIC; import static jadx.api.plugins.input.insns.Opcode.INVOKE_POLYMORPHIC_RANGE; import static jadx.api.plugins.input.insns.Opcode.PACKED_SWITCH; import static jadx.api.plugins.input.insns.Opcode.PACKED_SWITCH_PAYLOAD; import static jadx.api.plugins.input.insns.Opcode.SPARSE_SWITCH; import static jadx.api.plugins.input.insns.Opcode.SPARSE_SWITCH_PAYLOAD; public class Smali { private static SmaliInsnDecoder insnDecoder = null; private ICodeInfo codeInfo; private final Map insnMap = new HashMap<>(); // fullRawId of method as key private final boolean printFileOffset = true; private final boolean printBytecode = true; private boolean isJavaBytecode; private Smali() { } public static Smali disassemble(ClassNode cls) { cls = cls.getTopParentClass(); SmaliWriter code = new SmaliWriter(cls); Smali smali = new Smali(); smali.isJavaBytecode = cls.getInputFileName().endsWith(".class"); // TODO: add flag to api smali.writeClass(code, cls); smali.codeInfo = code.finish(); return smali; } public String getCode() { return codeInfo.getCodeStr(); } public int getMethodDefPos(String mthFullRawID) { SmaliMethodNode info = insnMap.get(mthFullRawID); if (info != null) { return info.getDefPos(); } return -1; } public int getRegCount(String mthFullRawID) { SmaliMethodNode info = insnMap.get(mthFullRawID); if (info != null) { return info.getRegCount(); } return -1; } public int getParamRegStart(String mthFullRawID) { SmaliMethodNode info = insnMap.get(mthFullRawID); if (info != null) { return info.getParamRegStart(); } return -1; } public int getInsnPosByCodeOffset(String mthFullRawID, long codeOffset) { SmaliMethodNode info = insnMap.get(mthFullRawID); if (info != null) { return info.getInsnPos(codeOffset); } return -1; } @Nullable public Entry getMthFullIDAndCodeOffsetByLine(int line) { for (Entry entry : insnMap.entrySet()) { Integer codeOffset = entry.getValue().getLineMapping().get(line); if (codeOffset != null) { return new SimpleEntry<>(entry.getKey(), codeOffset); } } return null; } public List getRegisterList(String mthFullRawID) { SmaliMethodNode node = insnMap.get(mthFullRawID); if (node != null) { return node.getRegList(); } return Collections.emptyList(); } /** * @return null for no result, FieldInfo for field, Integer for register. */ @Nullable public Object getResultRegOrField(String mthFullRawID, long codeOffset) { SmaliMethodNode info = insnMap.get(mthFullRawID); if (info != null) { InsnNode insn = info.getInsnNode(codeOffset); if (insn != null) { if (insn.getType() == InsnType.IPUT) { return ((IndexInsnNode) insn).getIndex(); } if (insn.getType() == InsnType.INVOKE) { if (insn instanceof InvokeNode) { if (insn.getArgsCount() > 0) { return ((RegisterArg) insn.getArg(0)).getRegNum(); } } } RegisterArg regArg = insn.getResult(); if (regArg != null) { return regArg.getRegNum(); } } } return null; } private void writeClass(SmaliWriter smali, ClassNode cls) { IClassData clsData = cls.getClsData(); if (clsData == null) { smali.startLine(String.format("###### Class %s is created by jadx", cls.getFullName())); return; } AttributeStorage attributes = new AttributeStorage(); attributes.add(clsData.getAttributes()); smali.startLine("Class: " + clsData.getType()) .startLine("AccessFlags: " + AccessFlags.format(clsData.getAccessFlags(), AccessFlagsScope.CLASS)) .startLine("SuperType: " + clsData.getSuperType()) .startLine("Interfaces: " + clsData.getInterfacesTypes()) .startLine("SourceFile: " + attributes.get(JadxAttrType.SOURCE_FILE)); AnnotationsAttr annotationsAttr = attributes.get(JadxAttrType.ANNOTATION_LIST); if (annotationsAttr != null) { Collection annos = annotationsAttr.getList(); if (!annos.isEmpty()) { smali.startLine(String.format("# %d annotations", annos.size())); writeAnnotations(smali, new ArrayList<>(annos)); smali.startLine(); } } List fields = new ArrayList<>(); int[] colWidths = new int[] { 0, 0 }; // first is access flag, second is name int[] mthIndex = new int[] { 0 }; LineInfo line = new LineInfo(); clsData.visitFieldsAndMethods( f -> { RawField fld = RawField.make(f); fields.add(fld); if (fld.accessFlag.length() > colWidths[0]) { colWidths[0] = fld.accessFlag.length(); } if (fld.name.length() > colWidths[1]) { colWidths[1] = fld.name.length(); } }, m -> { if (!fields.isEmpty()) { writeFields(smali, clsData, fields, colWidths); fields.clear(); } writeMethod(smali, cls.getMethods().get(mthIndex[0]++), m, line); line.reset(); }); if (!fields.isEmpty()) { // in case there's no methods. writeFields(smali, clsData, fields, colWidths); } for (ClassNode innerClass : cls.getInnerClasses()) { writeClass(smali, innerClass); } } private void writeFields(SmaliWriter smali, IClassData classData, List fields, int[] colWidths) { int staticIdx = 0; 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) { EncodedValue constVal = fld.attributes.get(JadxAttrType.CONSTANT_VALUE); if (constVal != null) { smali.add(" # init val = "); writeEncodedValue(smali, constVal, false); } } AnnotationsAttr annotationsAttr = fld.attributes.get(JadxAttrType.ANNOTATION_LIST); if (annotationsAttr != null) { smali.incIndent(); writeAnnotations(smali, annotationsAttr.getList()); 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(".method "); writeMethodDef(smali, mth, line); ICodeReader codeReader = mth.getCodeReader(); if (codeReader != null) { line.smaliMthNode.setParamRegStart(getParamStartRegNum(mth)); line.smaliMthNode.setRegCount(codeReader.getRegistersCount()); Map nodes = new HashMap<>(codeReader.getUnitsCount() / 2); line.smaliMthNode.setInsnNodes(nodes, codeReader.getUnitsCount()); line.smaliMthNode.initRegInfoList(codeReader.getRegistersCount(), codeReader.getUnitsCount()); 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); } // first pass to fill payload offsets for switch instructions codeReader.visitInstructions(insn -> { Opcode opcode = insn.getOpcode(); if (opcode == PACKED_SWITCH || opcode == SPARSE_SWITCH) { insn.decode(); line.addPayloadOffset(insn.getOffset(), insn.getTarget()); } }); codeReader.visitInstructions(insn -> { InsnNode node = decodeInsn(insn, line); nodes.put((long) insn.getOffset(), node); }); line.write(smali); insnMap.put(methodNode.getMethodInfo().getRawFullId(), line.smaliMthNode); smali.decIndent(); } smali.startLine(".end method"); } private void writeTries(ICodeReader codeReader, LineInfo line) { List tries = codeReader.getTries(); for (ITry aTry : tries) { int end = aTry.getEndOffset(); String tryEndTip = String.format(FMT_TRY_END_TAG, end); String tryStartTip = String.format(FMT_TRY_TAG, aTry.getStartOffset()); String tryStartTipExtra = " # :" + tryStartTip.substring(0, tryStartTip.length() - 1); line.addTip(aTry.getStartOffset(), tryStartTip, " # :" + tryEndTip.substring(0, tryEndTip.length() - 1)); line.addTip(end, tryEndTip, tryStartTipExtra); ICatch iCatch = aTry.getCatch(); int[] addresses = iCatch.getHandlers(); 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.getStartOffset(), tryStartTip, " # :" + catchTip.substring(0, catchTip.length() - 1)); } addr = iCatch.getCatchAllHandler(); if (addr > -1) { String catchAllTip = String.format(FMT_CATCH_ALL_TAG, addr); line.addTip(addr, catchAllTip, tryStartTipExtra); line.addTip(aTry.getStartOffset(), 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) { StringBuilder lw = line.getLineWriter(); lw.delete(0, lw.length()); fmtCols(insn, line); if (fmtPayloadInsn(insn, line)) { return; } lw.append(formatInsnName(insn)).append(" "); fmtRegs(insn, node.getType(), line); if (!tryFormatTargetIns(insn, node.getType(), line)) { if (hasLiteral(insn)) { lw.append(", ").append(literal(insn)); } else if (node.getType() == InsnType.INVOKE) { lw.append(", ").append(method(insn)); } else if (insn.getIndexType() == InsnIndexType.FIELD_REF) { lw.append(", ").append(field(insn)); } else if (insn.getIndexType() == InsnIndexType.STRING_REF) { lw.append(", ").append(str(insn)); } else if (insn.getIndexType() == InsnIndexType.TYPE_REF) { lw.append(", ").append(type(insn)); } else if (insn.getOpcode() == CONST_METHOD_HANDLE) { lw.append(", ").append(methodHandle(insn)); } else if (insn.getOpcode() == CONST_METHOD_TYPE) { lw.append(", ").append(proto(insn, insn.getIndex())); } } line.addInsnLine(insn.getOffset(), lw.toString()); } private String formatInsnName(InsnData insn) { if (isJavaBytecode) { // add api opcode, because registers not used return String.format("%-" + INSN_COL_WIDTH + "s | %-15s", insn.getOpcodeMnemonic(), insn.getOpcode().name().toLowerCase(Locale.ROOT).replace('_', '-')); } return String.format(FMT_INSN_COL, insn.getOpcodeMnemonic()); } 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)); } 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()); AnnotationsAttr annotationsAttr = new AttributeStorage(mth.getAttributes()).get(JadxAttrType.ANNOTATION_LIST); if (annotationsAttr != null && !annotationsAttr.isEmpty()) { smali.incIndent(); writeAnnotations(smali, annotationsAttr.getList()); smali.decIndent(); smali.startLine(); } } private boolean formatMthParamInfo(IMethodData mth, SmaliWriter smali, ICodeReader codeReader, LineInfo line) { List types = mth.getMethodRef().getArgTypes(); if (types.isEmpty()) { 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.getRegNum() == regNum) { int i = writeParamInfo(smali, line, regNum, paramStart, var.getName(), var.getType()); regNum += i; paramStart += i; paramCount++; } } } for (; paramCount < types.size(); paramCount++) { int i = writeParamInfo(smali, line, regNum, paramStart, "", types.get(paramCount)); regNum += i; paramStart += i; } return true; } private static int writeParamInfo(SmaliWriter smali, LineInfo line, int regNum, int paramNum, String dbgInfoName, String type) { smali.startLine(String.format(".param p%d, \"%s\":%s", paramNum, dbgInfoName, type)); String pName = "p" + paramNum; line.addRegName(regNum, pName); line.smaliMthNode.setParamReg(regNum, pName); if (isWideType(type)) { regNum++; dbgInfoName = "p" + (paramNum + 1); line.addRegName(regNum, dbgInfoName); line.smaliMthNode.setParamReg(regNum, dbgInfoName); return 2; } return 1; } private static int getParamStartRegNum(IMethodData mth) { ICodeReader codeReader = mth.getCodeReader(); if (codeReader != null) { int startNum = codeReader.getRegistersCount(); if (startNum > 0) { for (String argType : mth.getMethodRef().getArgTypes()) { if (isWideType(argType)) { startNum -= 2; } else { startNum -= 1; } } if (!hasStaticFlag(mth.getAccessFlags())) { startNum--; } return startNum; } } return -1; } private static boolean isWideType(String type) { return type.equals("D") || type.equals("J"); } private void writeAnnotations(SmaliWriter smali, List annoList) { if (annoList.size() > 0) { for (int i = 0; i < annoList.size(); i++) { smali.startLine(); writeAnnotation(smali, annoList.get(i)); if (i != annoList.size() - 1) { smali.startLine(); } } } } private void writeAnnotation(SmaliWriter smali, IAnnotation anno) { smali.add(".annotation") .add(" "); AnnotationVisibility vby = anno.getVisibility(); if (vby != null) { smali.add(vby.toString().toLowerCase()).add(" "); } smali.add(anno.getAnnotationClass()); anno.getValues().forEach((k, v) -> { smali.incIndent(); smali.startLine(k).add(" = "); writeEncodedValue(smali, v, true); smali.decIndent(); }); smali.startLine(".end annotation"); } private void formatDbgInfo(IDebugInfo dbgInfo, LineInfo line) { dbgInfo.getSourceLineMapping().forEach((codeOffset, srcLine) -> { if (codeOffset > -1) { line.addDebugLineTip(codeOffset, String.format(".line %d", srcLine), ""); } }); for (ILocalVar localVar : dbgInfo.getLocalVars()) { String type = localVar.getSignature(); if (type == null || type.trim().isEmpty()) { type = localVar.getType(); } if (localVar.getStartOffset() > -1) { line.addTip( localVar.getStartOffset(), String.format(".local v%d", localVar.getRegNum()), String.format(", \"%s\":%s", localVar.getName(), type)); } if (localVar.getEndOffset() > -1) { line.addTip( localVar.getEndOffset(), String.format(".end local v%d", localVar.getRegNum()), String.format(" # \"%s\":%s", localVar.getName(), type)); } } } private void writeEncodedValue(SmaliWriter smali, EncodedValue value, boolean wrapArray) { switch (value.getType()) { case ENCODED_ARRAY: smali.add("{"); if (wrapArray) { smali.incIndent(); smali.startLine(); } List values = (List) value.getValue(); for (int i = 0; i < values.size(); i++) { writeEncodedValue(smali, values.get(i), wrapArray); if (i != values.size() - 1) { smali.add(","); if (wrapArray) { smali.startLine(); } else { smali.add(" "); } } } if (wrapArray) { smali.decIndent(); smali.startLine("}"); } break; case ENCODED_NULL: smali.add("null"); break; case ENCODED_ANNOTATION: writeAnnotation(smali, (IAnnotation) value.getValue()); break; case ENCODED_BYTE: smali.add(TypeGen.formatByte((Byte) value.getValue(), false)); break; case ENCODED_SHORT: smali.add(TypeGen.formatShort((Short) value.getValue(), false)); break; case ENCODED_CHAR: smali.add(smali.getClassNode().root().getStringUtils().unescapeChar((Character) value.getValue())); break; case ENCODED_INT: smali.add(TypeGen.formatInteger((Integer) value.getValue(), false)); break; case ENCODED_LONG: smali.add(TypeGen.formatLong((Long) value.getValue(), false)); break; case ENCODED_FLOAT: smali.add(TypeGen.formatFloat((Float) value.getValue())); break; case ENCODED_DOUBLE: smali.add(TypeGen.formatDouble((Double) value.getValue())); break; case ENCODED_STRING: smali.add(smali.getClassNode().root().getStringUtils().unescapeString((String) value.getValue())); break; case ENCODED_TYPE: smali.add(ArgType.parse((String) value.getValue()) + ".class"); break; default: smali.add(String.valueOf(value.getValue())); } } private static final int CODE_OFFSET_COLUMN_WIDTH = 4; private static final int BYTECODE_COLUMN_WIDTH = 20 + 3; // 3 for ellipses. private static final String FMT_BYTECODE_COL = "%-" + (BYTECODE_COLUMN_WIDTH - 3) + "s"; private static final int INSN_COL_WIDTH = "const-method-handle".length(); private static final String FMT_INSN_COL = "%-" + INSN_COL_WIDTH + "s"; private static final String FMT_FILE_OFFSET = "%08x:"; private static final String FMT_CODE_OFFSET = "%04x:"; private static final String FMT_TARGET_OFFSET = "%04x"; private static final String FMT_GOTO = ":goto_" + FMT_TARGET_OFFSET; private static final String FMT_COND = ":cond_" + FMT_TARGET_OFFSET; private static final String FMT_DATA = ":array_" + FMT_TARGET_OFFSET; private static final String FMT_P_SWITCH = ":p_switch_" + FMT_TARGET_OFFSET; private static final String FMT_S_SWITCH = ":s_switch_" + FMT_TARGET_OFFSET; private static final String FMT_P_SWITCH_CASE = ":p_case_" + FMT_TARGET_OFFSET; private static final String FMT_S_SWITCH_CASE = ":s_case_" + FMT_TARGET_OFFSET; private static final String FMT_TRY_TAG = "try_" + FMT_TARGET_OFFSET + ":"; private static final String FMT_TRY_END_TAG = "try_end_" + FMT_TARGET_OFFSET + ":"; private static final String FMT_CATCH_TAG = "catch_" + FMT_TARGET_OFFSET + ":"; private static final String FMT_CATCH_ALL_TAG = "catch_all_" + FMT_TARGET_OFFSET + ":"; private static final String FMT_GOTO_TAG = "goto_" + FMT_TARGET_OFFSET + ":"; private static final String FMT_COND_TAG = "cond_" + FMT_TARGET_OFFSET + ":"; private static final String FMT_DATA_TAG = "array_" + FMT_TARGET_OFFSET + ":"; private static final String FMT_P_SWITCH_TAG = "p_switch_" + FMT_TARGET_OFFSET + ":"; private static final String FMT_S_SWITCH_TAG = "s_switch_" + FMT_TARGET_OFFSET + ":"; private static final String FMT_P_SWITCH_CASE_TAG = "p_case_" + FMT_TARGET_OFFSET + ":"; private static final String FMT_S_SWITCH_CASE_TAG = "s_case_" + FMT_TARGET_OFFSET + ":"; private void fmtRegs(InsnData insn, InsnType insnType, LineInfo line) { boolean appendBrace = insnType == InsnType.INVOKE || isRegList(insn); StringBuilder lw = line.getLineWriter(); if (insnType == InsnType.INVOKE) { int resultReg = insn.getResultReg(); if (resultReg != -1) { lw.append(line.getRegName(resultReg)).append(" <= "); } } if (appendBrace) { lw.append("{"); } if (isRangeRegIns(insn)) { lw.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) { lw.append(", "); } lw.append(line.getRegName(insn.getReg(i))); } } if (appendBrace) { lw.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++) { inHex.append(String.format("%02x", bytes[i])); if (i % 2 == 1) { inHex.append(' '); } } 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); } 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); } 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 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(); Integer switchOffset = line.payloadOffsetMap.get(insn.getOffset()); if (switchOffset == null) { throw new JadxRuntimeException("Unknown switch insn for payload at " + insn.getOffset()); } for (int i = 0; i < keys.length; i++) { int target = switchOffset + targets[i]; line.addInsnLine(insn.getOffset(), String.format("%scase %d: -> " + fmtTarget, indent, keys[i], target)); line.addTip(target, String.format(fmtTag, target), String.format(" # case %d", keys[i])); } line.addInsnLine(insn.getOffset(), basicIndent + ".end payload"); } private static String literal(InsnData insn) { long it = insn.getLiteral(); String tip = ""; if (it > Integer.MAX_VALUE) { if (isWideIns(insn)) { tip = " # double: " + Double.longBitsToDouble(it); } else if (getOpenCodeByte(insn) == 0x15) { // CONST_HIGH16 = 0x15; tip = " # float: " + Float.intBitsToFloat((int) it); } } else if (it <= 0) { return "" + it + tip; } return "0x" + Long.toHexString(it) + tip; } private static String str(InsnData insn) { return String.format("\"%s\" # string@%04x", insn.getIndexAsString() .replace("\n", "\\n") .replace("\t", "\\t"), insn.getIndex()); } private static String type(InsnData insn) { return String.format("%s # type@%04x", insn.getIndexAsType(), insn.getIndex()); } private static String field(InsnData insn) { return String.format("%s # field@%04x", insn.getIndexAsField().toString(), insn.getIndex()); } private static String method(InsnData insn) { Opcode op = insn.getOpcode(); if (op == INVOKE_CUSTOM || op == INVOKE_CUSTOM_RANGE) { insn.getIndexAsCallSite().load(); return String.format("%s # call_site@%04x", insn.getIndexAsCallSite().toString(), insn.getIndex()); } IMethodRef mthRef = insn.getIndexAsMethod(); mthRef.load(); if (op == INVOKE_POLYMORPHIC || op == INVOKE_POLYMORPHIC_RANGE) { return String.format("%s, %s # method@%04x, proto@%04x", mthRef.toString(), insn.getIndexAsProto(insn.getTarget()).toString(), insn.getIndex(), insn.getTarget()); } return String.format("%s # method@%04x", mthRef.toString(), insn.getIndex()); } private static String proto(InsnData insn, int protoIndex) { return String.format("%s # proto@%04x", insn.getIndexAsProto(protoIndex).toString(), protoIndex); } private static String methodHandle(InsnData insn) { return String.format("%s # method_handle@%04x", insn.getIndexAsMethodHandle().toString(), insn.getIndex()); } protected static boolean isRangeRegIns(InsnData insn) { switch (insn.getOpcode()) { case INVOKE_VIRTUAL_RANGE: case INVOKE_SUPER_RANGE: case INVOKE_DIRECT_RANGE: case INVOKE_STATIC_RANGE: case INVOKE_INTERFACE_RANGE: case FILLED_NEW_ARRAY_RANGE: case INVOKE_CUSTOM_RANGE: case INVOKE_POLYMORPHIC_RANGE: return true; } return false; } private static int getOpenCodeByte(InsnData insn) { return insn.getRawOpcodeUnit() & 0xff; } private static boolean isWideIns(InsnData insn) { return insn.getOpcode() == CONST_WIDE; } private static boolean hasLiteral(InsnData insn) { int opcode = getOpenCodeByte(insn); return insn.getOpcode() == CONST || insn.getOpcode() == CONST_WIDE || (opcode >= 0xd0 && opcode <= 0xe2); // add-int/lit16 to ushr-int/lit8 } private static boolean isRegList(InsnData insn) { return insn.getOpcode() == FILLED_NEW_ARRAY || insn.getOpcode() == FILLED_NEW_ARRAY_RANGE; } private class LineInfo { private SmaliMethodNode smaliMthNode = new SmaliMethodNode(); private final StringBuilder lineWriter = new StringBuilder(50); private String lastDebugTip = ""; private final Map> insnOffsetMap = new LinkedHashMap<>(); private final Map regNameMap = new HashMap<>(); private Map> tipMap = Collections.emptyMap(); private Map payloadOffsetMap = Collections.emptyMap(); public LineInfo() { } public StringBuilder getLineWriter() { return lineWriter; } public void reset() { lastDebugTip = ""; payloadOffsetMap = Collections.emptyMap(); tipMap = Collections.emptyMap(); insnOffsetMap.clear(); regNameMap.clear(); smaliMthNode = new SmaliMethodNode(); } public void addRegName(int regNum, String name) { regNameMap.put(regNum, name); } public String getRegName(int regNum) { String name = regNameMap.get(regNum); if (name == null) { name = "v" + regNum; } return name; } public void addInsnLine(int codeOffset, String insnLine) { List insnList = insnOffsetMap.computeIfAbsent(codeOffset, k -> new ArrayList<>(1)); insnList.add(insnLine); } public void addTip(int offset, String tip, String extra) { if (tipMap.isEmpty()) { tipMap = new LinkedHashMap<>(); } Map innerMap = tipMap.computeIfAbsent(offset, k -> new LinkedHashMap<>()); Object obj = innerMap.get(tip); if (obj != null) { if (obj instanceof String) { if (obj.equals("")) { innerMap.put(tip, 2); } else { List extras = new ArrayList<>(2); extras.add((String) obj); extras.add(extra); innerMap.put(tip, extras); } } else if (obj instanceof Integer) { innerMap.put(tip, ((int) obj) + 1); } else if (obj instanceof List) { if (!extra.equals("")) { List extras = (List) obj; extras.add(extra); } } } else { innerMap.put(tip, extra); } } public void addDebugLineTip(int offset, String tip, String extra) { if (tip.equals(lastDebugTip)) { return; } lastDebugTip = tip; if (tipMap.isEmpty()) { tipMap = new LinkedHashMap<>(); } Map innerMap = tipMap.computeIfAbsent(offset, k -> new LinkedHashMap<>()); innerMap.put(tip, extra); } public void addPayloadOffset(int curOffset, int payloadOffset) { if (payloadOffsetMap.isEmpty()) { payloadOffsetMap = new HashMap<>(); } payloadOffsetMap.put(payloadOffset, curOffset); } public void write(SmaliWriter smali) { int lineOffset = getInsnColStart(); for (Entry> entry : insnOffsetMap.entrySet()) { writeTip(smali, entry.getKey(), lineOffset); smaliMthNode.setInsnInfo(entry.getKey(), lineOffset + smali.getLength()); smaliMthNode.attachLine(smali.getLine(), entry.getKey()); smali.attachSourceLine(entry.getKey()); for (String s : entry.getValue()) { smali.add(s).startLine(); } } } private void writeTip(SmaliWriter smali, int codeOffset, int lineOffset) { Map tip = tipMap.get(codeOffset); if (tip != null) { for (Entry entry : tip.entrySet()) { int start = Math.max(0, lineOffset - entry.getKey().length()); if (start > 0) { smali.add(new String(new byte[start]).replace("\0", " ")); } if (entry.getValue() instanceof Integer) { smali.add(String.format("%s # %d refs", entry.getKey(), entry.getValue())) .startLine(); } else if (entry.getValue() instanceof String) { smali.add(String.format("%s%s", entry.getKey(), entry.getValue())) .startLine(); } else if (entry.getValue() instanceof List) { List extras = (List) entry.getValue(); smali.add(String.format("%s%s", entry.getKey(), extras.get(0))) .startLine(); String pad = new String(new byte[lineOffset]).replace("\0", " "); for (int i = 1; i < extras.size(); i++) { smali.add(String.format("%s%s", pad, extras.get(i))) .startLine(); } } else { smali.add(String.format("%s%s", entry.getKey(), entry.getValue())) .startLine(); } } } } } private static class SmaliInsnDecoder extends InsnDecoder { @Override protected @NotNull InsnNode decode(InsnData insn) { try { return super.decode(insn); } catch (Exception e) { switch (insn.getOpcode()) { case INVOKE_CUSTOM: case INVOKE_CUSTOM_RANGE: case INVOKE_POLYMORPHIC: case INVOKE_POLYMORPHIC_RANGE: case CONST_METHOD_HANDLE: case CONST_METHOD_TYPE: return new InsnNode(InsnType.INVOKE, insn.getRegsCount()); default: throw new RuntimeException(e); } } } public SmaliInsnDecoder(MethodNode mthNode) { super(mthNode); } @Override public InsnNode[] process(ICodeReader codeReader) { return null; } } private static class RawField { boolean isStatic; String accessFlag; String name; String type; AttributeStorage attributes; 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.attributes = new AttributeStorage(f.getAttributes()); return field; } } }