/* * This file is part of Mixin, licensed under the MIT License (MIT). * * Copyright (c) SpongePowered * 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.Collections; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import org.objectweb.asm.Opcodes; import org.objectweb.asm.Type; import org.objectweb.asm.tree.AbstractInsnNode; import org.objectweb.asm.tree.ClassNode; import org.objectweb.asm.tree.FrameNode; import org.objectweb.asm.tree.InsnList; import org.objectweb.asm.tree.LabelNode; import org.objectweb.asm.tree.LineNumberNode; import org.objectweb.asm.tree.LocalVariableNode; import org.objectweb.asm.tree.MethodNode; import org.objectweb.asm.tree.VarInsnNode; import org.objectweb.asm.tree.analysis.Analyzer; import org.objectweb.asm.tree.analysis.AnalyzerException; import org.objectweb.asm.tree.analysis.BasicValue; import org.objectweb.asm.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.util.asm.ASM; import org.spongepowered.asm.util.asm.MixinVerifier; import org.spongepowered.asm.util.throwables.LVTGeneratorError; /** * Utility methods for working with local variables using ASM */ public final class Locals { /** * A local variable entry added by mixin itself, eg. by an injector */ public static class SyntheticLocalVariableNode extends LocalVariableNode { public SyntheticLocalVariableNode(String name, String descriptor, String signature, LabelNode start, LabelNode end, int index) { super(name, descriptor, signature, start, end, index); } } /** * A local variable entry which is "dead" (has been removed from the known * frame) but is being retained in the computed frame as a ghost so that we * can decide whether we want to include it in the final frame or not. * *

Zombie nodes are currently only persisted for 1 instruction, so in the * cases where there is a frame node followed directly by the candidate insn * the zombies will be returned as valid results, but otherwise culled.

*/ static class ZombieLocalVariableNode extends LocalVariableNode { static final char CHOP = 'C'; static final char TRIM = 'X'; /** * Progenitor of this zombie, to allow "resurrection" (am I stretching * this metaphor too far?) */ final LocalVariableNode ancestor; final char type; /** * Number of instructions the zombie has "lived" for, incremented by the * state machine loop */ int lifetime; /** * Number of frames which have elapsed for this zombie, incremented by * the state machine loop when a frame node is encountered */ int frames; ZombieLocalVariableNode(LocalVariableNode ancestor, char type) { super(ancestor.name, ancestor.desc, ancestor.signature, ancestor.start, ancestor.end, ancestor.index); this.ancestor = ancestor; this.type = type; } boolean checkResurrect(Settings settings) { int insnThreshold = this.type == ZombieLocalVariableNode.CHOP ? settings.choppedInsnThreshold : settings.trimmedInsnThreshold; if (insnThreshold > -1 && this.lifetime > insnThreshold) { return false; } int frameThreshold = this.type == ZombieLocalVariableNode.CHOP ? settings.choppedFrameThreshold : settings.trimmedFrameThreshold; return frameThreshold == -1 || this.frames <= frameThreshold; } static ZombieLocalVariableNode of(LocalVariableNode ancestor, char type) { if (ancestor instanceof ZombieLocalVariableNode) { return (ZombieLocalVariableNode)ancestor; } return ancestor != null ? new ZombieLocalVariableNode(ancestor, type) : null; } @Override public String toString() { return String.format("Z(%s,%-2d)", this.type, this.lifetime); } } /** * Settings for getLocalsAt containing the tunable options for the * algorithm. This exists for two purposes: Firstly, wrapping tunables up in * a single object for convenience, but secondly providing some level of * forward compatibility for platforms that which to provide backward * compatibility to their own consumers. The {@link #flagsCustom} * field is* provided as a way of encoding arbitrary options that downstream * projects may wish to use to tune their own implementations. The {@link * #flags} field is reserved for mixin internal flags to be added at a later * date. */ public static class Settings { /** * When an incoming frame contains TOP entries, these are nearly always * bogus. If we previously knew the local in that slot, resurrect it. * Only resurrects TRIM zombies. */ public static int RESURRECT_FOR_BOGUS_TOP = 0x01; /** * When a LOAD grows the frame, resurrect any zombies in the exposed * portion of the frame, based on the thresholds configured. */ public static int RESURRECT_EXPOSED_ON_LOAD = 0x02; /** * When a STORE grows the frame, resurrect any zombies in the exposed * portion of the frame, based on the thresholds configured. */ public static int RESURRECT_EXPOSED_ON_STORE = 0x04; /** * Default flags */ public static int DEFAULT_FLAGS = Settings.RESURRECT_FOR_BOGUS_TOP | Settings.RESURRECT_EXPOSED_ON_LOAD | Settings.RESURRECT_EXPOSED_ON_STORE; /** * Default settings. CHOP zombies can be resurrected for 1 frame, TRIM * zombies can be resurrected forever */ public static Settings DEFAULT = new Settings(Settings.DEFAULT_FLAGS, 0, -1, 1, -1, -1); /** * Reserved flags for Mixin */ final int flags; /** * Platform-specific flags */ final int flagsCustom; /** * Number of instructions that a CHOPped local is eligible for * resurrection, -1 to ignore, 0 for none */ final int choppedInsnThreshold; /** * Number of frames that a CHOPped local is eligible for resurrection, * -1 to ignore, 0 for none */ final int choppedFrameThreshold; /** * Number of instructions that a TRIMmed local is eligible for * resurrection, -1 to ignore, 0 for none */ final int trimmedInsnThreshold; /** * Number of frames that a TRIMmed local is eligible for resurrection, * -1 to ignore, 0 for none */ final int trimmedFrameThreshold; /** * @param flags Mixin flags * @param flagsCustom Platform-specific flags * @param insnThreshold Number of instructions that a local (regardless * of death reason) is eligible for resurrection, -1 to ignore, * 0 for none * @param frameThreshold Number of frames that a local (regardless of * death reason) is eligible for resurrection,-1 to ignore, 0 for * none */ public Settings(int flags, int flagsCustom, int insnThreshold, int frameThreshold) { this(flags, flagsCustom, insnThreshold, frameThreshold, insnThreshold, frameThreshold); } /** * @param flags Mixin flags * @param flagsCustom Platform-specific flags * @param choppedInsnThreshold Number of instructions that a CHOPped * local is eligible for resurrection, -1 to ignore, 0 for none * @param choppedFrameThreshold Number of frames that a CHOPped local is * eligible for resurrection,-1 to ignore, 0 for none * @param trimmedInsnThreshold Number of instructions that a TRIMmed# * local is eligible for resurrection, -1 to ignore, 0 for none * @param trimmedFrameThreshold Number of frames that a TRIMmed local is * eligible for resurrection, -1 to ignore, 0 for none */ public Settings(int flags, int flagsCustom, int choppedInsnThreshold, int choppedFrameThreshold, int trimmedInsnThreshold, int trimmedFrameThreshold) { this.flags = flags; this.flagsCustom = flagsCustom; this.choppedInsnThreshold = choppedInsnThreshold; this.choppedFrameThreshold = choppedFrameThreshold; this.trimmedInsnThreshold = trimmedInsnThreshold; this.trimmedFrameThreshold = trimmedFrameThreshold; } boolean hasFlags(int flags) { return (this.flags & flags) == flags; } boolean hasCustomFlags(int flagsCustom) { return (this.flagsCustom & flagsCustom) == flagsCustom; } } /** * Frame type names just for the purposes of debug printing */ private static final String[] FRAME_TYPES = { "TOP", "INTEGER", "FLOAT", "DOUBLE", "LONG", "NULL", "UNINITIALIZED_THIS" }; /** * Cached local variable lists, to avoid having to recalculate them * (expensive) if multiple injectors are working with the same method */ private static final Map> calculatedLocalVariables = new HashMap>(); 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--; } } } /** *

Attempts to identify available locals at an arbitrary point in the * bytecode specified by node.

* *

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:

*
    *
  • Any available stack map frames
  • *
  • STORE opcodes
  • *
  • The local variable table
  • *
* *

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.

* *

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.

* * @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 * @param fabricCompatibility Fabric compatibility level * @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, int fabricCompatibility) { if (fabricCompatibility >= org.spongepowered.asm.mixin.FabricUtil.COMPATIBILITY_0_10_0) { return Locals.getLocalsAt(classNode, method, node, Settings.DEFAULT); } else { return getLocalsAt092(classNode, method, node); } } /** *

Attempts to identify available locals at an arbitrary point in the * bytecode specified by node.

* *

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:

*
    *
  • Any available stack map frames
  • *
  • STORE opcodes
  • *
  • The local variable table
  • *
* *

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.

* *

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.

* * @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 * @param settings Tunable settings for the state machine * @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, Settings settings) { for (int i = 0; i < 3 && (node instanceof LabelNode || node instanceof LineNumberNode); i++) { AbstractInsnNode nextNode = Locals.nextNode(method.instructions, node); if (nextNode instanceof FrameNode) { // Do not ffwd over frames break; } node = nextNode; } ClassInfo classInfo = ClassInfo.forName(classNode.name); if (classInfo == null) { throw new LVTGeneratorError("Could not load class metadata for " + classNode.name + " generating LVT for " + method.name); } Method methodInfo = classInfo.findMethod(method, method.access | ClassInfo.INCLUDE_INITIALISERS); if (methodInfo == null) { throw new LVTGeneratorError("Could not locate method metadata for " + method.name + " generating LVT in " + classNode.name); } List 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", Type.getObjectType(classNode.name).toString(), 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(); } final int initialFrameSize = local; int frameSize = local; int frameIndex = -1; int lastFrameSize = local; int knownFrameSize = local; VarInsnNode storeInsn = null; for (Iterator iter = method.instructions.iterator(); iter.hasNext();) { AbstractInsnNode insn = iter.next(); // Tick the zombies for (int l = 0; l < frame.length; l++) { if (frame[l] instanceof ZombieLocalVariableNode) { ZombieLocalVariableNode zombie = (ZombieLocalVariableNode)frame[l]; zombie.lifetime++; if (insn instanceof FrameNode) { zombie.frames++; } } } if (storeInsn != null) { LocalVariableNode storedLocal = Locals.getLocalVariableAt(classNode, method, insn, storeInsn.var); frame[storeInsn.var] = storedLocal; knownFrameSize = Math.max(knownFrameSize, storeInsn.var + 1); if (storedLocal != null && storeInsn.var < method.maxLocals - 1 && storedLocal.desc != null && Type.getType(storedLocal.desc).getSize() == 2) { frame[storeInsn.var + 1] = null; // TOP knownFrameSize = Math.max(knownFrameSize, storeInsn.var + 2); if (settings.hasFlags(Settings.RESURRECT_EXPOSED_ON_STORE)) { Locals.resurrect(frame, knownFrameSize, settings); } } storeInsn = null; } handleFrame: if (insn instanceof FrameNode) { frameIndex++; FrameNode frameNode = (FrameNode)insn; if (frameNode.type == Opcodes.F_SAME || frameNode.type == Opcodes.F_SAME1) { break handleFrame; } int frameNodeSize = Locals.computeFrameSize(frameNode, initialFrameSize); FrameData frameData = frameIndex < frames.size() ? frames.get(frameIndex) : null; if (frameData != null) { if (frameData.type == Opcodes.F_FULL) { knownFrameSize = lastFrameSize = frameSize = Math.max(initialFrameSize, Math.min(frameNodeSize, frameData.size)); } else { frameSize = Locals.getAdjustedFrameSize(frameSize, frameData, initialFrameSize); } } else { frameSize = Locals.getAdjustedFrameSize(frameSize, frameNode, initialFrameSize); } // Sanity check if (frameSize < initialFrameSize) { throw new IllegalStateException(String.format("Locals entered an invalid state evaluating %s::%s%s at instruction %d (%s). " + "Initial frame size is %d, calculated a frame size of %d with %s", classNode.name, method.name, method.desc, method.instructions.indexOf(insn), Bytecode.describeNode(insn, false), initialFrameSize, frameSize, frameData)); } if ((frameData == null && (frameNode.type == Opcodes.F_CHOP || frameNode.type == Opcodes.F_NEW)) || (frameData != null && frameData.type == Opcodes.F_CHOP)) { for (int framePos = frameSize; framePos < frame.length; framePos++) { frame[framePos] = ZombieLocalVariableNode.of(frame[framePos], ZombieLocalVariableNode.CHOP); } knownFrameSize = lastFrameSize = frameSize; break handleFrame; } int framePos = frameNode.type == Opcodes.F_APPEND ? lastFrameSize : 0; lastFrameSize = frameSize; // localPos tracks the location in the frame node's locals list, which doesn't leave space for TOP entries for (int localPos = 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, insn, 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) { // Explicit TOP entries are pretty much always bogus, but depending on our resurrection // strategy we may want to resurrect eligible zombies here. Real TOP entries are handled below if (frame[framePos] instanceof ZombieLocalVariableNode && settings.hasFlags(Settings.RESURRECT_FOR_BOGUS_TOP)) { ZombieLocalVariableNode zombie = (ZombieLocalVariableNode)frame[framePos]; if (zombie.type == ZombieLocalVariableNode.TRIM) { frame[framePos] = zombie.ancestor; } } } else if (isMarkerType) { frame[framePos] = null; } else if (is32bitValue || is64bitValue) { frame[framePos] = Locals.getLocalVariableAt(classNode, method, insn, framePos); if (is64bitValue) { framePos++; frame[framePos] = null; // TOP } } else { throw new LVTGeneratorError("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 >= frameSize && frameSize > 0) { if (framePos < knownFrameSize) { frame[framePos] = Locals.getLocalVariableAt(classNode, method, insn, framePos); } else { frame[framePos] = ZombieLocalVariableNode.of(frame[framePos], ZombieLocalVariableNode.TRIM); } } } else if (localType instanceof LabelNode) { // Uninitialised } else { throw new LVTGeneratorError("Invalid value " + localType + " in locals array at position " + localPos + " in " + classNode.name + "." + method.name + method.desc); } } } else if (insn instanceof VarInsnNode) { VarInsnNode varInsn = (VarInsnNode)insn; boolean isLoad = insn.getOpcode() >= Opcodes.ILOAD && insn.getOpcode() <= Opcodes.SALOAD; if (isLoad) { LocalVariableNode toLoad = Locals.getLocalVariableAt(classNode, method, insn, varInsn.var); frame[varInsn.var] = toLoad; int varSize = toLoad != null && toLoad.desc != null ? Type.getType(frame[varInsn.var].desc).getSize() : 1; knownFrameSize = Math.max(knownFrameSize, varInsn.var + varSize); if (settings.hasFlags(Settings.RESURRECT_EXPOSED_ON_LOAD)) { Locals.resurrect(frame, knownFrameSize, settings); } } else { // Update the LVT for the opcode AFTER this one, since we always want to know // the frame state BEFORE the *current* instruction to match the contract of // injection points storeInsn = varInsn; } } if (insn == node) { break; } } // Null out any "unknown" or mixin-provided locals for (int l = 0; l < frame.length; l++) { if (frame[l] instanceof ZombieLocalVariableNode) { ZombieLocalVariableNode zombie = (ZombieLocalVariableNode)frame[l]; // preserve zombies where the frame node which culled them was immediately prior to // the matched instruction, or *was itself* the matched instruction, the returned // frame will contain the original node (the zombie ancestor) frame[l] = (zombie.lifetime > 1) ? null : zombie.ancestor; } if ((frame[l] != null && frame[l].desc == null) || frame[l] instanceof SyntheticLocalVariableNode) { frame[l] = null; } } return frame; } private static LocalVariableNode[] getLocalsAt092(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 LVTGeneratorError("Could not load class metadata for " + classNode.name + " generating LVT for " + method.name); } Method methodInfo = classInfo.findMethod(method, method.access | ClassInfo.INCLUDE_INITIALISERS); if (methodInfo == null) { throw new LVTGeneratorError("Could not locate method metadata for " + method.name + " generating LVT in " + classNode.name); } List 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", Type.getObjectType(classNode.name).toString(), 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 frameSize = local; int frameIndex = -1; int lastFrameSize = local; VarInsnNode storeInsn = null; for (Iterator iter = method.instructions.iterator(); iter.hasNext();) { AbstractInsnNode insn = iter.next(); if (storeInsn != null) { frame[storeInsn.var] = Locals.getLocalVariableAt(classNode, method, insn, storeInsn.var); storeInsn = null; } handleFrame: if (insn instanceof FrameNode) { frameIndex++; FrameNode frameNode = (FrameNode)insn; if (frameNode.type == Opcodes.F_SAME || frameNode.type == Opcodes.F_SAME1) { break handleFrame; } FrameData frameData = frameIndex < frames.size() ? frames.get(frameIndex) : null; if (frameData != null) { if (frameData.type == Opcodes.F_FULL) { frameSize = Math.min(frameSize, frameData.locals); lastFrameSize = frameSize; } else { frameSize = Locals.getAdjustedFrameSize(frameSize, frameData.type, frameData.rawSize, 0); // Fabric: initialSize 0, also applies to frameData through rawSize instead of size } } else { frameSize = Locals.getAdjustedFrameSize(frameSize, frameNode, 0); // Fabric: initialSize 0 } if (frameNode.type == Opcodes.F_CHOP) { for (int framePos = frameSize; framePos < frame.length; framePos++) { frame[framePos] = null; } lastFrameSize = frameSize; break handleFrame; } int framePos = frameNode.type == Opcodes.F_APPEND ? lastFrameSize : 0; lastFrameSize = frameSize; // localPos tracks the location in the frame node's locals list, which doesn't leave space for TOP entries for (int localPos = 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, insn, 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, insn, framePos); if (is64bitValue) { framePos++; frame[framePos] = null; // TOP } } else { throw new LVTGeneratorError("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 >= frameSize && frameSize > 0) { frame[framePos] = null; } } else if (localType instanceof LabelNode) { // Uninitialised } else { throw new LVTGeneratorError("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; boolean isLoad = insn.getOpcode() >= Opcodes.ILOAD && insn.getOpcode() <= Opcodes.SALOAD; if (isLoad) { frame[varNode.var] = Locals.getLocalVariableAt(classNode, method, insn, varNode.var); } else { // Update the LVT for the opcode AFTER this one, since we always want to know // the frame state BEFORE the *current* instruction to match the contract of // injection points storeInsn = varNode; } } 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; } /** * Walks the supplied frame up to the specified knownFrameSize * and resurrects any zombies that meet the required criteria * * @param frame Frame to walk * @param knownFrameSize Known frame size in which to resurrect * @param settings Resurrection settings */ private static void resurrect(LocalVariableNode[] frame, int knownFrameSize, Settings settings) { for (int l = 0; l < knownFrameSize && l < frame.length; l++) { if (frame[l] instanceof ZombieLocalVariableNode) { ZombieLocalVariableNode zombie = (ZombieLocalVariableNode)frame[l]; if (zombie.checkResurrect(settings)) { frame[l] = zombie.ancestor; } } } } /** * 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 getLocalVariableTable(ClassNode classNode, MethodNode method) { if (method.localVariables.isEmpty()) { return Locals.getGeneratedLocalVariableTable(classNode, method); } return Collections.unmodifiableList(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 getGeneratedLocalVariableTable(ClassNode classNode, MethodNode method) { String methodId = String.format("%s.%s%s", classNode.name, method.name, method.desc); List localVars = Locals.calculatedLocalVariables.get(methodId); if (localVars != null) { return localVars; } localVars = Locals.generateLocalVariableTable(classNode, method); Locals.calculatedLocalVariables.put(methodId, localVars); return Collections.unmodifiableList(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 generateLocalVariableTable(ClassNode classNode, MethodNode method) { List interfaces = null; if (classNode.interfaces != null) { interfaces = new ArrayList(); 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 analyzer = new Analyzer( new MixinVerifier(ASM.API_VERSION, Type.getObjectType(classNode.name), objectType, interfaces, false)); try { analyzer.analyze(classNode.name, method); } catch (AnalyzerException ex) { ex.printStackTrace(); } // Get frames from the Analyzer Frame[] frames = analyzer.getFrames(); // Record the original size of hte method int methodSize = method.instructions.size(); // List of LocalVariableNodes to return List localVariables = new ArrayList(); 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 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 = lastKnownType[j]; Type localType = local.getType(); if (localType != null) { desc = localType.getSort() >= Type.ARRAY && "null".equals(localType.getInternalName()) ? Constants.OBJECT_DESC : localType.getDescriptor(); } 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; } /** * Compute a new frame size based on the supplied frame type and the size of * locals contained in the frame (this may differ from the number of actual * frame slots if the frame contains doubles or longs) * * @param currentSize current frame size * @param frameNode frame entry * @param initialFrameSize Method initial frame size * @return new frame size */ private static int getAdjustedFrameSize(int currentSize, FrameNode frameNode, int initialFrameSize) { return Locals.getAdjustedFrameSize(currentSize, frameNode.type, Locals.computeFrameSize(frameNode, initialFrameSize), initialFrameSize); } /** * Compute a new frame size based on the supplied frame type and the size of * locals contained in the frame (this may differ from the number of actual * frame slots if the frame contains doubles or longs) * * @param currentSize current frame size * @param frameData frame entry * @param initialFrameSize Method initial frame size * @return new frame size */ private static int getAdjustedFrameSize(int currentSize, FrameData frameData, int initialFrameSize) { return Locals.getAdjustedFrameSize(currentSize, frameData.type, frameData.size, initialFrameSize); } /** * Compute a new frame size based on the supplied frame type and the size of * locals contained in the frame (this may differ from the number of actual * frame slots if the frame contains doubles or longs) * * @param currentSize current frame size * @param type frame entry type * @param size frame entry size * @param initialFrameSize Method initial frame size * @return new frame size */ private static int getAdjustedFrameSize(int currentSize, int type, int size, int initialFrameSize) { switch (type) { case Opcodes.F_NEW: case Opcodes.F_FULL: return Math.max(initialFrameSize, size); case Opcodes.F_APPEND: return currentSize + size; case Opcodes.F_CHOP: return Math.max(initialFrameSize, currentSize - size); case Opcodes.F_SAME: case Opcodes.F_SAME1: return currentSize; default: return currentSize; } } /** * Compute the size required to accomodate the entries described by the * supplied frame node * * @param frameNode frame node with locals to compute * @param initialFrameSize Method initial frame size * @return size of frame node locals */ public static int computeFrameSize(FrameNode frameNode, int initialFrameSize) { if (frameNode.local == null) { return initialFrameSize; } int size = 0; for (Object local : frameNode.local) { if (local instanceof Integer) { size += (local == Opcodes.DOUBLE || local == Opcodes.LONG) ? 2 : 1; } else { size++; } } return Math.max(initialFrameSize, size); } /** * Debug function to return printable name of a frame entry * * @param frameEntry Frame entry * @return string representation of the supplied frame entry */ public static String getFrameTypeName(Object frameEntry) { if (frameEntry == null) { return "NULL"; } if (frameEntry instanceof String) { return Bytecode.getSimpleName(frameEntry.toString()); } if (frameEntry instanceof Integer) { int type = ((Integer)frameEntry).intValue(); if (type >= Locals.FRAME_TYPES.length) { return "INVALID"; } return Locals.FRAME_TYPES[type]; } return "?"; } }