本文屬于Java ASM系列三:Tree API當中的一篇。
在StackOverflow上有這樣一個問題:Find all instructions belonging to a specific method-call。在解決方法當中,就用到了SourceInterpreter
和SourceValue
類。在本文當中,我們將這個問題和解決思路簡單地進行介紹。
1. 實現思路
1.1. 回顧SourceInterpreter
首先,我們來回顧一下SourceInterpreter
的作用:記錄指令(instruction)與Frame當中值(SourceValue
)的關聯關系。
public class HelloWorld { public void test(int a, int b) { int c = a + b; System.out.println(c); }}
那么,借助于SourceInterpreter
類查看test
方法內某一條instuction將某一個SourceValue
加載(入棧)到Frame上:
test:(II)V000: iload_1 {[], [], [], []} | {}001: iload_2 {[], [], [], []} | {[iload_1]}002: iadd {[], [], [], []} | {[iload_1], [iload_2]}003: istore_3 {[], [], [], []} | {[iadd]}004: getstatic System.out {[], [], [], [istore_3]} | {}005: iload_3 {[], [], [], [istore_3]} | {[getstatic System.out]}006: invokevirtual PrintStream.println {[], [], [], [istore_3]} | {[getstatic System.out], [iload_3]}007: return {[], [], [], [istore_3]} | {}================================================================
由此,我們可以模仿一下,進一步記錄某一條instuction將某一個SourceValue
從Frame上消耗掉(出棧):
test:(II)V000: iload_1 {[], [], [], []} | {}001: iload_2 {[], [], [], []} | {[iadd]}002: iadd {[], [], [], []} | {[iadd], [iadd]}003: istore_3 {[], [], [], []} | {[istore_3]}004: getstatic System.out {[], [], [], []} | {}005: iload_3 {[], [], [], []} | {[invokevirtual PrintStream.println]}006: invokevirtual PrintStream.println {[], [], [], []} | {[invokevirtual PrintStream.println], [invokevirtual PrintStream.println]}007: return {[], [], [], []} | {}================================================================
那么,我們怎么實現這樣一個功能呢?用一個DestinationInterpreter
類來實現。
1.2. DestinationInterpreter
首先,我們編寫一個DestinationInterpreter
類。對于這個類,我們從兩點來把握:
- 第一點,抽象功能。要實現什么的功能呢?記錄operand stack上某一個元素是被哪一個指令(instruction)消耗掉(出棧)的。這個功能正好與
SourceInterpreter
類的功能相反。 - 第二點,具體實現。如何進行編碼實現呢?
DestinationInterpreter
類,是模仿著SourceInterpreter
類實現的。
import org.objectweb.asm.Opcodes;import org.objectweb.asm.Type;import org.objectweb.asm.tree.*;import org.objectweb.asm.tree.analysis.AnalyzerException;import org.objectweb.asm.tree.analysis.Interpreter;import org.objectweb.asm.tree.analysis.SourceValue;import java.util.HashSet;import java.util.List;public class DestinationInterpreter extends Interpreter implements Opcodes { public DestinationInterpreter() { super(ASM9); if (getClass() != DestinationInterpreter.class) { throw new IllegalStateException(); } } protected DestinationInterpreter(final int api) { super(api); } @Override public SourceValue newValue(Type type) { if (type == Type.VOID_TYPE) { return null; } return new SourceValue(type == null ? 1 : type.getSize(), new HashSet<>()); } @Override public SourceValue newOperation(AbstractInsnNode insn) { int size; switch (insn.getOpcode()) { case LCONST_0: case LCONST_1: case DCONST_0: case DCONST_1: size = 2; break; case LDC: Object value = ((LdcInsnNode) insn).cst; size = value instanceof Long || value instanceof Double ? 2 : 1; break; case GETSTATIC: size = Type.getType(((FieldInsnNode) insn).desc).getSize(); break; default: size = 1; break; } return new SourceValue(size, new HashSet<>()); } @Override public SourceValue copyOperation(AbstractInsnNode insn, SourceValue value) throws AnalyzerException { int opcode = insn.getOpcode(); if (opcode >= ISTORE && opcode <= ASTORE) { value.insns.add(insn); } return new SourceValue(value.getSize(), new HashSet<>()); } @Override public SourceValue unaryOperation(AbstractInsnNode insn, SourceValue value) throws AnalyzerException { value.insns.add(insn); int size; switch (insn.getOpcode()) { case LNEG: case DNEG: case I2L: case I2D: case L2D: case F2L: case F2D: case D2L: size = 2; break; case GETFIELD: size = Type.getType(((FieldInsnNode) insn).desc).getSize(); break; default: size = 1; break; } return new SourceValue(size, new HashSet<>()); } @Override public SourceValue binaryOperation(AbstractInsnNode insn, SourceValue value1, SourceValue value2) throws AnalyzerException { value1.insns.add(insn); value2.insns.add(insn); int size; switch (insn.getOpcode()) { case LALOAD: case DALOAD: case LADD: case DADD: case LSUB: case DSUB: case LMUL: case DMUL: case LDIV: case DDIV: case LREM: case DREM: case LSHL: case LSHR: case LUSHR: case LAND: case LOR: case LXOR: size = 2; break; default: size = 1; break; } return new SourceValue(size, new HashSet<>()); } @Override public SourceValue ternaryOperation(AbstractInsnNode insn, SourceValue value1, SourceValue value2, SourceValue value3) throws AnalyzerException { value1.insns.add(insn); value2.insns.add(insn); value3.insns.add(insn); return new SourceValue(1, new HashSet<>()); } @Override public SourceValue naryOperation(AbstractInsnNode insn, List extends SourceValue> values) throws AnalyzerException { if (values != null) { for (SourceValue v : values) { v.insns.add(insn); } } int size; int opcode = insn.getOpcode(); if (opcode == MULTIANEWARRAY) { size = 1; } else if (opcode == INVOKEDYNAMIC) { size = Type.getReturnType(((InvokeDynamicInsnNode) insn).desc).getSize(); } else { size = Type.getReturnType(((MethodInsnNode) insn).desc).getSize(); } return new SourceValue(size, new HashSet<>()); } @Override public void returnOperation(AbstractInsnNode insn, SourceValue value, SourceValue expected) throws AnalyzerException { // Nothing to do. } @Override public SourceValue merge(final SourceValue value1, final SourceValue value2) { return new SourceValue(Math.min(value1.size, value2.size), new HashSet<>()); }}
2. 示例:查找相關的指令
2.1. 預期目標
假如有一個HelloWorld
類:
public class HelloWorld { public void test() { int a = 1; int b = 2; int c = 3; int d = 4; int sum = add(a, b); int diff = sub(c, d); int result = mul(sum, diff); System.out.println(result); } public int add(int a, int b) { return a + b; } public int sub(int a, int b) { return a - b; } public int mul(int a, int b) { return a * b; }}
我們對test
方法進行分析,想實現的預期目標:如果想刪除對某一個方法的調用,有哪些指令會變得無效呢?
例如,想刪除add(a, b)
方法調用,直接使用int sum = 10;
,那么a
和b
兩個變量就不需要了:
public class HelloWorld { public void test() { int c = 3; int d = 4; int sum = 10; int diff = sub(c, d); int result = mul(sum, diff); System.out.println(result); } // ...}
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.ArrayList;import java.util.Arrays;import java.util.List;public class RelatedInstructionDiagnosis { public static int[] diagnose(String className, MethodNode mn, int insnIndex) throws AnalyzerException { // 第一步,判斷insnIndex范圍是否合理 InsnList instructions = mn.instructions; int size = instructions.size(); if (insnIndex < 0 || insnIndex >= size) { String message = String.format("the insnIndex argument should in range [0, %d]", size - 1); throw new IllegalArgumentException(message); } // 第二步,獲取兩個Frame Frame[] sourceFrames = getSourceFrames(className, mn); Frame[] destinationFrames = getDestinationFrames(className, mn); // 第三步,循環處理,所有結果記錄到這個intArrayList變量中 TIntArrayList intArrayList = new TIntArrayList(); // 循環tmpInsnList List tmpInsnList = new ArrayList<>(); AbstractInsnNode insnNode = instructions.get(insnIndex); tmpInsnList.add(insnNode); for (int i = 0; i < tmpInsnList.size(); i++) { AbstractInsnNode currentNode = tmpInsnList.get(i); int opcode = currentNode.getOpcode(); int index = instructions.indexOf(currentNode); intArrayList.add(index); // 第一種情況,處理load相關的opcode情況 Frame srcFrame = sourceFrames[index]; if (opcode >= Opcodes.ILOAD && opcode <= Opcodes.ALOAD) { VarInsnNode varInsnNode = (VarInsnNode) currentNode; int localIndex = varInsnNode.var; SourceValue value = srcFrame.getLocal(localIndex); for (AbstractInsnNode insn : value.insns) { if (!tmpInsnList.contains(insn)) { tmpInsnList.add(insn); } } } // 第二種情況,從dstFrame到srcFrame查找 Frame dstFrame = destinationFrames[index]; int stackSize = dstFrame.getStackSize(); for (int j = 0; j < stackSize; j++) { SourceValue value = dstFrame.getStack(j); if (value.insns.contains(currentNode)) { for (AbstractInsnNode insn : srcFrame.getStack(j).insns) { if (!tmpInsnList.contains(insn)) { tmpInsnList.add(insn); } } } } } // 第四步,將intArrayList變量轉換成int[],并進行排序 int[] array = intArrayList.toNativeArray(); Arrays.sort(array); return array; } private static Frame[] getSourceFrames(String className, MethodNode mn) throws AnalyzerException { Analyzer analyzer = new Analyzer<>(new SourceInterpreter()); return analyzer.analyze(className, mn); } private static Frame[] getDestinationFrames(String className, MethodNode mn) throws AnalyzerException { Analyzer analyzer = new Analyzer<>(new DestinationInterpreter()); return analyzer.analyze(className, mn); }}
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 methods = cn.methods; MethodNode mn = methods.get(1); int[] array = RelatedInstructionDiagnosis.diagnose(cn.name, mn, 11); System.out.println(Arrays.toString(array)); BoxDrawingUtils.printInstructionLinks(mn.instructions, array); }}
輸出結果:
[0, 1, 2, 3, 8, 9, 10, 11]┌──── 000: iconst_1├──── 001: istore_1├──── 002: iconst_2├──── 003: istore_2│ 004: iconst_3│ 005: istore_3│ 006: iconst_4│ 007: istore 4├──── 008: aload_0├──── 009: iload_1├──── 010: iload_2└──── 011: invokevirtual HelloWorld.add 012: istore 5 013: aload_0 014: iload_3 015: iload 4 016: invokevirtual HelloWorld.sub 017: istore 6 018: aload_0 019: iload 5 020: iload 6 021: invokevirtual HelloWorld.mul 022: istore 7 023: getstatic System.out 024: iload 7 025: invokevirtual PrintStream.println 026: return
3. 測試用例
3.1. switch-yes
public class HelloWorld { public void test(int val) { double doubleValue = Math.random(); switch (val) { case 10: System.out.println("val = 10"); break; case 20: System.out.println("val = 20"); break; case 30: System.out.println("val = 30"); break; case 40: System.out.println("val = 40"); break; default: System.out.println("val is unknown"); } System.out.println(doubleValue); // 分析這條語句 }}
輸出結果:
[0, 1, 29, 30, 31]┌──── 000: invokestatic Math.random├──── 001: dstore_2│ 002: iload_1│ 003: lookupswitch {│ 10: L0│ 20: L1│ 30: L2│ 40: L3│ default: L4│ }│ 004: L0│ 005: getstatic System.out│ 006: ldc "val = 10"│ 007: invokevirtual PrintStream.println│ 008: goto L5│ 009: L1│ 010: getstatic System.out│ 011: ldc "val = 20"│ 012: invokevirtual PrintStream.println│ 013: goto L5│ 014: L2│ 015: getstatic System.out│ 016: ldc "val = 30"│ 017: invokevirtual PrintStream.println│ 018: goto L5│ 019: L3│ 020: getstatic System.out│ 021: ldc "val = 40"│ 022: invokevirtual PrintStream.println│ 023: goto L5│ 024: L4│ 025: getstatic System.out│ 026: ldc "val is unknown"│ 027: invokevirtual PrintStream.println│ 028: L5├──── 029: getstatic System.out├──── 030: dload_2└──── 031: invokevirtual PrintStream.println 032: return
3.2. switch-no
在當前的解決思路中,還不能很好的處理創建對象(new
)的情況。
public class HelloWorld { public void test(int val) { Random rand = new Random(); // 注意,這里創建了Random對象 double doubleValue = rand.nextDouble(); switch (val) { case 10: System.out.println("val = 10"); break; case 20: System.out.println("val = 20"); break; case 30: System.out.println("val = 30"); break; case 40: System.out.println("val = 40"); break; default: System.out.println("val is unknown"); } System.out.println(doubleValue); // 分析這條語句 }}
輸出結果:
[0, 3, 4, 5, 6, 34, 35, 36]┌──── 000: new Random // new + dup + invokespecial三個指令一起來創建對象│ 001: dup // 當前的方法只分析出了new,而沒有分析出dup和invokespecial│ 002: invokespecial Random.├──── 003: astore_2├──── 004: aload_2├──── 005: invokevirtual Random.nextDouble├──── 006: dstore_3│ 007: iload_1│ 008: lookupswitch {│ 10: L0│ 20: L1│ 30: L2│ 40: L3│ default: L4│ }│ 009: L0│ 010: getstatic System.out│ 011: ldc "val = 10"│ 012: invokevirtual PrintStream.println│ 013: goto L5│ 014: L1│ 015: getstatic System.out│ 016: ldc "val = 20"│ 017: invokevirtual PrintStream.println│ 018: goto L5│ 019: L2│ 020: getstatic System.out│ 021: ldc "val = 30"│ 022: invokevirtual PrintStream.println│ 023: goto L5│ 024: L3│ 025: getstatic System.out│ 026: ldc "val = 40"│ 027: invokevirtual PrintStream.println│ 028: goto L5│ 029: L4│ 030: getstatic System.out│ 031: ldc "val is unknown"│ 032: invokevirtual PrintStream.println│ 033: L5├──── 034: getstatic System.out├──── 035: dload_3└──── 036: invokevirtual PrintStream.println 037: return
4. 總結
本文內容總結如下:
- 第一點,模擬
SourceInterpreter
類來編寫一個DestinationInterpreter
類,這兩個類的作用是相反的。 - 第二點,結合
SourceInterpreter
和DestinationInterpreter
類,用來查找相關的指令。