Contents
  1. 1. 读取类文件
  2. 2. 获取class的方法表
  3. 3. 定位实例生成的指令
    1. 3.1. 类实例
    2. 3.2. 数组实例
  4. 4. 插入代码

字节码增强(一)在理论上分析了插桩的方案,本篇博客将介绍如何使用javassist工具库,来完成插桩。

增强目标:在每次使用new关键字创建实例后,调用提前写好的native函数,将实例的引用o、变量名objNamenew的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);
}

需要处理的问题:

  1. 找到new关键字的位置及其在java代码中的行数
  2. 找到变量名
  3. 插入函数调用,调用native函数。

插桩方案

  • 使用javassist工具,遍历方法中的字节码,找到创建实例的指令。
  • 调整操作栈顶的数据,使得可以调用native函数
  • 插入字节码指令。

读取类文件

​ javassist库提供了ClassFile类,该类按照.class文件的结构进行了封装,只需将.class文件数据传入即可使用。

1
2
3
4
5
BufferedInputStream fin = new BufferedInputStream(new FileInputStream(inputClassFileName));
//inputClassFileName 即输入的.class文件的文件名
ClassFile classFile = new ClassFile(new DataInputStream(fin));
//ClassFile实例的创建与读文件差别不大。
//之后即可通过classFile对象来获取类文件中的信息。如版本号、类名、成员变量、方法、常量池等。

获取class的方法表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//MethodInfo 是javassist中的类,封装了类文件中的方法表。
//获取类文件中的所有方法,包括构造函数。
List<MethodInfo> list = classFile.getMethods();
Iterator<MethodInfo> it = list.iterator();
while (it.hasNext()) {
MethodInfo method = it.next();
// 获取方法的code表,其中有代码,和本地变量表等各种信息。
CodeAttribute codeAttribute = (CodeAttribute) method.getAttribute(CodeAttribute.tag);
//localVariableAttribute 为提前声明的全局变量。
// 获取方法的本地变量表,如果没有使用javac -g 编译java文件,不会产生变量表
localVariableAttribute = (LocalVariableAttribute) codeAttribute.getAttribute(LocalVariableAttribute.tag);
try {
instrument(method); // instrument 对方法进行字节码增强
} 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
// MethodInfo的getcodeAttribute函数可以拿到方法的字节码
CodeAttribute ca = method.getCodeAttribute();
// 通过迭代器可以遍历字节码指令。
// CodeIterator的迭代方式并不是一个字节一个字节迭代,而是一条指令一条指令地迭代。遍历下一个时,字节码指令和其后面的操作数会被当做一个项跳过
CodeIterator ci = ca.iterator();

// 注:在插桩之前需要对函数的最大操作栈深进行设置。
// 本次插桩需要插入函数调用,参数个数为3个,为了保证参数可以压入操作栈,需要将栈加深3层
ca.setMaxStack(ca.getMaxStack() + 3); // To prevent insufficient stack depth.
// print method bytecode
while (ci.hasNext()) {
// index的数值为字节码的索引位置。
int index = ci.next(); // the position of next opcode
// 变量op的数值为字节码的数值。
int op = ci.byteAt(index); // get the value of opcode
//遍历字节码,找到需要的指令,然后进行插入。
//...
}

类实例

​ 一个实例的正常创建过程是这样的

1
2
3
4
5
6
7
8
9
10
11
12
//首先new指令创建出实例引用,此时栈顶会有一个引用
0 new #3 <T>
// dup 指令会将栈顶再次压栈,此时栈顶有两个引用
3 dup
// 调用该引用的构造函数,栈顶会弹出一个引用来调用函数,然后在构造函数内对实例进行一系列操作。
4 invokespecial #4 <T.<init>>
// 如果这个变量是本地变量,使用这个指令将栈顶的引用弹出并存储到本地变量表。存储在表的索引位置根据指令后的数字决定。
7 astore_1

//java 代码如下
T t = new T();

我们需要做的是,在invokespecial之后插入几条命令,调用native函数,而在调用前我们需要在操作栈中设置好函数参数。

注:在构造函数的第一句会调用父类构造函数,也会有invokespecial <init>指令,此时不需要再进行插桩。调用父类构造函数时需要使用this引用,所以在之前会调用aload_0this引用入栈。

​ 而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
// 调用指令创建,#6为类信息在常量池的编号,二维int型数组
4 multianewarray #6 <[[I> dim 2
8 astore_2
9 bipush 100
/*
Array Type atype
T_BOOLEAN 4
T_CHAR 5
T_FLOAT 6
T_DOUBLE 7
T_BYTE 8
T_SHORT 9
T_INT 10
T_LONG 11
*/
11 newarray 10 (int)
13 astore_3
//java 代码如下
int[][] 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
/**
*
* @param ci 字节码迭代器
* @param index INVOKESPECIAL NEWARRAY MULTIANEWARRAY ANEWARRAY 指令的索引
* @param step index+step的位置为插入代码的位置
* @param method 需要增强的方法
* @param methodName 增强的方法的名称
* @throws BadBytecode
*/
private static void insertNativeC(CodeIterator ci, int index, int step, MethodInfo method, String methodName) throws BadBytecode {
Bytecode bytecode = new Bytecode(classFile.getConstPool());
// stackInObj函数 将引用入栈,如果是局部变量则返回在局部变量表中的位置,否则返回-1
int positionInLocalVariableTable = stackInObj(ci, index, step, bytecode);

String objName;
if (positionInLocalVariableTable > 0 && localVariableAttribute != null) {
objName = localVariableAttribute.variableName(positionInLocalVariableTable);

} else {
objName = "withoutName";
}
//压入bipush指令与操作数:代码行数
bytecode.addOpcode(BIPUSH);
bytecode.add(method.getLineNumber(index));
//压入ldc指令,指向变量名字符串
bytecode.addLdc(objName);
//参数入栈完毕,插入函数调用
bytecode.addInvokestatic("NativeC", methodName, "(Ljava/lang/Object;ILjava/lang/String;)V");
byte[] bytes = bytecode.get();
//将bytecode中的缓存插入字节码中
ci.insert(bytes);

}
/**
* 将初始化的实例对象压入栈
*
* @param ci
* @param index
* @param step
* @param bytecode
* @throws BadBytecode
*/
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();//skip astore
// 插入aload指令,根据局部变量表中的位置使用不同的指令
bytecode.addAload(position);
return position;
} else {
// 如果是匿名对象,无法从本地变量表中找到引用,所以使用DUP指令,将栈顶的引用重复入栈。
bytecode.addOpcode(Opcode.DUP);
return -1;
}
}
Contents
  1. 1. 读取类文件
  2. 2. 获取class的方法表
  3. 3. 定位实例生成的指令
    1. 3.1. 类实例
    2. 3.2. 数组实例
  4. 4. 插入代码