Contents
  1. 1. 基于栈的指令-字节码指令
  2. 2. Class文件的结构
  3. 3. 插桩方案

​ 所谓的字节码增强,即通过程序对.class文件进行修改。因为字节码较java代码更加统一化,所以修改.class文件也更加的方便.

增强目标:在每次使用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函数。

基于栈的指令-字节码指令

​ 在广大的语言世界中,有两种指令集。一种是像汇编一样的基于寄存器的指令,另一种是像字节码这样的基于栈的指令。

​ 在字节码指令集中,有的指令后需要操作数(有的操作数是一个字节,有的是两个字节甚至更多),而有的不需要。几乎所有的字节码指令都会对操作栈产生影响:或是将一些数据压入栈中,或是将数据弹出,或是弹出一些数进行操作后再将新结果压入栈中。这不废话嘛

什么是操作数栈?

操作数栈是一种栈,栈中存储着一些数据,即操作数。它是栈帧中的一个数据结构。起到了和汇编中寄存器一样的作用。

​ 个人感觉,基于栈的指令还是很好懂的。(不过在找到官方的字节码指令文档之前还是很迷糊,文档中给出了每条指令对操作栈的影响)

​ 以下是一些字节码指令的例子。

  • 字节码指令

    • iconst_1

      当调用字节码指令iconst_1 时,会将常量1压栈

    • iadd

      当调用字节码指令iadd时,会将栈顶两个数据弹栈,如果都是int型变量则会进行加法运算,最后将运算结果压入栈中。

      详细的介绍可以看《深入理解Java虚拟机》8.5.3基于栈的解释器执行过程。

  • 调用函数的字节码指令

    invokedynamic invokeinterface invokespecial invokestatic invokevirtual

    其中invokespecial是调用构造函数的指令 (也可以调用一些其他的特殊函数 )invokestatic指令则可以调用静态函数。

    这两个函数后都要跟两字节的操作数,用来表示被调用函数在常量池中的索引。同时,如果想要成功调用函数,需要保证操作栈栈顶有调用函数所需的参数。

    • invokespecial

      ​ 调用函数前要依次将,调用该函数的对象的引用、所有的函数参数(从左到右)压栈。

      ​ 实例在new之后并不会初始化,只是分配了一块空间,还需要调用<init>函数(即构造函数),该函数由invokespecial指令调用

    • invokestatic

      ​ 调用函数前要依次将,从左到右所有的函数参数压栈。

  • 其他的一些会用到的指令

    • dup

      ​ 将一个和操作栈栈顶相同的数据压栈。

    • newnewarrayanewarraymultianewarray

      ​ new指令会将新生成的对象引用压栈。

      ​ newarray指令会将长度出栈,数组引用压栈。创建基本类型数组时调用。

      ​ anewarray创建引用类型数组时调用。

      ​ multianewarray创建多维数组时调用。

    • astoreaload

      ​ 这两条指令会影响操作栈和本地变量表。非匿名对象都会存在本地变量表中。

      astore后跟一个大于3的单字节操作数,执行后将弹出栈顶数据,存入指定位置的本地变量表。

      小于等于3的使用astore_0astore_1astore_2astore_3

      aload后跟一个大于3的单字节操作数,执行后将指定位置的本地变量表存入栈顶。

      小于等于3的使用aload_0aload_1aload_2aload_3

  • 一些字节码和java代码对应的例子。

1
2
3
4
5
6
7
8
9
10
11
12
13
 0 new #3 <T>
3 dup
4 invokespecial #4 <T.<init>>
7 astore_1
8 aload_1
9 bipush 10
11 putfield #2 <T.a>
14 return

public void fun1() {
T t = new T();
t.a = 10;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
 0 new #3 <T>
3 dup
4 invokespecial #4 <T.<init>>
7 astore_1
8 aload_1
9 bipush 10
11 putfield #2 <T.a>
14 invokestatic #5 <java/lang/Thread.currentThread>
17 aload_1
18 invokestatic #6 <NativeC._new>
21 return

public void fun1() {
T t = new T();
t.a = 10;
NativeC._new(Thread.currentThread(), t);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
 0 new #3 <T>
3 dup
4 invokespecial #4 <T.<init>>
7 astore_1
8 aload_1
9 bipush 10
11 putfield #2 <T.a>
14 new #3 <T>
17 dup
18 invokespecial #4 <T.<init>>
21 astore_2
22 aload_2
23 bipush 100
25 putfield #2 <T.a>
28 return

public void fun1() {
T t = new T();
t.a = 10;
T t2 = new T();
t2.a = 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
 0 new #3 <T>
3 dup
4 invokespecial #4 <T.<init>>
7 astore_1
8 aload_1
9 bipush 10
11 putfield #2 <T.a>
14 invokestatic #5 <java/lang/Thread.currentThread>
17 aload_1
18 invokestatic #6 <NativeC._new>
21 new #3 <T>
24 dup
25 invokespecial #4 <T.<init>>
28 astore_2
29 aload_2
30 bipush 100
32 putfield #2 <T.a>
35 invokestatic #5 <java/lang/Thread.currentThread>
38 aload_2
39 invokestatic #6 <NativeC._new>
42 return

public void fun1() {
T t = new T();
t.a = 10;
NativeC._new(Thread.currentThread(), t);

T t2 = new T();
t2.a = 100;
NativeC._new(Thread.currentThread(), t2);
}
1
2
3
4
5
6
7
8
9
0 new #3 <T>
3 dup
4 invokespecial #4 <T.<init>>
7 astore_1
8 return

public void fun1() {
T t = new T();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
 0 new #3 <T>
3 dup
4 invokespecial #4 <T.<init>>
7 astore_1
8 invokestatic #5 <java/lang/Thread.currentThread>
11 aload_1
12 invokestatic #6 <NativeC._new>
15 return

public void fun1() {
T t = new T();
NativeC._new(Thread.currentThread(), t);
}

1
2
3
4
5
6
7
8
9
0 new #3 <T>
3 dup
4 invokespecial #4 <T.<init>>
7 pop
8 return

public void fun1() {
new T();
}
1
2
3
4
5
6
7
8
9
10
 0 invokestatic #3 <java/lang/Thread.currentThread>
3 new #4 <T>
6 dup
7 invokespecial #5 <T.<init>>
10 invokestatic #6 <NativeC._new>
13 return

public void fun1() {
NativeC._new(Thread.currentThread(), new T());
}
1
2
3
4
5
6
7
8
9
10
11
12
13
 0 new #3 <T>
3 dup
4 invokespecial #4 <T.<init>>
7 astore_1
8 aload_1
9 invokestatic #5 <java/lang/Thread.currentThread>
12 invokestatic #6 <NativeC._new>
15 return

public void fun1() {
T t = new T();
NativeC._new(t,Thread.currentThread());
}
1
2
3
4
5
6
7
8
9
10
 0 new #3 <T>
3 dup
4 invokespecial #4 <T.<init>>
7 invokestatic #5 <java/lang/Thread.currentThread>
10 invokestatic #6 <NativeC._new>
13 return

public void fun1() {
NativeC._new(new T(),Thread.currentThread());
}

Class文件的结构

​ .class文件是将java文件编译后生成的,其中包含了类的各种信息。详细内容可见《深入理解JAVA虚拟机》第六章内容。

​ 根据本次增强目标,我们需要在每一个实例创建后,调用提前写好的static native方法 newObj(该方法作为NativeC.java的静态函数,调用时需要在常量池中可以找到),并将该实例作为函数参数传入,只需要了解Class文件中方法表 、常量池的部分即可。

  • 关于方法表的要求

    方法表项是个复杂的结构,其包含如访问标记、描述符、属性表集合等信息,而我们的字节码就存储在属性表中。属性表中提供给方法表使用的属性有很多,其中我们需要使用到的为Code。

    • code属性

      ​ code属性中有很多信息,其中我们涉及到的有:max_stack(方法中允许使用的操作栈层数),LocalVariableTable(方法中的局部变量表),以及最重要的code(字节码)。

    • 要求

      1. 因为需要自己插入新的代码,会影响操作栈层数,必须保证max_stack够
      2. 需要找到新实例生成的代码,并用到该实例,调用native方法,需要清楚字节码指令的执行方式。
  • 关于常量池的要求(其实在后续代码中也没有用到常量池,javassist工具偷偷的修改了文件的常量池)

    ​ 假设待增强文件为A.class,如果想要能够在A的代码中调用其他类的函数,需要将这个函数的字符引用加入到常量池中(即CONSTANT_Methodref_info),而因为该常量需要存有两个引用,一是指向声明该函数的类的描述符(即CONSTANT_Class_info),二是指向名称及类型描述符(即CONSTANT_NameAndType),所以理论上如果要能调用该native函数需要添加这三项到常量池

    下图为函数的字符引用信息,通过jclasslib工具得到,右侧上下分别为声明该函数的类的描述符指向名称及类型描述符。#64,#65代表他们在常量池中的索引位

    constant_info1

插桩方案

  • 使用javassist工具,遍历方法中的字节码,找到创建实例的指令。

  • 调整操作栈顶的数据,使得可以调用native函数

  • 插入字节码指令。

Contents
  1. 1. 基于栈的指令-字节码指令
  2. 2. Class文件的结构
  3. 3. 插桩方案