/* LocalOptimizer Copyright (C) 1999 Jochen Hoenicke. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2, or (at your option) * any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; see the file COPYING. If not, write to * the Free Software Foundation, 675 Mass Ave, Cambridge, MA 02139, USA. * * $Id$ */ package jode.obfuscator.modules; import java.util.*; import jode.bytecode.*; import jode.obfuscator.*; import jode.AssertError; import jode.GlobalOptions; ///#def COLLECTIONS java.util import java.util.Iterator; import java.util.ListIterator; ///#enddef /** * This class takes some bytecode and tries to minimize the number * of locals used. It will also remove unnecessary stores. * * This class can only work on verified code. There should also be no * deadcode, since the verifier doesn't check that deadcode behaves * okay. * * This is done in two phases. First we determine which locals are * the same, and which locals have a overlapping life time. In the * second phase we will then redistribute the locals with a coloring * graph algorithm. * * The idea for the first phase is: For each read we follow the * instruction flow backward to find the corresponding writes. We can * also merge with another control flow that has a different read, in * this case we merge with that read, too. * * The tricky part is the subroutine handling. We follow the local * that is used in a ret and find the corresponding jsr target (there * must be only one, if the verifier should accept this class). While * we do this we remember in the info of the ret, which locals are * used in that subroutine. * * When we know the jsr target<->ret correlation, we promote from the * nextByAddr of every jsr the locals that are accessed by the * subroutine to the corresponding ret and the others to the jsr. Also * we will promote all reads from the jsr targets to the jsr. * * If you think this might be to complicated, keep in mind that jsr's * are not only left by the ret instructions, but also "spontanously" * (by not reading the return address again). */ public class LocalOptimizer implements Opcodes, CodeTransformer { /** * This class keeps track of which locals must be the same, which * name and type each local (if there is a local variable table) and * which other locals have an intersecting life time. */ class LocalInfo { LocalInfo shadow = null; public LocalInfo getReal() { LocalInfo real = this; while (real.shadow != null) real = real.shadow; return real; } String name; String type; Vector usingInstrs = new Vector(); Vector conflictingLocals = new Vector(); int size; int newSlot = -1; LocalInfo() { } LocalInfo(InstrInfo instr) { usingInstrs.addElement(instr); } void conflictsWith(LocalInfo l) { if (shadow != null) { getReal().conflictsWith(l); } else { l = l.getReal(); if (!conflictingLocals.contains(l)) { conflictingLocals.addElement(l); l.conflictingLocals.addElement(this); } } } void combineInto(LocalInfo l) { if (shadow != null) { getReal().combineInto(l); return; } l = l.getReal(); if (this == l) return; shadow = l; if (shadow.name == null) { shadow.name = name; shadow.type = type; } Enumeration enum = usingInstrs.elements(); while (enum.hasMoreElements()) { InstrInfo instr = (InstrInfo) enum.nextElement(); instr.local = l; l.usingInstrs.addElement(instr); } } public int getFirstAddr() { int minAddr = Integer.MAX_VALUE; Enumeration enum = usingInstrs.elements(); while (enum.hasMoreElements()) { InstrInfo info = (InstrInfo) enum.nextElement(); if (info.instr.getAddr() < minAddr) minAddr = info.instr.getAddr(); } return minAddr; } } private static class TodoQueue { public final InstrInfo LAST = new InstrInfo(); InstrInfo first = LAST; public void add(InstrInfo info) { if (info.nextTodo == null) { /* only enqueue if not already on queue */ info.nextTodo = first; first = info; } } public boolean isEmpty() { return first == LAST; } public InstrInfo remove() { if (first == LAST) throw new NoSuchElementException(); InstrInfo result = first; first = result.nextTodo; result.nextTodo = null; return result; } } /** * This class contains information for each instruction. */ static class InstrInfo { /** * The next changed InstrInfo, or null, if this instr info did * not changed. */ InstrInfo nextTodo; /** * The LocalInfo that this instruction manipulates, or null * if this is not an ret, iinc, load or store instruction. */ LocalInfo local; /** * For each slot, this contains the InstrInfo of one of the * next Instruction, that may read from that slot, without * prior writing. */ InstrInfo[] nextReads; /** * This only has a value for ret instructions. In that case * this bitset contains all locals, that may be used between * jsr and ret. */ BitSet usedBySub; /** * For each slot if get() is true, no instruction may read * this slot, since it may contain different locals, depending * on flow. */ LocalInfo[] lifeLocals; /** * If instruction is the destination of a jsr, this contains * the single allowed ret instruction info, or null if there * is no ret at all (or not yet detected). */ InstrInfo retInfo; /** * If this instruction is a ret, this contains the single * allowed jsr target to which this ret belongs. */ InstrInfo jsrTargetInfo; /** * The Instruction of this info */ Instruction instr; /** * The next info in the chain. */ InstrInfo nextInfo; } BytecodeInfo bc; TodoQueue changedInfos; InstrInfo firstInfo; Hashtable instrInfos; boolean produceLVT; int maxlocals; LocalInfo[] paramLocals; public LocalOptimizer() { } /** * Merges the given vector to a new vector. Both vectors may * be null in which case they are interpreted as empty vectors. * The vectors will never changed, but the result may be one * of the given vectors. */ Vector merge(Vector v1, Vector v2) { if (v1 == null || v1.isEmpty()) return v2; if (v2 == null || v2.isEmpty()) return v1; Vector result = (Vector) v1.clone(); Enumeration enum = v2.elements(); while (enum.hasMoreElements()) { Object elem = enum.nextElement(); if (!result.contains(elem)) result.addElement(elem); } return result; } void promoteReads(InstrInfo info, Instruction preInstr, BitSet mergeSet, boolean inverted) { InstrInfo preInfo = (InstrInfo) instrInfos.get(preInstr); int omitLocal = -1; if (preInstr.getOpcode() >= opc_istore && preInstr.getOpcode() <= opc_astore) { /* This is a store */ omitLocal = preInstr.getLocalSlot(); if (info.nextReads[omitLocal] != null) preInfo.local.combineInto(info.nextReads[omitLocal].local); } for (int i=0; i < maxlocals; i++) { if (info.nextReads[i] != null && i != omitLocal && (mergeSet == null || mergeSet.get(i) != inverted)) { if (preInfo.nextReads[i] == null) { preInfo.nextReads[i] = info.nextReads[i]; changedInfos.add(preInfo); } else { preInfo.nextReads[i].local .combineInto(info.nextReads[i].local); } } } } void promoteReads(InstrInfo info, Instruction preInstr) { promoteReads(info, preInstr, null, false); } public LocalVariableInfo findLVTEntry(LocalVariableInfo[] lvt, int slot, int addr) { LocalVariableInfo match = null; for (int i=0; i < lvt.length; i++) { if (lvt[i].slot == slot && lvt[i].start.getAddr() <= addr && lvt[i].end.getAddr() >= addr) { if (match != null && (!match.name.equals(lvt[i].name) || !match.type.equals(lvt[i].type))) { /* Multiple matches..., give no info */ return null; } match = lvt[i]; } } return match; } public LocalVariableInfo findLVTEntry(LocalVariableInfo[] lvt, Instruction instr) { int addr; if (instr.getOpcode() >= opc_istore && instr.getOpcode() <= opc_astore) addr = instr.getNextAddr(); else addr = instr.getAddr(); return findLVTEntry(lvt, instr.getLocalSlot(), addr); } public void calcLocalInfo() { maxlocals = bc.getMaxLocals(); Handler[] handlers = bc.getExceptionHandlers(); LocalVariableInfo[] lvt = bc.getLocalVariableTable(); if (lvt != null) produceLVT = true; /* Initialize paramLocals */ { String methodType = bc.getMethodInfo().getType(); int paramCount = (bc.getMethodInfo().isStatic() ? 0 : 1) + TypeSignature.getArgumentSize(methodType); paramLocals = new LocalInfo[paramCount]; int slot = 0; if (!bc.getMethodInfo().isStatic()) { LocalInfo local = new LocalInfo(); if (lvt != null) { LocalVariableInfo lvi = findLVTEntry(lvt, 0, 0); if (lvi != null) { local.name = lvi.name; local.type = lvi.type; } } local.size = 1; paramLocals[slot++] = local; } int pos = 1; while (pos < methodType.length() && methodType.charAt(pos) != ')') { LocalInfo local = new LocalInfo(); if (lvt != null) { LocalVariableInfo lvi = findLVTEntry(lvt, slot, 0); if (lvi != null) { local.name = lvi.name; } } int start = pos; pos = TypeSignature.skipType(methodType, pos); local.type = methodType.substring(start, pos); local.size = TypeSignature.getTypeSize(local.type); paramLocals[slot] = local; slot += local.size; } } /* Initialize the InstrInfos and LocalInfos */ changedInfos = new TodoQueue(); instrInfos = new Hashtable(); { InstrInfo info = firstInfo = new InstrInfo(); Iterator i = bc.getInstructions().iterator(); while (true) { Instruction instr = (Instruction) i.next(); instrInfos.put(instr, info); info.instr = instr; info.nextReads = new InstrInfo[maxlocals]; if (instr.hasLocalSlot()) { info.local = new LocalInfo(info); if (lvt != null) { LocalVariableInfo lvi = findLVTEntry(lvt, instr); if (lvi != null) { info.local.name = lvi.name; info.local.type = lvi.type; } } info.local.size = 1; switch (instr.getOpcode()) { case opc_lload: case opc_dload: info.local.size = 2; /* fall through */ case opc_iload: case opc_fload: case opc_aload: case opc_iinc: /* this is a load instruction */ info.nextReads[instr.getLocalSlot()] = info; changedInfos.add(info); break; case opc_ret: /* this is a ret instruction */ info.usedBySub = new BitSet(); info.nextReads[instr.getLocalSlot()] = info; changedInfos.add(info); break; case opc_lstore: case opc_dstore: info.local.size = 2; //case opc_istore: case opc_fstore: case opc_astore: } } if (!i.hasNext()) break; info = info.nextInfo = new InstrInfo(); } } /* find out which locals are the same. */ while (!changedInfos.isEmpty()) { InstrInfo info = changedInfos.remove(); Instruction instr = info.instr; /* Mark the local as used in all ret instructions */ if (instr.hasLocalSlot()) { int slot = instr.getLocalSlot(); for (int i=0; i< maxlocals; i++) { InstrInfo retInfo = info.nextReads[i]; if (retInfo != null && retInfo.instr.getOpcode() == opc_ret && !retInfo.usedBySub.get(slot)) { retInfo.usedBySub.set(slot); if (retInfo.jsrTargetInfo != null) changedInfos.add(retInfo.jsrTargetInfo); } } } Instruction prevInstr = instr.getPrevByAddr(); if (prevInstr != null) { if (!prevInstr.doesAlwaysJump()) promoteReads(info, prevInstr); else if (prevInstr.getOpcode() == opc_jsr) { /* Prev instr is a jsr, promote reads to the * corresponding ret. */ InstrInfo jsrInfo = (InstrInfo) instrInfos.get(prevInstr.getSingleSucc()); if (jsrInfo.retInfo != null) { /* Now promote reads that are modified by the * subroutine to the ret, and those that are not * to the jsr instruction. */ promoteReads(info, jsrInfo.retInfo.instr, jsrInfo.retInfo.usedBySub, false); promoteReads(info, prevInstr, jsrInfo.retInfo.usedBySub, true); } } } if (instr.getPreds() != null) { for (int i = 0; i < instr.getPreds().length; i++) { Instruction predInstr = instr.getPreds()[i]; if (instr.getPreds()[i].getOpcode() == opc_jsr) { /* This is the target of a jsr instr. */ if (info.instr.getOpcode() != opc_astore) { /* XXX Grrr, the bytecode verifier doesn't * test if a jsr starts with astore. So * it is possible to do something else * before putting the ret address into a * local. */ throw new AssertError("Non standard jsr"); } InstrInfo retInfo = info.nextInfo.nextReads [info.instr.getLocalSlot()]; if (retInfo != null) { if (retInfo.instr.getOpcode() != opc_ret) throw new AssertError ("reading return address"); info.retInfo = retInfo; retInfo.jsrTargetInfo = info; /* Now promote reads from the instruction * after the jsr to the ret instruction if * they are modified by the subroutine, * and to the jsr instruction otherwise. */ Instruction nextInstr = predInstr.getNextByAddr(); InstrInfo nextInfo = (InstrInfo) instrInfos.get(nextInstr); promoteReads(nextInfo, retInfo.instr, retInfo.usedBySub, false); promoteReads(nextInfo, predInstr, retInfo.usedBySub, true); } } promoteReads(info, instr.getPreds()[i]); } } for (int i=0; i < handlers.length; i++) { if (handlers[i].catcher == instr) { for (Instruction preInstr = handlers[i].start; preInstr != handlers[i].end.getNextByAddr(); preInstr = preInstr.getNextByAddr()) { promoteReads(info, preInstr); } } } } changedInfos = null; /* Now merge with the parameters * The params should be the locals in firstInfo.nextReads */ for (int i=0; i< paramLocals.length; i++) { if (firstInfo.nextReads[i] != null) { firstInfo.nextReads[i].local.combineInto(paramLocals[i]); paramLocals[i] = paramLocals[i].getReal(); } } } public void stripLocals() { ListIterator iter = bc.getInstructions().listIterator(); for (InstrInfo info = firstInfo; info != null; info = info.nextInfo) { Instruction instr = (Instruction) iter.next(); if (info.local != null && info.local.usingInstrs.size() == 1) { /* If this is a store, whose value is never read; it can * be removed, i.e replaced by a pop. */ switch (instr.getOpcode()) { case opc_istore: case opc_fstore: case opc_astore: iter.set(new Instruction(opc_pop)); break; case opc_lstore: case opc_dstore: iter.set(new Instruction(opc_pop2)); break; default: } } } } void distributeLocals(Vector locals) { if (locals.size() == 0) return; /* Find the local with the least conflicts. */ int min = Integer.MAX_VALUE; LocalInfo bestLocal = null; Enumeration enum = locals.elements(); while (enum.hasMoreElements()) { LocalInfo li = (LocalInfo) enum.nextElement(); int conflicts = 0; Enumeration conflenum = li.conflictingLocals.elements(); while (conflenum.hasMoreElements()) { if (((LocalInfo)conflenum.nextElement()).newSlot != -2) conflicts++; } if (conflicts < min) { min = conflicts; bestLocal = li; } } /* Mark the local as taken */ locals.removeElement(bestLocal); bestLocal.newSlot = -2; /* Now distribute the remaining locals recursively. */ distributeLocals(locals); /* Finally find a new slot */ next_slot: for (int slot = 0; ; slot++) { Enumeration conflenum = bestLocal.conflictingLocals.elements(); while (conflenum.hasMoreElements()) { LocalInfo conflLocal = (LocalInfo)conflenum.nextElement(); if (bestLocal.size == 2 && conflLocal.newSlot == slot+1) { slot++; continue next_slot; } if (conflLocal.size == 2 && conflLocal.newSlot+1 == slot) continue next_slot; if (conflLocal.newSlot == slot) { if (conflLocal.size == 2) slot++; continue next_slot; } } bestLocal.newSlot = slot; break; } } public void distributeLocals() { /* give locals new slots. This is a graph coloring * algorithm (the optimal solution is NP complete, but this * should be a good approximation). */ /* first give the params the same slot as they had before. */ for (int i=0; i= BytecodeInfo.opc_istore && info.instr.getOpcode() <= BytecodeInfo.opc_astore) { /* This is a store. It conflicts with every local, whose * value will be read without write. * * If this is inside a ret, it also conflicts with * locals, that are not used inside, and where any jsr * would conflict with. */ for (int i=0; i < maxlocals; i++) { if (i != info.instr.getLocalSlot() && info.nextReads[i] != null) info.local.conflictsWith(info.nextReads[i].local); if (info.nextInfo.nextReads[i] != null && info.nextInfo.nextReads[i].jsrTargetInfo != null) { Instruction[] jsrs = info.nextInfo.nextReads[i] .jsrTargetInfo.instr.getPreds(); for (int j=0; j< jsrs.length; j++) { InstrInfo jsrInfo = (InstrInfo) instrInfos.get(jsrs[j]); for (int k=0; k < maxlocals; k++) { if (!info.nextInfo.nextReads[i].usedBySub .get(k) && jsrInfo.nextReads[k] != null) info.local.conflictsWith (jsrInfo.nextReads[k].local); } } } } } } /* Now put the locals that need a color into a vector. */ Vector locals = new Vector(); for (InstrInfo info = firstInfo; info != null; info = info.nextInfo) { if (info.local != null && info.local.newSlot == -1 && !locals.contains(info.local)) locals.addElement(info.local); } /* Now distribute slots recursive. */ distributeLocals(locals); /* Update the instructions and calculate new maxlocals. */ maxlocals = paramLocals.length; for (InstrInfo info = firstInfo; info != null; info = info.nextInfo) { if (info.local != null) { if (info.local.newSlot+info.local.size > maxlocals) maxlocals = info.local.newSlot + info.local.size; info.instr.setLocalSlot(info.local.newSlot); } } bc.setMaxLocals(maxlocals); /* Update LocalVariableTable */ if (produceLVT) buildNewLVT(); } private InstrInfo CONFLICT = new InstrInfo(); boolean promoteLifeLocals(LocalInfo[] newLife, InstrInfo nextInfo) { if (nextInfo.lifeLocals == null) { nextInfo.lifeLocals = (LocalInfo[]) newLife.clone(); return true; } boolean changed = false; for (int i=0; i< maxlocals; i++) { LocalInfo local = nextInfo.lifeLocals[i]; if (local == null) /* A conflict has already happened, or this slot * may not have been initialized. */ continue; local = local.getReal(); LocalInfo newLocal = newLife[i]; if (newLocal != null) newLocal = newLocal.getReal(); if (local != newLocal) { nextInfo.lifeLocals[i] = null; changed = true; } } return changed; } public void buildNewLVT() { /* First we recalculate the usedBySub, to use the new local numbers. */ for (InstrInfo info = firstInfo; info != null; info = info.nextInfo) if (info.usedBySub != null) info.usedBySub = new BitSet(); for (InstrInfo info = firstInfo; info != null; info = info.nextInfo) { if (info.local != null) { for (int i=0; i < info.nextReads.length; i++) { if (info.nextReads[i] != null && info.nextReads[i].instr.getOpcode() == opc_ret) info.nextReads[i].usedBySub.set(info.local.newSlot); } } } /* Now we begin with the first Instruction and follow program flow. * We remember which locals are life in lifeLocals. */ firstInfo.lifeLocals = new LocalInfo[maxlocals]; for (int i=0; i < paramLocals.length; i++) firstInfo.lifeLocals[i] = paramLocals[i]; Stack changedInfo = new Stack(); changedInfo.push(firstInfo); Handler[] handlers = bc.getExceptionHandlers(); while (!changedInfo.isEmpty()) { InstrInfo info = (InstrInfo) changedInfo.pop(); Instruction instr = info.instr; LocalInfo[] newLife = info.lifeLocals; if (instr.hasLocalSlot()) { int slot = instr.getLocalSlot(); LocalInfo instrLocal = info.local.getReal(); newLife = (LocalInfo[]) newLife.clone(); newLife[slot] = instrLocal; if (instrLocal.name != null) { for (int j=0; j< newLife.length; j++) { if (j != slot && newLife[j] != null && instrLocal.name.equals(newLife[j].name)) { /* This local changed the slot. */ newLife[j] = null; } } } } if (!instr.doesAlwaysJump()) { InstrInfo nextInfo = info.nextInfo; if (promoteLifeLocals(newLife, nextInfo)) changedInfo.push(nextInfo); } if (instr.hasSuccs()) { Instruction[] succs = instr.getSuccs(); for (int i = 0; i < succs.length; i++) { InstrInfo nextInfo = (InstrInfo) instrInfos.get(succs[i]); if (promoteLifeLocals(newLife, nextInfo)) changedInfo.push(nextInfo); } } for (int i=0; i < handlers.length; i++) { if (handlers[i].start.compareTo(instr) <= 0 && handlers[i].end.compareTo(instr) >= 0) { InstrInfo nextInfo = (InstrInfo) instrInfos.get(handlers[i].catcher); if (promoteLifeLocals(newLife, nextInfo)) changedInfo.push(nextInfo); } } if (info.instr.getOpcode() == opc_jsr) { /* On a jsr we do a special merge */ Instruction jsrTargetInstr = info.instr.getSingleSucc(); InstrInfo jsrTargetInfo = (InstrInfo) instrInfos.get(jsrTargetInstr); InstrInfo retInfo = jsrTargetInfo.retInfo; if (retInfo != null && retInfo.lifeLocals != null) { LocalInfo[] retLife = (LocalInfo[]) newLife.clone(); for (int i=0; i< maxlocals; i++) { if (retInfo.usedBySub.get(i)) retLife[i] = retInfo.lifeLocals[i]; } if (promoteLifeLocals(retLife, info.nextInfo)) changedInfo.push(info.nextInfo); } } if (info.jsrTargetInfo != null) { /* On a ret we do a special merge */ Instruction jsrTargetInstr = info.jsrTargetInfo.instr; for (int j=0; j< jsrTargetInstr.getPreds().length; j++) { InstrInfo jsrInfo = (InstrInfo) instrInfos.get(jsrTargetInstr.getPreds()[j]); if (jsrInfo.lifeLocals == null) /* life locals are not calculated, yet */ continue; LocalInfo[] retLife = (LocalInfo[]) newLife.clone(); for (int i=0; i< maxlocals; i++) { if (!info.usedBySub.get(i)) retLife[i] = jsrInfo.lifeLocals[i]; } if (promoteLifeLocals(retLife, jsrInfo.nextInfo)) changedInfo.push(jsrInfo.nextInfo); } } } Vector lvtEntries = new Vector(); LocalVariableInfo[] lvi = new LocalVariableInfo[maxlocals]; LocalInfo[] currentLocal = new LocalInfo[maxlocals]; for (int i=0; i< paramLocals.length; i++) { if (paramLocals[i] != null) { currentLocal[i] = paramLocals[i]; if (currentLocal[i].name != null) { lvi[i] = new LocalVariableInfo(); lvtEntries.addElement(lvi[i]); lvi[i].name = currentLocal[i].name; /* XXX obfuscation? */ lvi[i].type = Main.getClassBundle() .getTypeAlias(currentLocal[i].type); lvi[i].start = (Instruction) bc.getInstructions().get(0); lvi[i].slot = i; } } } Instruction lastInstr = null; for (InstrInfo info = firstInfo; info != null; info = info.nextInfo) { for (int i=0; i< maxlocals; i++) { LocalInfo lcl = info.lifeLocals != null ? info.lifeLocals[i] : null; if (lcl != currentLocal[i] && (lcl == null || currentLocal[i] == null || lcl.name == null || lcl.type == null || !lcl.name.equals(currentLocal[i].name) || !lcl.type.equals(currentLocal[i].type))) { if (lvi[i] != null) { lvi[i].end = info.instr.getPrevByAddr(); } lvi[i] = null; currentLocal[i] = lcl; if (currentLocal[i] != null && currentLocal[i].name != null && currentLocal[i].type != null) { lvi[i] = new LocalVariableInfo(); lvtEntries.addElement(lvi[i]); lvi[i].name = currentLocal[i].name; lvi[i].type = Main.getClassBundle() .getTypeAlias(currentLocal[i].type); lvi[i].start = info.instr; lvi[i].slot = i; } } } lastInstr = info.instr; } for (int i=0; i< maxlocals; i++) { if (lvi[i] != null) lvi[i].end = lastInstr; } LocalVariableInfo[] lvt = new LocalVariableInfo[lvtEntries.size()]; lvtEntries.copyInto(lvt); bc.setLocalVariableTable(lvt); } public void dumpLocals() { Vector locals = new Vector(); for (InstrInfo info = firstInfo; info != null; info = info.nextInfo) { GlobalOptions.err.println(info.instr.getDescription()); GlobalOptions.err.print("nextReads: "); for (int i=0; i