[原创]ASM动态修改JAVA函数之函数字节码初探

ASM是很是强大的JAVA字节码生成和修改工具,具备性能优异、文档齐全、比较易用等优势。官方网站:http://asm.ow2.org/html

要想熟练的使用ASM,须要对java字节码有必定的了解,本文重点对java函数的字节码进行介绍。本文部份内容参考官方文档:http://download.forge.objectweb.org/asm/asm4-guide.pdfjava

1.JAVA虚拟机执行模型android

在JVM执行模型里,每一个方法都是在线程中执行,而每一个线程对应本身的栈,每一个栈由帧组成。每一个帧对应一个方法调用,每次调用一个方法,web

会将新帧压入当前线程的执行栈,当方法返回时(异常退出也是返回),再将这个帧从执行栈弹出。数组

每一个帧主要包括两部分,一个局部变量表和一个操做数栈,关系以下图所示:数据结构

这里注意,局部变量表是根据索引访问的列表,相似数组;而操做数栈则是“后入先出”的栈,这里很是重要,由于java函数的字节码指令基本上都是对这两个数据结构进行操做。jvm

局部变量表和操做数栈的大小取决于方法代码,在编译时计算,并随字节码指令一块儿写入class文件中,ide

    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后,都是用类型描述符表示的,以下图:

方法也一样会被编译成方法描述符,以下:

字节码指令是由操做码和参数组成:

  • 操做码是一个字节代码名,由助记符号表示,例如操做码0,对应的是NOP,表示无任何操做的指令;操做码21,对应ILOAD,表示读取局部变量表某个位置的int值。
  • 参数是储存在编译后代码中的静态值。

字节码指令分为两种:

  • 一种是用来在局部变量表和操做数栈之间传送值的。好比FSTORE i指令从操做数栈弹出一个float值,并存入索引i对应的局部变量表中。而DLOAD j指令则是读取局部变量表中索引j和j+1对应的double值(思考一下为何是j和j+1),并将它压入操做数栈。
  • 另外一部分字节码指令仅用来处理操做数栈。好比xADD(x对应I、L、F、D)指令从操做数栈弹出两个数值作加法,而后将结果压入栈。再好比INVOKESTATIC用于调用静态方法,该指令会从操做数栈弹出n+1个值(n是静态方法的n个参数,+1对应目标对象),并压回方法调用的结果。

仍是用上面的代码举例子,咱们直接看字节码:

  // 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