Java tutorial
/* * Copyright (C) 2015 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 dodola.anole.lib; import com.android.annotations.NonNull; import com.android.utils.AsmUtils; import org.objectweb.asm.AnnotationVisitor; import org.objectweb.asm.ClassVisitor; import org.objectweb.asm.FieldVisitor; import org.objectweb.asm.Label; import org.objectweb.asm.MethodVisitor; import org.objectweb.asm.Opcodes; import org.objectweb.asm.Type; import org.objectweb.asm.commons.GeneratorAdapter; import org.objectweb.asm.commons.Method; import org.objectweb.asm.tree.ClassNode; import org.objectweb.asm.tree.LabelNode; import org.objectweb.asm.tree.LineNumberNode; import org.objectweb.asm.tree.MethodNode; import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; /** * Visitor for classes that will eventually be replaceable at runtime. * <p> * Since classes cannot be replaced in an existing class loader, we use a delegation model to * redirect any method implementation to the AndroidInstantRuntime. * <p> * This redirection happens only when a new class implementation is available. A new version * will register itself in a static synthetic field called $change. Each method will be enhanced * with a piece of code to check if a new version is available by looking at the $change field * and redirect if necessary. * <p> * Redirection will be achieved by calling a */ public class IncrementalSupportVisitor extends IncrementalVisitor { private boolean disableRedirectionForClass = false; private static final class VisitorBuilder implements IncrementalVisitor.VisitorBuilder { private VisitorBuilder() { } @NonNull @Override public IncrementalVisitor build(@NonNull ClassNode classNode, @NonNull List<ClassNode> parentNodes, @NonNull ClassVisitor classVisitor) { return new IncrementalSupportVisitor(classNode, parentNodes, classVisitor); } @Override @NonNull public String getMangledRelativeClassFilePath(@NonNull String originalClassFilePath) { return originalClassFilePath; } @NonNull @Override public OutputType getOutputType() { return OutputType.INSTRUMENT; } } public static final IncrementalVisitor.VisitorBuilder VISITOR_BUILDER = new VisitorBuilder(); public IncrementalSupportVisitor(@NonNull ClassNode classNode, @NonNull List<ClassNode> parentNodes, @NonNull ClassVisitor classVisitor) { super(classNode, parentNodes, classVisitor); } /** * Ensures that the class contains a $change field used for referencing the * IncrementalChange dispatcher. * <p/> * Also updates package_private visiblity to public so we can call into this class from * outside the package. */ @Override public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) { visitedClassName = name; visitedSuperName = superName; super.visitField(Opcodes.ACC_PUBLIC | Opcodes.ACC_STATIC | Opcodes.ACC_VOLATILE | Opcodes.ACC_SYNTHETIC, "$change", getRuntimeTypeName(CHANGE_TYPE), null, null); access = transformClassAccessForInstantRun(access); super.visit(version, access, name, signature, superName, interfaces); } @Override public AnnotationVisitor visitAnnotation(String desc, boolean visible) { if (desc.equals(DISABLE_ANNOTATION_TYPE.getDescriptor())) { disableRedirectionForClass = true; } return super.visitAnnotation(desc, visible); } @Override public FieldVisitor visitField(int access, String name, String desc, String signature, Object value) { access = transformAccessForInstantRun(access); return super.visitField(access, name, desc, signature, value); } /** * Insert Constructor specific logic({@link ConstructorArgsRedirection} and * {@link ConstructorDelegationDetector}) for constructor redirecting or * normal method redirecting ({@link MethodRedirection}) for other methods. */ @Override public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) { access = transformAccessForInstantRun(access); MethodVisitor defaultVisitor = super.visitMethod(access, name, desc, signature, exceptions); MethodNode method = getMethodByNameInClass(name, desc, classNode); // does the method use blacklisted APIs. boolean hasIncompatibleChange = InstantRunMethodVerifier .verifyMethod(method) != InstantRunVerifierStatus.COMPATIBLE; if (hasIncompatibleChange || disableRedirectionForClass || !isAccessCompatibleWithInstantRun(access) || name.equals(AsmUtils.CLASS_INITIALIZER)) { return defaultVisitor; } else { ISMethodVisitor mv = new ISMethodVisitor(defaultVisitor, access, name, desc); if (name.equals(AsmUtils.CONSTRUCTOR)) { ConstructorDelegationDetector.Constructor constructor = ConstructorDelegationDetector .deconstruct(visitedClassName, method); LabelNode start = new LabelNode(); LabelNode after = new LabelNode(); method.instructions.insert(constructor.loadThis, start); if (constructor.lineForLoad != -1) { // Record the line number from the start of LOAD_0 for uninitialized 'this'. // This allows a breakpoint to be set at the line with this(...) or super(...) // call in the constructor. method.instructions.insert(constructor.loadThis, new LineNumberNode(constructor.lineForLoad, start)); } method.instructions.insert(constructor.delegation, after); mv.addRedirection(new ConstructorArgsRedirection(start, visitedClassName, constructor.args.name + "." + constructor.args.desc, after, Type.getArgumentTypes(constructor.delegation.desc))); mv.addRedirection(new MethodRedirection(after, constructor.body.name + "." + constructor.body.desc, Type.getReturnType(desc))); } else { mv.addRedirection(new MethodRedirection(new LabelNode(mv.getStartLabel()), name + "." + desc, Type.getReturnType(desc))); } method.accept(mv); return null; } } /** * If a class is package private, make it public so instrumented code living in a different * class loader can instantiate them. * * @param access the original class/method/field access. * @return the new access or the same one depending on the original access rights. */ private static int transformClassAccessForInstantRun(int access) { AccessRight accessRight = AccessRight.fromNodeAccess(access); return accessRight == AccessRight.PACKAGE_PRIVATE ? access | Opcodes.ACC_PUBLIC : access; } /** * If a method/field is not private, make it public. This is to workaround the fact * <ul>Our restart.dex files are loaded with a different class loader than the main dex file * class loader on restart. so we need methods/fields to be public</ul> * <ul>Our reload.dex are loaded from a different class loader as well but methods/fields * are accessed through reflection, yet you need class visibility.</ul> * <p> * remember that in Java, protected methods or fields can be acessed by classes in the same * package : * {@see https://docs.oracle.com/javase/tutorial/java/javaOO/accesscontrol.html} * * @param access the original class/method/field access. * @return the new access or the same one depending on the original access rights. */ private static int transformAccessForInstantRun(int access) { AccessRight accessRight = AccessRight.fromNodeAccess(access); if (accessRight != AccessRight.PRIVATE) { access &= ~Opcodes.ACC_PROTECTED; access &= ~Opcodes.ACC_PRIVATE; return access | Opcodes.ACC_PUBLIC; } return access; } private class ISMethodVisitor extends GeneratorAdapter { private boolean disableRedirection = false; private int change; private final List<Type> args; private final List<Redirection> redirections; private final Map<Label, Redirection> resolvedRedirections; private final Label start; public ISMethodVisitor(MethodVisitor mv, int access, String name, String desc) { super(Opcodes.ASM5, mv, access, name, desc); this.change = -1; this.redirections = new ArrayList<Redirection>(); this.resolvedRedirections = new HashMap<Label, Redirection>(); this.args = new ArrayList<Type>(Arrays.asList(Type.getArgumentTypes(desc))); this.start = new Label(); boolean isStatic = (access & Opcodes.ACC_STATIC) != 0; // if this is not a static, we add a fictional first parameter what will contain the // "this" reference which can be loaded with ILOAD_0 bytecode. if (!isStatic) { args.add(0, Type.getType(Object.class)); } } @Override public AnnotationVisitor visitAnnotation(String desc, boolean visible) { if (desc.equals(DISABLE_ANNOTATION_TYPE.getDescriptor())) { disableRedirection = true; } return super.visitAnnotation(desc, visible); } /** * inserts a new local '$change' in each method that contains a reference to the type's * IncrementalChange dispatcher, this is done to avoid threading issues. * <p/> * Pseudo code: * <code> * $package/IncrementalChange $local1 = $className$.$change; * </code> */ @Override public void visitCode() { if (!disableRedirection) { // Labels cannot be used directly as they are volatile between different visits, // so we must use LabelNode and resolve before visiting for better performance. for (Redirection redirection : redirections) { resolvedRedirections.put(redirection.getPosition().getLabel(), redirection); } super.visitLabel(start); change = newLocal(CHANGE_TYPE); visitFieldInsn(Opcodes.GETSTATIC, visitedClassName, "$change", getRuntimeTypeName(CHANGE_TYPE)); storeLocal(change); redirectAt(start); } super.visitCode(); } @Override public void visitLabel(Label label) { super.visitLabel(label); redirectAt(label); } private void redirectAt(Label label) { if (disableRedirection) return; Redirection redirection = resolvedRedirections.get(label); if (redirection != null) { // A special line number to mark this area of code. super.visitLineNumber(0, label); redirection.redirect(this, change, args); } } public void addRedirection(@NonNull Redirection redirection) { redirections.add(redirection); } @Override public void visitLocalVariable(String name, String desc, String signature, Label start, Label end, int index) { // In dex format, the argument names are separated from the local variable names. It // seems to be needed to declare the local argument variables from the beginning of // the methods for dex to pick that up. By inserting code before the first label we // break that. In Java this is fine, and the debugger shows the right thing. However // if we don't readjust the local variables, we just don't see the arguments. if (!disableRedirection && index < args.size()) { start = this.start; } super.visitLocalVariable(name, desc, signature, start, end, index); } public Label getStartLabel() { return start; } } /** * Decorated {@link MethodNode} that maintains a reference to the class declaring the method. */ private static class MethodReference { final MethodNode method; final ClassNode owner; private MethodReference(MethodNode method, ClassNode owner) { this.method = method; this.owner = owner; } } /*** * Inserts a trampoline to this class so that the updated methods can make calls to super * class methods. * <p/> * Pseudo code for this trampoline: * <code> * Object access$super($classType instance, String name, object[] args) { * switch(name) { * case "firstMethod.(Ljava/lang/String;Ljava/lang/Object;)Ljava/lang/Object;": * return super~instance.firstMethod((String)arg[0], arg[1]); * case "secondMethod.(Ljava/lang/String;I)V": * return super~instance.firstMethod((String)arg[0], arg[1]); * <p> * default: * StringBuilder $local1 = new StringBuilder(); * $local1.append("Method not found "); * $local1.append(name); * $local1.append(" in " $classType $super implementation"); * throw new $package/InstantReloadException($local1.toString()); * } * </code> */ private void createAccessSuper() { int access = Opcodes.ACC_STATIC | Opcodes.ACC_PUBLIC | Opcodes.ACC_SYNTHETIC | Opcodes.ACC_VARARGS; Method m = new Method("access$super", "(L" + visitedClassName + ";Ljava/lang/String;[Ljava/lang/Object;)Ljava/lang/Object;"); MethodVisitor visitor = super.visitMethod(access, m.getName(), m.getDescriptor(), null, null); final GeneratorAdapter mv = new GeneratorAdapter(access, m, visitor); // Gather all methods from itself and its superclasses to generate a giant access$super // implementation. // This will work fine as long as we don't support adding methods to a class. final Map<String, MethodReference> uniqueMethods = new HashMap<String, MethodReference>(); if (parentNodes.isEmpty()) { // if we cannot determine the parents for this class, let's blindly add all the // method of the current class as a gateway to a possible parent version. addAllNewMethods(uniqueMethods, classNode); } else { // otherwise, use the parent list. for (ClassNode parentNode : parentNodes) { addAllNewMethods(uniqueMethods, parentNode); } } new StringSwitch() { @Override void visitString() { mv.visitVarInsn(Opcodes.ALOAD, 1); } @Override void visitCase(String methodName) { MethodReference methodRef = uniqueMethods.get(methodName); mv.visitVarInsn(Opcodes.ALOAD, 0); Type[] args = Type.getArgumentTypes(methodRef.method.desc); int argc = 0; for (Type t : args) { mv.visitVarInsn(Opcodes.ALOAD, 2); mv.push(argc); mv.visitInsn(Opcodes.AALOAD); ByteCodeUtils.unbox(mv, t); argc++; } if (TRACING_ENABLED) { trace(mv, "super selected ", methodRef.owner.name, methodRef.method.name, methodRef.method.desc); } // Call super on the other object, yup this works cos we are on the right place to // call from. mv.visitMethodInsn(Opcodes.INVOKESPECIAL, methodRef.owner.name, methodRef.method.name, methodRef.method.desc, false); Type ret = Type.getReturnType(methodRef.method.desc); if (ret.getSort() == Type.VOID) { mv.visitInsn(Opcodes.ACONST_NULL); } else { mv.box(ret); } mv.visitInsn(Opcodes.ARETURN); } @Override void visitDefault() { writeMissingMessageWithHash(mv, visitedClassName); } }.visit(mv, uniqueMethods.keySet()); mv.visitMaxs(0, 0); mv.visitEnd(); } /*** * Inserts a trampoline to this class so that the updated methods can make calls to * constructors. * <p> * <p/> * Pseudo code for this trampoline: * <code> * ClassName(Object[] args, Marker unused) { * String name = (String) args[0]; * if (name.equals( * "java/lang/ClassName.(Ljava/lang/String;Ljava/lang/Object;)Ljava/lang/Object;")) { * this((String)arg[1], arg[2]); * return * } * if (name.equals("SuperClassName.(Ljava/lang/String;I)V")) { * super((String)arg[1], (int)arg[2]); * return; * } * ... * StringBuilder $local1 = new StringBuilder(); * $local1.append("Method not found "); * $local1.append(name); * $local1.append(" in " $classType $super implementation"); * throw new $package/InstantReloadException($local1.toString()); * } * </code> */ private void createDispatchingThis() { // Gather all methods from itself and its superclasses to generate a giant constructor // implementation. // This will work fine as long as we don't support adding constructors to classes. final Map<String, MethodNode> uniqueMethods = new HashMap<String, MethodNode>(); addAllNewConstructors(uniqueMethods, classNode, true /*keepPrivateConstructors*/); for (ClassNode parentNode : parentNodes) { addAllNewConstructors(uniqueMethods, parentNode, false /*keepPrivateConstructors*/); } int access = Opcodes.ACC_PUBLIC | Opcodes.ACC_SYNTHETIC; Method m = new Method(AsmUtils.CONSTRUCTOR, ConstructorArgsRedirection.DISPATCHING_THIS_SIGNATURE); MethodVisitor visitor = super.visitMethod(0, m.getName(), m.getDescriptor(), null, null); final GeneratorAdapter mv = new GeneratorAdapter(access, m, visitor); mv.visitCode(); // Mark this code as redirection code Label label = new Label(); mv.visitLineNumber(0, label); // Get and store the constructor canonical name. mv.visitVarInsn(Opcodes.ALOAD, 1); mv.push(0); mv.visitInsn(Opcodes.AALOAD); mv.unbox(Type.getType("Ljava/lang/String;")); final int constructorCanonicalName = mv.newLocal(Type.getType("Ljava/lang/String;")); mv.storeLocal(constructorCanonicalName); new StringSwitch() { @Override void visitString() { mv.loadLocal(constructorCanonicalName); } @Override void visitCase(String canonicalName) { MethodNode methodNode = uniqueMethods.get(canonicalName); String owner = canonicalName.split("\\.")[0]; // Parse method arguments and mv.visitVarInsn(Opcodes.ALOAD, 0); Type[] args = Type.getArgumentTypes(methodNode.desc); int argc = 0; for (Type t : args) { mv.visitVarInsn(Opcodes.ALOAD, 1); mv.push(argc + 1); mv.visitInsn(Opcodes.AALOAD); ByteCodeUtils.unbox(mv, t); argc++; } mv.visitMethodInsn(Opcodes.INVOKESPECIAL, owner, AsmUtils.CONSTRUCTOR, methodNode.desc, false); mv.visitInsn(Opcodes.RETURN); } @Override void visitDefault() { writeMissingMessageWithHash(mv, visitedClassName); } }.visit(mv, uniqueMethods.keySet()); mv.visitMaxs(1, 3); mv.visitEnd(); } @Override public void visitEnd() { createAccessSuper(); createDispatchingThis(); super.visitEnd(); } /** * Add all unseen methods from the passed ClassNode's methods. {@see ClassNode#methods} * * @param methods the methods already encountered in the ClassNode hierarchy * @param classNode the class to save all new methods from. */ private static void addAllNewMethods(Map<String, MethodReference> methods, ClassNode classNode) { //noinspection unchecked for (MethodNode method : (List<MethodNode>) classNode.methods) { if (method.name.equals(AsmUtils.CONSTRUCTOR) || method.name.equals("<clinit>")) { continue; } String name = method.name + "." + method.desc; if (isAccessCompatibleWithInstantRun(method.access) && !methods.containsKey(name) && (method.access & Opcodes.ACC_STATIC) == 0 && (method.access & Opcodes.ACC_PRIVATE) == 0) { methods.put(name, new MethodReference(method, classNode)); } } } /** * Add all constructors from the passed ClassNode's methods. {@see ClassNode#methods} * * @param methods the constructors already encountered in the ClassNode hierarchy * @param classNode the class to save all new methods from. * @param keepPrivateConstructors whether to keep the private constructors. */ private void addAllNewConstructors(Map<String, MethodNode> methods, ClassNode classNode, boolean keepPrivateConstructors) { //noinspection unchecked for (MethodNode method : (List<MethodNode>) classNode.methods) { if (!method.name.equals(AsmUtils.CONSTRUCTOR)) { continue; } if (!isAccessCompatibleWithInstantRun(method.access)) { continue; } if (!keepPrivateConstructors && (method.access & Opcodes.ACC_PRIVATE) != 0) { continue; } if (!classNode.name.equals(visitedClassName) && !classNode.name.equals(visitedSuperName)) { continue; } String key = classNode.name + "." + method.desc; if (methods.containsKey(key)) { continue; } methods.put(key, method); } } /** * Command line invocation entry point. Expects 2 parameters, first is the source directory * with .class files as produced by the Java compiler, second is the output directory where to * store the bytecode enhanced version. * * @param args the command line arguments. * @throws IOException if some files cannot be read or written. */ public static void mainMe(String args1, String arg2, String arg3) throws IOException { IncrementalVisitor.main(new String[] { args1, arg2, arg3 }, VISITOR_BUILDER); } }