[原创]ASM动态修改JAVA函数之函数字节码初探
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函数之函数字节码初探的更多相关文章
- 深入理解java:1.2. 字节码执行引擎
执行引擎是Java虚拟机的核心组成部分之一. 首先,想想C++和Java在编译和运行时到底有啥不一样? 下图左边,C++发布的就是机器指令, 而下图右边Java发布的是字节码,字节码在运行时通过JVM ...
- 深入浅出Java探针技术2---java字节码生成框架ASM、Javassist和byte buddy的使用
目前Java字节码生成框架大致有ASM.Javassist和byte buddy三种 ASM框架介绍及使用 1.ASM介绍 ASM是一种Java字节码操控框架,能够以二进制形式修改已有的类或是生成类, ...
- [19/04/20-星期六] Java的动态性_字节码操作(Javassist类库(jar包),assist:帮助、援助)
一.概念 [基本] /** * */ package cn.sxt.jvm; import javassist.ClassPool; import javassist.CtClass; import ...
- 《java虚拟机》----虚拟机字节码执行引擎
No1: 物理机的执行引擎是直接建立在处理器.硬件.指令集合操作系统层面上的,而虚拟机的执行引擎则是由自己实现的,因此可以自行制定指令集与执行引擎的结构体系,并且能够执行那些不被硬件直接支持的指令集格 ...
- Java方法调用的字节码指令学习
Java1.8环境下,我们在编写程序时会进行各种方法调用,虚拟机在执行这些调用的时候会用到不同的字节码指令,共有如下五种: invokespecial:调用私有实例方法: invokestatic:调 ...
- java虚拟机(十四)--字节码指令
字节码指令其实是很重要的,在之前学习String等内容,深入到字节码层面很容易找到答案,而不是只是在网上寻找答案,还有可能是错误的. PS:本文基于jdk1.8 首先写个简单的类: public cl ...
- JAVA虚拟机:虚拟机字节码执行引擎
“虚拟机”是一个相对“物理机”的概念,这两种机器都有代码执行能力. 物理机的执行引擎是直接建立在处理器.硬件.指令集和操作系统层面上的. 虚拟机的执行引擎由自己实现,自行制定指令集与执行引擎的结构体系 ...
- java面试题jvm字节码的加载与卸载
虚拟机把描述类的数据从class文件加载到内存,并对数据进行校验,转换分析和初始化,最终形成可以被虚拟节直接使用的JAVA类型,这就是虚拟机的类加载机制. 类从被加载到虚拟机内存到卸载出内存的生命周期 ...
- java中i=i++字节码分析
原文出处: Ticmy 1 2 int i = 0; i = i++; 结果还是0为什么? 程序的执行顺序是这样的:因为++在后面,所以先使用i,"使用"的含义就是i++这个表达式 ...
随机推荐
- MYSQL:基础—存储过程
MYSQL:基础-存储过程 快速入门 理解: 迄今为止,我们学过的大多数SQL语句都是针对一个或多个表的单条语句.但是并不是所有的操作都是可以用一条语句来完成的,经常有一些操作是需要多条语句配合才能完 ...
- Python之日期与时间处理模块(date和datetime)
本节内容 前言 相关术语的解释 时间的表现形式 time模块 datetime模块 时间格式码 总结 前言 在开发工作中,我们经常需要用到日期与时间,如: 作为日志信息的内容输出 计算某个功能的执行时 ...
- java_监控工具jvisualvm
转:使用 VisualVM 进行性能分析及调优 启动:jvisualvm 首先到JDK安装目录/bin目录下,双击jvisualvm.exe文件启动 需要注意的是:当OS所在分区是FAT格式时,Vis ...
- ubuntu ssh-keygen Permission denied
ubuntu下生成github上的ssh keys,执行: ssh-keygen 直接执行: sudo chown user1:user1 /home/user1/.ssh -R 成功生成.
- C# 类型和变量
C# 中的类型有两种:值类型 (value type) 和引用类型 (reference type).值类型的变量直接包含它们的数据,而引用类型的变量存储对它们的数据的引用,后者称为对象.对于引用类型 ...
- 用phpcms如何将静态页面制作成企业网站(中)
上篇博客中讲到了该修改网页的中间部分 中间的内容是这样的,有标题和内容,里面的内容被代码替代,运行起来就这样的 里面的内容就可以在后台管理那里添加 再来看代码部分 <div class=&quo ...
- 关于NoClassDefFoundError和ClassNotFoundException异常
java.lang.NoClassDefFoundError 和 java.lang.ClassNotFoundException 都是 Java 语言定义的标准异常.从异常类的名称看似乎都跟类的定义 ...
- YoMail 邮箱客户端的社会化之路,起于邮箱,不止于邮件
你还记不记得上一次用邮箱处理私人事务是什么时候?从什么时候开始邮箱于你而言,唯一功能沦为了收取各种网站的验证信息? 电子邮件实际上非常适合于工作上使用,比起其他通信工具,或者社会化媒体,电子邮件在工作 ...
- 转:KVC与KVO机制
由于Objective-C是基于Smalltalk进行设计的,所以它具有动态加载.动态绑定等特性.Key-value coding (KVC) 和 key-value observing (KVO) ...
- Tomcat使用Memcached Session Manager管理Session
Tomcat使用Memcached Session Manager管理Session 废话不多说,直接进入主题.项目使用阿里云负载均衡+ECS服务器集群进行部署,Tomcat使用8.5版本.阿里云负载 ...