#yyds干货盘点#Java ASM系列((091)冗余变量分析)

人生难得几回搏,此时不搏待何时。这篇文章主要讲述#yyds干货盘点#Java ASM系列:(091)冗余变量分析相关的知识,希望能为你提供帮助。
本文属于Java ASM系列三:Tree API当中的一篇。
1. 如何判断变量是否冗余如果在IntelliJ IDEA当中编写如下的代码,它会提示str2str3局部变量是多余的:

public class HelloWorld { public void test() { String str1 = "Hello ASM"; Object obj1 = new Object(); // Local variable str2 is redundant String str2 = str1; Object obj2 = new Object(); // Local variable str3 is redundant String str3 = str2; Object obj3 = new Object(); int length = str3.length(); System.out.println(length); } }

1.1. 整体思路
结合AnalyzerSimpleVerifier类,我们可以查看Frame的变化情况:
test:()V 000:ldc "Hello ASM"{HelloWorld, ., ., ., ., ., ., .} | {} 001:astore_1{HelloWorld, ., ., ., ., ., ., .} | {String} 002:new Object{HelloWorld, String, ., ., ., ., ., .} | {} 003:dup{HelloWorld, String, ., ., ., ., ., .} | {Object} 004:invokespecial Object.< init> {HelloWorld, String, ., ., ., ., ., .} | {Object, Object} 005:astore_2{HelloWorld, String, ., ., ., ., ., .} | {Object} 006:aload_1{HelloWorld, String, Object, ., ., ., ., .} | {} 007:astore_3{HelloWorld, String, Object, ., ., ., ., .} | {String} 008:new Object{HelloWorld, String, Object, String, ., ., ., .} | {} 009:dup{HelloWorld, String, Object, String, ., ., ., .} | {Object} 010:invokespecial Object.< init> {HelloWorld, String, Object, String, ., ., ., .} | {Object, Object} 011:astore 4{HelloWorld, String, Object, String, ., ., ., .} | {Object} 012:aload_3{HelloWorld, String, Object, String, Object, ., ., .} | {} 013:astore 5{HelloWorld, String, Object, String, Object, ., ., .} | {String} 014:new Object{HelloWorld, String, Object, String, Object, String, ., .} | {} 015:dup{HelloWorld, String, Object, String, Object, String, ., .} | {Object} 016:invokespecial Object.< init> {HelloWorld, String, Object, String, Object, String, ., .} | {Object, Object} 017:astore 6{HelloWorld, String, Object, String, Object, String, ., .} | {Object} 018:aload 5{HelloWorld, String, Object, String, Object, String, Object, .} | {} 019:invokevirtual String.length{HelloWorld, String, Object, String, Object, String, Object, .} | {String} 020:istore 7{HelloWorld, String, Object, String, Object, String, Object, .} | {I} 021:getstatic System.out{HelloWorld, String, Object, String, Object, String, Object, I} | {} 022:iload 7{HelloWorld, String, Object, String, Object, String, Object, I} | {PrintStream} 023:invokevirtual PrintStream.println{HelloWorld, String, Object, String, Object, String, Object, I} | {PrintStream, I} 024:return{HelloWorld, String, Object, String, Object, String, Object, I} | {} ================================================================

我们的整体思路是这样的:
  • 在每一个Frame当中,它有local variable和operand stack两部分组成。
  • 程序中定义的“变量”是存储在local variable当中。
  • 在理想的情况下,一个“变量”对应于local variable当中的一个位置;如果一个“变量”对应于local variable当中的两个或多个位置,那么我们就认为“变量”出现了冗余。
那么,针对某一个具体的frame,相应的实现思路上是这样的:
  • 判断local[0]local[1]是否相同,如果相同,那么表示local[1]是冗余的变量。
  • 判断local[0]local[2]是否相同,如果相同,那么表示local[2]是冗余的变量。
  • ...
  • 判断local[0]local[n]是否相同,如果相同,那么表示local[n]是冗余的变量。
  • 判断local[1]local[2]是否相同,如果相同,那么表示local[2]是冗余的变量。
  • 判断local[1]local[3]是否相同,如果相同,那么表示local[3]是冗余的变量。
  • ...
需要注意的一点就是,如果local variable当中存储“未初始化的值”(BasicValue.UNINITIALIZED_VALUE),那么我们就不进行处理了。
具体来说,“未初始化的值”(BasicValue.UNINITIALIZED_VALUE)有两种情况:
  • 第一种情况,在方法刚进入的时候,local variable有些位置存储的就是“未初始化的值”(BasicValue.UNINITIALIZED_VALUE)。
  • 第二种情况,在存储longdouble类型的数据时,它占用两个位置,其中第二个位置就是“未初始化的值”(BasicValue.UNINITIALIZED_VALUE)。
1.2. 为什么选择SimpleVerifier
在ASM当中,Interpreter类是一个抽象类,其中提供的子类有BasicInterpreterBasicVerifierSimpleVerifierSourceInterpreter类。那么,我们到底应该选择哪一个呢?
┌───┬───────────────────┬─────────────┬───────┐ │ 0 │Interpreter│Value│ Range │ ├───┼───────────────────┼─────────────┼───────┤ │ 1 │ BasicInterpreter│ BasicValue│7│ ├───┼───────────────────┼─────────────┼───────┤ │ 2 │BasicVerifier│ BasicValue│7│ ├───┼───────────────────┼─────────────┼───────┤ │ 3 │SimpleVerifier│ BasicValue│N│ ├───┼───────────────────┼─────────────┼───────┤ │ 4 │ SourceInterpreter │ SourceValue │N│ └───┴───────────────────┴─────────────┴───────┘

首先,不能选择BasicInterpreterBasicVerifier类。因为它们使用7个值(BasicValue类定义的7个静态字段)来模拟Frame的变化,这7个值的“表达能力”很弱。如果一个对象是String类型,另一个对象是Object类型,这两个对象都会被表示成BasicValue.REFERENCE_VALUE,没有办法进行区分。
其次,不能选择SourceInterpreter类。因为它定义的copyOperation方法中会创建一个新的对象(new SourceValue(value.getSize(), insn)),不能识别为同一个对象。
public class SourceInterpreter extends Interpreter< SourceValue> implements Opcodes { @Override public SourceValue copyOperation(final AbstractInsnNode insn, final SourceValue value) { return new SourceValue(value.getSize(), insn); } }

为什么要关注这个copyOperation方法呢?因为copyOperation方法负责处理load和store相关的指令。
public abstract class Interpreter< V extends Value> { /** * Interprets a bytecode instruction that moves a value on the stack or to or from local variables. * This method is called for the following opcodes: * * ILOAD, LLOAD, FLOAD, DLOAD, ALOAD, * ISTORE, LSTORE, FSTORE, DSTORE, ASTORE, * DUP, DUP_X1, DUP_X2, DUP2, DUP2_X1, DUP2_X2, SWAP * */ public abstract V copyOperation(AbstractInsnNode insn, V value) throws AnalyzerException; }

最后,选择SimpleVerifier是合适的。一方面,它能区分不同的类型(class)、区分不同的对象实例(object instance);另一方面,在copyOperation方法中保证了对象的一致性,传入的是value,返回的仍然是value。更准确的来说,SimpleVerifier是继承了父类BasicVerifier类的copyOperation方法。
public class BasicVerifier extends BasicInterpreter { @Override public BasicValue copyOperation(final AbstractInsnNode insn, final BasicValue value) throws AnalyzerException { //... return value; } }

2. 示例:冗余变量分析 2.1. 预期目标
在下面的代码中,会提示str2str3局部变量是多余的:
public class HelloWorld { public void test() { String str1 = "Hello ASM"; Object obj1 = new Object(); // Local variable str2 is redundant String str2 = str1; Object obj2 = new Object(); // Local variable str3 is redundant String str3 = str2; Object obj3 = new Object(); int length = str3.length(); System.out.println(length); } }

我们的预期目标:识别出str2str3是冗余变量。
2.2. 编码实现
import org.objectweb.asm.Opcodes; import org.objectweb.asm.tree.AbstractInsnNode; import org.objectweb.asm.tree.InsnList; import org.objectweb.asm.tree.MethodNode; import org.objectweb.asm.tree.VarInsnNode; import org.objectweb.asm.tree.analysis.*; import java.util.Arrays; public class RedundantVariableDiagnosis { public static int[] diagnose(String className, MethodNode mn) throws AnalyzerException { // 第一步,准备工作。使用SimpleVerifier进行分析,得到frames信息 Analyzer< BasicValue> analyzer = new Analyzer< > (new SimpleVerifier()); Frame< BasicValue> [] frames = analyzer.analyze(className, mn); // 第二步,利用frames信息,查看local variable当中哪些slot数据出现了冗余 TIntArrayList localIndexList = new TIntArrayList(); for (Frame< BasicValue> f : frames) { int locals = f.getLocals(); for (int i = 0; i < locals; i++) { BasicValue val1 = f.getLocal(i); if (val1 == BasicValue.UNINITIALIZED_VALUE) { continue; } for (int j = i + 1; j < locals; j++) { BasicValue val2 = f.getLocal(j); if (val2 == BasicValue.UNINITIALIZED_VALUE) { continue; } if (val1 == val2) { if (!localIndexList.contains(j)) { localIndexList.add(j); } } } } }// 第三步,将slot的索引值(local index)转换成instruction的索引值(insn index) TIntArrayList insnIndexList = new TIntArrayList(); InsnList instructions = mn.instructions; int size = instructions.size(); for (int i = 0; i < size; i++) { AbstractInsnNode node = instructions.get(i); int opcode = node.getOpcode(); if (opcode > = Opcodes.ISTORE & & opcode < = Opcodes.ASTORE) { VarInsnNode varInsnNode = (VarInsnNode) node; if (localIndexList.contains(varInsnNode.var)) { if (!insnIndexList.contains(i)) { insnIndexList.add(i); } } } }// 第四步,将insnIndexList转换成int[]形式 int[] array = insnIndexList.toNativeArray(); Arrays.sort(array); return array; } }

2.3. 进行分析
public class HelloWorldAnalysisTree { public static void main(String[] args) throws Exception { String relative_path = "sample/HelloWorld.class"; String filepath = FileUtils.getFilePath(relative_path); byte[] bytes = FileUtils.readBytes(filepath); //(1)构建ClassReader ClassReader cr = new ClassReader(bytes); //(2)生成ClassNode int api = Opcodes.ASM9; ClassNode cn = new ClassNode(api); int parsingOptions = ClassReader.SKIP_DEBUG | ClassReader.SKIP_FRAMES; cr.accept(cn, parsingOptions); //(3)进行分析 List< MethodNode> methods = cn.methods; MethodNode mn = methods.get(1); int[] array = RedundantVariableDiagnosis.diagnose(cn.name, mn); System.out.println(Arrays.toString(array)); BoxDrawingUtils.printInstructionLinks(mn.instructions, array); } }

输出结果:
[7, 13] 000: ldc "Hello ASM" 001: astore_1 002: new Object 003: dup 004: invokespecial Object.< init> 005: astore_2 006: aload_1 ┌──── 007: astore_3 │008: new Object │009: dup │010: invokespecial Object.< init> │011: astore 4 │012: aload_3 └──── 013: astore 5 014: new Object 015: dup 016: invokespecial Object.< init> 017: astore 6 018: aload 5 019: invokevirtual String.length 020: istore 7 021: getstatic System.out 022: iload 7 023: invokevirtual PrintStream.println 024: return

3. 测试用例 3.1. primitive type - no
本文介绍的方法不适合对primitive type进行分析:
  • 所有int类型的值都用BasicValue.INT_VALUE表示,不能对两个不同的值进行区分
  • 所有float类型的值都用BasicValue.FLOAT_VALUE表示,不能对两个不同的值进行区分
  • 所有long类型的值都用BasicValue.LONG_VALUE表示,不能对两个不同的值进行区分
  • 所有double类型的值都用BasicValue.DOUBLE_VALUE表示,不能对两个不同的值进行区分
public class HelloWorld { public void test() { int a = 1; int b = 2; int c = a + b; int d = a - b; int e = c * d; System.out.println(e); } }

输出结果(错误):
[3, 7, 11, 15] 000: iconst_1 001: istore_1 002: iconst_2 ┌──── 003: istore_2 │004: iload_1 │005: iload_2 │006: iadd ├──── 007: istore_3 │008: iload_1 │009: iload_2 │010: isub ├──── 011: istore 4 │012: iload_3 │013: iload 4 │014: imul └──── 015: istore 5 016: getstatic System.out 017: iload 5 018: invokevirtual PrintStream.println 019: return

3.2. return-no
本文介绍的方法也不适用于return语句的判断。在下面的代码中,会提示result局部变量是多余的:
public class HelloWorld { public Object test() { // Local variable result is redundant Object result = new Object(); return result; } }

我觉得,可以使用astore aload areturn的指令组合来识别这种情况,不一定要使用Frame的分析做到。
4. 总结【#yyds干货盘点#Java ASM系列((091)冗余变量分析)】本文内容总结如下:
  • 第一点,如何判断一个变量是否冗余呢?看看local variable当中是否有两个或多个相同的值。
  • 第二点,代码示例,编码实现冗余变量分析。

    推荐阅读