字节码增强(一) 在理论上分析了插桩的方案,本篇博客将介绍如何使用javassist工具库,来完成插桩。
增强目标: 在每次使用new
关键字创建实例后,调用提前写好的native函数,将实例的引用o
、变量名objName
、new
的java代码行数lineNumber
作为参数传入。
这个navtive函数长这个样子:
1 2 3 4 5 public class NativeC { public static native void newObj (Object o ,int lineNumber,String objName) ; public static native void newArr (Object o ,int lineNumber,String objName) ; }
需要处理的问题:
找到new
关键字的位置及其在java代码中的行数
找到变量名
插入函数调用,调用native函数。
插桩方案
使用javassist工具,遍历方法中的字节码,找到创建实例的指令。
调整操作栈顶的数据,使得可以调用native函数
插入字节码指令。
读取类文件 javassist库提供了ClassFile
类,该类按照.class文件的结构进行了封装,只需将.class文件数据传入即可使用。
1 2 3 4 5 BufferedInputStream fin = new BufferedInputStream (new FileInputStream (inputClassFileName));ClassFile classFile = new ClassFile (new DataInputStream (fin));
获取class的方法表 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 List<MethodInfo> list = classFile.getMethods(); Iterator<MethodInfo> it = list.iterator(); while (it.hasNext()) { MethodInfo method = it.next(); CodeAttribute codeAttribute = (CodeAttribute) method.getAttribute(CodeAttribute.tag); localVariableAttribute = (LocalVariableAttribute) codeAttribute.getAttribute(LocalVariableAttribute.tag); try { instrument(method); } catch (BadBytecode badBytecode ) { badBytecode.printStackTrace(); } }
定位实例生成的指令 实例的创建分为如下几类:
类实例(即引用类型变量)
查找invokespecial <init>
因为实例的创建需要调用构造函数,所以我放弃了查找new
指令,而是查找调用构造函数的指令INVOKESPECIAL <init>
指令(其中<init>是invokespecial
指令后的操作数,指构造函数)。
在实际字节码文件中,所有的函数都有各自的数字序号标签(存在常量池中),跟在invokespecial命令后的为函数的标签而非函数名。
基本类型数组
查找newarray
引用类型数组
查找anewarray
多维数组
查找multianewarray
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 CodeAttribute ca = method.getCodeAttribute();CodeIterator ci = ca.iterator();ca.setMaxStack(ca.getMaxStack() + 3 ); while (ci.hasNext()) { int index = ci.next(); int op = ci.byteAt(index); }
类实例 一个实例的正常创建过程是这样的
1 2 3 4 5 6 7 8 9 10 11 12 0 new #3 <T>3 dup4 invokespecial #4 <T.<init>>7 astore_1T t = new T ();
我们需要做的是,在invokespecial之后插入几条命令,调用native函数,而在调用前我们需要在操作栈中设置好函数参数。
注:在构造函数的第一句会调用父类构造函数,也会有invokespecial <init>
指令,此时不需要再进行插桩。调用父类构造函数时需要使用this
引用,所以在之前会调用aload_0
将this
引用入栈。
而new
出来的实例一定会有new
指令,所以需要在找到invokespecial <init>
指令后进行判断,是否真的是创建的实例。
数组实例 数组对象的创建并不像实例创建那么复杂,只需要设置好操作栈然后调用指令即可。不像类实例会出现其他的情况,关于数组实例的插桩直接找指令即可。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 0 bipush 10 2 bipush 11 4 multianewarray #6 <[[I> dim 2 8 astore_2 9 bipush 100 11 newarray 10 (int )13 astore_3int [][] a = new int [10 ][11 ];int []b = new int [100 ];
插入代码 因为需要插入的代码一致性很高,所以可以作为一个函数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 private static void insertNativeC (CodeIterator ci, int index, int step, MethodInfo method, String methodName) throws BadBytecode { Bytecode bytecode = new Bytecode (classFile.getConstPool()); int positionInLocalVariableTable = stackInObj(ci, index, step, bytecode); String objName; if (positionInLocalVariableTable > 0 && localVariableAttribute != null ) { objName = localVariableAttribute.variableName(positionInLocalVariableTable); } else { objName = "withoutName" ; } bytecode.addOpcode(BIPUSH); bytecode.add(method.getLineNumber(index)); bytecode.addLdc(objName); bytecode.addInvokestatic("NativeC" , methodName, "(Ljava/lang/Object;ILjava/lang/String;)V" ); byte [] bytes = bytecode.get(); ci.insert(bytes); } private static int stackInObj (CodeIterator ci, int index, int step, Bytecode bytecode) throws BadBytecode { int position = isAnonymousInit(ci, index, step); if (position >= 0 ) { ci.next(); bytecode.addAload(position); return position; } else { bytecode.addOpcode(Opcode.DUP); return -1 ; } }