[原创]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++这个表达式 ...
随机推荐
- ThreadLocal模式的原理
在JDK的早期版本中,提供了一种解决多线程并发问题的方案:java.lang.ThreadLocal类.ThreadLocal类在维护变量时,实际使用了当前线程(Thread)中的一个叫做Thread ...
- 分布式缓存技术memcached学习系列(四)—— 一致性hash算法原理
分布式一致性hash算法简介 当你看到"分布式一致性hash算法"这个词时,第一时间可能会问,什么是分布式,什么是一致性,hash又是什么.在分析分布式一致性hash算法原理之前, ...
- 浅谈时间复杂度- 算法衡量标准Big O
写在前面: 今天有一场考试,考到了Big-O的知识点,考到了一道原题,原题的答案我记住了,但实际题目有一些改动导致答案有所改动,为此作者决定重新整理一下复杂度相关知识点 Efficiency and ...
- android 项目更改包名的方法
本文章全文转载: http://www.2cto.com/kf/201304/206747.html 1.在项目上右键,选择android tools->rename application p ...
- java udp 发送小数数字(较难)
代码全部来自:http://825635381.iteye.com/blog/2046882,在这里非常感谢了,我运行测试了下,非常正确,谢谢啊 服务端程序: package udpServer; i ...
- React-intl 实现多语言
前言 React 做国际化,我推荐使用 React-intl , 这个库提供了 React 组件和Api两种方式来格式化日期,数字和字符串等.知道这个库了,那让我们开始使用它 组件用法 为了和Reac ...
- 【异构计算】在Windows下使用OpenCL配置
前言 目前,NVIDIA 和 AMD 的 Windows driver 均有支持OpenCL(NVIDIA 的正式版 driver 是从自195.62 版开始,而 AMD则是从9.11 版开始).NV ...
- HTML学习一
今天主要学习HTML的 table 标签 和 form 标签,作品如下: <html> <head> <title>新增雇员</title> </ ...
- linux挂载本地windows分区或目录
linux挂载本地windows分区或目录 一.linux挂载本地windows硬盘分区 向虚拟机Centos添加本地windows硬盘 注:(添加物理硬盘后,在centos操作会直接写入本地硬盘) ...
- BZOJ 1198: [HNOI2006]军机调度(搜索)
直接暴搜就行了= = CODE: #include<cstdio> #include<iostream> #include<cstring> #include< ...