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

方法也同样会被编译成方法描述符,如下:

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

  • 操作码是一个字节代码名,由助记符号表示,例如操作码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

[原创]ASM动态修改JAVA函数之函数字节码初探的更多相关文章

  1. 深入理解java:1.2. 字节码执行引擎

    执行引擎是Java虚拟机的核心组成部分之一. 首先,想想C++和Java在编译和运行时到底有啥不一样? 下图左边,C++发布的就是机器指令, 而下图右边Java发布的是字节码,字节码在运行时通过JVM ...

  2. 深入浅出Java探针技术2---java字节码生成框架ASM、Javassist和byte buddy的使用

    目前Java字节码生成框架大致有ASM.Javassist和byte buddy三种 ASM框架介绍及使用 1.ASM介绍 ASM是一种Java字节码操控框架,能够以二进制形式修改已有的类或是生成类, ...

  3. [19/04/20-星期六] Java的动态性_字节码操作(Javassist类库(jar包),assist:帮助、援助)

    一.概念 [基本] /** * */ package cn.sxt.jvm; import javassist.ClassPool; import javassist.CtClass; import ...

  4. 《java虚拟机》----虚拟机字节码执行引擎

    No1: 物理机的执行引擎是直接建立在处理器.硬件.指令集合操作系统层面上的,而虚拟机的执行引擎则是由自己实现的,因此可以自行制定指令集与执行引擎的结构体系,并且能够执行那些不被硬件直接支持的指令集格 ...

  5. Java方法调用的字节码指令学习

    Java1.8环境下,我们在编写程序时会进行各种方法调用,虚拟机在执行这些调用的时候会用到不同的字节码指令,共有如下五种: invokespecial:调用私有实例方法: invokestatic:调 ...

  6. java虚拟机(十四)--字节码指令

    字节码指令其实是很重要的,在之前学习String等内容,深入到字节码层面很容易找到答案,而不是只是在网上寻找答案,还有可能是错误的. PS:本文基于jdk1.8 首先写个简单的类: public cl ...

  7. JAVA虚拟机:虚拟机字节码执行引擎

    “虚拟机”是一个相对“物理机”的概念,这两种机器都有代码执行能力. 物理机的执行引擎是直接建立在处理器.硬件.指令集和操作系统层面上的. 虚拟机的执行引擎由自己实现,自行制定指令集与执行引擎的结构体系 ...

  8. java面试题jvm字节码的加载与卸载

    虚拟机把描述类的数据从class文件加载到内存,并对数据进行校验,转换分析和初始化,最终形成可以被虚拟节直接使用的JAVA类型,这就是虚拟机的类加载机制. 类从被加载到虚拟机内存到卸载出内存的生命周期 ...

  9. java中i=i++字节码分析

    原文出处: Ticmy 1 2 int i = 0; i = i++; 结果还是0为什么? 程序的执行顺序是这样的:因为++在后面,所以先使用i,"使用"的含义就是i++这个表达式 ...

随机推荐

  1. iOS网络层设计感想

    App的开发无外乎从网络端获取数据显示在屏幕上,数据做些缓存或者持久化,所以网络层极为重要.原来只是把AFNetwork二次封装了一下,使得调用变得很简单,并没有深层次的考虑一些问题. 前言 参考: ...

  2. 【.net 深呼吸】项目中是否有必要删去多余的引用

    很多大伙伴们常常会苦思一个问题:项目代码中用不到的引用,是不是应该删除,以避免代码在编译后存在太多的无意义引用? 其实,这个问题,你完全可以自己去应证的,咋应证呢?知道反射吗,对了,只要你知道这玩意儿 ...

  3. redis集群原理

    redis是单线程,但是一般的作为缓存使用的话,redis足够了,因为它的读写速度太快了.   官方的一个简单测试: 测试完成了50个并发执行100000个请求. 设置和获取的值是一个256字节字符串 ...

  4. GCD 多线程 ---的记录 iOS

    先写一个GCD static UserInfoVoModel *userInfoShare = nil; +(instancetype)shareUserInfoVoModel { static di ...

  5. java多线程四种实现模板

    假设一个项目拥有三块独立代码块,需要执行,什么时候用多线程? 这些代码块某些时候需要同时运行,彼此独立,那么需要用到多线程操作更快... 这里把模板放在这里,需要用的时候寻找合适的来选用. 总体分为两 ...

  6. 使用批处理根据项目工程文件生成Nuget包并发布(支持.NET Core)

    最近在使用之前自己编写的批处理给.NET Core项目打包时出问题了,发现之前的脚本根本不适用了,折腾了半天,总算解决了.因此在这里分享下经验,并且奉上整理好的脚本. Nuget包这里就不多介绍了,需 ...

  7. 数组&&函数数组

    数组:一次性定义多个同类型的变量,数组在 内存中存储空间必须是连续的(查询比较快)定义数组: int a[]; int[] a;分配空间: a=new int[5]; 自动为数组元素赋以默认值 a[0 ...

  8. 2011 Multi-University Training Contest 1 - Host by HNU

    A.A + B problem(待填坑) B.Cat VS Dog(二分图匹配) 喜欢cat和喜欢dog的人构成了二分图,如果两个人有冲突则连一条边,则问题转化为二分图最大点独立集问题.ans=n-最 ...

  9. 响应式布局中的CSS相对量

    一个响应式布局,要能够根据设备屏幕尺寸的改变,动态的调整页面内容,展现不同的设计风格. 在进行响应式的 CSS 代码编写过程中,经常会用到一些相对尺寸,以达到相对定位的目的.例如,常见的响应式布局中需 ...

  10. struts2接收参数的5种方法

    以下形式中最常用的是前两种 1. 使用Action的属性: 在action 里面定义要接收的参数,并提供相应的setter,getter,和提交参数的名称一致, 并不用做数据类型的转换相应提交方式可以 ...