Commit 5c250a7a authored by swift_gan's avatar swift_gan

add dexmaker cache

parent 0b884655
...@@ -74,7 +74,7 @@ public class SandHook { ...@@ -74,7 +74,7 @@ public class SandHook {
public static synchronized void hook(HookWrapper.HookEntity entity) throws HookErrorException { public static synchronized void hook(HookWrapper.HookEntity entity) throws HookErrorException {
if (entity == null) if (entity == null)
throw new HookErrorException("null entity"); throw new HookErrorException("null hook entity");
Member target = entity.target; Member target = entity.target;
Method hook = entity.hook; Method hook = entity.hook;
......
...@@ -26,7 +26,7 @@ android { ...@@ -26,7 +26,7 @@ android {
dependencies { dependencies {
implementation fileTree(include: ['*.jar'], dir: 'libs') implementation fileTree(include: ['*.jar'], dir: 'libs')
implementation 'com.linkedin.dexmaker:dexmaker:2.21.0' implementation 'com.jakewharton.android.repackaged:dalvik-dx:9.0.0_r3'
compileOnly project(':hooklib') compileOnly project(':hooklib')
} }
......
/*
* Copyright (C) 2017 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.dx;
import com.android.dx.dex.file.ClassDefItem;
import com.android.dx.rop.annotation.Annotation;
import com.android.dx.rop.annotation.AnnotationVisibility;
import com.android.dx.rop.annotation.Annotations;
import com.android.dx.rop.annotation.NameValuePair;
import com.android.dx.rop.cst.*;
import java.lang.annotation.ElementType;
import java.util.HashMap;
/**
* Identifies an annotation on a program element, see {@link ElementType}.
*
* Currently it is only targeting Class, Method, Field and Parameter because those are supported by
* {@link com.android.dx.dex.file.AnnotationsDirectoryItem} so far.
*
* <p><strong>NOTE:</strong>
* So far it only supports adding method annotation. The annotation of class, field and parameter
* will be implemented later.
*
* <p><strong>WARNING:</strong>
* The declared element of an annotation type should either have a default value or be set a value via
* {@code AnnotationId.set(Element)}. Otherwise it will incur
* {@link java.lang.annotation.IncompleteAnnotationException} when accessing the annotation element
* through reflection. The example is as follows:
* <pre>
* {@code @Retention(RetentionPolicy.RUNTIME)}
* {@code @Target({ElementType.METHOD})}
* {@code @interface MethodAnnotation {
* boolean elementBoolean();
* // boolean elementBoolean() default false;
* }
*
* TypeId<?> GENERATED = TypeId.get("LGenerated;");
* MethodId<?, Void> methodId = GENERATED.getMethod(VOID, "call");
* Code code = dexMaker.declare(methodId, PUBLIC);
* code.returnVoid();
*
* TypeId<MethodAnnotation> annotationTypeId = TypeId.get(MethodAnnotation.class);
* AnnotationId<?, MethodAnnotation> annotationId = AnnotationId.get(GENERATED,
* annotationTypeId, ElementType.METHOD);
*
* AnnotationId.Element element = new AnnotationId.Element("elementBoolean", true);
* annotationId.set(element);
* annotationId.addToMethod(dexMaker, methodId);
* }
* </pre>
*
* @param <D> the type that declares the program element.
* @param <V> the annotation type. It should be a known type before compile.
*/
public final class AnnotationId<D, V> {
private final TypeId<D> declaringType;
private final TypeId<V> type;
/** The type of program element to be annotated */
private final ElementType annotatedElement;
/** The elements this annotation holds */
private final HashMap<String, NameValuePair> elements;
private AnnotationId(TypeId<D> declaringType, TypeId<V> type, ElementType annotatedElement) {
this.declaringType = declaringType;
this.type = type;
this.annotatedElement = annotatedElement;
this.elements = new HashMap<>();
}
/**
* Construct an instance. It initially contains no elements.
*
* @param declaringType the type declaring the program element.
* @param type the annotation type.
* @param annotatedElement the program element type to be annotated.
* @return an annotation {@code AnnotationId<D,V>} instance.
*/
public static <D, V> AnnotationId<D, V> get(TypeId<D> declaringType, TypeId<V> type,
ElementType annotatedElement) {
if (annotatedElement != ElementType.TYPE &&
annotatedElement != ElementType.METHOD &&
annotatedElement != ElementType.FIELD &&
annotatedElement != ElementType.PARAMETER) {
throw new IllegalArgumentException("element type is not supported to annotate yet.");
}
return new AnnotationId<>(declaringType, type, annotatedElement);
}
/**
* Set an annotation element of this instance.
* If there is a preexisting element with the same name, it will be
* replaced by this method.
*
* @param element {@code non-null;} the annotation element to be set.
*/
public void set(Element element) {
if (element == null) {
throw new NullPointerException("element == null");
}
CstString pairName = new CstString(element.getName());
Constant pairValue = Element.toConstant(element.getValue());
NameValuePair nameValuePair = new NameValuePair(pairName, pairValue);
elements.put(element.getName(), nameValuePair);
}
/**
* Add this annotation to a method.
*
* @param dexMaker DexMaker instance.
* @param method Method to be added to.
*/
public void addToMethod(DexMaker dexMaker, MethodId<?, ?> method) {
if (annotatedElement != ElementType.METHOD) {
throw new IllegalStateException("This annotation is not for method");
}
if (!method.declaringType.equals(declaringType)) {
throw new IllegalArgumentException("Method" + method + "'s declaring type is inconsistent with" + this);
}
ClassDefItem classDefItem = dexMaker.getTypeDeclaration(declaringType).toClassDefItem();
if (classDefItem == null) {
throw new NullPointerException("No class defined item is found");
} else {
CstMethodRef cstMethodRef = method.constant;
if (cstMethodRef == null) {
throw new NullPointerException("Method reference is NULL");
} else {
// Generate CstType
CstType cstType = CstType.intern(type.ropType);
// Generate Annotation
Annotation annotation = new Annotation(cstType, AnnotationVisibility.RUNTIME);
// Add generated annotation
Annotations annotations = new Annotations();
for (NameValuePair nvp : elements.values()) {
annotation.add(nvp);
}
annotations.add(annotation);
classDefItem.addMethodAnnotations(cstMethodRef, annotations, dexMaker.getDexFile());
}
}
}
/**
* A wrapper of <code>NameValuePair</code> represents a (name, value) pair used as the contents
* of an annotation.
*
* An {@code Element} instance is stored in {@code AnnotationId.elements} by calling {@code
* AnnotationId.set(Element)}.
*
* <p><strong>WARNING: </strong></p>
* the name should be exact same as the annotation element declared in the annotation type
* which is referred by field {@code AnnotationId.type},otherwise the annotation will fail
* to add and {@code java.lang.reflect.Method.getAnnotations()} will return nothing.
*
*/
public static final class Element {
/** {@code non-null;} the name */
private final String name;
/** {@code non-null;} the value */
private final Object value;
/**
* Construct an instance.
*
* @param name {@code non-null;} the name
* @param value {@code non-null;} the value
*/
public Element(String name, Object value) {
if (name == null) {
throw new NullPointerException("name == null");
}
if (value == null) {
throw new NullPointerException("value == null");
}
this.name = name;
this.value = value;
}
public String getName() {
return name;
}
public Object getValue() {
return value;
}
/** {@inheritDoc} */
@Override
public String toString() {
return "[" + name + ", " + value + "]";
}
/** {@inheritDoc} */
@Override
public int hashCode() {
return name.hashCode() * 31 + value.hashCode();
}
/** {@inheritDoc} */
@Override
public boolean equals(Object other) {
if (! (other instanceof Element)) {
return false;
}
Element otherElement = (Element) other;
return name.equals(otherElement.name)
&& value.equals(otherElement.value);
}
/**
* Convert a value of an element to a {@code Constant}.
* <p><strong>Warning:</strong> Array or TypeId value is not supported yet.
*
* @param value an annotation element value.
* @return a Constant
*/
static Constant toConstant(Object value) {
Class clazz = value.getClass();
if (clazz.isEnum()) {
CstString descriptor = new CstString(TypeId.get(clazz).getName());
CstString name = new CstString(((Enum)value).name());
CstNat cstNat = new CstNat(name, descriptor);
return new CstEnumRef(cstNat);
} else if (clazz.isArray()) {
throw new UnsupportedOperationException("Array is not supported yet");
} else if (value instanceof TypeId) {
throw new UnsupportedOperationException("TypeId is not supported yet");
} else {
return Constants.getConstant(value);
}
}
}
}
/*
* Copyright (C) 2012 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.dx;
import java.io.File;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
/**
* Uses heuristics to guess the application's private data directory.
*/
class AppDataDirGuesser {
// Copied from UserHandle, indicates range of uids allocated for a user.
public static final int PER_USER_RANGE = 100000;
public File guess() {
try {
ClassLoader classLoader = guessSuitableClassLoader();
// Check that we have an instance of the PathClassLoader.
Class<?> clazz = Class.forName("dalvik.system.PathClassLoader");
clazz.cast(classLoader);
// Use the toString() method to calculate the data directory.
String pathFromThisClassLoader = getPathFromThisClassLoader(classLoader, clazz);
File[] results = guessPath(pathFromThisClassLoader);
if (results.length > 0) {
return results[0];
}
} catch (ClassCastException ignored) {
} catch (ClassNotFoundException ignored) {
}
return null;
}
private ClassLoader guessSuitableClassLoader() {
return AppDataDirGuesser.class.getClassLoader();
}
private String getPathFromThisClassLoader(ClassLoader classLoader, Class<?> pathClassLoaderClass) {
// Prior to ICS, we can simply read the "path" field of the
// PathClassLoader.
try {
Field pathField = pathClassLoaderClass.getDeclaredField("path");
pathField.setAccessible(true);
return (String) pathField.get(classLoader);
} catch (NoSuchFieldException ignored) {
} catch (IllegalAccessException ignored) {
} catch (ClassCastException ignored) {
}
// Parsing toString() method: yuck. But no other way to get the path.
String result = classLoader.toString();
return processClassLoaderString(result);
}
/**
* Given the result of a ClassLoader.toString() call, process the result so that guessPath
* can use it. There are currently two variants. For Android 4.3 and later, the string
* "DexPathList" should be recognized and the array of dex path elements is parsed. for
* earlier versions, the last nested array ('[' ... ']') is enclosing the string we are
* interested in.
*/
static String processClassLoaderString(String input) {
if (input.contains("DexPathList")) {
return processClassLoaderString43OrLater(input);
} else {
return processClassLoaderString42OrEarlier(input);
}
}
private static String processClassLoaderString42OrEarlier(String input) {
/* The toString output looks like this:
* dalvik.system.PathClassLoader[dexPath=path/to/apk,libraryPath=path/to/libs]
*/
int index = input.lastIndexOf('[');
input = (index == -1) ? input : input.substring(index + 1);
index = input.indexOf(']');
input = (index == -1) ? input : input.substring(0, index);
return input;
}
private static String processClassLoaderString43OrLater(String input) {
/* The toString output looks like this:
* dalvik.system.PathClassLoader[DexPathList[[zip file "/data/app/{NAME}", ...], nativeLibraryDirectories=[...]]]
*/
int start = input.indexOf("DexPathList") + "DexPathList".length();
if (input.length() > start + 4) { // [[ + ]]
String trimmed = input.substring(start);
int end = trimmed.indexOf(']');
if (trimmed.charAt(0) == '[' && trimmed.charAt(1) == '[' && end >= 0) {
trimmed = trimmed.substring(2, end);
// Comma-separated list, Arrays.toString output.
String split[] = trimmed.split(",");
// Clean up parts. Each path element is the type of the element plus the path in
// quotes.
for (int i = 0; i < split.length; i++) {
int quoteStart = split[i].indexOf('"');
int quoteEnd = split[i].lastIndexOf('"');
if (quoteStart > 0 && quoteStart < quoteEnd) {
split[i] = split[i].substring(quoteStart + 1, quoteEnd);
}
}
// Need to rejoin components.
StringBuilder sb = new StringBuilder();
for (String s : split) {
if (sb.length() > 0) {
sb.append(':');
}
sb.append(s);
}
return sb.toString();
}
}
// This is technically a parsing failure. Return the original string, maybe a later
// stage can still salvage this.
return input;
}
File[] guessPath(String input) {
List<File> results = new ArrayList<>();
for (String potential : splitPathList(input)) {
if (!potential.startsWith("/data/app/")) {
continue;
}
int start = "/data/app/".length();
int end = potential.lastIndexOf(".apk");
if (end != potential.length() - 4) {
continue;
}
int dash = potential.indexOf("-");
if (dash != -1) {
end = dash;
}
String packageName = potential.substring(start, end);
File dataDir = getWriteableDirectory("/data/data/" + packageName);
if (dataDir == null) {
// If we can't access "/data/data", try to guess user specific data directory.
dataDir = guessUserDataDirectory(packageName);
}
if (dataDir != null) {
File cacheDir = new File(dataDir, "cache");
// The cache directory might not exist -- create if necessary
if (fileOrDirExists(cacheDir) || cacheDir.mkdir()) {
if (isWriteableDirectory(cacheDir)) {
results.add(cacheDir);
}
}
}
}
return results.toArray(new File[results.size()]);
}
static String[] splitPathList(String input) {
String trimmed = input;
if (input.startsWith("dexPath=")) {
int start = "dexPath=".length();
int end = input.indexOf(',');
trimmed = (end == -1) ? input.substring(start) : input.substring(start, end);
}
return trimmed.split(":");
}
boolean fileOrDirExists(File file) {
return file.exists();
}
boolean isWriteableDirectory(File file) {
return file.isDirectory() && file.canWrite();
}
Integer getProcessUid() {
/* Uses reflection to try to fetch process UID. It will only work when executing on
* Android device. Otherwise, returns null.
*/
try {
Method myUid = Class.forName("android.os.Process").getMethod("myUid");
// Invoke the method on a null instance, since it's a static method.
return (Integer) myUid.invoke(/* instance= */ null);
} catch (Exception e) {
// Catch any exceptions thrown and default to returning a null.
return null;
}
}
File guessUserDataDirectory(String packageName) {
Integer uid = getProcessUid();
if (uid == null) {
// If we couldn't retrieve process uid, return null.
return null;
}
// We're trying to get the ID of the Android user that's running the process. It can be
// inferred from the UID of the current process.
int userId = uid / PER_USER_RANGE;
return getWriteableDirectory(String.format("/data/user/%d/%s", userId, packageName));
}
private File getWriteableDirectory(String pathName) {
File dir = new File(pathName);
return isWriteableDirectory(dir) ? dir : null;
}
}
/*
* Copyright (C) 2011 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.dx;
import com.android.dx.rop.code.Rop;
import com.android.dx.rop.code.Rops;
import com.android.dx.rop.type.TypeList;
/**
* An operation on two values of the same type.
*
* <p>Math operations ({@link #ADD}, {@link #SUBTRACT}, {@link #MULTIPLY},
* {@link #DIVIDE}, and {@link #REMAINDER}) support ints, longs, floats and
* doubles.
*
* <p>Bit operations ({@link #AND}, {@link #OR}, {@link #XOR}, {@link
* #SHIFT_LEFT}, {@link #SHIFT_RIGHT}, {@link #UNSIGNED_SHIFT_RIGHT}) support
* ints and longs.
*
* <p>Division by zero behaves differently depending on the operand type.
* For int and long operands, {@link #DIVIDE} and {@link #REMAINDER} throw
* {@link ArithmeticException} if {@code b == 0}. For float and double operands,
* the operations return {@code NaN}.
*/
public enum BinaryOp {
/** {@code a + b} */
ADD() {
@Override
Rop rop(TypeList types) {
return Rops.opAdd(types);
}
},
/** {@code a - b} */
SUBTRACT() {
@Override
Rop rop(TypeList types) {
return Rops.opSub(types);
}
},
/** {@code a * b} */
MULTIPLY() {
@Override
Rop rop(TypeList types) {
return Rops.opMul(types);
}
},
/** {@code a / b} */
DIVIDE() {
@Override
Rop rop(TypeList types) {
return Rops.opDiv(types);
}
},
/** {@code a % b} */
REMAINDER() {
@Override
Rop rop(TypeList types) {
return Rops.opRem(types);
}
},
/** {@code a & b} */
AND() {
@Override
Rop rop(TypeList types) {
return Rops.opAnd(types);
}
},
/** {@code a | b} */
OR() {
@Override
Rop rop(TypeList types) {
return Rops.opOr(types);
}
},
/** {@code a ^ b} */
XOR() {
@Override
Rop rop(TypeList types) {
return Rops.opXor(types);
}
},
/** {@code a << b} */
SHIFT_LEFT() {
@Override
Rop rop(TypeList types) {
return Rops.opShl(types);
}
},
/** {@code a >> b} */
SHIFT_RIGHT() {
@Override
Rop rop(TypeList types) {
return Rops.opShr(types);
}
},
/** {@code a >>> b} */
UNSIGNED_SHIFT_RIGHT() {
@Override
Rop rop(TypeList types) {
return Rops.opUshr(types);
}
};
abstract Rop rop(TypeList types);
}
/*
* Copyright (C) 2011 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.dx;
import com.android.dx.rop.code.BasicBlockList;
import com.android.dx.rop.code.Insn;
import com.android.dx.rop.code.PlainCstInsn;
import com.android.dx.rop.code.PlainInsn;
import com.android.dx.rop.code.RegisterSpecList;
import com.android.dx.rop.code.Rop;
import com.android.dx.rop.code.Rops;
import com.android.dx.rop.code.SourcePosition;
import com.android.dx.rop.code.ThrowingCstInsn;
import com.android.dx.rop.code.ThrowingInsn;
import com.android.dx.rop.cst.CstInteger;
import com.android.dx.rop.type.StdTypeList;
import com.android.dx.rop.type.Type;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import static com.android.dx.rop.code.Rop.BRANCH_GOTO;
import static com.android.dx.rop.code.Rop.BRANCH_NONE;
import static com.android.dx.rop.code.Rop.BRANCH_RETURN;
import static com.android.dx.rop.type.Type.BT_BYTE;
import static com.android.dx.rop.type.Type.BT_CHAR;
import static com.android.dx.rop.type.Type.BT_INT;
import static com.android.dx.rop.type.Type.BT_SHORT;
/**
* Builds a sequence of instructions.
*
* <h3>Locals</h3>
* All data manipulation takes place in local variables. Each parameter gets its
* own local by default; access these using {@link #getParameter
* getParameter()}. Non-static methods and constructors also have a {@code this}
* parameter; it's available as {@link #getThis getThis()}. Allocate a new local
* variable using {@link #newLocal newLocal()}, and assign a default value to it
* with {@link #loadConstant loadConstant()}. Copy a value from one local to
* another with {@link #move move()}.
*
* <p>Every local variable has a fixed type. This is either a primitive type (of
* any size) or a reference type. This class emits instructions appropriate to
* the types they operate on. Not all operations are local on all types;
* attempting to emit such an operation will fail with an unchecked exception.
*
* <h3>Math and Bit Operations</h3>
* Transform a single value into another related value using {@link
* #op(UnaryOp,Local,Local) op(UnaryOp, Local, Local)}. Transform two values
* into a third value using {@link #op(BinaryOp,Local,Local,Local) op(BinaryOp,
* Local, Local, Local)}. In either overload the first {@code Local} parameter
* is where the result will be sent; the other {@code Local} parameters are the
* inputs.
*
* <h3>Comparisons</h3>
* There are three different comparison operations each with different
* constraints:
* <ul>
* <li>{@link #compareLongs compareLongs()} compares two locals each
* containing a {@code long} primitive. This is the only operation that
* can compare longs. The result of the comparison is written to another
* {@code int} local.</li>
* <li>{@link #compareFloatingPoint compareFloatingPoint()} compares two
* locals; both {@code float} primitives or both {@code double}
* primitives. This is the only operation that can compare floating
* point values. This comparison takes an extra parameter that sets
* the desired result if either parameter is {@code NaN}. The result of
* the comparison is wrtten to another {@code int} local.
* <li>{@link #compare compare()} compares two locals. The {@link
* Comparison#EQ} and {@link Comparison#NE} options compare either
* {@code int} primitives or references. The other options compare only
* {@code int} primitives. This comparison takes a {@link Label} that
* will be jumped to if the comparison is true. If the comparison is
* false the next instruction in sequence will be executed.
* </ul>
* There's no single operation to compare longs and jump, or to compare ints and
* store the result in a local. Accomplish these goals by chaining multiple
* operations together.
*
* <h3>Branches, Labels and Returns</h3>
* Basic control flow is expressed using jumps and labels. Each label must be
* marked exactly once and may be jumped to any number of times. Create a label
* using its constructor: {@code new Label()}, and mark it using {@link #mark
* mark(Label)}. All jumps to a label will execute instructions starting from
* that label. You can jump to a label that hasn't yet been marked (jumping
* forward) or to a label that has already been marked (jumping backward). Jump
* unconditionally with {@link #jump jump(Label)} or conditionally based on a
* comparison using {@link #compare compare()}.
*
* <p>Most methods should contain a return instruction. Void methods
* should use {@link #returnVoid()}; non-void methods should use {@link
* #returnValue returnValue()} with a local whose return type matches the
* method's return type. Constructors are considered void methods and should
* call {@link #returnVoid()}. Methods may make multiple returns. Methods
* containing no return statements must either loop infinitely or throw
* unconditionally; it is not legal to end a sequence of instructions without a
* jump, return or throw.
*
* <h3>Throwing and Catching</h3>
* This API uses labels to handle thrown exceptions, errors and throwables. Call
* {@link #addCatchClause addCatchClause()} to register the target label and
* throwable class. All statements that follow will jump to that catch clause if
* they throw a {@link Throwable} assignable to that type. Use {@link
* #removeCatchClause removeCatchClause()} to unregister the throwable class.
*
* <p>Throw an throwable by first assigning it to a local and then calling
* {@link #throwValue throwValue()}. Control flow will jump to the nearest label
* assigned to a type assignable to the thrown type. In this context, "nearest"
* means the label requiring the fewest stack frames to be popped.
*
* <h3>Calling methods</h3>
* A method's caller must know its return type, name, parameters, and invoke
* kind. Lookup a method on a type using {@link TypeId#getMethod
* TypeId.getMethod()}. This is more onerous than Java language invokes, which
* can infer the target method using the target object and parameters. There are
* four invoke kinds:
* <ul>
* <li>{@link #invokeStatic invokeStatic()} is used for static methods.</li>
* <li>{@link #invokeDirect invokeDirect()} is used for private instance
* methods and for constructors to call their superclass's
* constructor.</li>
* <li>{@link #invokeInterface invokeInterface()} is used to invoke a method
* whose declaring type is an interface.</li>
* <li>{@link #invokeVirtual invokeVirtual()} is used to invoke any other
* method. The target must not be static, private, a constructor, or an
* interface method.</li>
* <li>{@link #invokeSuper invokeSuper()} is used to invoke the closest
* superclass's virtual method. The target must not be static, private,
* a constructor method, or an interface method.</li>
* <li>{@link #newInstance newInstance()} is used to invoke a
* constructor.</li>
* </ul>
* All invoke methods take a local for the return value. For void methods this
* local is unused and may be null.
*
* <h3>Field Access</h3>
* Read static fields using {@link #sget sget()}; write them using {@link
* #sput sput()}. For instance values you'll need to specify the declaring
* instance; use {@link #getThis getThis()} in an instance method to use {@code
* this}. Read instance values using {@link #iget iget()} and write them with
* {@link #iput iput()}.
*
* <h3>Array Access</h3>
* Allocate an array using {@link #newArray newArray()}. Read an array's length
* with {@link #arrayLength arrayLength()} and its elements with {@link #aget
* aget()}. Write an array's elements with {@link #aput aput()}.
*
* <h3>Types</h3>
* Use {@link #cast cast()} to perform either a <strong>numeric cast</strong> or
* a <strong>type cast</strong>. Interrogate the type of a value in a local
* using {@link #instanceOfType instanceOfType()}.
*
* <h3>Synchronization</h3>
* Acquire a monitor using {@link #monitorEnter monitorEnter()}; release it with
* {@link #monitorExit monitorExit()}. It is the caller's responsibility to
* guarantee that enter and exit calls are balanced, even in the presence of
* exceptions thrown.
*
* <strong>Warning:</strong> Even if a method has the {@code synchronized} flag,
* dex requires instructions to acquire and release monitors manually. A method
* declared with {@link java.lang.reflect.Modifier#SYNCHRONIZED SYNCHRONIZED}
* but without manual calls to {@code monitorEnter()} and {@code monitorExit()}
* will not be synchronized when executed.
*/
public final class Code {
private final MethodId<?, ?> method;
/**
* All allocated labels. Although the order of the labels in this list
* shouldn't impact behavior, it is used to determine basic block indices.
*/
private final List<Label> labels = new ArrayList<Label>();
/**
* The label currently receiving instructions. This is null if the most
* recent instruction was a return or goto.
*/
private Label currentLabel;
/** true once we've fixed the positions of the parameter registers */
private boolean localsInitialized;
private final Local<?> thisLocal;
/**
* The parameters on this method. If this is non-static, the first parameter
* is 'thisLocal' and we have to offset the user's indices by one.
*/
private final List<Local<?>> parameters = new ArrayList<Local<?>>();
private final List<Local<?>> locals = new ArrayList<Local<?>>();
private SourcePosition sourcePosition = SourcePosition.NO_INFO;
private final List<TypeId<?>> catchTypes = new ArrayList<TypeId<?>>();
private final List<Label> catchLabels = new ArrayList<Label>();
private StdTypeList catches = StdTypeList.EMPTY;
Code(DexMaker.MethodDeclaration methodDeclaration) {
this.method = methodDeclaration.method;
if (methodDeclaration.isStatic()) {
thisLocal = null;
} else {
thisLocal = Local.get(this, method.declaringType);
parameters.add(thisLocal);
}
for (TypeId<?> parameter : method.parameters.types) {
parameters.add(Local.get(this, parameter));
}
this.currentLabel = new Label();
adopt(this.currentLabel);
this.currentLabel.marked = true;
}
/**
* Allocates a new local variable of type {@code type}. It is an error to
* allocate a local after instructions have been emitted.
*/
public <T> Local<T> newLocal(TypeId<T> type) {
if (localsInitialized) {
throw new IllegalStateException("Cannot allocate locals after adding instructions");
}
Local<T> result = Local.get(this, type);
locals.add(result);
return result;
}
/**
* Returns the local for the parameter at index {@code index} and of type
* {@code type}.
*/
public <T> Local<T> getParameter(int index, TypeId<T> type) {
if (thisLocal != null) {
index++; // adjust for the hidden 'this' parameter
}
return coerce(parameters.get(index), type);
}
/**
* Returns the local for {@code this} of type {@code type}. It is an error
* to call {@code getThis()} if this is a static method.
*/
public <T> Local<T> getThis(TypeId<T> type) {
if (thisLocal == null) {
throw new IllegalStateException("static methods cannot access 'this'");
}
return coerce(thisLocal, type);
}
@SuppressWarnings("unchecked") // guarded by an equals check
private <T> Local<T> coerce(Local<?> local, TypeId<T> expectedType) {
if (!local.type.equals(expectedType)) {
throw new IllegalArgumentException(
"requested " + expectedType + " but was " + local.type);
}
return (Local<T>) local;
}
/**
* Assigns registers to locals. From the spec:
* "the N arguments to a method land in the last N registers of the
* method's invocation frame, in order. Wide arguments consume two
* registers. Instance methods are passed a this reference as their
* first argument."
*
* In addition to assigning registers to each of the locals, this creates
* instructions to move parameters into their initial registers. These
* instructions are inserted before the code's first real instruction.
*/
void initializeLocals() {
if (localsInitialized) {
throw new AssertionError();
}
localsInitialized = true;
int reg = 0;
for (Local<?> local : locals) {
reg += local.initialize(reg);
}
int firstParamReg = reg;
List<Insn> moveParameterInstructions = new ArrayList<Insn>();
for (Local<?> local : parameters) {
CstInteger paramConstant = CstInteger.make(reg - firstParamReg);
reg += local.initialize(reg);
moveParameterInstructions.add(new PlainCstInsn(Rops.opMoveParam(local.type.ropType),
sourcePosition, local.spec(), RegisterSpecList.EMPTY, paramConstant));
}
labels.get(0).instructions.addAll(0, moveParameterInstructions);
}
/**
* Returns the number of registers to hold the parameters. This includes the
* 'this' parameter if it exists.
*/
int paramSize() {
int result = 0;
for (Local<?> local : parameters) {
result += local.size();
}
return result;
}
// labels
/**
* Assigns {@code target} to this code.
*/
private void adopt(Label target) {
if (target.code == this) {
return; // already adopted
}
if (target.code != null) {
throw new IllegalArgumentException("Cannot adopt label; it belongs to another Code");
}
target.code = this;
labels.add(target);
}
/**
* Start defining instructions for the named label.
*/
public void mark(Label label) {
adopt(label);
if (label.marked) {
throw new IllegalStateException("already marked");
}
label.marked = true;
if (currentLabel != null) {
jump(label); // blocks must end with a branch, return or throw
}
currentLabel = label;
}
/**
* Transfers flow control to the instructions at {@code target}. It is an
* error to jump to a label not marked on this {@code Code}.
*/
public void jump(Label target) {
adopt(target);
addInstruction(new PlainInsn(Rops.GOTO, sourcePosition, null, RegisterSpecList.EMPTY),
target);
}
/**
* Registers {@code catchClause} as a branch target for all instructions
* in this frame that throw a class assignable to {@code toCatch}. This
* includes methods invoked from this frame. Deregister the clause using
* {@link #removeCatchClause removeCatchClause()}. It is an error to
* register a catch clause without also {@link #mark marking it} in the same
* {@code Code} instance.
*/
public void addCatchClause(TypeId<? extends Throwable> toCatch, Label catchClause) {
if (catchTypes.contains(toCatch)) {
throw new IllegalArgumentException("Already caught: " + toCatch);
}
adopt(catchClause);
catchTypes.add(toCatch);
catches = toTypeList(catchTypes);
catchLabels.add(catchClause);
}
/**
* Deregisters the catch clause label for {@code toCatch} and returns it.
*/
public Label removeCatchClause(TypeId<? extends Throwable> toCatch) {
int index = catchTypes.indexOf(toCatch);
if (index == -1) {
throw new IllegalArgumentException("No catch clause: " + toCatch);
}
catchTypes.remove(index);
catches = toTypeList(catchTypes);
return catchLabels.remove(index);
}
public void moveException(Local<?> result) {
addInstruction(new PlainInsn(Rops.opMoveException(Type.THROWABLE),
SourcePosition.NO_INFO, result.spec(), RegisterSpecList.EMPTY));
}
/**
* Throws the throwable in {@code toThrow}.
*/
public void throwValue(Local<? extends Throwable> toThrow) {
addInstruction(new ThrowingInsn(Rops.THROW, sourcePosition,
RegisterSpecList.make(toThrow.spec()), catches));
}
private StdTypeList toTypeList(List<TypeId<?>> types) {
StdTypeList result = new StdTypeList(types.size());
for (int i = 0; i < types.size(); i++) {
result.set(i, types.get(i).ropType);
}
return result;
}
private void addInstruction(Insn insn) {
addInstruction(insn, null);
}
/**
* @param branch the branches to follow; interpretation depends on the
* instruction's branchingness.
*/
private void addInstruction(Insn insn, Label branch) {
if (currentLabel == null || !currentLabel.marked) {
throw new IllegalStateException("no current label");
}
currentLabel.instructions.add(insn);
switch (insn.getOpcode().getBranchingness()) {
case BRANCH_NONE:
if (branch != null) {
throw new IllegalArgumentException("unexpected branch: " + branch);
}
return;
case BRANCH_RETURN:
if (branch != null) {
throw new IllegalArgumentException("unexpected branch: " + branch);
}
currentLabel = null;
break;
case BRANCH_GOTO:
if (branch == null) {
throw new IllegalArgumentException("branch == null");
}
currentLabel.primarySuccessor = branch;
currentLabel = null;
break;
case Rop.BRANCH_IF:
if (branch == null) {
throw new IllegalArgumentException("branch == null");
}
splitCurrentLabel(branch, Collections.<Label>emptyList());
break;
case Rop.BRANCH_THROW:
if (branch != null) {
throw new IllegalArgumentException("unexpected branch: " + branch);
}
splitCurrentLabel(null, new ArrayList<Label>(catchLabels));
break;
default:
throw new IllegalArgumentException();
}
}
/**
* Closes the current label and starts a new one.
*
* @param catchLabels an immutable list of catch labels
*/
private void splitCurrentLabel(Label alternateSuccessor, List<Label> catchLabels) {
Label newLabel = new Label();
adopt(newLabel);
currentLabel.primarySuccessor = newLabel;
currentLabel.alternateSuccessor = alternateSuccessor;
currentLabel.catchLabels = catchLabels;
currentLabel = newLabel;
currentLabel.marked = true;
}
// instructions: locals
/**
* Copies the constant value {@code value} to {@code target}. The constant
* must be a primitive, String, Class, TypeId, or null.
*/
public <T> void loadConstant(Local<T> target, T value) {
Rop rop = value == null
? Rops.CONST_OBJECT_NOTHROW
: Rops.opConst(target.type.ropType);
if (rop.getBranchingness() == BRANCH_NONE) {
addInstruction(new PlainCstInsn(rop, sourcePosition, target.spec(),
RegisterSpecList.EMPTY, Constants.getConstant(value)));
} else {
addInstruction(new ThrowingCstInsn(rop, sourcePosition,
RegisterSpecList.EMPTY, catches, Constants.getConstant(value)));
moveResult(target, true);
}
}
/**
* Copies the value in {@code source} to {@code target}.
*/
public <T> void move(Local<T> target, Local<T> source) {
addInstruction(new PlainInsn(Rops.opMove(source.type.ropType),
sourcePosition, target.spec(), source.spec()));
}
// instructions: unary and binary
/**
* Executes {@code op} and sets {@code target} to the result.
*/
public <T> void op(UnaryOp op, Local<T> target, Local<T> source) {
addInstruction(new PlainInsn(op.rop(source.type), sourcePosition,
target.spec(), source.spec()));
}
/**
* Executes {@code op} and sets {@code target} to the result. For most
* binary operations, the types of {@code a} and {@code b} must be the same.
* Shift operations (like {@link BinaryOp#SHIFT_LEFT}) require {@code b} to
* be an {@code int}, even when {@code a} is a {@code long}.
*/
public <T1, T2> void op(BinaryOp op, Local<T1> target, Local<T1> a, Local<T2> b) {
Rop rop = op.rop(StdTypeList.make(a.type.ropType, b.type.ropType));
RegisterSpecList sources = RegisterSpecList.make(a.spec(), b.spec());
if (rop.getBranchingness() == BRANCH_NONE) {
addInstruction(new PlainInsn(rop, sourcePosition, target.spec(), sources));
} else {
addInstruction(new ThrowingInsn(rop, sourcePosition, sources, catches));
moveResult(target, true);
}
}
// instructions: branches
/**
* Compare ints or references. If the comparison is true, execution jumps to
* {@code trueLabel}. If it is false, execution continues to the next
* instruction.
*/
public <T> void compare(Comparison comparison, Label trueLabel, Local<T> a, Local<T> b) {
adopt(trueLabel);
Rop rop = comparison.rop(StdTypeList.make(a.type.ropType, b.type.ropType));
addInstruction(new PlainInsn(rop, sourcePosition, null,
RegisterSpecList.make(a.spec(), b.spec())), trueLabel);
}
/**
* Check if an int or reference equals to zero. If the comparison is true,
* execution jumps to {@code trueLabel}. If it is false, execution continues to
* the next instruction.
*/
public <T> void compareZ(Comparison comparison, Label trueLabel, Local<?> a) {
adopt(trueLabel);
Rop rop = comparison.rop(StdTypeList.make(a.type.ropType));
addInstruction(new PlainInsn(rop, sourcePosition, null,
RegisterSpecList.make(a.spec())), trueLabel);
}
/**
* Compare floats or doubles. This stores -1 in {@code target} if {@code
* a < b}, 0 in {@code target} if {@code a == b} and 1 in target if {@code
* a > b}. This stores {@code nanValue} in {@code target} if either value
* is {@code NaN}.
*/
public <T extends Number> void compareFloatingPoint(
Local<Integer> target, Local<T> a, Local<T> b, int nanValue) {
Rop rop;
if (nanValue == 1) {
rop = Rops.opCmpg(a.type.ropType);
} else if (nanValue == -1) {
rop = Rops.opCmpl(a.type.ropType);
} else {
throw new IllegalArgumentException("expected 1 or -1 but was " + nanValue);
}
addInstruction(new PlainInsn(rop, sourcePosition, target.spec(),
RegisterSpecList.make(a.spec(), b.spec())));
}
/**
* Compare longs. This stores -1 in {@code target} if {@code
* a < b}, 0 in {@code target} if {@code a == b} and 1 in target if {@code
* a > b}.
*/
public void compareLongs(Local<Integer> target, Local<Long> a, Local<Long> b) {
addInstruction(new PlainInsn(Rops.CMPL_LONG, sourcePosition, target.spec(),
RegisterSpecList.make(a.spec(), b.spec())));
}
// instructions: fields
/**
* Copies the value in instance field {@code fieldId} of {@code instance} to
* {@code target}.
*/
public <D, V> void iget(FieldId<D, ? extends V> fieldId, Local<V> target, Local<D> instance) {
addInstruction(new ThrowingCstInsn(Rops.opGetField(target.type.ropType), sourcePosition,
RegisterSpecList.make(instance.spec()), catches, fieldId.constant));
moveResult(target, true);
}
/**
* Copies the value in {@code source} to the instance field {@code fieldId}
* of {@code instance}.
*/
public <D, V> void iput(FieldId<D, V> fieldId, Local<? extends D> instance, Local<? extends V> source) {
addInstruction(new ThrowingCstInsn(Rops.opPutField(source.type.ropType), sourcePosition,
RegisterSpecList.make(source.spec(), instance.spec()), catches, fieldId.constant));
}
/**
* Copies the value in the static field {@code fieldId} to {@code target}.
*/
public <V> void sget(FieldId<?, ? extends V> fieldId, Local<V> target) {
addInstruction(new ThrowingCstInsn(Rops.opGetStatic(target.type.ropType), sourcePosition,
RegisterSpecList.EMPTY, catches, fieldId.constant));
moveResult(target, true);
}
/**
* Copies the value in {@code source} to the static field {@code fieldId}.
*/
public <V> void sput(FieldId<?, V> fieldId, Local<? extends V> source) {
addInstruction(new ThrowingCstInsn(Rops.opPutStatic(source.type.ropType), sourcePosition,
RegisterSpecList.make(source.spec()), catches, fieldId.constant));
}
// instructions: invoke
/**
* Calls the constructor {@code constructor} using {@code args} and assigns
* the new instance to {@code target}.
*/
public <T> void newInstance(Local<T> target, MethodId<T, Void> constructor, Local<?>... args) {
if (target == null) {
throw new IllegalArgumentException();
}
addInstruction(new ThrowingCstInsn(Rops.NEW_INSTANCE, sourcePosition,
RegisterSpecList.EMPTY, catches, constructor.declaringType.constant));
moveResult(target, true);
invokeDirect(constructor, null, target, args);
}
/**
* Calls the static method {@code method} using {@code args} and assigns the
* result to {@code target}.
*
* @param target the local to receive the method's return value, or {@code
* null} if the return type is {@code void} or if its value not needed.
*/
public <R> void invokeStatic(MethodId<?, R> method, Local<? super R> target, Local<?>... args) {
invoke(Rops.opInvokeStatic(method.prototype(true)), method, target, null, args);
}
/**
* Calls the non-private instance method {@code method} of {@code instance}
* using {@code args} and assigns the result to {@code target}.
*
* @param method a non-private, non-static, method declared on a class. May
* not be an interface method or a constructor.
* @param target the local to receive the method's return value, or {@code
* null} if the return type is {@code void} or if its value not needed.
*/
public <D, R> void invokeVirtual(MethodId<D, R> method, Local<? super R> target,
Local<? extends D> instance, Local<?>... args) {
invoke(Rops.opInvokeVirtual(method.prototype(true)), method, target, instance, args);
}
/**
* Calls {@code method} of {@code instance} using {@code args} and assigns
* the result to {@code target}.
*
* @param method either a private method or the superclass's constructor in
* a constructor's call to {@code super()}.
* @param target the local to receive the method's return value, or {@code
* null} if the return type is {@code void} or if its value not needed.
*/
public <D, R> void invokeDirect(MethodId<D, R> method, Local<? super R> target,
Local<? extends D> instance, Local<?>... args) {
invoke(Rops.opInvokeDirect(method.prototype(true)), method, target, instance, args);
}
/**
* Calls the closest superclass's virtual method {@code method} of {@code
* instance} using {@code args} and assigns the result to {@code target}.
*
* @param target the local to receive the method's return value, or {@code
* null} if the return type is {@code void} or if its value not needed.
*/
public <D, R> void invokeSuper(MethodId<D, R> method, Local<? super R> target,
Local<? extends D> instance, Local<?>... args) {
invoke(Rops.opInvokeSuper(method.prototype(true)), method, target, instance, args);
}
/**
* Calls the interface method {@code method} of {@code instance} using
* {@code args} and assigns the result to {@code target}.
*
* @param method a method declared on an interface.
* @param target the local to receive the method's return value, or {@code
* null} if the return type is {@code void} or if its value not needed.
*/
public <D, R> void invokeInterface(MethodId<D, R> method, Local<? super R> target,
Local<? extends D> instance, Local<?>... args) {
invoke(Rops.opInvokeInterface(method.prototype(true)), method, target, instance, args);
}
private <D, R> void invoke(Rop rop, MethodId<D, R> method, Local<? super R> target,
Local<? extends D> object, Local<?>... args) {
addInstruction(new ThrowingCstInsn(rop, sourcePosition, concatenate(object, args),
catches, method.constant));
if (target != null) {
moveResult(target, false);
}
}
// instructions: types
/**
* Tests if the value in {@code source} is assignable to {@code type}. If it
* is, {@code target} is assigned to 1; otherwise {@code target} is assigned
* to 0.
*/
public void instanceOfType(Local<?> target, Local<?> source, TypeId<?> type) {
addInstruction(new ThrowingCstInsn(Rops.INSTANCE_OF, sourcePosition,
RegisterSpecList.make(source.spec()), catches, type.constant));
moveResult(target, true);
}
/**
* Performs either a numeric cast or a type cast.
*
* <h3>Numeric Casts</h3>
* Converts a primitive to a different representation. Numeric casts may
* be lossy. For example, converting the double {@code 1.8d} to an integer
* yields {@code 1}, losing the fractional part. Converting the integer
* {@code 0x12345678} to a short yields {@code 0x5678}, losing the high
* bytes. The following numeric casts are supported:
*
* <p><table border="1" summary="Supported Numeric Casts">
* <tr><th>From</th><th>To</th></tr>
* <tr><td>int</td><td>byte, char, short, long, float, double</td></tr>
* <tr><td>long</td><td>int, float, double</td></tr>
* <tr><td>float</td><td>int, long, double</td></tr>
* <tr><td>double</td><td>int, long, float</td></tr>
* </table>
*
* <p>For some primitive conversions it will be necessary to chain multiple
* cast operations. For example, to go from float to short one would first
* cast float to int and then int to short.
*
* <p>Numeric casts never throw {@link ClassCastException}.
*
* <h3>Type Casts</h3>
* Checks that a reference value is assignable to the target type. If it is
* assignable it is copied to the target local. If it is not assignable a
* {@link ClassCastException} is thrown.
*/
public void cast(Local<?> target, Local<?> source) {
if (source.getType().ropType.isReference()) {
addInstruction(new ThrowingCstInsn(Rops.CHECK_CAST, sourcePosition,
RegisterSpecList.make(source.spec()), catches, target.type.constant));
moveResult(target, true);
} else {
addInstruction(new PlainInsn(getCastRop(source.type.ropType, target.type.ropType),
sourcePosition, target.spec(), source.spec()));
}
}
private Rop getCastRop(com.android.dx.rop.type.Type sourceType,
com.android.dx.rop.type.Type targetType) {
if (sourceType.getBasicType() == BT_INT) {
switch (targetType.getBasicType()) {
case BT_SHORT:
return Rops.TO_SHORT;
case BT_CHAR:
return Rops.TO_CHAR;
case BT_BYTE:
return Rops.TO_BYTE;
}
}
return Rops.opConv(targetType, sourceType);
}
// instructions: arrays
/**
* Sets {@code target} to the length of the array in {@code array}.
*/
public <T> void arrayLength(Local<Integer> target, Local<T> array) {
addInstruction(new ThrowingInsn(Rops.ARRAY_LENGTH, sourcePosition,
RegisterSpecList.make(array.spec()), catches));
moveResult(target, true);
}
/**
* Assigns {@code target} to a newly allocated array of length {@code
* length}. The array's type is the same as {@code target}'s type.
*/
public <T> void newArray(Local<T> target, Local<Integer> length) {
addInstruction(new ThrowingCstInsn(Rops.opNewArray(target.type.ropType), sourcePosition,
RegisterSpecList.make(length.spec()), catches, target.type.constant));
moveResult(target, true);
}
/**
* Assigns the element at {@code index} in {@code array} to {@code target}.
*/
public void aget(Local<?> target, Local<?> array, Local<Integer> index) {
addInstruction(new ThrowingInsn(Rops.opAget(target.type.ropType), sourcePosition,
RegisterSpecList.make(array.spec(), index.spec()), catches));
moveResult(target, true);
}
/**
* Assigns {@code source} to the element at {@code index} in {@code array}.
*/
public void aput(Local<?> array, Local<Integer> index, Local<?> source) {
addInstruction(new ThrowingInsn(Rops.opAput(source.type.ropType), sourcePosition,
RegisterSpecList.make(source.spec(), array.spec(), index.spec()), catches));
}
// instructions: return
/**
* Returns from a {@code void} method. After a return it is an error to
* define further instructions after a return without first {@link #mark
* marking} an existing unmarked label.
*/
public void returnVoid() {
if (!method.returnType.equals(TypeId.VOID)) {
throw new IllegalArgumentException("declared " + method.returnType
+ " but returned void");
}
addInstruction(new PlainInsn(Rops.RETURN_VOID, sourcePosition, null,
RegisterSpecList.EMPTY));
}
/**
* Returns the value in {@code result} to the calling method. After a return
* it is an error to define further instructions after a return without
* first {@link #mark marking} an existing unmarked label.
*/
public void returnValue(Local<?> result) {
if (!result.type.equals(method.returnType)) {
// TODO: this is probably too strict.
throw new IllegalArgumentException("declared " + method.returnType
+ " but returned " + result.type);
}
addInstruction(new PlainInsn(Rops.opReturn(result.type.ropType), sourcePosition,
null, RegisterSpecList.make(result.spec())));
}
private void moveResult(Local<?> target, boolean afterNonInvokeThrowingInsn) {
Rop rop = afterNonInvokeThrowingInsn
? Rops.opMoveResultPseudo(target.type.ropType)
: Rops.opMoveResult(target.type.ropType);
addInstruction(new PlainInsn(rop, sourcePosition, target.spec(), RegisterSpecList.EMPTY));
}
// instructions; synchronized
/**
* Awaits the lock on {@code monitor}, and acquires it.
*/
public void monitorEnter(Local<?> monitor) {
addInstruction(new ThrowingInsn(Rops.MONITOR_ENTER, sourcePosition,
RegisterSpecList.make(monitor.spec()), catches));
}
/**
* Releases the held lock on {@code monitor}.
*/
public void monitorExit(Local<?> monitor) {
addInstruction(new ThrowingInsn(Rops.MONITOR_EXIT, sourcePosition,
RegisterSpecList.make(monitor.spec()), catches));
}
// produce BasicBlocks for dex
BasicBlockList toBasicBlocks() {
if (!localsInitialized) {
initializeLocals();
}
cleanUpLabels();
BasicBlockList result = new BasicBlockList(labels.size());
for (int i = 0; i < labels.size(); i++) {
result.set(i, labels.get(i).toBasicBlock());
}
return result;
}
/**
* Removes empty labels and assigns IDs to non-empty labels.
*/
private void cleanUpLabels() {
int id = 0;
for (Iterator<Label> i = labels.iterator(); i.hasNext();) {
Label label = i.next();
if (label.isEmpty()) {
i.remove();
} else {
label.compact();
label.id = id++;
}
}
}
private static RegisterSpecList concatenate(Local<?> first, Local<?>[] rest) {
int offset = (first != null) ? 1 : 0;
RegisterSpecList result = new RegisterSpecList(offset + rest.length);
if (first != null) {
result.set(0, first.spec());
}
for (int i = 0; i < rest.length; i++) {
result.set(i + offset, rest[i].spec());
}
return result;
}
}
/*
* Copyright (C) 2011 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.dx;
import com.android.dx.rop.code.Rop;
import com.android.dx.rop.code.Rops;
import com.android.dx.rop.type.TypeList;
/**
* A comparison between two values of the same type.
*/
public enum Comparison {
/** {@code a < b}. Supports int only. */
LT() {
@Override
Rop rop(TypeList types) {
return Rops.opIfLt(types);
}
},
/** {@code a <= b}. Supports int only. */
LE() {
@Override
Rop rop(TypeList types) {
return Rops.opIfLe(types);
}
},
/** {@code a == b}. Supports int and reference types. */
EQ() {
@Override
Rop rop(TypeList types) {
return Rops.opIfEq(types);
}
},
/** {@code a >= b}. Supports int only. */
GE() {
@Override
Rop rop(TypeList types) {
return Rops.opIfGe(types);
}
},
/** {@code a > b}. Supports int only. */
GT() {
@Override
Rop rop(TypeList types) {
return Rops.opIfGt(types);
}
},
/** {@code a != b}. Supports int and reference types. */
NE() {
@Override
Rop rop(TypeList types) {
return Rops.opIfNe(types);
}
};
abstract Rop rop(TypeList types);
}
/*
* Copyright (C) 2011 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.dx;
import com.android.dx.rop.cst.CstBoolean;
import com.android.dx.rop.cst.CstByte;
import com.android.dx.rop.cst.CstChar;
import com.android.dx.rop.cst.CstDouble;
import com.android.dx.rop.cst.CstFloat;
import com.android.dx.rop.cst.CstInteger;
import com.android.dx.rop.cst.CstKnownNull;
import com.android.dx.rop.cst.CstLong;
import com.android.dx.rop.cst.CstShort;
import com.android.dx.rop.cst.CstString;
import com.android.dx.rop.cst.CstType;
import com.android.dx.rop.cst.TypedConstant;
/**
* Factory for rop constants.
*/
final class Constants {
private Constants() {}
/**
* Returns a rop constant for the specified value.
*
* @param value null, a boxed primitive, String, Class, or TypeId.
*/
static TypedConstant getConstant(Object value) {
if (value == null) {
return CstKnownNull.THE_ONE;
} else if (value instanceof Boolean) {
return CstBoolean.make((Boolean) value);
} else if (value instanceof Byte) {
return CstByte.make((Byte) value);
} else if (value instanceof Character) {
return CstChar.make((Character) value);
} else if (value instanceof Double) {
return CstDouble.make(Double.doubleToLongBits((Double) value));
} else if (value instanceof Float) {
return CstFloat.make(Float.floatToIntBits((Float) value));
} else if (value instanceof Integer) {
return CstInteger.make((Integer) value);
} else if (value instanceof Long) {
return CstLong.make((Long) value);
} else if (value instanceof Short) {
return CstShort.make((Short) value);
} else if (value instanceof String) {
return new CstString((String) value);
} else if (value instanceof Class) {
return new CstType(TypeId.get((Class<?>) value).ropType);
} else if (value instanceof TypeId) {
return new CstType(((TypeId) value).ropType);
} else {
throw new UnsupportedOperationException("Not a constant: " + value);
}
}
}
/*
* Copyright (C) 2011 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.dx;
import com.android.dex.DexFormat;
import com.android.dx.dex.DexOptions;
import com.android.dx.dex.code.DalvCode;
import com.android.dx.dex.code.PositionList;
import com.android.dx.dex.code.RopTranslator;
import com.android.dx.dex.file.ClassDefItem;
import com.android.dx.dex.file.DexFile;
import com.android.dx.dex.file.EncodedField;
import com.android.dx.dex.file.EncodedMethod;
import com.android.dx.rop.code.AccessFlags;
import com.android.dx.rop.code.LocalVariableInfo;
import com.android.dx.rop.code.RopMethod;
import com.android.dx.rop.cst.CstString;
import com.android.dx.rop.cst.CstType;
import com.android.dx.rop.type.StdTypeList;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.StringReader;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Modifier;
import java.util.Arrays;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;
import java.util.jar.JarEntry;
import java.util.jar.JarOutputStream;
import dalvik.system.PathClassLoader;
import static com.android.dx.rop.code.AccessFlags.ACC_CONSTRUCTOR;
import static java.lang.reflect.Modifier.PRIVATE;
import static java.lang.reflect.Modifier.STATIC;
/**
* Generates a <strong>D</strong>alvik <strong>EX</strong>ecutable (dex)
* file for execution on Android. Dex files define classes and interfaces,
* including their member methods and fields, executable code, and debugging
* information. They also define annotations, though this API currently has no
* facility to create a dex file that contains annotations.
*
* <p>This library is intended to satisfy two use cases:
* <ul>
* <li><strong>For runtime code generation.</strong> By embedding this library
* in your Android application, you can dynamically generate and load
* executable code. This approach takes advantage of the fact that the
* host environment and target environment are both Android.
* <li><strong>For compile time code generation.</strong> You may use this
* library as a part of a compiler that targets Android. In this scenario
* the generated dex file must be installed on an Android device before it
* can be executed.
* </ul>
*
* <h3>Example: Fibonacci</h3>
* To illustrate how this API is used, we'll use DexMaker to generate a class
* equivalent to the following Java source: <pre> {@code
*
* package com.publicobject.fib;
*
* public class Fibonacci {
* public static int fib(int i) {
* if (i < 2) {
* return i;
* }
* return fib(i - 1) + fib(i - 2);
* }
* }}</pre>
*
* <p>We start by creating a {@link TypeId} to identify the generated {@code
* Fibonacci} class. DexMaker identifies types by their internal names like
* {@code Ljava/lang/Object;} rather than their Java identifiers like {@code
* java.lang.Object}. <pre> {@code
*
* TypeId<?> fibonacci = TypeId.get("Lcom/google/dexmaker/examples/Fibonacci;");
* }</pre>
*
* <p>Next we declare the class. It allows us to specify the type's source file
* for stack traces, its modifiers, its superclass, and the interfaces it
* implements. In this case, {@code Fibonacci} is a public class that extends
* from {@code Object}: <pre> {@code
*
* String fileName = "Fibonacci.generated";
* DexMaker dexMaker = new DexMaker();
* dexMaker.declare(fibonacci, fileName, Modifier.PUBLIC, TypeId.OBJECT);
* }</pre>
* It is illegal to declare members of a class without also declaring the class
* itself.
*
* <p>To make it easier to go from our Java method to dex instructions, we'll
* manually translate it to pseudocode fit for an assembler. We need to replace
* control flow like {@code if()} blocks and {@code for()} loops with labels and
* branches. We'll also avoid performing multiple operations in one statement,
* using local variables to hold intermediate values as necessary:
* <pre> {@code
*
* int constant1 = 1;
* int constant2 = 2;
* if (i < constant2) goto baseCase;
* int a = i - constant1;
* int b = i - constant2;
* int c = fib(a);
* int d = fib(b);
* int result = c + d;
* return result;
* baseCase:
* return i;
* }</pre>
*
* <p>We look up the {@code MethodId} for the method on the declaring type. This
* takes the method's return type (possibly {@link TypeId#VOID}), its name and
* its parameters types. Next we declare the method, specifying its modifiers by
* bitwise ORing constants from {@link Modifier}. The declare
* call returns a {@link Code} object, which we'll use to define the method's
* instructions. <pre> {@code
*
* MethodId<?, Integer> fib = fibonacci.getMethod(TypeId.INT, "fib", TypeId.INT);
* Code code = dexMaker.declare(fib, Modifier.PUBLIC | Modifier.STATIC);
* }</pre>
*
* <p>One limitation of {@code DexMaker}'s API is that it requires all local
* variables to be created before any instructions are emitted. Use {@link
* Code#newLocal newLocal()} to create a new local variable. The method's
* parameters are exposed as locals using {@link Code#getParameter
* getParameter()}. For non-static methods the {@code this} pointer is exposed
* using {@link Code#getThis getThis()}. Here we declare all of the local
* variables that we'll need for our {@code fib()} method: <pre> {@code
*
* Local<Integer> i = code.getParameter(0, TypeId.INT);
* Local<Integer> constant1 = code.newLocal(TypeId.INT);
* Local<Integer> constant2 = code.newLocal(TypeId.INT);
* Local<Integer> a = code.newLocal(TypeId.INT);
* Local<Integer> b = code.newLocal(TypeId.INT);
* Local<Integer> c = code.newLocal(TypeId.INT);
* Local<Integer> d = code.newLocal(TypeId.INT);
* Local<Integer> result = code.newLocal(TypeId.INT);
* }</pre>
*
* <p>Notice that {@link Local} has a type parameter of {@code Integer}. This is
* useful for generating code that works with existing types like {@code String}
* and {@code Integer}, but it can be a hindrance when generating code that
* involves new types. For this reason you may prefer to use raw types only and
* add {@code @SuppressWarnings("unsafe")} on your calling code. This will yield
* the same result but you won't get IDE support if you make a type error.
*
* <p>We're ready to start defining our method's instructions. The {@link Code}
* class catalogs the available instructions and their use. <pre> {@code
*
* code.loadConstant(constant1, 1);
* code.loadConstant(constant2, 2);
* Label baseCase = new Label();
* code.compare(Comparison.LT, baseCase, i, constant2);
* code.op(BinaryOp.SUBTRACT, a, i, constant1);
* code.op(BinaryOp.SUBTRACT, b, i, constant2);
* code.invokeStatic(fib, c, a);
* code.invokeStatic(fib, d, b);
* code.op(BinaryOp.ADD, result, c, d);
* code.returnValue(result);
* code.mark(baseCase);
* code.returnValue(i);
* }</pre>
*
* <p>We're done defining the dex file. We just need to write it to the
* filesystem or load it into the current process. For this example we'll load
* the generated code into the current process. This only works when the current
* process is running on Android. We use {@link #generateAndLoad
* generateAndLoad()} which takes the class loader that will be used as our
* generated code's parent class loader. It also requires a directory where
* temporary files can be written. <pre> {@code
*
* ClassLoader loader = dexMaker.generateAndLoad(
* FibonacciMaker.class.getClassLoader(), getDataDirectory());
* }</pre>
* Finally we'll use reflection to lookup our generated class on its class
* loader and invoke its {@code fib()} method: <pre> {@code
*
* Class<?> fibonacciClass = loader.loadClass("com.google.dexmaker.examples.Fibonacci");
* Method fibMethod = fibonacciClass.getMethod("fib", int.class);
* System.out.println(fibMethod.invoke(null, 8));
* }</pre>
*/
public final class DexMaker {
private final Map<TypeId<?>, TypeDeclaration> types = new LinkedHashMap<>();
// Only warn about not being able to deal with blacklisted methods once. Often this is no
// problem and warning on every class load is too spammy.
private static boolean didWarnBlacklistedMethods;
private static boolean didWarnNonBaseDexClassLoader;
private ClassLoader sharedClassLoader;
private DexFile outputDex;
private boolean markAsTrusted;
/**
* Creates a new {@code DexMaker} instance, which can be used to create a
* single dex file.
*/
public DexMaker() {
}
TypeDeclaration getTypeDeclaration(TypeId<?> type) {
TypeDeclaration result = types.get(type);
if (result == null) {
result = new TypeDeclaration(type);
types.put(type, result);
}
return result;
}
/**
* Declares {@code type}.
*
* @param flags a bitwise combination of {@link Modifier#PUBLIC}, {@link
* Modifier#FINAL} and {@link Modifier#ABSTRACT}.
*/
public void declare(TypeId<?> type, String sourceFile, int flags,
TypeId<?> supertype, TypeId<?>... interfaces) {
TypeDeclaration declaration = getTypeDeclaration(type);
int supportedFlags = Modifier.PUBLIC | Modifier.FINAL | Modifier.ABSTRACT
| AccessFlags.ACC_SYNTHETIC;
if ((flags & ~supportedFlags) != 0) {
throw new IllegalArgumentException("Unexpected flag: "
+ Integer.toHexString(flags));
}
if (declaration.declared) {
throw new IllegalStateException("already declared: " + type);
}
declaration.declared = true;
declaration.flags = flags;
declaration.supertype = supertype;
declaration.sourceFile = sourceFile;
declaration.interfaces = new TypeList(interfaces);
}
/**
* Declares a method or constructor.
*
* @param flags a bitwise combination of {@link Modifier#PUBLIC}, {@link
* Modifier#PRIVATE}, {@link Modifier#PROTECTED}, {@link Modifier#STATIC},
* {@link Modifier#FINAL} and {@link Modifier#SYNCHRONIZED}.
* <p><strong>Warning:</strong> the {@link Modifier#SYNCHRONIZED} flag
* is insufficient to generate a synchronized method. You must also use
* {@link Code#monitorEnter} and {@link Code#monitorExit} to acquire
* a monitor.
*/
public Code declare(MethodId<?, ?> method, int flags) {
TypeDeclaration typeDeclaration = getTypeDeclaration(method.declaringType);
if (typeDeclaration.methods.containsKey(method)) {
throw new IllegalStateException("already declared: " + method);
}
int supportedFlags = Modifier.PUBLIC | Modifier.PRIVATE | Modifier.PROTECTED
| Modifier.STATIC | Modifier.FINAL | Modifier.SYNCHRONIZED
| AccessFlags.ACC_SYNTHETIC | AccessFlags.ACC_BRIDGE;
if ((flags & ~supportedFlags) != 0) {
throw new IllegalArgumentException("Unexpected flag: "
+ Integer.toHexString(flags));
}
// replace the SYNCHRONIZED flag with the DECLARED_SYNCHRONIZED flag
if ((flags & Modifier.SYNCHRONIZED) != 0) {
flags = (flags & ~Modifier.SYNCHRONIZED) | AccessFlags.ACC_DECLARED_SYNCHRONIZED;
}
if (method.isConstructor() || method.isStaticInitializer()) {
flags |= ACC_CONSTRUCTOR;
}
MethodDeclaration methodDeclaration = new MethodDeclaration(method, flags);
typeDeclaration.methods.put(method, methodDeclaration);
return methodDeclaration.code;
}
/**
* Declares a field.
*
* @param flags a bitwise combination of {@link Modifier#PUBLIC}, {@link
* Modifier#PRIVATE}, {@link Modifier#PROTECTED}, {@link Modifier#STATIC},
* {@link Modifier#FINAL}, {@link Modifier#VOLATILE}, and {@link
* Modifier#TRANSIENT}.
* @param staticValue a constant representing the initial value for the
* static field, possibly null. This must be null if this field is
* non-static.
*/
public void declare(FieldId<?, ?> fieldId, int flags, Object staticValue) {
TypeDeclaration typeDeclaration = getTypeDeclaration(fieldId.declaringType);
if (typeDeclaration.fields.containsKey(fieldId)) {
throw new IllegalStateException("already declared: " + fieldId);
}
int supportedFlags = Modifier.PUBLIC | Modifier.PRIVATE | Modifier.PROTECTED
| Modifier.STATIC | Modifier.FINAL | Modifier.VOLATILE | Modifier.TRANSIENT
| AccessFlags.ACC_SYNTHETIC;
if ((flags & ~supportedFlags) != 0) {
throw new IllegalArgumentException("Unexpected flag: "
+ Integer.toHexString(flags));
}
if ((flags & Modifier.STATIC) == 0 && staticValue != null) {
throw new IllegalArgumentException("staticValue is non-null, but field is not static");
}
FieldDeclaration fieldDeclaration = new FieldDeclaration(fieldId, flags, staticValue);
typeDeclaration.fields.put(fieldId, fieldDeclaration);
}
/**
* Generates a dex file and returns its bytes.
*/
public byte[] generate() {
if (outputDex == null) {
DexOptions options = new DexOptions();
options.minSdkVersion = DexFormat.API_NO_EXTENDED_OPCODES;
outputDex = new DexFile(options);
}
for (TypeDeclaration typeDeclaration : types.values()) {
outputDex.add(typeDeclaration.toClassDefItem());
}
try {
return outputDex.toDex(null, false);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
// Generate a file name for the jar by taking a checksum of MethodIds and
// parent class types.
private String generateFileName() {
int checksum = 1;
Set<TypeId<?>> typesKeySet = types.keySet();
Iterator<TypeId<?>> it = typesKeySet.iterator();
int[] checksums = new int[typesKeySet.size()];
int i = 0;
while (it.hasNext()) {
TypeId<?> typeId = it.next();
TypeDeclaration decl = getTypeDeclaration(typeId);
Set<MethodId> methodSet = decl.methods.keySet();
if (decl.supertype != null) {
int sum = 31 * decl.supertype.hashCode() + decl.interfaces.hashCode();
checksums[i++] = 31 * sum + methodSet.hashCode();
}
}
Arrays.sort(checksums);
for (int sum : checksums) {
checksum *= 31;
checksum += sum;
}
return "Generated_" + checksum +".jar";
}
/**
* Set shared class loader to use.
*
* <p>If a class wants to call package private methods of another class they need to share a
* class loader. One common case for this requirement is a mock class wanting to mock package
* private methods of the original class.
*
* <p>If the classLoader is not a subclass of {@code dalvik.system.BaseDexClassLoader} this
* option is ignored.
*
* @param classLoader the class loader the new class should be loaded by
*/
public void setSharedClassLoader(ClassLoader classLoader) {
this.sharedClassLoader = classLoader;
}
public void markAsTrusted() {
this.markAsTrusted = true;
}
private ClassLoader generateClassLoader(File result, File dexCache, ClassLoader parent) {
try {
boolean shareClassLoader = sharedClassLoader != null;
ClassLoader preferredClassLoader = null;
if (parent != null) {
preferredClassLoader = parent;
} else if (sharedClassLoader != null) {
preferredClassLoader = sharedClassLoader;
}
Class baseDexClassLoaderClass = Class.forName("dalvik.system.BaseDexClassLoader");
if (shareClassLoader) {
if (!baseDexClassLoaderClass.isAssignableFrom(preferredClassLoader.getClass())) {
if (!preferredClassLoader.getClass().getName().equals(
"java.lang.BootClassLoader")) {
if (!didWarnNonBaseDexClassLoader) {
System.err.println("Cannot share classloader as shared classloader '"
+ preferredClassLoader + "' is not a subclass of '"
+ baseDexClassLoaderClass
+ "'");
didWarnNonBaseDexClassLoader = true;
}
}
shareClassLoader = false;
}
}
// Try to load the class so that it can call hidden APIs. This is required for spying
// on system classes as real-methods of these classes might call blacklisted APIs
if (markAsTrusted) {
try {
if (shareClassLoader) {
preferredClassLoader.getClass().getMethod("addDexPath", String.class,
Boolean.TYPE).invoke(preferredClassLoader, result.getPath(), true);
return preferredClassLoader;
} else {
return (ClassLoader) baseDexClassLoaderClass
.getConstructor(String.class, File.class, String.class,
ClassLoader.class, Boolean.TYPE)
.newInstance(result.getPath(), dexCache.getAbsoluteFile(), null,
preferredClassLoader, true);
}
} catch (InvocationTargetException e) {
if (e.getCause() instanceof SecurityException) {
if (!didWarnBlacklistedMethods) {
System.err.println("Cannot allow to call blacklisted super methods. "
+ "This might break spying on system classes." + e.getCause());
didWarnBlacklistedMethods = true;
}
} else {
throw e;
}
}
}
if (shareClassLoader) {
preferredClassLoader.getClass().getMethod("addDexPath", String.class).invoke(
preferredClassLoader, result.getPath());
return preferredClassLoader;
} else {
return (ClassLoader) Class.forName("dalvik.system.DexClassLoader")
.getConstructor(String.class, String.class, String.class, ClassLoader.class)
.newInstance(result.getPath(), dexCache.getAbsolutePath(), null,
preferredClassLoader);
}
} catch (ClassNotFoundException e) {
throw new UnsupportedOperationException("load() requires a Dalvik VM", e);
} catch (InvocationTargetException e) {
throw new RuntimeException(e.getCause());
} catch (InstantiationException e) {
throw new AssertionError();
} catch (NoSuchMethodException e) {
throw new AssertionError();
} catch (IllegalAccessException e) {
throw new AssertionError();
}
}
public ClassLoader loadClassDirect(ClassLoader parent, File dexCache, String dexFileName) {
File result = new File(dexCache, dexFileName);
// Check that the file exists. If it does, return a DexClassLoader and skip all
// the dex bytecode generation.
if (result.exists()) {
return generateClassLoader(result, dexCache, parent);
} else {
return null;
}
}
public ClassLoader generateAndLoad(ClassLoader parent, File dexCache) throws IOException {
return generateAndLoad(parent, dexCache, generateFileName());
}
/**
* Generates a dex file and loads its types into the current process.
*
* <h3>Picking a dex cache directory</h3>
* The {@code dexCache} should be an application-private directory. If
* you pass a world-writable directory like {@code /sdcard} a malicious app
* could inject code into your process. Most applications should use this:
* <pre> {@code
*
* File dexCache = getApplicationContext().getDir("dx", Context.MODE_PRIVATE);
* }</pre>
* If the {@code dexCache} is null, this method will consult the {@code
* dexmaker.dexcache} system property. If that exists, it will be used for
* the dex cache. If it doesn't exist, this method will attempt to guess
* the application's private data directory as a last resort. If that fails,
* this method will fail with an unchecked exception. You can avoid the
* exception by either providing a non-null value or setting the system
* property.
*
* @param parent the parent ClassLoader to be used when loading our
* generated types (if set, overrides
* {@link #setSharedClassLoader(ClassLoader) shared class loader}.
* @param dexCache the destination directory where generated and optimized
* dex files will be written. If null, this class will try to guess the
* application's private data dir.
*/
public ClassLoader generateAndLoad(ClassLoader parent, File dexCache, String dexFileName) throws IOException {
if (dexCache == null) {
String property = System.getProperty("dexmaker.dexcache");
if (property != null) {
dexCache = new File(property);
} else {
dexCache = new AppDataDirGuesser().guess();
if (dexCache == null) {
throw new IllegalArgumentException("dexcache == null (and no default could be"
+ " found; consider setting the 'dexmaker.dexcache' system property)");
}
}
}
File result = new File(dexCache, dexFileName);
if (result.exists()) {
try {
deleteOldDex(result);
} catch (Throwable throwable) {}
}
byte[] dex = generate();
/*
* This implementation currently dumps the dex to the filesystem. It
* jars the emitted .dex for the benefit of Gingerbread and earlier
* devices, which can't load .dex files directly.
*
* TODO: load the dex from memory where supported.
*/
result.createNewFile();
JarOutputStream jarOut = new JarOutputStream(new FileOutputStream(result));
JarEntry entry = new JarEntry(DexFormat.DEX_IN_JAR_NAME);
entry.setSize(dex.length);
jarOut.putNextEntry(entry);
jarOut.write(dex);
jarOut.closeEntry();
jarOut.close();
return generateClassLoader(result, dexCache, parent);
}
public void deleteOldDex(File dexFile) {
dexFile.delete();
String dexDir = dexFile.getParent();
File oatDir = new File(dexDir, "/oat/");
File oatDirArm = new File(oatDir, "/arm/");
File oatDirArm64 = new File(oatDir, "/arm64/");
if (!oatDir.exists())
return;
String nameStart = dexFile.getName().replaceAll(".jar", "");
doDeleteOatFiles(oatDir, nameStart);
doDeleteOatFiles(oatDirArm, nameStart);
doDeleteOatFiles(oatDirArm64, nameStart);
}
private void doDeleteOatFiles(File dir, String nameStart) {
if (!dir.exists())
return;
File[] oats = dir.listFiles();
if (oats == null)
return;
for (File oatFile:oats) {
if (oatFile.isFile() && oatFile.getName().startsWith(nameStart))
oatFile.delete();
}
}
DexFile getDexFile() {
if (outputDex == null) {
DexOptions options = new DexOptions();
options.minSdkVersion = DexFormat.API_NO_EXTENDED_OPCODES;
outputDex = new DexFile(options);
}
return outputDex;
}
static class TypeDeclaration {
private final TypeId<?> type;
/** declared state */
private boolean declared;
private int flags;
private TypeId<?> supertype;
private String sourceFile;
private TypeList interfaces;
private ClassDefItem classDefItem;
private final Map<FieldId, FieldDeclaration> fields = new LinkedHashMap<>();
private final Map<MethodId, MethodDeclaration> methods = new LinkedHashMap<>();
TypeDeclaration(TypeId<?> type) {
this.type = type;
}
ClassDefItem toClassDefItem() {
if (!declared) {
throw new IllegalStateException("Undeclared type " + type + " declares members: "
+ fields.keySet() + " " + methods.keySet());
}
DexOptions dexOptions = new DexOptions();
dexOptions.minSdkVersion = DexFormat.API_NO_EXTENDED_OPCODES;
CstType thisType = type.constant;
if (classDefItem == null) {
classDefItem = new ClassDefItem(thisType, flags, supertype.constant,
interfaces.ropTypes, new CstString(sourceFile));
for (MethodDeclaration method : methods.values()) {
EncodedMethod encoded = method.toEncodedMethod(dexOptions);
if (method.isDirect()) {
classDefItem.addDirectMethod(encoded);
} else {
classDefItem.addVirtualMethod(encoded);
}
}
for (FieldDeclaration field : fields.values()) {
EncodedField encoded = field.toEncodedField();
if (field.isStatic()) {
classDefItem.addStaticField(encoded, Constants.getConstant(field.staticValue));
} else {
classDefItem.addInstanceField(encoded);
}
}
}
return classDefItem;
}
}
static class FieldDeclaration {
final FieldId<?, ?> fieldId;
private final int accessFlags;
private final Object staticValue;
FieldDeclaration(FieldId<?, ?> fieldId, int accessFlags, Object staticValue) {
if ((accessFlags & STATIC) == 0 && staticValue != null) {
throw new IllegalArgumentException("instance fields may not have a value");
}
this.fieldId = fieldId;
this.accessFlags = accessFlags;
this.staticValue = staticValue;
}
EncodedField toEncodedField() {
return new EncodedField(fieldId.constant, accessFlags);
}
public boolean isStatic() {
return (accessFlags & STATIC) != 0;
}
}
static class MethodDeclaration {
final MethodId<?, ?> method;
private final int flags;
private final Code code;
public MethodDeclaration(MethodId<?, ?> method, int flags) {
this.method = method;
this.flags = flags;
this.code = new Code(this);
}
boolean isStatic() {
return (flags & STATIC) != 0;
}
boolean isDirect() {
return (flags & (STATIC | PRIVATE | ACC_CONSTRUCTOR)) != 0;
}
EncodedMethod toEncodedMethod(DexOptions dexOptions) {
RopMethod ropMethod = new RopMethod(code.toBasicBlocks(), 0);
LocalVariableInfo locals = null;
DalvCode dalvCode = RopTranslator.translate(
ropMethod, PositionList.NONE, locals, code.paramSize(), dexOptions);
return new EncodedMethod(method.constant, flags, dalvCode, StdTypeList.EMPTY);
}
}
}
/*
* Copyright (C) 2011 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.dx;
import com.android.dx.rop.cst.CstFieldRef;
import com.android.dx.rop.cst.CstNat;
import com.android.dx.rop.cst.CstString;
/**
* Identifies a field.
*
* @param <D> the type declaring this field
* @param <V> the type of value this field holds
*/
public final class FieldId<D, V> {
final TypeId<D> declaringType;
final TypeId<V> type;
final String name;
/** cached converted state */
final CstNat nat;
final CstFieldRef constant;
FieldId(TypeId<D> declaringType, TypeId<V> type, String name) {
if (declaringType == null || type == null || name == null) {
throw new NullPointerException();
}
this.declaringType = declaringType;
this.type = type;
this.name = name;
this.nat = new CstNat(new CstString(name), new CstString(type.name));
this.constant = new CstFieldRef(declaringType.constant, nat);
}
public TypeId<D> getDeclaringType() {
return declaringType;
}
public TypeId<V> getType() {
return type;
}
public String getName() {
return name;
}
@Override
public boolean equals(Object o) {
return o instanceof FieldId
&& ((FieldId<?, ?>) o).declaringType.equals(declaringType)
&& ((FieldId<?, ?>) o).name.equals(name);
}
@Override
public int hashCode() {
return declaringType.hashCode() + 37 * name.hashCode();
}
@Override
public String toString() {
return declaringType + "." + name;
}
}
/*
* Copyright (C) 2011 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.dx;
import com.android.dx.rop.code.BasicBlock;
import com.android.dx.rop.code.Insn;
import com.android.dx.rop.code.InsnList;
import com.android.dx.util.IntList;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/**
* A branch target in a list of instructions.
*/
public final class Label {
final List<Insn> instructions = new ArrayList<>();
Code code;
boolean marked = false;
/** an immutable list of labels corresponding to the types in the catch list */
List<Label> catchLabels = Collections.emptyList();
/** contains the next instruction if no branch occurs */
Label primarySuccessor;
/** contains the instruction to jump to if the if is true */
Label alternateSuccessor;
int id = -1;
public Label() {}
boolean isEmpty() {
return instructions.isEmpty();
}
void compact() {
for (int i = 0; i < catchLabels.size(); i++) {
while (catchLabels.get(i).isEmpty()) {
catchLabels.set(i, catchLabels.get(i).primarySuccessor);
}
}
while (primarySuccessor != null && primarySuccessor.isEmpty()) {
primarySuccessor = primarySuccessor.primarySuccessor;
}
while (alternateSuccessor != null && alternateSuccessor.isEmpty()) {
alternateSuccessor = alternateSuccessor.primarySuccessor;
}
}
BasicBlock toBasicBlock() {
InsnList result = new InsnList(instructions.size());
for (int i = 0; i < instructions.size(); i++) {
result.set(i, instructions.get(i));
}
result.setImmutable();
int primarySuccessorIndex = -1;
IntList successors = new IntList();
for (Label catchLabel : catchLabels) {
successors.add(catchLabel.id);
}
if (primarySuccessor != null) {
primarySuccessorIndex = primarySuccessor.id;
successors.add(primarySuccessorIndex);
}
if (alternateSuccessor != null) {
successors.add(alternateSuccessor.id);
}
successors.setImmutable();
return new BasicBlock(id, result, successors, primarySuccessorIndex);
}
}
/*
* Copyright (C) 2011 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.dx;
import com.android.dx.rop.code.RegisterSpec;
/**
* A temporary variable that holds a single value of a known type.
*/
public final class Local<T> {
private final Code code;
final TypeId<T> type;
private int reg = -1;
private RegisterSpec spec;
private Local(Code code, TypeId<T> type) {
this.code = code;
this.type = type;
}
static <T> Local<T> get(Code code, TypeId<T> type) {
return new Local<T>(code, type);
}
/**
* Assigns registers to this local.
*
* @return the number of registers required.
*/
int initialize(int nextAvailableRegister) {
this.reg = nextAvailableRegister;
this.spec = RegisterSpec.make(nextAvailableRegister, type.ropType);
return size();
}
/**
* Returns the number of registered required to hold this local.
*/
int size() {
return type.ropType.getCategory();
}
RegisterSpec spec() {
if (spec == null) {
code.initializeLocals();
if (spec == null) {
throw new AssertionError();
}
}
return spec;
}
public TypeId getType() {
return type;
}
@Override
public String toString() {
return "v" + reg + "(" + type + ")";
}
}
/*
* Copyright (C) 2011 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.dx;
import com.android.dx.rop.cst.CstMethodRef;
import com.android.dx.rop.cst.CstNat;
import com.android.dx.rop.cst.CstString;
import com.android.dx.rop.type.Prototype;
import java.util.List;
/**
* Identifies a method or constructor.
*
* @param <D> the type declaring this field
* @param <R> the return type of this method
*/
public final class MethodId<D, R> {
final TypeId<D> declaringType;
final TypeId<R> returnType;
final String name;
final TypeList parameters;
/** cached converted state */
final CstNat nat;
final CstMethodRef constant;
MethodId(TypeId<D> declaringType, TypeId<R> returnType, String name, TypeList parameters) {
if (declaringType == null || returnType == null || name == null || parameters == null) {
throw new NullPointerException();
}
this.declaringType = declaringType;
this.returnType = returnType;
this.name = name;
this.parameters = parameters;
this.nat = new CstNat(new CstString(name), new CstString(descriptor(false)));
this.constant = new CstMethodRef(declaringType.constant, nat);
}
public TypeId<D> getDeclaringType() {
return declaringType;
}
public TypeId<R> getReturnType() {
return returnType;
}
/**
* Returns true if this method is a constructor for its declaring class.
*/
public boolean isConstructor() {
return name.equals("<init>");
}
/**
* Returns true if this method is the static initializer for its declaring class.
*/
public boolean isStaticInitializer() {
return name.equals("<clinit>");
}
/**
* Returns the method's name. This is "&lt;init&gt;" if this is a constructor
* or "&lt;clinit&gt;" if a static initializer
*/
public String getName() {
return name;
}
public List<TypeId<?>> getParameters() {
return parameters.asList();
}
/**
* Returns a descriptor like "(Ljava/lang/Class;[I)Ljava/lang/Object;".
*/
String descriptor(boolean includeThis) {
StringBuilder result = new StringBuilder();
result.append("(");
if (includeThis) {
result.append(declaringType.name);
}
for (TypeId t : parameters.types) {
result.append(t.name);
}
result.append(")");
result.append(returnType.name);
return result.toString();
}
Prototype prototype(boolean includeThis) {
return Prototype.intern(descriptor(includeThis));
}
@Override
public boolean equals(Object o) {
return o instanceof MethodId
&& ((MethodId<?, ?>) o).declaringType.equals(declaringType)
&& ((MethodId<?, ?>) o).name.equals(name)
&& ((MethodId<?, ?>) o).parameters.equals(parameters)
&& ((MethodId<?, ?>) o).returnType.equals(returnType);
}
@Override
public int hashCode() {
int result = 17;
result = 31 * result + declaringType.hashCode();
result = 31 * result + name.hashCode();
result = 31 * result + parameters.hashCode();
result = 31 * result + returnType.hashCode();
return result;
}
@Override
public String toString() {
return declaringType + "." + name + "(" + parameters + ")";
}
}
/*
* Copyright (C) 2011 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.dx;
import com.android.dx.rop.cst.CstType;
import java.util.HashMap;
import java.util.Map;
/**
* A primitive type, interface or class.
*
* <p><strong>Warning:</strong> Use care when dealing with boxed primitive
* types. Java's lack of support for parameterized primitive types means that
* a primitive type like {@code int} and its boxed type {@code
* java.lang.Integer} have the same type parameter: {@code TypeId<Integer>}.
* These types are different and it will be a runtime error if the boxed type
* {@code java.lang.Integer} is used where the primitive type {@code int} is
* expected.
*/
public final class TypeId<T> {
/** The {@code boolean} primitive type. */
public static final TypeId<Boolean> BOOLEAN = new TypeId<>(com.android.dx.rop.type.Type.BOOLEAN);
/** The {@code byte} primitive type. */
public static final TypeId<Byte> BYTE = new TypeId<>(com.android.dx.rop.type.Type.BYTE);
/** The {@code char} primitive type. */
public static final TypeId<Character> CHAR = new TypeId<>(com.android.dx.rop.type.Type.CHAR);
/** The {@code double} primitive type. */
public static final TypeId<Double> DOUBLE = new TypeId<>(com.android.dx.rop.type.Type.DOUBLE);
/** The {@code float} primitive type. */
public static final TypeId<Float> FLOAT = new TypeId<>(com.android.dx.rop.type.Type.FLOAT);
/** The {@code int} primitive type. */
public static final TypeId<Integer> INT = new TypeId<>(com.android.dx.rop.type.Type.INT);
/** The {@code long} primitive type. */
public static final TypeId<Long> LONG = new TypeId<>(com.android.dx.rop.type.Type.LONG);
/** The {@code short} primitive type. */
public static final TypeId<Short> SHORT = new TypeId<>(com.android.dx.rop.type.Type.SHORT);
/** The {@code void} primitive type. Only used as a return type. */
public static final TypeId<Void> VOID = new TypeId<>(com.android.dx.rop.type.Type.VOID);
/** The {@code Object} type. */
public static final TypeId<Object> OBJECT = new TypeId<>(com.android.dx.rop.type.Type.OBJECT);
/** The {@code String} type. */
public static final TypeId<String> STRING = new TypeId<>(com.android.dx.rop.type.Type.STRING);
private static final Map<Class<?>, TypeId<?>> PRIMITIVE_TO_TYPE = new HashMap<>();
static {
PRIMITIVE_TO_TYPE.put(boolean.class, BOOLEAN);
PRIMITIVE_TO_TYPE.put(byte.class, BYTE);
PRIMITIVE_TO_TYPE.put(char.class, CHAR);
PRIMITIVE_TO_TYPE.put(double.class, DOUBLE);
PRIMITIVE_TO_TYPE.put(float.class, FLOAT);
PRIMITIVE_TO_TYPE.put(int.class, INT);
PRIMITIVE_TO_TYPE.put(long.class, LONG);
PRIMITIVE_TO_TYPE.put(short.class, SHORT);
PRIMITIVE_TO_TYPE.put(void.class, VOID);
}
final String name;
/** cached converted values */
final com.android.dx.rop.type.Type ropType;
final CstType constant;
TypeId(com.android.dx.rop.type.Type ropType) {
this(ropType.getDescriptor(), ropType);
}
TypeId(String name, com.android.dx.rop.type.Type ropType) {
if (name == null || ropType == null) {
throw new NullPointerException();
}
this.name = name;
this.ropType = ropType;
this.constant = CstType.intern(ropType);
}
/**
* @param name a descriptor like "Ljava/lang/Class;".
*/
public static <T> TypeId<T> get(String name) {
return new TypeId<>(name, com.android.dx.rop.type.Type.internReturnType(name));
}
public static <T> TypeId<T> get(Class<T> type) {
if (type.isPrimitive()) {
// guarded by equals
@SuppressWarnings("unchecked")
TypeId<T> result = (TypeId<T>) PRIMITIVE_TO_TYPE.get(type);
return result;
}
String name = type.getName().replace('.', '/');
return get(type.isArray() ? name : 'L' + name + ';');
}
public <V> FieldId<T, V> getField(TypeId<V> type, String name) {
return new FieldId<>(this, type, name);
}
public MethodId<T, Void> getConstructor(TypeId<?>... parameters) {
return new MethodId<>(this, VOID, "<init>", new TypeList(parameters));
}
public MethodId<T, Void> getStaticInitializer() {
return new MethodId<>(this, VOID, "<clinit>", new TypeList(new TypeId[0]));
}
public <R> MethodId<T, R> getMethod(TypeId<R> returnType, String name, TypeId<?>... parameters) {
return new MethodId<>(this, returnType, name, new TypeList(parameters));
}
public String getName() {
return name;
}
@Override
public boolean equals(Object o) {
return o instanceof TypeId
&& ((TypeId) o).name.equals(name);
}
@Override
public int hashCode() {
return name.hashCode();
}
@Override
public String toString() {
return name;
}
}
/*
* Copyright (C) 2011 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.dx;
import com.android.dx.rop.type.StdTypeList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
/**
* An immutable of types.
*/
final class TypeList {
final TypeId<?>[] types;
final StdTypeList ropTypes;
TypeList(TypeId<?>[] types) {
this.types = types.clone();
this.ropTypes = new StdTypeList(types.length);
for (int i = 0; i < types.length; i++) {
ropTypes.set(i, types[i].ropType);
}
}
/**
* Returns an immutable list.
*/
public List<TypeId<?>> asList() {
return Collections.unmodifiableList(Arrays.asList(types));
}
@Override
public boolean equals(Object o) {
return o instanceof TypeList && Arrays.equals(((TypeList) o).types, types);
}
@Override
public int hashCode() {
return Arrays.hashCode(types);
}
@Override
public String toString() {
StringBuilder result = new StringBuilder();
for (int i = 0; i < types.length; i++) {
if (i > 0) {
result.append(", ");
}
result.append(types[i]);
}
return result.toString();
}
}
/*
* Copyright (C) 2012 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.dx;
import com.android.dx.rop.code.Rop;
import com.android.dx.rop.code.Rops;
/**
* An operation on one value.
*/
public enum UnaryOp {
/** {@code ~a}. Supports int and long. */
NOT() {
@Override
Rop rop(TypeId<?> type) {
return Rops.opNot(type.ropType);
}
},
/** {@code -a}. Supports int, long, float and double. */
NEGATE() {
@Override
Rop rop(TypeId<?> type) {
return Rops.opNeg(type.ropType);
}
};
abstract Rop rop(TypeId<?> type);
}
/*
* Copyright (C) 2011 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.dx.stock;
import com.android.dx.Code;
import com.android.dx.Comparison;
import com.android.dx.DexMaker;
import com.android.dx.FieldId;
import com.android.dx.Label;
import com.android.dx.Local;
import com.android.dx.MethodId;
import com.android.dx.TypeId;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.UndeclaredThrowableException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import static java.lang.reflect.Modifier.ABSTRACT;
import static java.lang.reflect.Modifier.PRIVATE;
import static java.lang.reflect.Modifier.PUBLIC;
import static java.lang.reflect.Modifier.STATIC;
/**
* Creates dynamic proxies of concrete classes.
* <p>
* This is similar to the {@code java.lang.reflect.Proxy} class, but works for classes instead of
* interfaces.
* <h3>Example</h3>
* The following example demonstrates the creation of a dynamic proxy for {@code java.util.Random}
* which will always return 4 when asked for integers, and which logs method calls to every method.
* <pre>
* InvocationHandler handler = new InvocationHandler() {
* &#64;Override
* public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
* if (method.getName().equals("nextInt")) {
* // Chosen by fair dice roll, guaranteed to be random.
* return 4;
* }
* Object result = ProxyBuilder.callSuper(proxy, method, args);
* System.out.println("Method: " + method.getName() + " args: "
* + Arrays.toString(args) + " result: " + result);
* return result;
* }
* };
* Random debugRandom = ProxyBuilder.forClass(Random.class)
* .dexCache(getInstrumentation().getTargetContext().getDir("dx", Context.MODE_PRIVATE))
* .handler(handler)
* .build();
* assertEquals(4, debugRandom.nextInt());
* debugRandom.setSeed(0);
* assertTrue(debugRandom.nextBoolean());
* </pre>
* <h3>Usage</h3>
* Call {@link #forClass(Class)} for the Class you wish to proxy. Call
* {@link #handler(InvocationHandler)} passing in an {@link InvocationHandler}, and then call
* {@link #build()}. The returned instance will be a dynamically generated subclass where all method
* calls will be delegated to the invocation handler, except as noted below.
* <p>
* The static method {@link #callSuper(Object, Method, Object...)} allows you to access the original
* super method for a given proxy. This allows the invocation handler to selectively override some
* methods but not others.
* <p>
* By default, the {@link #build()} method will call the no-arg constructor belonging to the class
* being proxied. If you wish to call a different constructor, you must provide arguments for both
* {@link #constructorArgTypes(Class[])} and {@link #constructorArgValues(Object[])}.
* <p>
* This process works only for classes with public and protected level of visibility.
* <p>
* You may proxy abstract classes. You may not proxy final classes.
* <p>
* Only non-private, non-final, non-static methods will be dispatched to the invocation handler.
* Private, static or final methods will always call through to the superclass as normal.
* <p>
* The {@link #finalize()} method on {@code Object} will not be proxied.
* <p>
* You must provide a dex cache directory via the {@link #dexCache(File)} method. You should take
* care not to make this a world-writable directory, so that third parties cannot inject code into
* your application. A suitable parameter for these output directories would be something like
* this:
* <pre>{@code
* getApplicationContext().getDir("dx", Context.MODE_PRIVATE);
* }</pre>
* <p>
* If the base class to be proxied leaks the {@code this} pointer in the constructor (bad practice),
* that is to say calls a non-private non-final method from the constructor, the invocation handler
* will not be invoked. As a simple concrete example, when proxying Random we discover that it
* internally calls setSeed during the constructor. The proxy will not intercept this call during
* proxy construction, but will intercept as normal afterwards. This behaviour may be subject to
* change in future releases.
* <p>
* This class is <b>not thread safe</b>.
*/
public final class ProxyBuilder<T> {
// Version of ProxyBuilder. It should be updated if the implementation
// of the generated proxy class changes.
public static final int VERSION = 1;
private static final String FIELD_NAME_HANDLER = "$__handler";
private static final String FIELD_NAME_METHODS = "$__methodArray";
/**
* A cache of all proxy classes ever generated. At the time of writing,
* Android's runtime doesn't support class unloading so there's little
* value in using weak references.
*/
private static final Map<ProxiedClass<?>, Class<?>> generatedProxyClasses
= Collections.synchronizedMap(new HashMap<ProxiedClass<?>, Class<?>>());
private final Class<T> baseClass;
private ClassLoader parentClassLoader = ProxyBuilder.class.getClassLoader();
private InvocationHandler handler;
private File dexCache;
private Class<?>[] constructorArgTypes = new Class[0];
private Object[] constructorArgValues = new Object[0];
private List<Class<?>> interfaces = new ArrayList<>();
private Method[] methods;
private boolean sharedClassLoader;
private boolean markTrusted;
private ProxyBuilder(Class<T> clazz) {
baseClass = clazz;
}
public static <T> ProxyBuilder<T> forClass(Class<T> clazz) {
return new ProxyBuilder<T>(clazz);
}
/**
* Specifies the parent ClassLoader to use when creating the proxy.
*
* <p>If null, {@code ProxyBuilder.class.getClassLoader()} will be used.
*/
public ProxyBuilder<T> parentClassLoader(ClassLoader parent) {
parentClassLoader = parent;
return this;
}
public ProxyBuilder<T> handler(InvocationHandler handler) {
this.handler = handler;
return this;
}
/**
* Sets the directory where executable code is stored. See {@link
* DexMaker#generateAndLoad DexMaker.generateAndLoad()} for guidance on
* choosing a secure location for the dex cache.
*/
public ProxyBuilder<T> dexCache(File dexCacheParent) {
dexCache = new File(dexCacheParent, "v" + Integer.toString(VERSION));
dexCache.mkdir();
return this;
}
public ProxyBuilder<T> implementing(Class<?>... interfaces) {
List<Class<?>> list = this.interfaces;
for (Class<?> i : interfaces) {
if (!i.isInterface()) {
throw new IllegalArgumentException("Not an interface: " + i.getName());
}
if (!list.contains(i)) {
list.add(i);
}
}
return this;
}
public ProxyBuilder<T> constructorArgValues(Object... constructorArgValues) {
this.constructorArgValues = constructorArgValues;
return this;
}
public ProxyBuilder<T> constructorArgTypes(Class<?>... constructorArgTypes) {
this.constructorArgTypes = constructorArgTypes;
return this;
}
public ProxyBuilder<T> onlyMethods(Method[] methods) {
this.methods = methods;
return this;
}
public ProxyBuilder<T> withSharedClassLoader() {
this.sharedClassLoader = true;
return this;
}
public ProxyBuilder<T> markTrusted() {
this.markTrusted = true;
return this;
}
/**
* Create a new instance of the class to proxy.
*
* @throws UnsupportedOperationException if the class we are trying to create a proxy for is
* not accessible.
* @throws IOException if an exception occurred writing to the {@code dexCache} directory.
* @throws UndeclaredThrowableException if the constructor for the base class to proxy throws
* a declared exception during construction.
* @throws IllegalArgumentException if the handler is null, if the constructor argument types
* do not match the constructor argument values, or if no such constructor exists.
*/
public T build() throws IOException {
check(handler != null, "handler == null");
check(constructorArgTypes.length == constructorArgValues.length,
"constructorArgValues.length != constructorArgTypes.length");
Class<? extends T> proxyClass = buildProxyClass();
Constructor<? extends T> constructor;
try {
constructor = proxyClass.getConstructor(constructorArgTypes);
} catch (NoSuchMethodException e) {
throw new IllegalArgumentException("No constructor for " + baseClass.getName()
+ " with parameter types " + Arrays.toString(constructorArgTypes));
}
T result;
try {
result = constructor.newInstance(constructorArgValues);
} catch (InstantiationException e) {
// Should not be thrown, generated class is not abstract.
throw new AssertionError(e);
} catch (IllegalAccessException e) {
// Should not be thrown, the generated constructor is accessible.
throw new AssertionError(e);
} catch (InvocationTargetException e) {
// Thrown when the base class constructor throws an exception.
throw launderCause(e);
}
setInvocationHandler(result, handler);
return result;
}
// TODO: test coverage for this
/**
* Generate a proxy class. Note that new instances of this class will not automatically have an
* an invocation handler, even if {@link #handler(InvocationHandler)} was called. The handler
* must be set on each instance after it is created, using
* {@link #setInvocationHandler(Object, InvocationHandler)}.
*/
public Class<? extends T> buildProxyClass() throws IOException {
ClassLoader requestedClassloader;
if (sharedClassLoader) {
requestedClassloader = baseClass.getClassLoader();
} else {
requestedClassloader = parentClassLoader;
}
// try the cache to see if we've generated this one before
// we only populate the map with matching types
ProxiedClass<T> cacheKey =
new ProxiedClass<>(baseClass, interfaces, requestedClassloader, sharedClassLoader);
@SuppressWarnings("unchecked")
Class<? extends T> proxyClass = (Class) generatedProxyClasses.get(cacheKey);
if (proxyClass != null) {
return proxyClass; // cache hit!
}
// the cache missed; generate the class
DexMaker dexMaker = new DexMaker();
String generatedName = getMethodNameForProxyOf(baseClass, interfaces);
TypeId<? extends T> generatedType = TypeId.get("L" + generatedName + ";");
TypeId<T> superType = TypeId.get(baseClass);
generateConstructorsAndFields(dexMaker, generatedType, superType, baseClass);
Method[] methodsToProxy;
if (methods == null) {
methodsToProxy = getMethodsToProxyRecursive();
} else {
methodsToProxy = methods;
}
// Sort the results array so that they are in a deterministic fashion.
//
// We use the same parameters to sort as used in {@link MethodId#hashCode}. This is needed
// as e.g. making a method "public" instead of "protected" should not change the id's of the
// methods. If the id's would change the classes loaded from the cache would be incorrect.
Arrays.sort(methodsToProxy, new Comparator<Method>() {
@Override
public int compare(Method method1, Method method2) {
String m1Signature = method1.getDeclaringClass() + method1.getName() + Arrays.toString(method1.getParameterTypes()) + method1.getReturnType();
String m2Signature = method2.getDeclaringClass() + method2.getName() + Arrays.toString(method2.getParameterTypes()) + method2.getReturnType();
return m1Signature.compareTo(m2Signature);
}
});
generateCodeForAllMethods(dexMaker, generatedType, methodsToProxy, superType);
dexMaker.declare(generatedType, generatedName + ".generated", PUBLIC, superType, getInterfacesAsTypeIds());
if (sharedClassLoader) {
dexMaker.setSharedClassLoader(requestedClassloader);
}
if (markTrusted) {
// The proxied class might have blacklisted methods. Blacklisting methods (and fields)
// is a new feature of Android P:
//
// https://android-developers.googleblog.com/2018/02/
// improving-stability-by-reducing-usage.html
//
// The newly generated class might not be allowed to call methods of the proxied class
// if it is not trusted. As it is not clear which classes have blacklisted methods, mark
// all generated classes as trusted.
dexMaker.markAsTrusted();
}
ClassLoader classLoader;
if (sharedClassLoader) {
classLoader = dexMaker.generateAndLoad(null, dexCache);
} else {
classLoader = dexMaker.generateAndLoad(parentClassLoader, dexCache);
}
try {
proxyClass = loadClass(classLoader, generatedName);
} catch (IllegalAccessError e) {
// Thrown when the base class is not accessible.
throw new UnsupportedOperationException(
"cannot proxy inaccessible class " + baseClass, e);
} catch (ClassNotFoundException e) {
// Should not be thrown, we're sure to have generated this class.
throw new AssertionError(e);
}
setMethodsStaticField(proxyClass, methodsToProxy);
generatedProxyClasses.put(cacheKey, proxyClass);
return proxyClass;
}
// The type cast is safe: the generated type will extend the base class type.
@SuppressWarnings("unchecked")
private Class<? extends T> loadClass(ClassLoader classLoader, String generatedName)
throws ClassNotFoundException {
return (Class<? extends T>) classLoader.loadClass(generatedName);
}
private static RuntimeException launderCause(InvocationTargetException e) {
Throwable cause = e.getCause();
// Errors should be thrown as they are.
if (cause instanceof Error) {
throw (Error) cause;
}
// RuntimeException can be thrown as-is.
if (cause instanceof RuntimeException) {
throw (RuntimeException) cause;
}
// Declared exceptions will have to be wrapped.
throw new UndeclaredThrowableException(cause);
}
private static void setMethodsStaticField(Class<?> proxyClass, Method[] methodsToProxy) {
try {
Field methodArrayField = proxyClass.getDeclaredField(FIELD_NAME_METHODS);
methodArrayField.setAccessible(true);
methodArrayField.set(null, methodsToProxy);
} catch (NoSuchFieldException e) {
// Should not be thrown, generated proxy class has been generated with this field.
throw new AssertionError(e);
} catch (IllegalAccessException e) {
// Should not be thrown, we just set the field to accessible.
throw new AssertionError(e);
}
}
/**
* Returns the proxy's {@link InvocationHandler}.
*
* @throws IllegalArgumentException if the object supplied is not a proxy created by this class.
*/
public static InvocationHandler getInvocationHandler(Object instance) {
try {
Field field = instance.getClass().getDeclaredField(FIELD_NAME_HANDLER);
field.setAccessible(true);
return (InvocationHandler) field.get(instance);
} catch (NoSuchFieldException e) {
throw new IllegalArgumentException("Not a valid proxy instance", e);
} catch (IllegalAccessException e) {
// Should not be thrown, we just set the field to accessible.
throw new AssertionError(e);
}
}
/**
* Sets the proxy's {@link InvocationHandler}.
* <p>
* If you create a proxy with {@link #build()}, the proxy will already have a handler set,
* provided that you configured one with {@link #handler(InvocationHandler)}.
* <p>
* If you generate a proxy class with {@link #buildProxyClass()}, instances of the proxy class
* will not automatically have a handler set, and it is necessary to use this method with each
* instance.
*
* @throws IllegalArgumentException if the object supplied is not a proxy created by this class.
*/
public static void setInvocationHandler(Object instance, InvocationHandler handler) {
try {
Field handlerField = instance.getClass().getDeclaredField(FIELD_NAME_HANDLER);
handlerField.setAccessible(true);
handlerField.set(instance, handler);
} catch (NoSuchFieldException e) {
throw new IllegalArgumentException("Not a valid proxy instance", e);
} catch (IllegalAccessException e) {
// Should not be thrown, we just set the field to accessible.
throw new AssertionError(e);
}
}
// TODO: test coverage for isProxyClass
/**
* Returns true if {@code c} is a proxy class created by this builder.
*/
public static boolean isProxyClass(Class<?> c) {
// TODO: use a marker interface instead?
try {
c.getDeclaredField(FIELD_NAME_HANDLER);
return true;
} catch (NoSuchFieldException e) {
return false;
}
}
/**
* Add
*
* <pre>
* abstractMethodErrorMessage = method + " cannot be called";
* abstractMethodError = new AbstractMethodError(abstractMethodErrorMessage);
* throw abstractMethodError;
* </pre>
*
* to the {@code code}.
*
* @param code The code to add to
* @param method The method that is abstract
* @param abstractMethodErrorMessage The {@link Local} to store the error message
* @param abstractMethodError The {@link Local} to store the error object
*/
private static void throwAbstractMethodError(Code code, Method method,
Local<String> abstractMethodErrorMessage,
Local<AbstractMethodError> abstractMethodError) {
TypeId<AbstractMethodError> abstractMethodErrorClass = TypeId.get(AbstractMethodError.class);
MethodId<AbstractMethodError, Void> abstractMethodErrorConstructor =
abstractMethodErrorClass.getConstructor(TypeId.STRING);
code.loadConstant(abstractMethodErrorMessage, "'" + method + "' cannot be called");
code.newInstance(abstractMethodError, abstractMethodErrorConstructor,
abstractMethodErrorMessage);
code.throwValue(abstractMethodError);
}
private static <T, G extends T> void generateCodeForAllMethods(DexMaker dexMaker,
TypeId<G> generatedType, Method[] methodsToProxy, TypeId<T> superclassType) {
TypeId<InvocationHandler> handlerType = TypeId.get(InvocationHandler.class);
TypeId<Method[]> methodArrayType = TypeId.get(Method[].class);
FieldId<G, InvocationHandler> handlerField =
generatedType.getField(handlerType, FIELD_NAME_HANDLER);
FieldId<G, Method[]> allMethods =
generatedType.getField(methodArrayType, FIELD_NAME_METHODS);
TypeId<Method> methodType = TypeId.get(Method.class);
TypeId<Object[]> objectArrayType = TypeId.get(Object[].class);
MethodId<InvocationHandler, Object> methodInvoke = handlerType.getMethod(TypeId.OBJECT,
"invoke", TypeId.OBJECT, methodType, objectArrayType);
for (int m = 0; m < methodsToProxy.length; ++m) {
/*
* If the 5th method on the superclass Example that can be overridden were to look like
* this:
*
* public int doSomething(Bar param0, int param1) {
* ...
* }
*
* Then the following dex byte code will generate a method on the proxy that looks
* something like this (in idiomatic Java):
*
* // if doSomething is not abstract
* public int doSomething(Bar param0, int param1) {
* if ($__handler == null) {
* return super.doSomething(param0, param1);
* }
* return __handler.invoke(this, __methodArray[4],
* new Object[] { param0, Integer.valueOf(param1) });
* }
*
* // if doSomething is abstract
* public int doSomething(Bar param0, int param1) {
* if ($__handler == null) {
* throw new AbstractMethodError("'doSomething' cannot be called");
* }
* return __handler.invoke(this, __methodArray[4],
* new Object[] { param0, Integer.valueOf(param1) });
* }
*/
Method method = methodsToProxy[m];
String name = method.getName();
Class<?>[] argClasses = method.getParameterTypes();
TypeId<?>[] argTypes = new TypeId<?>[argClasses.length];
for (int i = 0; i < argTypes.length; ++i) {
argTypes[i] = TypeId.get(argClasses[i]);
}
Class<?> returnType = method.getReturnType();
TypeId<?> resultType = TypeId.get(returnType);
MethodId<?, ?> methodId = generatedType.getMethod(resultType, name, argTypes);
TypeId<AbstractMethodError> abstractMethodErrorClass =
TypeId.get(AbstractMethodError.class);
Code code = dexMaker.declare(methodId, PUBLIC);
Local<G> localThis = code.getThis(generatedType);
Local<InvocationHandler> localHandler = code.newLocal(handlerType);
Local<Object> invokeResult = code.newLocal(TypeId.OBJECT);
Local<Integer> intValue = code.newLocal(TypeId.INT);
Local<Object[]> args = code.newLocal(objectArrayType);
Local<Integer> argsLength = code.newLocal(TypeId.INT);
Local<Object> temp = code.newLocal(TypeId.OBJECT);
Local<?> resultHolder = code.newLocal(resultType);
Local<Method[]> methodArray = code.newLocal(methodArrayType);
Local<Method> thisMethod = code.newLocal(methodType);
Local<Integer> methodIndex = code.newLocal(TypeId.INT);
Class<?> aBoxedClass = PRIMITIVE_TO_BOXED.get(returnType);
Local<?> aBoxedResult = null;
if (aBoxedClass != null) {
aBoxedResult = code.newLocal(TypeId.get(aBoxedClass));
}
Local<InvocationHandler> nullHandler = code.newLocal(handlerType);
Local<?>[] superArgs2 = null;
Local<?> superResult2 = null;
MethodId<T, ?> superMethod = null;
Local<String> abstractMethodErrorMessage = null;
Local<AbstractMethodError> abstractMethodError = null;
if ((method.getModifiers() & ABSTRACT) == 0) {
superArgs2 = new Local<?>[argClasses.length];
superResult2 = code.newLocal(resultType);
superMethod = superclassType.getMethod(resultType, name, argTypes);
} else {
abstractMethodErrorMessage = code.newLocal(TypeId.STRING);
abstractMethodError = code.newLocal(abstractMethodErrorClass);
}
code.loadConstant(methodIndex, m);
code.sget(allMethods, methodArray);
code.aget(thisMethod, methodArray, methodIndex);
code.loadConstant(argsLength, argTypes.length);
code.newArray(args, argsLength);
code.iget(handlerField, localHandler, localThis);
// if (proxy == null)
code.loadConstant(nullHandler, null);
Label handlerNullCase = new Label();
code.compare(Comparison.EQ, handlerNullCase, nullHandler, localHandler);
// This code is what we execute when we have a valid proxy: delegate to invocation
// handler.
for (int p = 0; p < argTypes.length; ++p) {
code.loadConstant(intValue, p);
Local<?> parameter = code.getParameter(p, argTypes[p]);
Local<?> unboxedIfNecessary = boxIfRequired(code, parameter, temp);
code.aput(args, intValue, unboxedIfNecessary);
}
code.invokeInterface(methodInvoke, invokeResult, localHandler,
localThis, thisMethod, args);
generateCodeForReturnStatement(code, returnType, invokeResult, resultHolder,
aBoxedResult);
// This code is executed if proxy is null: call the original super method.
// This is required to handle the case of construction of an object which leaks the
// "this" pointer.
code.mark(handlerNullCase);
if ((method.getModifiers() & ABSTRACT) == 0) {
for (int i = 0; i < superArgs2.length; ++i) {
superArgs2[i] = code.getParameter(i, argTypes[i]);
}
if (void.class.equals(returnType)) {
code.invokeSuper(superMethod, null, localThis, superArgs2);
code.returnVoid();
} else {
invokeSuper(superMethod, code, localThis, superArgs2, superResult2);
code.returnValue(superResult2);
}
} else {
throwAbstractMethodError(code, method, abstractMethodErrorMessage,
abstractMethodError);
}
/*
* And to allow calling the original super method, the following is also generated:
*
* public String super$doSomething$java_lang_String(Bar param0, int param1) {
* int result = super.doSomething(param0, param1);
* return result;
* }
*/
MethodId<G, ?> callsSuperMethod = generatedType.getMethod(
resultType, superMethodName(method), argTypes);
Code superCode = dexMaker.declare(callsSuperMethod, PUBLIC);
if ((method.getModifiers() & ABSTRACT) == 0) {
Local<G> superThis = superCode.getThis(generatedType);
Local<?>[] superArgs = new Local<?>[argClasses.length];
for (int i = 0; i < superArgs.length; ++i) {
superArgs[i] = superCode.getParameter(i, argTypes[i]);
}
if (void.class.equals(returnType)) {
superCode.invokeSuper(superMethod, null, superThis, superArgs);
superCode.returnVoid();
} else {
Local<?> superResult = superCode.newLocal(resultType);
invokeSuper(superMethod, superCode, superThis, superArgs, superResult);
superCode.returnValue(superResult);
}
} else {
Local<String> superAbstractMethodErrorMessage = superCode.newLocal(TypeId.STRING);
Local<AbstractMethodError> superAbstractMethodError = superCode.newLocal
(abstractMethodErrorClass);
throwAbstractMethodError(superCode, method, superAbstractMethodErrorMessage,
superAbstractMethodError);
}
}
}
@SuppressWarnings({"unchecked", "rawtypes"})
private static void invokeSuper(MethodId superMethod, Code superCode,
Local superThis, Local[] superArgs, Local superResult) {
superCode.invokeSuper(superMethod, superResult, superThis, superArgs);
}
private static Local<?> boxIfRequired(Code code, Local<?> parameter, Local<Object> temp) {
MethodId<?, ?> unboxMethod = PRIMITIVE_TYPE_TO_UNBOX_METHOD.get(parameter.getType());
if (unboxMethod == null) {
return parameter;
}
code.invokeStatic(unboxMethod, temp, parameter);
return temp;
}
public static Object callSuper(Object proxy, Method method, Object... args) throws Throwable {
try {
return proxy.getClass()
.getMethod(superMethodName(method), method.getParameterTypes())
.invoke(proxy, args);
} catch (InvocationTargetException e) {
throw e.getCause();
}
}
/**
* The super method must include the return type, otherwise its ambiguous
* for methods with covariant return types.
*/
private static String superMethodName(Method method) {
String returnType = method.getReturnType().getName();
return "super$" + method.getName() + "$"
+ returnType.replace('.', '_').replace('[', '_').replace(';', '_');
}
private static void check(boolean condition, String message) {
if (!condition) {
throw new IllegalArgumentException(message);
}
}
private static <T, G extends T> void generateConstructorsAndFields(DexMaker dexMaker,
TypeId<G> generatedType, TypeId<T> superType, Class<T> superClass) {
TypeId<InvocationHandler> handlerType = TypeId.get(InvocationHandler.class);
TypeId<Method[]> methodArrayType = TypeId.get(Method[].class);
FieldId<G, InvocationHandler> handlerField = generatedType.getField(
handlerType, FIELD_NAME_HANDLER);
dexMaker.declare(handlerField, PRIVATE, null);
FieldId<G, Method[]> allMethods = generatedType.getField(
methodArrayType, FIELD_NAME_METHODS);
dexMaker.declare(allMethods, PRIVATE | STATIC, null);
for (Constructor<T> constructor : getConstructorsToOverwrite(superClass)) {
if (constructor.getModifiers() == Modifier.FINAL) {
continue;
}
TypeId<?>[] types = classArrayToTypeArray(constructor.getParameterTypes());
MethodId<?, ?> method = generatedType.getConstructor(types);
Code constructorCode = dexMaker.declare(method, PUBLIC);
Local<G> thisRef = constructorCode.getThis(generatedType);
Local<?>[] params = new Local[types.length];
for (int i = 0; i < params.length; ++i) {
params[i] = constructorCode.getParameter(i, types[i]);
}
MethodId<T, ?> superConstructor = superType.getConstructor(types);
constructorCode.invokeDirect(superConstructor, null, thisRef, params);
constructorCode.returnVoid();
}
}
// The type parameter on Constructor is the class in which the constructor is declared.
// The getDeclaredConstructors() method gets constructors declared only in the given class,
// hence this cast is safe.
@SuppressWarnings("unchecked")
private static <T> Constructor<T>[] getConstructorsToOverwrite(Class<T> clazz) {
return (Constructor<T>[]) clazz.getDeclaredConstructors();
}
private TypeId<?>[] getInterfacesAsTypeIds() {
TypeId<?>[] result = new TypeId<?>[interfaces.size()];
int i = 0;
for (Class<?> implemented : interfaces) {
result[i++] = TypeId.get(implemented);
}
return result;
}
/**
* Gets all {@link Method} objects we can proxy in the hierarchy of the
* supplied class.
*/
private Method[] getMethodsToProxyRecursive() {
Set<MethodSetEntry> methodsToProxy = new HashSet<>();
Set<MethodSetEntry> seenFinalMethods = new HashSet<>();
// Traverse the class hierarchy to ensure that all concrete methods (which could be marked
// as final) are visited before any abstract methods from interfaces.
for (Class<?> c = baseClass; c != null; c = c.getSuperclass()) {
getMethodsToProxy(methodsToProxy, seenFinalMethods, c);
}
// Now traverse the interface hierarchy, starting with the ones implemented by the class,
// followed by any extra interfaces.
for (Class<?> c = baseClass; c != null; c = c.getSuperclass()) {
for (Class<?> i : c.getInterfaces()) {
getMethodsToProxy(methodsToProxy, seenFinalMethods, i);
}
}
for (Class<?> c : interfaces) {
getMethodsToProxy(methodsToProxy, seenFinalMethods, c);
}
Method[] results = new Method[methodsToProxy.size()];
int i = 0;
for (MethodSetEntry entry : methodsToProxy) {
results[i++] = entry.originalMethod;
}
return results;
}
private void getMethodsToProxy(Set<MethodSetEntry> sink, Set<MethodSetEntry> seenFinalMethods,
Class<?> c) {
for (Method method : c.getDeclaredMethods()) {
if ((method.getModifiers() & Modifier.FINAL) != 0) {
// Skip final methods, we can't override them. We
// also need to remember them, in case the same
// method exists in a parent class.
MethodSetEntry entry = new MethodSetEntry(method);
seenFinalMethods.add(entry);
// We may have seen this method already, from an interface
// implemented by a child class. We need to remove it here.
sink.remove(entry);
continue;
}
if ((method.getModifiers() & STATIC) != 0) {
// Skip static methods, overriding them has no effect.
continue;
}
if (!Modifier.isPublic(method.getModifiers())
&& !Modifier.isProtected(method.getModifiers())
&& (!sharedClassLoader || Modifier.isPrivate(method.getModifiers()))) {
// Skip private methods, since they are invoked through direct
// invocation (as opposed to virtual). Therefore, it would not
// be possible to intercept any private method defined inside
// the proxy class except through reflection.
// Skip package-private methods as well (for non-shared class
// loaders). The proxy class does
// not actually inherit package-private methods from the parent
// class because it is not a member of the parent's package.
// This is even true if the two classes have the same package
// name, as they use different class loaders.
continue;
}
if (method.getName().equals("finalize") && method.getParameterTypes().length == 0) {
// Skip finalize method, it's likely important that it execute as normal.
continue;
}
MethodSetEntry entry = new MethodSetEntry(method);
if (seenFinalMethods.contains(entry)) {
// This method is final in a child class.
// We can't override it.
continue;
}
sink.add(entry);
}
// Only visit the interfaces of this class if it is itself an interface. That prevents
// visiting interfaces of a class before its super classes.
if (c.isInterface()) {
for (Class<?> i : c.getInterfaces()) {
getMethodsToProxy(sink, seenFinalMethods, i);
}
}
}
private static <T> String getMethodNameForProxyOf(Class<T> clazz, List<Class<?>> interfaces) {
String interfacesHash = Integer.toHexString(interfaces.hashCode());
return clazz.getName().replace(".", "/") + "_" + interfacesHash + "_Proxy";
}
private static TypeId<?>[] classArrayToTypeArray(Class<?>[] input) {
TypeId<?>[] result = new TypeId[input.length];
for (int i = 0; i < input.length; ++i) {
result[i] = TypeId.get(input[i]);
}
return result;
}
/**
* Calculates the correct return statement code for a method.
* <p>
* A void method will not return anything. A method that returns a primitive will need to
* unbox the boxed result. Otherwise we will cast the result.
*/
// This one is tricky to fix, I gave up.
@SuppressWarnings({ "rawtypes", "unchecked" })
private static void generateCodeForReturnStatement(Code code, Class methodReturnType,
Local localForResultOfInvoke, Local localOfMethodReturnType, Local aBoxedResult) {
if (PRIMITIVE_TO_UNBOX_METHOD.containsKey(methodReturnType)) {
code.cast(aBoxedResult, localForResultOfInvoke);
MethodId unboxingMethodFor = getUnboxMethodForPrimitive(methodReturnType);
code.invokeVirtual(unboxingMethodFor, localOfMethodReturnType, aBoxedResult);
code.returnValue(localOfMethodReturnType);
} else if (void.class.equals(methodReturnType)) {
code.returnVoid();
} else {
code.cast(localOfMethodReturnType, localForResultOfInvoke);
code.returnValue(localOfMethodReturnType);
}
}
private static MethodId<?, ?> getUnboxMethodForPrimitive(Class<?> methodReturnType) {
return PRIMITIVE_TO_UNBOX_METHOD.get(methodReturnType);
}
private static final Map<Class<?>, Class<?>> PRIMITIVE_TO_BOXED;
static {
PRIMITIVE_TO_BOXED = new HashMap<>();
PRIMITIVE_TO_BOXED.put(boolean.class, Boolean.class);
PRIMITIVE_TO_BOXED.put(int.class, Integer.class);
PRIMITIVE_TO_BOXED.put(byte.class, Byte.class);
PRIMITIVE_TO_BOXED.put(long.class, Long.class);
PRIMITIVE_TO_BOXED.put(short.class, Short.class);
PRIMITIVE_TO_BOXED.put(float.class, Float.class);
PRIMITIVE_TO_BOXED.put(double.class, Double.class);
PRIMITIVE_TO_BOXED.put(char.class, Character.class);
}
private static final Map<TypeId<?>, MethodId<?, ?>> PRIMITIVE_TYPE_TO_UNBOX_METHOD;
static {
PRIMITIVE_TYPE_TO_UNBOX_METHOD = new HashMap<>();
for (Map.Entry<Class<?>, Class<?>> entry : PRIMITIVE_TO_BOXED.entrySet()) {
TypeId<?> primitiveType = TypeId.get(entry.getKey());
TypeId<?> boxedType = TypeId.get(entry.getValue());
MethodId<?, ?> valueOfMethod = boxedType.getMethod(boxedType, "valueOf", primitiveType);
PRIMITIVE_TYPE_TO_UNBOX_METHOD.put(primitiveType, valueOfMethod);
}
}
/**
* Map from primitive type to method used to unbox a boxed version of the primitive.
* <p>
* This is required for methods whose return type is primitive, since the
* {@link InvocationHandler} will return us a boxed result, and we'll need to convert it back to
* primitive value.
*/
private static final Map<Class<?>, MethodId<?, ?>> PRIMITIVE_TO_UNBOX_METHOD;
static {
Map<Class<?>, MethodId<?, ?>> map = new HashMap<>();
map.put(boolean.class, TypeId.get(Boolean.class).getMethod(TypeId.BOOLEAN, "booleanValue"));
map.put(int.class, TypeId.get(Integer.class).getMethod(TypeId.INT, "intValue"));
map.put(byte.class, TypeId.get(Byte.class).getMethod(TypeId.BYTE, "byteValue"));
map.put(long.class, TypeId.get(Long.class).getMethod(TypeId.LONG, "longValue"));
map.put(short.class, TypeId.get(Short.class).getMethod(TypeId.SHORT, "shortValue"));
map.put(float.class, TypeId.get(Float.class).getMethod(TypeId.FLOAT, "floatValue"));
map.put(double.class, TypeId.get(Double.class).getMethod(TypeId.DOUBLE, "doubleValue"));
map.put(char.class, TypeId.get(Character.class).getMethod(TypeId.CHAR, "charValue"));
PRIMITIVE_TO_UNBOX_METHOD = map;
}
/**
* Wrapper class to let us disambiguate {@link Method} objects.
* <p>
* The purpose of this class is to override the {@link #equals(Object)} and {@link #hashCode()}
* methods so we can use a {@link Set} to remove duplicate methods that are overrides of one
* another. For these purposes, we consider two methods to be equal if they have the same
* name, return type, and parameter types.
*/
public static class MethodSetEntry {
public final String name;
public final Class<?>[] paramTypes;
public final Class<?> returnType;
public final Method originalMethod;
public MethodSetEntry(Method method) {
originalMethod = method;
name = method.getName();
paramTypes = method.getParameterTypes();
returnType = method.getReturnType();
}
@Override
public boolean equals(Object o) {
if (o instanceof MethodSetEntry) {
MethodSetEntry other = (MethodSetEntry) o;
return name.equals(other.name)
&& returnType.equals(other.returnType)
&& Arrays.equals(paramTypes, other.paramTypes);
}
return false;
}
@Override
public int hashCode() {
int result = 17;
result += 31 * result + name.hashCode();
result += 31 * result + returnType.hashCode();
result += 31 * result + Arrays.hashCode(paramTypes);
return result;
}
}
/**
* A class that was already proxied.
*/
private static class ProxiedClass<U> {
final Class<U> clazz;
final List<Class<?>> interfaces;
/**
* Class loader requested when the proxy class was generated. This might not be the
* class loader of {@code clazz} as not all class loaders can be shared.
*
* @see DexMaker#generateClassLoader(File, File, ClassLoader)
*/
final ClassLoader requestedClassloader;
final boolean sharedClassLoader;
@Override
public boolean equals(Object other) {
if (this == other) {
return true;
}
if (other == null || getClass() != other.getClass()) {
return false;
}
ProxiedClass<?> that = (ProxiedClass<?>) other;
return clazz == that.clazz
&& interfaces.equals(that.interfaces)
&& requestedClassloader == that.requestedClassloader
&& sharedClassLoader == that.sharedClassLoader;
}
@Override
public int hashCode() {
return clazz.hashCode() + interfaces.hashCode() + requestedClassloader.hashCode()
+ (sharedClassLoader ? 1 : 0);
}
private ProxiedClass(Class<U> clazz, List<Class<?>> interfaces,
ClassLoader requestedClassloader, boolean sharedClassLoader) {
this.clazz = clazz;
this.interfaces = new ArrayList<>(interfaces);
this.requestedClassloader = requestedClassloader;
this.sharedClassLoader = sharedClassLoader;
}
}
}
...@@ -4,7 +4,7 @@ import android.app.Application; ...@@ -4,7 +4,7 @@ import android.app.Application;
import android.content.Context; import android.content.Context;
import com.swift.sandhook.xposedcompat.classloaders.ComposeClassLoader; import com.swift.sandhook.xposedcompat.classloaders.ComposeClassLoader;
import com.swift.sandhook.xposedcompat.classloaders.XposedClassLoader; import com.swift.sandhook.xposedcompat.methodgen.DynamicBridge;
import com.swift.sandhook.xposedcompat.utils.ApplicationUtils; import com.swift.sandhook.xposedcompat.utils.ApplicationUtils;
import com.swift.sandhook.xposedcompat.utils.ProcessUtils; import com.swift.sandhook.xposedcompat.utils.ProcessUtils;
...@@ -73,4 +73,18 @@ public class XposedCompat { ...@@ -73,4 +73,18 @@ public class XposedCompat {
} }
} }
public static boolean clearCache() {
try {
cacheDir.delete();
cacheDir.mkdirs();
return true;
} catch (Throwable throwable) {
return false;
}
}
public static void clearOatCache() {
DynamicBridge.clearOatFile();
}
} }
package com.swift.sandhook.xposedcompat.methodgen; package com.swift.sandhook.xposedcompat.methodgen;
import android.os.Trace;
import com.swift.sandhook.xposedcompat.XposedCompat; import com.swift.sandhook.xposedcompat.XposedCompat;
import com.swift.sandhook.xposedcompat.utils.DexLog; import com.swift.sandhook.xposedcompat.utils.DexLog;
import com.swift.sandhook.xposedcompat.utils.FileUtils;
import java.io.File; import java.io.File;
import java.lang.reflect.Constructor; import java.lang.reflect.Constructor;
...@@ -21,7 +22,6 @@ public final class DynamicBridge { ...@@ -21,7 +22,6 @@ public final class DynamicBridge {
private static final HookerDexMaker dexMaker = new HookerDexMaker(); private static final HookerDexMaker dexMaker = new HookerDexMaker();
private static final AtomicBoolean dexPathInited = new AtomicBoolean(false); private static final AtomicBoolean dexPathInited = new AtomicBoolean(false);
private static File dexDir; private static File dexDir;
private static File dexOptDir;
public static void onForkPost() { public static void onForkPost() {
dexPathInited.set(false); dexPathInited.set(false);
...@@ -45,28 +45,35 @@ public final class DynamicBridge { ...@@ -45,28 +45,35 @@ public final class DynamicBridge {
// delete previous compiled dex to prevent potential crashing // delete previous compiled dex to prevent potential crashing
// TODO find a way to reuse them in consideration of performance // TODO find a way to reuse them in consideration of performance
try { try {
// we always choose to use device encrypted storage data on android N and later
// in case some app is installing hooks before phone is unlocked
String fixedAppDataDir = XposedCompat.cacheDir.getAbsolutePath(); String fixedAppDataDir = XposedCompat.cacheDir.getAbsolutePath();
dexDir = new File(fixedAppDataDir, "/sandxposed/"); dexDir = new File(fixedAppDataDir, "/sandxposed/");
dexOptDir = new File(dexDir, "oat");
dexDir.mkdirs();
try {
FileUtils.delete(dexOptDir);
} catch (Throwable throwable) {
}
} catch (Throwable throwable) { } catch (Throwable throwable) {
DexLog.e("error when init dex path", throwable); DexLog.e("error when init dex path", throwable);
} }
} }
Trace.beginSection("SandHook-Xposed");
long timeStart = System.currentTimeMillis();
dexMaker.start(hookMethod, additionalHookInfo, dexMaker.start(hookMethod, additionalHookInfo,
XposedCompat.classLoader, dexDir == null ? null : dexDir.getAbsolutePath()); XposedCompat.classLoader, dexDir == null ? null : dexDir.getAbsolutePath());
DexLog.d("hook method <" + hookMethod.toString() + "> use " + (System.currentTimeMillis() - timeStart) + " ms.");
Trace.endSection();
hookedInfo.put(hookMethod, dexMaker.getCallBackupMethod()); hookedInfo.put(hookMethod, dexMaker.getCallBackupMethod());
} catch (Exception e) { } catch (Exception e) {
DexLog.e("error occur when generating dex. dexDir=" + dexDir, e); DexLog.e("error occur when generating dex. dexDir=" + dexDir, e);
} }
} }
public static void clearOatFile() {
String fixedAppDataDir = XposedCompat.cacheDir.getAbsolutePath();
File dexOatDir = new File(fixedAppDataDir, "/sandxposed/oat/");
if (!dexOatDir.exists())
return;
try {
dexOatDir.delete();
} catch (Throwable throwable) {
}
}
private static boolean checkMember(Member member) { private static boolean checkMember(Member member) {
if (member instanceof Method) { if (member instanceof Method) {
......
...@@ -25,6 +25,7 @@ import java.util.concurrent.atomic.AtomicLong; ...@@ -25,6 +25,7 @@ import java.util.concurrent.atomic.AtomicLong;
import de.robv.android.xposed.XC_MethodHook; import de.robv.android.xposed.XC_MethodHook;
import de.robv.android.xposed.XposedBridge; import de.robv.android.xposed.XposedBridge;
import static com.swift.sandhook.xposedcompat.utils.DexMakerUtils.MD5;
import static com.swift.sandhook.xposedcompat.utils.DexMakerUtils.autoBoxIfNecessary; import static com.swift.sandhook.xposedcompat.utils.DexMakerUtils.autoBoxIfNecessary;
import static com.swift.sandhook.xposedcompat.utils.DexMakerUtils.autoUnboxIfNecessary; import static com.swift.sandhook.xposedcompat.utils.DexMakerUtils.autoUnboxIfNecessary;
import static com.swift.sandhook.xposedcompat.utils.DexMakerUtils.createResultLocals; import static com.swift.sandhook.xposedcompat.utils.DexMakerUtils.createResultLocals;
...@@ -79,8 +80,6 @@ public class HookerDexMaker { ...@@ -79,8 +80,6 @@ public class HookerDexMaker {
private static final MethodId<XposedBridge, Void> logStrMethodId = private static final MethodId<XposedBridge, Void> logStrMethodId =
xposedBridgeTypeId.getMethod(TypeId.VOID, "log", TypeId.STRING); xposedBridgeTypeId.getMethod(TypeId.VOID, "log", TypeId.STRING);
private static AtomicLong sClassNameSuffix = new AtomicLong(1);
private FieldId<?, XposedBridge.AdditionalHookInfo> mHookInfoFieldId; private FieldId<?, XposedBridge.AdditionalHookInfo> mHookInfoFieldId;
private FieldId<?, Member> mMethodFieldId; private FieldId<?, Member> mMethodFieldId;
private MethodId<?, ?> mBackupMethodId; private MethodId<?, ?> mBackupMethodId;
...@@ -175,15 +174,30 @@ public class HookerDexMaker { ...@@ -175,15 +174,30 @@ public class HookerDexMaker {
} else { } else {
mAppClassLoader = appClassLoader; mAppClassLoader = appClassLoader;
} }
doMake();
}
private void doMake() throws Exception {
mDexMaker = new DexMaker(); mDexMaker = new DexMaker();
// Generate a Hooker class. // Generate a Hooker class.
String className = CLASS_NAME_PREFIX + sClassNameSuffix.getAndIncrement(); String className = getClassName(mMember);
String classDesc = CLASS_DESC_PREFIX + className + ";"; String dexName = className + ".jar";
mHookerTypeId = TypeId.get(classDesc);
HookWrapper.HookEntity hookEntity = null;
//try load cache first
try {
ClassLoader loader = mDexMaker.loadClassDirect(mAppClassLoader, new File(mDexDirPath), dexName);
if (loader != null) {
hookEntity = loadHookerClass(loader, className);
}
} catch (Throwable throwable) {}
//do generate
if (hookEntity == null) {
hookEntity = doMake(className, dexName);
}
SandHook.hook(hookEntity);
}
private HookWrapper.HookEntity doMake(String className, String dexName) throws Exception {
mHookerTypeId = TypeId.get(CLASS_DESC_PREFIX + className + ";");
mDexMaker.declare(mHookerTypeId, className + ".generated", Modifier.PUBLIC, TypeId.OBJECT); mDexMaker.declare(mHookerTypeId, className + ".generated", Modifier.PUBLIC, TypeId.OBJECT);
generateFields(); generateFields();
generateSetupMethod(); generateSetupMethod();
...@@ -195,9 +209,12 @@ public class HookerDexMaker { ...@@ -195,9 +209,12 @@ public class HookerDexMaker {
if (TextUtils.isEmpty(mDexDirPath)) { if (TextUtils.isEmpty(mDexDirPath)) {
throw new IllegalArgumentException("dexDirPath should not be empty!!!"); throw new IllegalArgumentException("dexDirPath should not be empty!!!");
} }
// Create the dex file and load it. // Create the dex file and load it.
loader = mDexMaker.generateAndLoad(mAppClassLoader, new File(mDexDirPath)); loader = mDexMaker.generateAndLoad(mAppClassLoader, new File(mDexDirPath), dexName);
return loadHookerClass(loader, className);
}
private HookWrapper.HookEntity loadHookerClass(ClassLoader loader, String className) throws Exception {
mHookClass = loader.loadClass(className); mHookClass = loader.loadClass(className);
// Execute our newly-generated code in-process. // Execute our newly-generated code in-process.
mHookClass.getMethod(METHOD_NAME_SETUP, Member.class, XposedBridge.AdditionalHookInfo.class) mHookClass.getMethod(METHOD_NAME_SETUP, Member.class, XposedBridge.AdditionalHookInfo.class)
...@@ -205,7 +222,11 @@ public class HookerDexMaker { ...@@ -205,7 +222,11 @@ public class HookerDexMaker {
mHookMethod = mHookClass.getMethod(METHOD_NAME_HOOK, mActualParameterTypes); mHookMethod = mHookClass.getMethod(METHOD_NAME_HOOK, mActualParameterTypes);
mBackupMethod = mHookClass.getMethod(METHOD_NAME_BACKUP, mActualParameterTypes); mBackupMethod = mHookClass.getMethod(METHOD_NAME_BACKUP, mActualParameterTypes);
mCallBackupMethod = mHookClass.getMethod(METHOD_NAME_CALL_BACKUP, mActualParameterTypes); mCallBackupMethod = mHookClass.getMethod(METHOD_NAME_CALL_BACKUP, mActualParameterTypes);
SandHook.hook(new HookWrapper.HookEntity(mMember, mHookMethod, mBackupMethod)); return new HookWrapper.HookEntity(mMember, mHookMethod, mBackupMethod);
}
private String getClassName(Member originMethod) {
return CLASS_NAME_PREFIX + "_" + originMethod.getName() + "_" + MD5(originMethod.toString());
} }
public Method getHookMethod() { public Method getHookMethod() {
......
...@@ -12,6 +12,9 @@ import com.android.dx.rop.type.Type; ...@@ -12,6 +12,9 @@ import com.android.dx.rop.type.Type;
import java.lang.reflect.InvocationTargetException; import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method; import java.lang.reflect.Method;
import java.math.BigInteger;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
...@@ -253,4 +256,15 @@ public class DexMakerUtils { ...@@ -253,4 +256,15 @@ public class DexMakerUtils {
return null; return null;
} }
} }
public static String MD5(String source) {
try {
MessageDigest messageDigest = MessageDigest.getInstance("MD5");
messageDigest.update(source.getBytes());
return new BigInteger(1, messageDigest.digest()).toString(32);
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
}
return source;
}
} }
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment