ASM 是非常强大的 JAVA 字节码生成和修改工具,具有性能优异、文档齐全、比较易用等优点。官方网站:http://asm.ow2.org/
要想熟练的使用 ASM,需要对 java 字节码有一定的了解,本文重点对 java 函数的字节码进行介绍。本文部分内容参考官方文档:http://download.forge.objectweb.org/asm/asm4-guide.pdf
1.JAVA 虚拟机执行模型
在 JVM 执行模型里,每个方法都是在线程中执行,而每个线程对应自己的栈,每个栈由帧组成。每个帧对应一个方法调用,每次调用一个方法,
会将新帧压入当前线程的执行栈,当方法返回时(异常退出也是返回),再将这个帧从执行栈弹出。
每个帧主要包括两部分,一个局部变量表和一个操作数栈,关系如下图所示:
这里注意,局部变量表是根据索引访问的列表,类似数组;而操作数栈则是 "后入先出" 的栈,这里非常重要,因为 java 函数的字节码指令基本上都是对这两个数据结构进行操作。
局部变量表和操作数栈的大小取决于方法代码,在编译时计算,并随字节码指令一起写入 class 文件中,
- public int gogo() {
- Log.i("zkw", "hello");
- return 888;
- }
这是一个 java 方法,编译成 class 之后内容如下:
- // access flags 0x1
- public gogo()I
- LDC "zkw"
- LDC "hello"
- INVOKESTATIC android/util/Log.i (Ljava/lang/String;Ljava/lang/String;)I
- POP
- SIPUSH 888
- IRETURN
- MAXSTACK = 2
- MAXLOCALS = 1
最下面两行的 MAXSTACK 和 MAXLOCALS 的值就是操作数栈和局部变量表的大小。
局部变量表和操作数栈中的每个槽 (slot) 可以保存除 long 和 double 之外的任意 java 值,而 long 和 double 需要两个槽,比如向局部变量表储存一个 int 和一个 long,则表中第一个位置是 int 值,第二和第三个位置存的是 long 值。
还有一点需要注意,如果是非静态方法,局部变量表的第 0 个位置为 "this"。
2. 字节代码指令
Java 类型被编译成 class 后,都是用类型描述符表示的,如下图:
方法也同样会被编译成方法描述符,如下:
字节码指令是由操作码和参数组成:
字节码指令分为两种:
还是用上面的代码举例子,我们直接看字节码:
- // access flags 0x1
- public gogo()I
- LDC "zkw"
- LDC "hello"
- INVOKESTATIC android/util/Log.i (Ljava/lang/String;Ljava/lang/String;)I
- POP
- SIPUSH 888
- IRETURN
- MAXSTACK = 2
- MAXLOCALS = 1
LDC 是将参数中的值压入操作数栈,所以前两行执行完,操作数栈应该长这样 [...,"zkw","hello"],前面... 是之前压入的值,
然后 INVOKESTATIC 指令弹出之前压入的参数,然后调用 Log.i 静态方法,最后将 int 结果压入栈,此时操作数栈应该长这样 [...,int 结果]
由于没有使用 Log.i 的返回值,所以直接将返回值从操作数栈 POP 出去,
接下来 SIPUSH 将 888 压入操作数栈,此时栈长这样 [...,888]
然后 IRETURN 从操作数栈弹出 int 值并返回,方法调用结束。
这里我们没有看到对局部变量表的操作,下面稍微修改下 gogo 方法:
- public int gogo() {
- int a = Log.i("zkw", "hello");
- return a;
- }
为了看到如何操作局部变量表,我们获取 Log.i 返回的 int 值,并将其 return,编译之后如下:
- // access flags 0x1
- public gogo()I
- LDC "zkw"
- LDC "hello"
- INVOKESTATIC android/util/Log.i (Ljava/lang/String;Ljava/lang/String;)I
- ISTORE 1
- ILOAD 1
- IRETURN
- MAXSTACK = 2
- MAXLOCALS = 2
当 INVOKESTATIC 指令执行之后,操作数栈为 [...,int 值],局部变量表为 [this]
看到 INVOKESTATIC 之后,多了个 ISTORE 指令,ISTORE 1 指令是弹出操作数栈栈顶的值 (也就是 log.i 的返回值),将其存入局部变量表索引为 1 的位置 (思考一下为什么不是 0),当 ISTORE 执行完,操作数栈为 [...],局部变量表为 [this,int 值]。
然后执行 ILOAD 1,该指令取出局部变量表 1 位置的值,并压入操作数栈,此时操作数栈为 [...int 值],局部变量表为 [this]。
然后 IRETURN 从操作数栈弹出 int 值,并将其 return,执行结束。
3. 栈映射帧
java1.6 之后还引入了栈映射帧,用于加快虚拟机中类验证过程的速度。这个映射帧主要记录每个指令执行前的局部变量表和操作数栈中包含的类型状态。这个帧和所谓的栈帧没有关系,这个映射帧仅仅标示当前局部变量表和操作数栈的状态。
当 jvm 进入一个方法时,根据方法描述符就可以确定初始帧的状态,例如方法 com.demo.Foo.gogo(int a) 的局部变量表的初始状态为 [com.demo.Foo, I],而操作数栈初始状态肯定是空的。所以这个方法的初始帧为 [com.demo.Foo, I],[]
为了节省空间,编译方法时并不会为每条指令生成一个映射帧,事实上,它仅为跳转指令 (包括 if else,try cache 等) 生成映射帧。
为了节省更多空间,对每个需要生成映射帧的地方做压缩,仅仅储存与前一帧的差别,比如与前一帧的状态一样时,使用 F_SAME 助记符,当比前一帧增加了 3 个以内的局部变量时,使用 F_APPEND [],当增加了 3 个以上的局部变量时,使用 F_FULL []。说了这么多可能有点晕了,看例子吧。
我们修改上面的例子,增加一些局部变量和条件判断:
- public int gogo(int c) {
- int a = Log.i("zkw", "hello");
- float f = 0.4f;
- if (a > 0) {
- Log.i("zkw", ">>0");
- } else {
- Log.i("zkw", "<<0");
- }
- return a;
- }
代码中增加了两个局部变量 a 和 f,看看编译后的字节码:
- // access flags 0x1
- public gogo(I)I
- LDC "zkw"
- LDC "hello"
- INVOKESTATIC android/util/Log.i (Ljava/lang/String;Ljava/lang/String;)I
- ISTORE 2
- LDC 0.4
- FSTORE 3
- ILOAD 2
- IFLE L0
- LDC "zkw"
- LDC ">>0"
- INVOKESTATIC android/util/Log.i (Ljava/lang/String;Ljava/lang/String;)I
- POP
- GOTO L1
- L0
- FRAME APPEND [I F]
- LDC "zkw"
- LDC "<<0"
- INVOKESTATIC android/util/Log.i (Ljava/lang/String;Ljava/lang/String;)I
- POP
- L1
- FRAME SAME
- ILOAD 2
- IRETURN
- MAXSTACK = 2
- MAXLOCALS = 4
我们假定这个方法是 com.demo.Foo 类的,那么这个方法的初始帧状态应该是 [com.demo.Foo, I],[],字节码中不会标示初始帧状态。
然后代码继续往下走,我们增加了两个局部变量 int a 和 float f,所以帧状态出现变化,这个变化会在第一个跳转目标里展示出来,请看 L0 下面的 FRAME APPEND [I F],意思是相比于之前的帧状态增加了两个局部变量,类型是 int 和 float,此时帧状态更新成 [com.demo.Foo, I, I, F],[]。
之后遇见了下一个跳转目标 L1,这时候的局部变量没有变化,所以使用 FRAME SAME 标示。
这些 FRAME 指令仅仅是标示帧状态的变化,没有对局部变量表和操作数栈做任何操作,目的是加快 java 虚拟机中类验证过程的速度。
之前说 F_APPEND 是标示增加 3 个之内的帧变化,那 3 个之外呢,我们继续修改 gogo 方法,增加两个局部变量:
- public int gogo(int c) {
- int a = Log.i("zkw", "hello");
- float f = 0.4f;
- short s = 12;
- long l = 10003983839L;
- if (a > 0) {
- Log.i("zkw", ">>0");
- } else {
- Log.i("zkw", "<<0");
- }
- return a;
- }
看到我们增加了 short s 和 long l,看看编译后啥样:
- // access flags 0x1
- public gogo(I)I
- LDC "zkw"
- LDC "hello"
- INVOKESTATIC android/util/Log.i (Ljava/lang/String;Ljava/lang/String;)I
- ISTORE 2
- LDC 0.4
- FSTORE 3
- BIPUSH 12
- ISTORE 4
- LDC 10003983839
- LSTORE 5
- ILOAD 2
- IFLE L0
- LDC "zkw"
- LDC ">>0"
- INVOKESTATIC android/util/Log.i (Ljava/lang/String;Ljava/lang/String;)I
- POP
- GOTO L1
- L0
- FRAME FULL [com/demo/Foo I I F I J] []
- LDC "zkw"
- LDC "<<0"
- INVOKESTATIC android/util/Log.i (Ljava/lang/String;Ljava/lang/String;)I
- POP
- L1
- FRAME SAME
- ILOAD 2
- IRETURN
- MAXSTACK = 2
- MAXLOCALS = 7
看到标红的那行,使用了 FRAME FULL 的指令,后面参数就是完全的局部变量表状态。
来源: http://www.cnblogs.com/coding-way/p/6600647.html