blob: 8161fe49b08acded6e89b0a6d4aa1474ec52381e [file] [log] [blame]
/*
* [The "BSD licence"]
* Copyright (c) 2010 Ben Gruver (JesusFreke)
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* 3. The name of the author may not be used to endorse or promote products
* derived from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
* IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
* IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
* NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
* THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package org.jf.baksmali.Adaptors;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists;
import org.jf.baksmali.Adaptors.Debug.DebugMethodItem;
import org.jf.baksmali.Adaptors.Format.InstructionMethodItemFactory;
import org.jf.baksmali.BaksmaliOptions;
import org.jf.dexlib2.AccessFlags;
import org.jf.dexlib2.Format;
import org.jf.dexlib2.Opcode;
import org.jf.dexlib2.ReferenceType;
import org.jf.dexlib2.analysis.AnalysisException;
import org.jf.dexlib2.analysis.AnalyzedInstruction;
import org.jf.dexlib2.analysis.MethodAnalyzer;
import org.jf.dexlib2.dexbacked.DexBackedDexFile.InvalidItemIndex;
import org.jf.dexlib2.iface.*;
import org.jf.dexlib2.iface.debug.DebugItem;
import org.jf.dexlib2.iface.instruction.Instruction;
import org.jf.dexlib2.iface.instruction.OffsetInstruction;
import org.jf.dexlib2.iface.instruction.ReferenceInstruction;
import org.jf.dexlib2.iface.instruction.formats.Instruction31t;
import org.jf.dexlib2.iface.reference.MethodReference;
import org.jf.dexlib2.immutable.instruction.ImmutableInstruction31t;
import org.jf.dexlib2.util.InstructionOffsetMap;
import org.jf.dexlib2.util.InstructionOffsetMap.InvalidInstructionOffset;
import org.jf.dexlib2.util.ReferenceUtil;
import org.jf.dexlib2.util.SyntheticAccessorResolver;
import org.jf.dexlib2.util.SyntheticAccessorResolver.AccessedMember;
import org.jf.dexlib2.util.TypeUtils;
import org.jf.util.ExceptionWithContext;
import org.jf.util.IndentingWriter;
import org.jf.util.SparseIntArray;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.io.IOException;
import java.util.*;
public class MethodDefinition {
@Nonnull public final ClassDefinition classDef;
@Nonnull public final Method method;
@Nonnull public final MethodImplementation methodImpl;
@Nonnull public final ImmutableList<Instruction> instructions;
@Nonnull public final List<Instruction> effectiveInstructions;
@Nonnull public final ImmutableList<MethodParameter> methodParameters;
public RegisterFormatter registerFormatter;
@Nonnull private final LabelCache labelCache = new LabelCache();
@Nonnull private final SparseIntArray packedSwitchMap;
@Nonnull private final SparseIntArray sparseSwitchMap;
@Nonnull private final InstructionOffsetMap instructionOffsetMap;
public MethodDefinition(@Nonnull ClassDefinition classDef, @Nonnull Method method,
@Nonnull MethodImplementation methodImpl) {
this.classDef = classDef;
this.method = method;
this.methodImpl = methodImpl;
try {
//TODO: what about try/catch blocks inside the dead code? those will need to be commented out too. ugh.
instructions = ImmutableList.copyOf(methodImpl.getInstructions());
methodParameters = ImmutableList.copyOf(method.getParameters());
effectiveInstructions = Lists.newArrayList(instructions);
packedSwitchMap = new SparseIntArray(0);
sparseSwitchMap = new SparseIntArray(0);
instructionOffsetMap = new InstructionOffsetMap(instructions);
int endOffset = instructionOffsetMap.getInstructionCodeOffset(instructions.size()-1) +
instructions.get(instructions.size()-1).getCodeUnits();
for (int i=0; i<instructions.size(); i++) {
Instruction instruction = instructions.get(i);
Opcode opcode = instruction.getOpcode();
if (opcode == Opcode.PACKED_SWITCH) {
boolean valid = true;
int codeOffset = instructionOffsetMap.getInstructionCodeOffset(i);
int targetOffset = codeOffset + ((OffsetInstruction)instruction).getCodeOffset();
try {
targetOffset = findPayloadOffset(targetOffset, Opcode.PACKED_SWITCH_PAYLOAD);
} catch (InvalidSwitchPayload ex) {
valid = false;
}
if (valid) {
if (packedSwitchMap.get(targetOffset, -1) != -1) {
Instruction payloadInstruction =
findSwitchPayload(targetOffset, Opcode.PACKED_SWITCH_PAYLOAD);
targetOffset = endOffset;
effectiveInstructions.set(i, new ImmutableInstruction31t(opcode,
((Instruction31t)instruction).getRegisterA(), targetOffset-codeOffset));
effectiveInstructions.add(payloadInstruction);
endOffset += payloadInstruction.getCodeUnits();
}
packedSwitchMap.append(targetOffset, codeOffset);
}
} else if (opcode == Opcode.SPARSE_SWITCH) {
boolean valid = true;
int codeOffset = instructionOffsetMap.getInstructionCodeOffset(i);
int targetOffset = codeOffset + ((OffsetInstruction)instruction).getCodeOffset();
try {
targetOffset = findPayloadOffset(targetOffset, Opcode.SPARSE_SWITCH_PAYLOAD);
} catch (InvalidSwitchPayload ex) {
valid = false;
// The offset to the payload instruction was invalid. Nothing to do, except that we won't
// add this instruction to the map.
}
if (valid) {
if (sparseSwitchMap.get(targetOffset, -1) != -1) {
Instruction payloadInstruction =
findSwitchPayload(targetOffset, Opcode.SPARSE_SWITCH_PAYLOAD);
targetOffset = endOffset;
effectiveInstructions.set(i, new ImmutableInstruction31t(opcode,
((Instruction31t)instruction).getRegisterA(), targetOffset-codeOffset));
effectiveInstructions.add(payloadInstruction);
endOffset += payloadInstruction.getCodeUnits();
}
sparseSwitchMap.append(targetOffset, codeOffset);
}
}
}
} catch (Exception ex) {
String methodString;
try {
methodString = ReferenceUtil.getMethodDescriptor(method);
} catch (Exception ex2) {
throw ExceptionWithContext.withContext(ex, "Error while processing method");
}
throw ExceptionWithContext.withContext(ex, "Error while processing method %s", methodString);
}
}
public static void writeEmptyMethodTo(IndentingWriter writer, Method method,
BaksmaliOptions options) throws IOException {
writer.write(".method ");
writeAccessFlags(writer, method.getAccessFlags());
writer.write(method.getName());
writer.write("(");
ImmutableList<MethodParameter> methodParameters = ImmutableList.copyOf(method.getParameters());
for (MethodParameter parameter: methodParameters) {
writer.write(parameter.getType());
}
writer.write(")");
writer.write(method.getReturnType());
writer.write('\n');
writer.indent(4);
writeParameters(writer, method, methodParameters, options);
String containingClass = null;
if (options.implicitReferences) {
containingClass = method.getDefiningClass();
}
AnnotationFormatter.writeTo(writer, method.getAnnotations(), containingClass);
writer.deindent(4);
writer.write(".end method\n");
}
public void writeTo(IndentingWriter writer) throws IOException {
int parameterRegisterCount = 0;
if (!AccessFlags.STATIC.isSet(method.getAccessFlags())) {
parameterRegisterCount++;
}
writer.write(".method ");
writeAccessFlags(writer, method.getAccessFlags());
writer.write(method.getName());
writer.write("(");
for (MethodParameter parameter: methodParameters) {
String type = parameter.getType();
writer.write(type);
parameterRegisterCount++;
if (TypeUtils.isWideType(type)) {
parameterRegisterCount++;
}
}
writer.write(")");
writer.write(method.getReturnType());
writer.write('\n');
writer.indent(4);
if (classDef.options.localsDirective) {
writer.write(".locals ");
writer.printSignedIntAsDec(methodImpl.getRegisterCount() - parameterRegisterCount);
} else {
writer.write(".registers ");
writer.printSignedIntAsDec(methodImpl.getRegisterCount());
}
writer.write('\n');
writeParameters(writer, method, methodParameters, classDef.options);
if (registerFormatter == null) {
registerFormatter = new RegisterFormatter(classDef.options, methodImpl.getRegisterCount(),
parameterRegisterCount);
}
String containingClass = null;
if (classDef.options.implicitReferences) {
containingClass = method.getDefiningClass();
}
AnnotationFormatter.writeTo(writer, method.getAnnotations(), containingClass);
writer.write('\n');
List<MethodItem> methodItems = getMethodItems();
for (MethodItem methodItem: methodItems) {
if (methodItem.writeTo(writer)) {
writer.write('\n');
}
}
writer.deindent(4);
writer.write(".end method\n");
}
public Instruction findSwitchPayload(int targetOffset, Opcode type) {
int targetIndex;
try {
targetIndex = instructionOffsetMap.getInstructionIndexAtCodeOffset(targetOffset);
} catch (InvalidInstructionOffset ex) {
throw new InvalidSwitchPayload(targetOffset);
}
//TODO: does dalvik let you pad with multiple nops?
//TODO: does dalvik let a switch instruction point to a non-payload instruction?
Instruction instruction = instructions.get(targetIndex);
if (instruction.getOpcode() != type) {
// maybe it's pointing to a NOP padding instruction. Look at the next instruction
if (instruction.getOpcode() == Opcode.NOP) {
targetIndex += 1;
if (targetIndex < instructions.size()) {
instruction = instructions.get(targetIndex);
if (instruction.getOpcode() == type) {
return instruction;
}
}
}
throw new InvalidSwitchPayload(targetOffset);
} else {
return instruction;
}
}
public int findPayloadOffset(int targetOffset, Opcode type) {
int targetIndex;
try {
targetIndex = instructionOffsetMap.getInstructionIndexAtCodeOffset(targetOffset);
} catch (InvalidInstructionOffset ex) {
throw new InvalidSwitchPayload(targetOffset);
}
//TODO: does dalvik let you pad with multiple nops?
//TODO: does dalvik let a switch instruction point to a non-payload instruction?
Instruction instruction = instructions.get(targetIndex);
if (instruction.getOpcode() != type) {
// maybe it's pointing to a NOP padding instruction. Look at the next instruction
if (instruction.getOpcode() == Opcode.NOP) {
targetIndex += 1;
if (targetIndex < instructions.size()) {
instruction = instructions.get(targetIndex);
if (instruction.getOpcode() == type) {
return instructionOffsetMap.getInstructionCodeOffset(targetIndex);
}
}
}
throw new InvalidSwitchPayload(targetOffset);
} else {
return targetOffset;
}
}
private static void writeAccessFlags(IndentingWriter writer, int accessFlags)
throws IOException {
for (AccessFlags accessFlag: AccessFlags.getAccessFlagsForMethod(accessFlags)) {
writer.write(accessFlag.toString());
writer.write(' ');
}
}
private static void writeParameters(IndentingWriter writer, Method method,
List<? extends MethodParameter> parameters,
BaksmaliOptions options) throws IOException {
boolean isStatic = AccessFlags.STATIC.isSet(method.getAccessFlags());
int registerNumber = isStatic?0:1;
for (MethodParameter parameter: parameters) {
String parameterType = parameter.getType();
String parameterName = parameter.getName();
Collection<? extends Annotation> annotations = parameter.getAnnotations();
if ((options.debugInfo && parameterName != null) || annotations.size() != 0) {
writer.write(".param p");
writer.printSignedIntAsDec(registerNumber);
if (parameterName != null && options.debugInfo) {
writer.write(", ");
ReferenceFormatter.writeStringReference(writer, parameterName);
}
writer.write(" # ");
writer.write(parameterType);
writer.write("\n");
if (annotations.size() > 0) {
writer.indent(4);
String containingClass = null;
if (options.implicitReferences) {
containingClass = method.getDefiningClass();
}
AnnotationFormatter.writeTo(writer, annotations, containingClass);
writer.deindent(4);
writer.write(".end param\n");
}
}
registerNumber++;
if (TypeUtils.isWideType(parameterType)) {
registerNumber++;
}
}
}
@Nonnull public LabelCache getLabelCache() {
return labelCache;
}
public int getPackedSwitchBaseAddress(int packedSwitchPayloadCodeOffset) {
return packedSwitchMap.get(packedSwitchPayloadCodeOffset, -1);
}
public int getSparseSwitchBaseAddress(int sparseSwitchPayloadCodeOffset) {
return sparseSwitchMap.get(sparseSwitchPayloadCodeOffset, -1);
}
private List<MethodItem> getMethodItems() {
ArrayList<MethodItem> methodItems = new ArrayList<MethodItem>();
if ((classDef.options.registerInfo != 0) || (classDef.options.normalizeVirtualMethods) ||
(classDef.options.deodex && needsAnalyzed())) {
addAnalyzedInstructionMethodItems(methodItems);
} else {
addInstructionMethodItems(methodItems);
}
addTries(methodItems);
if (classDef.options.debugInfo) {
addDebugInfo(methodItems);
}
if (classDef.options.sequentialLabels) {
setLabelSequentialNumbers();
}
for (LabelMethodItem labelMethodItem: labelCache.getLabels()) {
methodItems.add(labelMethodItem);
}
Collections.sort(methodItems);
return methodItems;
}
private boolean needsAnalyzed() {
for (Instruction instruction: methodImpl.getInstructions()) {
if (instruction.getOpcode().odexOnly()) {
return true;
}
}
return false;
}
private void addInstructionMethodItems(List<MethodItem> methodItems) {
int currentCodeAddress = 0;
for (int i=0; i<effectiveInstructions.size(); i++) {
Instruction instruction = effectiveInstructions.get(i);
MethodItem methodItem = InstructionMethodItemFactory.makeInstructionFormatMethodItem(this,
currentCodeAddress, instruction);
methodItems.add(methodItem);
if (i != effectiveInstructions.size() - 1) {
methodItems.add(new BlankMethodItem(currentCodeAddress));
}
if (classDef.options.codeOffsets) {
methodItems.add(new MethodItem(currentCodeAddress) {
@Override
public double getSortOrder() {
return -1000;
}
@Override
public boolean writeTo(IndentingWriter writer) throws IOException {
writer.write("#@");
writer.printUnsignedLongAsHex(codeAddress & 0xFFFFFFFFL);
return true;
}
});
}
if (classDef.options.accessorComments && classDef.options.syntheticAccessorResolver != null &&
(instruction instanceof ReferenceInstruction)) {
Opcode opcode = instruction.getOpcode();
if (opcode.referenceType == ReferenceType.METHOD) {
MethodReference methodReference = null;
try {
methodReference = (MethodReference)((ReferenceInstruction)instruction).getReference();
} catch (InvalidItemIndex ex) {
// just ignore it for now. We'll deal with it later, when processing the instructions
// themselves
}
if (methodReference != null &&
SyntheticAccessorResolver.looksLikeSyntheticAccessor(methodReference.getName())) {
AccessedMember accessedMember =
classDef.options.syntheticAccessorResolver.getAccessedMember(methodReference);
if (accessedMember != null) {
methodItems.add(new SyntheticAccessCommentMethodItem(accessedMember, currentCodeAddress));
}
}
}
}
currentCodeAddress += instruction.getCodeUnits();
}
}
private void addAnalyzedInstructionMethodItems(List<MethodItem> methodItems) {
MethodAnalyzer methodAnalyzer = new MethodAnalyzer(classDef.options.classPath, method,
classDef.options.inlineResolver, classDef.options.normalizeVirtualMethods);
AnalysisException analysisException = methodAnalyzer.getAnalysisException();
if (analysisException != null) {
// TODO: need to keep track of whether any errors occurred, so we can exit with a non-zero result
methodItems.add(new CommentMethodItem(
String.format("AnalysisException: %s", analysisException.getMessage()),
analysisException.codeAddress, Integer.MIN_VALUE));
analysisException.printStackTrace(System.err);
}
List<AnalyzedInstruction> instructions = methodAnalyzer.getAnalyzedInstructions();
int currentCodeAddress = 0;
for (int i=0; i<instructions.size(); i++) {
AnalyzedInstruction instruction = instructions.get(i);
MethodItem methodItem = InstructionMethodItemFactory.makeInstructionFormatMethodItem(
this, currentCodeAddress, instruction.getInstruction());
methodItems.add(methodItem);
if (instruction.getInstruction().getOpcode().format == Format.UnresolvedOdexInstruction) {
methodItems.add(new CommentedOutMethodItem(
InstructionMethodItemFactory.makeInstructionFormatMethodItem(
this, currentCodeAddress, instruction.getOriginalInstruction())));
}
if (i != instructions.size() - 1) {
methodItems.add(new BlankMethodItem(currentCodeAddress));
}
if (classDef.options.codeOffsets) {
methodItems.add(new MethodItem(currentCodeAddress) {
@Override
public double getSortOrder() {
return -1000;
}
@Override
public boolean writeTo(IndentingWriter writer) throws IOException {
writer.write("#@");
writer.printUnsignedLongAsHex(codeAddress & 0xFFFFFFFFL);
return true;
}
});
}
if (classDef.options.registerInfo != 0 &&
!instruction.getInstruction().getOpcode().format.isPayloadFormat) {
methodItems.add(
new PreInstructionRegisterInfoMethodItem(classDef.options.registerInfo,
methodAnalyzer, registerFormatter, instruction, currentCodeAddress));
methodItems.add(
new PostInstructionRegisterInfoMethodItem(registerFormatter, instruction, currentCodeAddress));
}
currentCodeAddress += instruction.getInstruction().getCodeUnits();
}
}
private void addTries(List<MethodItem> methodItems) {
List<? extends TryBlock<? extends ExceptionHandler>> tryBlocks = methodImpl.getTryBlocks();
if (tryBlocks.size() == 0) {
return;
}
int lastInstructionAddress = instructionOffsetMap.getInstructionCodeOffset(instructions.size() - 1);
int codeSize = lastInstructionAddress + instructions.get(instructions.size() - 1).getCodeUnits();
for (TryBlock<? extends ExceptionHandler> tryBlock: tryBlocks) {
int startAddress = tryBlock.getStartCodeAddress();
int endAddress = startAddress + tryBlock.getCodeUnitCount();
if (startAddress >= codeSize) {
throw new RuntimeException(String.format("Try start offset %d is past the end of the code block.",
startAddress));
}
// Note: not >=. endAddress == codeSize is valid, when the try covers the last instruction
if (endAddress > codeSize) {
throw new RuntimeException(String.format("Try end offset %d is past the end of the code block.",
endAddress));
}
/**
* The end address points to the address immediately after the end of the last
* instruction that the try block covers. We want the .catch directive and end_try
* label to be associated with the last covered instruction, so we need to get
* the address for that instruction
*/
int lastCoveredIndex = instructionOffsetMap.getInstructionIndexAtCodeOffset(endAddress - 1, false);
int lastCoveredAddress = instructionOffsetMap.getInstructionCodeOffset(lastCoveredIndex);
for (ExceptionHandler handler: tryBlock.getExceptionHandlers()) {
int handlerAddress = handler.getHandlerCodeAddress();
if (handlerAddress >= codeSize) {
throw new ExceptionWithContext(
"Exception handler offset %d is past the end of the code block.", handlerAddress);
}
//use the address from the last covered instruction
CatchMethodItem catchMethodItem = new CatchMethodItem(classDef.options, labelCache, lastCoveredAddress,
handler.getExceptionType(), startAddress, endAddress, handlerAddress);
methodItems.add(catchMethodItem);
}
}
}
private void addDebugInfo(final List<MethodItem> methodItems) {
for (DebugItem debugItem: methodImpl.getDebugItems()) {
methodItems.add(DebugMethodItem.build(registerFormatter, debugItem));
}
}
private void setLabelSequentialNumbers() {
HashMap<String, Integer> nextLabelSequenceByType = new HashMap<String, Integer>();
ArrayList<LabelMethodItem> sortedLabels = new ArrayList<LabelMethodItem>(labelCache.getLabels());
//sort the labels by their location in the method
Collections.sort(sortedLabels);
for (LabelMethodItem labelMethodItem: sortedLabels) {
Integer labelSequence = nextLabelSequenceByType.get(labelMethodItem.getLabelPrefix());
if (labelSequence == null) {
labelSequence = 0;
}
labelMethodItem.setLabelSequence(labelSequence);
nextLabelSequenceByType.put(labelMethodItem.getLabelPrefix(), labelSequence + 1);
}
}
@Nullable
private String getContainingClassForImplicitReference() {
if (classDef.options.implicitReferences) {
return classDef.classDef.getType();
}
return null;
}
public static class LabelCache {
protected HashMap<LabelMethodItem, LabelMethodItem> labels = new HashMap<LabelMethodItem, LabelMethodItem>();
public LabelCache() {
}
public LabelMethodItem internLabel(LabelMethodItem labelMethodItem) {
LabelMethodItem internedLabelMethodItem = labels.get(labelMethodItem);
if (internedLabelMethodItem != null) {
return internedLabelMethodItem;
}
labels.put(labelMethodItem, labelMethodItem);
return labelMethodItem;
}
public Collection<LabelMethodItem> getLabels() {
return labels.values();
}
}
public static class InvalidSwitchPayload extends ExceptionWithContext {
private final int payloadOffset;
public InvalidSwitchPayload(int payloadOffset) {
super("No switch payload at offset: %d", payloadOffset);
this.payloadOffset = payloadOffset;
}
public int getPayloadOffset() {
return payloadOffset;
}
}
}