1010import org .objectweb .asm .Type ;
1111import org .objectweb .asm .tree .AbstractInsnNode ;
1212import org .objectweb .asm .tree .ClassNode ;
13+ import org .objectweb .asm .tree .FieldInsnNode ;
1314import org .objectweb .asm .tree .IincInsnNode ;
1415import org .objectweb .asm .tree .InsnList ;
1516import org .objectweb .asm .tree .InsnNode ;
2829import software .coley .recaf .services .transform .JvmTransformerContext ;
2930import software .coley .recaf .services .transform .TransformationException ;
3031import software .coley .recaf .util .AccessFlag ;
32+ import software .coley .recaf .util .AsmInsnUtil ;
33+ import software .coley .recaf .util .Types ;
3134import software .coley .recaf .util .analysis .Nullness ;
3235import software .coley .recaf .util .analysis .value .ArrayValue ;
3336import software .coley .recaf .util .analysis .value .DoubleValue ;
4346import software .coley .recaf .workspace .model .resource .WorkspaceResource ;
4447
4548import java .util .Collections ;
49+ import java .util .Iterator ;
50+ import java .util .NavigableSet ;
4651import java .util .Set ;
47- import java .util .SortedSet ;
4852import java .util .TreeSet ;
4953import java .util .stream .Collectors ;
5054
@@ -125,7 +129,6 @@ public void transform(@Nonnull JvmTransformerContext context, @Nonnull Workspace
125129 Frame <ReValue > frame = frames [i ];
126130 if (frame == null )
127131 continue ; // Skip dead code
128- ReValue top = frame .getStack (frame .getStackSize () - 1 );
129132 LocalAccessState state = states .computeIfAbsent (key (vin .var , getTypeForVarInsn (vin ).getSort ()), k -> new LocalAccessState (vin .var ));
130133 state .addRead (i , vin );
131134 }
@@ -135,13 +138,13 @@ else if (isVarStore(op) && insn instanceof VarInsnNode vin) {
135138 Frame <ReValue > frame = frames [i ];
136139 if (frame == null )
137140 continue ; // Skip dead code
138- ReValue top = frame .getStack (frame .getStackSize () - 1 );
139141 LocalAccessState state = states .computeIfAbsent (key (vin .var , getTypeForVarInsn (vin ).getSort ()), k -> new LocalAccessState (vin .var ));
140142 state .addWrite (i , vin );
141143
142144 // If we're moving forward in the method from the beginning, and haven't seen
143145 // the variable being used yet or any kind of control flow, then we can safely
144146 // just replace the state with the value on the stack instead of merging it.
147+ ReValue top = frame .getStack (frame .getStackSize () - 1 );
145148 if (state .getReadsUpTo (i ).isEmpty () && !controlFlowObserved && !throwingObserved )
146149 state .setState (top );
147150 else
@@ -230,9 +233,11 @@ else if (!throwingObserved) {
230233 // Remove variable writes if:
231234 // - They are never read from
232235 // - They are inlinable values (arrays for instance cannot be inlined as a single value providing instruction)
233- // - They are read from, but only used with a single known constant value (which we inline above).
236+ // - They are read from, but only used with a single known constant value (which we inline above)
234237 Type varType = getTypeForVarInsn (vin );
235- LocalAccessState state = states .get (key (vin .var , varType .getSort ()));
238+ int varSort = varType .getSort ();
239+ int varKey = key (vin .var , varSort );
240+ LocalAccessState state = states .get (varKey );
236241 if (state != null && state .isInlinableValue () && (state .getReads ().isEmpty () || state .isEffectiveConstant ())) {
237242 AbstractInsnNode previous = insn .getPrevious ();
238243 if (OpaqueConstantFoldingTransformer .isSupportedValueProducer (previous )) {
@@ -246,6 +251,36 @@ else if (!throwingObserved) {
246251 }
247252 dirty = true ;
248253 }
254+
255+ // Remove variable writes if:
256+ // - This write is never read from, even if the variable is read from later after a different assignment.
257+ // Between the potential unused write (this one) and the following write preceding the first read
258+ // there must not be any control-flow altering instructions.
259+ // TODO: Implement this (probably requires better state tracking refactoring)
260+
261+ // Remove variable writes + usages if:
262+ // - The variable is a redundant copy of an existing other variable
263+ for (int keyX : states .keySet ()) {
264+ // Check if the current store is redundant with another variable.
265+ int slotX = slotFromKey (keyX );
266+ if (slotX != vin .var && isRedundantStore (states , instructions , i , slotX , vin .var , varSort )) {
267+ LocalAccessState stateY = states .get (varKey );
268+
269+ // Replace usage of the redundant variable.
270+ if (!stateY .getReads ().isEmpty ()) {
271+ replaceRedundantVariableUsage (instructions , slotX , vin .var , varSort );
272+ stateY .getReads ().clear ();
273+ }
274+
275+ // Update the tracking state of the redundant variable.
276+ stateY .getWrites ().removeIf (l -> l .instruction == vin );
277+
278+ // Replace store instruction with POP.
279+ if (instructions .indexOf (vin ) >= 0 )
280+ instructions .set (vin , new InsnNode (Types .isWide (varType ) ? POP2 : POP ));
281+ dirty = true ;
282+ }
283+ }
249284 } else if (op == IINC && insn instanceof IincInsnNode iinc ) {
250285 // Remove variable increments if:
251286 // - They value is known at all points of variable use
@@ -262,6 +297,151 @@ else if (!throwingObserved) {
262297 context .setNode (bundle , initialClassState , node );
263298 }
264299
300+ /**
301+ * Determines if a store operation to a variable is redundant, meaning it mirrors
302+ * the value of another variable, and neither variable is modified between their usage.
303+ *
304+ * @param states
305+ * Variable access states.
306+ * @param instructions
307+ * Method instructions.
308+ * @param slotX
309+ * The original variable index.
310+ * @param slotY
311+ * The target variable index to check for redundancy.
312+ * @param typeSort
313+ * The type sort of the variable, to handle all primitives + object types.
314+ *
315+ * @return {@code true} when the store to slotY is redundant.
316+ */
317+ private static boolean isRedundantStore (@ Nonnull Int2ObjectMap <LocalAccessState > states ,
318+ @ Nonnull InsnList instructions ,
319+ int insnIndex , int slotX , int slotY , int typeSort ) {
320+ LocalAccessState stateX = states .get (key (slotX , typeSort ));
321+ LocalAccessState stateY = states .get (key (slotY , typeSort ));
322+
323+ // If no state exists for either slot, we cannot conclude redundancy.
324+ if (stateX == null || stateY == null )
325+ return false ;
326+
327+ // Redundancy only applies if there is a single write to Y.
328+ NavigableSet <LocalAccess > writesToY = stateY .getWrites ();
329+ if (writesToY .size () != 1 )
330+ return false ;
331+
332+ // Get the single write instruction to Y.
333+ LocalAccess writeToY = writesToY .first ();
334+ AbstractInsnNode writeInsnY = writeToY .instruction ;
335+ if (!(writeInsnY instanceof VarInsnNode writeVin ))
336+ return false ;
337+
338+ // Check if the instruction type matches the expected type.
339+ if (!isMatchingStore (typeSort , writeVin .getOpcode ()))
340+ return false ;
341+
342+ // Check if the prior instruction is a cast.
343+ // We want to keep patterns like 'Collection -> List' being stored into new variables.
344+ AbstractInsnNode previousInsn = writeInsnY .getPrevious ();
345+ if (previousInsn == null || previousInsn .getOpcode () == CHECKCAST )
346+ return false ;
347+
348+ // Contents of both locals must be equal.
349+ if (!(previousInsn instanceof VarInsnNode prevVin
350+ && isVarLoad (prevVin .getOpcode ())
351+ && prevVin .var == slotX ))
352+ return false ;
353+
354+ // Verify there are no intervening writes to X or Y between the observed instructions.
355+ int writeIndexY = instructions .indexOf (writeInsnY );
356+ for (int i = instructions .indexOf (previousInsn ) + 1 ; i < writeIndexY ; i ++) {
357+ AbstractInsnNode insn = instructions .get (i );
358+
359+ // Check if there are any writes to X or Y in the scope.
360+ // Any intermediate modification invalidates redundancy.
361+ if (insn instanceof VarInsnNode vin ) {
362+ int var = vin .var ;
363+ int opcode = vin .getOpcode ();
364+ if ((var == slotX || var == slotY ) && isVarStore (opcode ))
365+ return false ;
366+ }
367+ }
368+
369+ // Check that X is defined before Y.
370+ // A variable defined later cannot make an earlier variable redundant.
371+ NavigableSet <LocalAccess > writesToX = stateX .getWrites ();
372+ if (writesToX .isEmpty ())
373+ return false ;
374+ LocalAccess firstWriteToX = writesToX .first (); // First write to X
375+ int writeIndexX = instructions .indexOf (firstWriteToX .instruction );
376+ if (writeIndexX >= writeIndexY )
377+ return false ;
378+
379+ // The store to Y is redundant. Every use case of it can be replaced with X.
380+ return true ;
381+ }
382+
383+ /**
384+ * Replaces usage of the redundant variable with the original variable.
385+ *
386+ * @param instructions
387+ * Method instructions.
388+ * @param slotX
389+ * The original variable index.
390+ * @param slotY
391+ * The target variable index that is redundant.
392+ * @param typeSort
393+ * The variable's type sort.
394+ */
395+ private static void replaceRedundantVariableUsage (@ Nonnull InsnList instructions , int slotX , int slotY , int typeSort ) {
396+ AbstractInsnNode replacement = AsmInsnUtil .createVarLoad (slotX , typeSort );
397+ for (int i = 0 ; i < instructions .size (); i ++) {
398+ AbstractInsnNode insn = instructions .get (i );
399+ if (insn instanceof VarInsnNode vin && vin .var == slotY ) {
400+ int op = vin .getOpcode ();
401+ if (isVarLoad (op ))
402+ instructions .set (insn , replacement .clone (null ));
403+ }
404+ }
405+ }
406+
407+ /**
408+ * @param typeSort
409+ * The variable type sort.
410+ * @param opcode
411+ * The opcode to check.
412+ *
413+ * @return {@code true} if the opcode matches the respective store operation for the type.
414+ */
415+ private static boolean isMatchingStore (int typeSort , int opcode ) {
416+ return switch (typeSort ) {
417+ case Type .INT -> opcode == ISTORE ;
418+ case Type .FLOAT -> opcode == FSTORE ;
419+ case Type .LONG -> opcode == LSTORE ;
420+ case Type .DOUBLE -> opcode == DSTORE ;
421+ case Type .OBJECT -> opcode == ASTORE ;
422+ default -> false ;
423+ };
424+ }
425+
426+ /**
427+ * @param typeSort
428+ * The variable type sort.
429+ * @param opcode
430+ * The opcode to check.
431+ *
432+ * @return {@code true} if the opcode matches the respective load operation for the type.
433+ */
434+ private static boolean isMatchingLoad (int typeSort , int opcode ) {
435+ return switch (typeSort ) {
436+ case Type .INT -> opcode == ILOAD ;
437+ case Type .FLOAT -> opcode == FLOAD ;
438+ case Type .LONG -> opcode == LLOAD ;
439+ case Type .DOUBLE -> opcode == DLOAD ;
440+ case Type .OBJECT -> opcode == ALOAD ;
441+ default -> false ;
442+ };
443+ }
444+
265445 @ Nonnull
266446 @ Override
267447 public Set <Class <? extends ClassTransformer >> recommendedSuccessors () {
@@ -288,6 +468,16 @@ private static int key(int slot, int typeSort) {
288468 return slot | (typeSort << 16 );
289469 }
290470
471+ /**
472+ * @param key
473+ * Key of typed variable.
474+ *
475+ * @return Variable index stored in the key.
476+ */
477+ private static int slotFromKey (int key ) {
478+ return key & 0xFFFF ;
479+ }
480+
291481 /**
292482 * State tracking for when variables are read from and written to, along with the common merged value.
293483 * <br>
@@ -298,8 +488,8 @@ private static int key(int slot, int typeSort) {
298488 */
299489 private static class LocalAccessState {
300490 private final int index ;
301- private SortedSet <LocalAccess > reads ;
302- private SortedSet <LocalAccess > writes ;
491+ private NavigableSet <LocalAccess > reads ;
492+ private NavigableSet <LocalAccess > writes ;
303493 private ReValue mergedValue ;
304494 private boolean isArray ;
305495
@@ -342,14 +532,14 @@ public void mergeState(@Nonnull ReValue value) throws IllegalValueException {
342532 }
343533
344534 @ Nonnull
345- public SortedSet <LocalAccess > getReads () {
535+ public NavigableSet <LocalAccess > getReads () {
346536 if (reads == null )
347- return Collections .emptySortedSet ();
537+ return Collections .emptyNavigableSet ();
348538 return reads ;
349539 }
350540
351541 @ Nonnull
352- public SortedSet <LocalAccess > getReadsUpTo (int offset ) {
542+ public NavigableSet <LocalAccess > getReadsUpTo (int offset ) {
353543 // TODO: This is a linear check, which can be defeated by basic flow obfuscation
354544 // - We need to rewalk the control flow and find all the paths that lead to this instruction/offset
355545 // then get any local accesses that are along that path.
@@ -361,9 +551,17 @@ public SortedSet<LocalAccess> getReadsUpTo(int offset) {
361551 }
362552
363553 @ Nonnull
364- public SortedSet <LocalAccess > getWrites () {
554+ public NavigableSet <LocalAccess > getWritesUpTo (int offset ) {
555+ // TODO: Same linear check problem as above
556+ return getWrites ().stream ()
557+ .filter (l -> l .offset < offset )
558+ .collect (Collectors .toCollection (TreeSet ::new ));
559+ }
560+
561+ @ Nonnull
562+ public NavigableSet <LocalAccess > getWrites () {
365563 if (writes == null )
366- return Collections .emptySortedSet ();
564+ return Collections .emptyNavigableSet ();
367565 return writes ;
368566 }
369567
0 commit comments