/* * This file is part of Mixin, licensed under the MIT License (MIT). * * Copyright (c) SpongePowered <https://www.spongepowered.org> * Copyright (c) contributors * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.spongepowered.asm.util; import java.util.ArrayList; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import org.spongepowered.asm.lib.Opcodes; import org.spongepowered.asm.lib.Type; import org.spongepowered.asm.lib.tree.AbstractInsnNode; import org.spongepowered.asm.lib.tree.ClassNode; import org.spongepowered.asm.lib.tree.FrameNode; import org.spongepowered.asm.lib.tree.InsnList; import org.spongepowered.asm.lib.tree.LabelNode; import org.spongepowered.asm.lib.tree.LineNumberNode; import org.spongepowered.asm.lib.tree.LocalVariableNode; import org.spongepowered.asm.lib.tree.MethodNode; import org.spongepowered.asm.lib.tree.VarInsnNode; import org.spongepowered.asm.lib.tree.analysis.Analyzer; import org.spongepowered.asm.lib.tree.analysis.AnalyzerException; import org.spongepowered.asm.lib.tree.analysis.BasicValue; import org.spongepowered.asm.lib.tree.analysis.Frame; import org.spongepowered.asm.mixin.transformer.ClassInfo; import org.spongepowered.asm.mixin.transformer.ClassInfo.FrameData; import org.spongepowered.asm.mixin.transformer.ClassInfo.Method; import org.spongepowered.asm.mixin.transformer.verify.MixinVerifier; import org.spongepowered.asm.util.throwables.LVTGeneratorException; /** * Utility methods for working with local variables using ASM */ public final class Locals { /** * Cached local variable lists, to avoid having to recalculate them * (expensive) if multiple injectors are working with the same method */ private static final Map<String, List<LocalVariableNode>> calculatedLocalVariables = new HashMap<String, List<LocalVariableNode>>(); private Locals() { // utility class } /** * Injects appropriate LOAD opcodes into the supplied InsnList for each * entry in the supplied locals array starting at pos * * @param locals Local types (can contain nulls for uninitialised, TOP, or * RETURN values in locals) * @param insns Instruction List to inject into * @param pos Start position * @param limit maximum number of locals to consume */ public static void loadLocals(Type[] locals, InsnList insns, int pos, int limit) { for (; pos < locals.length && limit > 0; pos++) { if (locals[pos] != null) { insns.add(new VarInsnNode(locals[pos].getOpcode(Opcodes.ILOAD), pos)); limit--; } } } /** * <p>Attempts to identify available locals at an arbitrary point in the * bytecode specified by node.</p> * * <p>This method builds an approximate view of the locals available at an * arbitrary point in the bytecode by examining the following features in * the bytecode:</p> * <ul> * <li>Any available stack map frames</li> * <li>STORE opcodes</li> * <li>The local variable table</li> * </ul> * * <p>Inference proceeds by walking the bytecode from the start of the * method looking for stack frames and STORE opcodes. When either of these * is encountered, an attempt is made to cross-reference the values in the * stack map or STORE opcode with the value in the local variable table * which covers the code range. Stack map frames overwrite the entire * simulated local variable table with their own value types, STORE opcodes * overwrite only the local slot to which they pertain. Values in the * simulated locals array are spaced according to their size (unlike the * representation in FrameNode) and this TOP, NULL and UNINTITIALIZED_THIS * opcodes will be represented as null values in the simulated frame.</p> * * <p>This code does not currently simulate the prescribed JVM behaviour * where overwriting the second slot of a DOUBLE or LONG actually * invalidates the DOUBLE or LONG stored in the previous location, so we * have to hope (for now) that this behaviour isn't emitted by the compiler * or any upstream transformers. I may have to re-think this strategy if * this situation is encountered in the wild.</p> * * @param classNode ClassNode containing the method, used to initialise the * implicit "this" reference in simple methods with no stack frames * @param method MethodNode to explore * @param node Node indicating the position at which to determine the locals * state. The locals will be enumerated UP TO the specified node, so * bear in mind that if the specified node is itself a STORE opcode, * then we will be looking at the state of the locals PRIOR to its * invocation * @return A sparse array containing a view (hopefully) of the locals at the * specified location */ public static LocalVariableNode[] getLocalsAt(ClassNode classNode, MethodNode method, AbstractInsnNode node) { for (int i = 0; i < 3 && (node instanceof LabelNode || node instanceof LineNumberNode); i++) { node = Locals.nextNode(method.instructions, node); } ClassInfo classInfo = ClassInfo.forName(classNode.name); if (classInfo == null) { throw new LVTGeneratorException("Could not load class metadata for " + classNode.name + " generating LVT for " + method.name); } Method methodInfo = classInfo.findMethod(method); if (methodInfo == null) { throw new LVTGeneratorException("Could not locate method metadata for " + method.name + " generating LVT in " + classNode.name); } List<FrameData> frames = methodInfo.getFrames(); LocalVariableNode[] frame = new LocalVariableNode[method.maxLocals]; int local = 0, index = 0; // Initialise implicit "this" reference in non-static methods if ((method.access & Opcodes.ACC_STATIC) == 0) { frame[local++] = new LocalVariableNode("this", classNode.name, null, null, null, 0); } // Initialise method arguments for (Type argType : Type.getArgumentTypes(method.desc)) { frame[local] = new LocalVariableNode("arg" + index++, argType.toString(), null, null, null, local); local += argType.getSize(); } int initialFrameSize = local; int frameIndex = -1, locals = 0; for (Iterator<AbstractInsnNode> iter = method.instructions.iterator(); iter.hasNext();) { AbstractInsnNode insn = iter.next(); if (insn instanceof FrameNode) { frameIndex++; FrameNode frameNode = (FrameNode)insn; FrameData frameData = frameIndex < frames.size() ? frames.get(frameIndex) : null; locals = frameData != null && frameData.type == Opcodes.F_FULL ? Math.max(locals, frameNode.local.size()) : frameNode.local.size(); // localPos tracks the location in the frame node's locals list, which doesn't leave space for TOP entries for (int localPos = 0, framePos = 0; framePos < frame.length; framePos++, localPos++) { // Get the local at the current position in the FrameNode's locals list final Object localType = (localPos < frameNode.local.size()) ? frameNode.local.get(localPos) : null; if (localType instanceof String) { // String refers to a reference type frame[framePos] = Locals.getLocalVariableAt(classNode, method, node, framePos); } else if (localType instanceof Integer) { // Integer refers to a primitive type or other marker boolean isMarkerType = localType == Opcodes.UNINITIALIZED_THIS || localType == Opcodes.NULL; boolean is32bitValue = localType == Opcodes.INTEGER || localType == Opcodes.FLOAT; boolean is64bitValue = localType == Opcodes.DOUBLE || localType == Opcodes.LONG; if (localType == Opcodes.TOP) { // Do nothing, explicit TOP entries are pretty much always bogus, and real ones are handled below } else if (isMarkerType) { frame[framePos] = null; } else if (is32bitValue || is64bitValue) { frame[framePos] = Locals.getLocalVariableAt(classNode, method, node, framePos); if (is64bitValue) { framePos++; frame[framePos] = null; // TOP } } else { throw new LVTGeneratorException("Unrecognised locals opcode " + localType + " in locals array at position " + localPos + " in " + classNode.name + "." + method.name + method.desc); } } else if (localType == null) { if (framePos >= initialFrameSize && framePos >= locals && locals > 0) { frame[framePos] = null; } } else { throw new LVTGeneratorException("Invalid value " + localType + " in locals array at position " + localPos + " in " + classNode.name + "." + method.name + method.desc); } } } else if (insn instanceof VarInsnNode) { VarInsnNode varNode = (VarInsnNode) insn; frame[varNode.var] = Locals.getLocalVariableAt(classNode, method, node, varNode.var); } else if (insn == node) { break; } } // Null out any "unknown" locals for (int l = 0; l < frame.length; l++) { if (frame[l] != null && frame[l].desc == null) { frame[l] = null; } } return frame; } /** * Attempts to locate the appropriate entry in the local variable table for * the specified local variable index at the location specified by node. * * @param classNode Containing class * @param method Method * @param node Instruction defining the location to get the local variable * table at * @param var Local variable index * @return a LocalVariableNode containing information about the local * variable at the specified location in the specified local slot */ public static LocalVariableNode getLocalVariableAt(ClassNode classNode, MethodNode method, AbstractInsnNode node, int var) { return Locals.getLocalVariableAt(classNode, method, method.instructions.indexOf(node), var); } /** * Attempts to locate the appropriate entry in the local variable table for * the specified local variable index at the location specified by pos. * * @param classNode Containing class * @param method Method * @param var Local variable index * @param pos The opcode index to get the local variable table at * @return a LocalVariableNode containing information about the local * variable at the specified location in the specified local slot */ private static LocalVariableNode getLocalVariableAt(ClassNode classNode, MethodNode method, int pos, int var) { LocalVariableNode localVariableNode = null; LocalVariableNode fallbackNode = null; for (LocalVariableNode local : Locals.getLocalVariableTable(classNode, method)) { if (local.index != var) { continue; } if (Locals.isOpcodeInRange(method.instructions, local, pos)) { localVariableNode = local; } else if (localVariableNode == null) { fallbackNode = local; } } if (localVariableNode == null && !method.localVariables.isEmpty()) { for (LocalVariableNode local : Locals.getGeneratedLocalVariableTable(classNode, method)) { if (local.index == var && Locals.isOpcodeInRange(method.instructions, local, pos)) { localVariableNode = local; } } } return localVariableNode != null ? localVariableNode : fallbackNode; } private static boolean isOpcodeInRange(InsnList insns, LocalVariableNode local, int pos) { return insns.indexOf(local.start) < pos && insns.indexOf(local.end) > pos; } /** * Fetches or generates the local variable table for the specified method. * Since Mojang strip the local variable table as part of the obfuscation * process, we need to generate the local variable table when running * obfuscated. We cache the generated tables so that we only need to do the * relatively expensive calculation once per method we encounter. * * @param classNode Containing class * @param method Method * @return local variable table */ public static List<LocalVariableNode> getLocalVariableTable(ClassNode classNode, MethodNode method) { if (method.localVariables.isEmpty()) { return Locals.getGeneratedLocalVariableTable(classNode, method); } return method.localVariables; } /** * Gets the generated the local variable table for the specified method. * * @param classNode Containing class * @param method Method * @return generated local variable table */ public static List<LocalVariableNode> getGeneratedLocalVariableTable(ClassNode classNode, MethodNode method) { String methodId = String.format("%s.%s%s", classNode.name, method.name, method.desc); List<LocalVariableNode> localVars = Locals.calculatedLocalVariables.get(methodId); if (localVars != null) { return localVars; } localVars = Locals.generateLocalVariableTable(classNode, method); Locals.calculatedLocalVariables.put(methodId, localVars); return localVars; } /** * Use ASM Analyzer to generate the local variable table for the specified * method * * @param classNode Containing class * @param method Method * @return generated local variable table */ public static List<LocalVariableNode> generateLocalVariableTable(ClassNode classNode, MethodNode method) { List<Type> interfaces = null; if (classNode.interfaces != null) { interfaces = new ArrayList<Type>(); for (String iface : classNode.interfaces) { interfaces.add(Type.getObjectType(iface)); } } Type objectType = null; if (classNode.superName != null) { objectType = Type.getObjectType(classNode.superName); } // Use Analyzer to generate the bytecode frames Analyzer<BasicValue> analyzer = new Analyzer<BasicValue>( new MixinVerifier(Type.getObjectType(classNode.name), objectType, interfaces, false)); try { analyzer.analyze(classNode.name, method); } catch (AnalyzerException ex) { ex.printStackTrace(); } // Get frames from the Analyzer Frame<BasicValue>[] frames = analyzer.getFrames(); // Record the original size of hte method int methodSize = method.instructions.size(); // List of LocalVariableNodes to return List<LocalVariableNode> localVariables = new ArrayList<LocalVariableNode>(); LocalVariableNode[] localNodes = new LocalVariableNode[method.maxLocals]; // LocalVariableNodes for current frame BasicValue[] locals = new BasicValue[method.maxLocals]; // locals in previous frame, used to work out what changes between frames LabelNode[] labels = new LabelNode[methodSize]; // Labels to add to the method, for the markers String[] lastKnownType = new String[method.maxLocals]; // Traverse the frames and work out when locals begin and end for (int i = 0; i < methodSize; i++) { Frame<BasicValue> f = frames[i]; if (f == null) { continue; } LabelNode label = null; for (int j = 0; j < f.getLocals(); j++) { BasicValue local = f.getLocal(j); if (local == null && locals[j] == null) { continue; } if (local != null && local.equals(locals[j])) { continue; } if (label == null) { AbstractInsnNode existingLabel = method.instructions.get(i); if (existingLabel instanceof LabelNode) { label = (LabelNode) existingLabel; } else { labels[i] = label = new LabelNode(); } } if (local == null && locals[j] != null) { localVariables.add(localNodes[j]); localNodes[j].end = label; localNodes[j] = null; } else if (local != null) { if (locals[j] != null) { localVariables.add(localNodes[j]); localNodes[j].end = label; localNodes[j] = null; } String desc = (local.getType() != null) ? local.getType().getDescriptor() : lastKnownType[j]; localNodes[j] = new LocalVariableNode("var" + j, desc, null, label, null, j); if (desc != null) { lastKnownType[j] = desc; } } locals[j] = local; } } // Reached the end of the method so flush all current locals and mark the end LabelNode label = null; for (int k = 0; k < localNodes.length; k++) { if (localNodes[k] != null) { if (label == null) { label = new LabelNode(); method.instructions.add(label); } localNodes[k].end = label; localVariables.add(localNodes[k]); } } // Insert generated labels into the method body for (int n = methodSize - 1; n >= 0; n--) { if (labels[n] != null) { method.instructions.insert(method.instructions.get(n), labels[n]); } } return localVariables; } /** * Get the insn immediately following the specified insn, or return the same * insn if the insn is the last insn in the list * * @param insns Insn list to fetch from * @param insn Insn node * @return Next insn or the same insn if last in the list */ private static AbstractInsnNode nextNode(InsnList insns, AbstractInsnNode insn) { int index = insns.indexOf(insn) + 1; if (index > 0 && index < insns.size()) { return insns.get(index); } return insn; } }