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. C# 得到本机局域网IP地址

    C#获取本机IP搜集整理7种方法:http://blog.csdn.net/frombegintoend/article/details/7713097 使用IPHostEntry获取本机局域网地址 ...

  2. 一个案例深入Python中的__new__和__init__

    准备 在Python中,一切皆对象. 既然一切皆对象,那么类也是对象,我们暂且称之为 类对象.来个简单例子(本篇文章的所有案例都是运行在Python3.4中): class foo(): pass p ...

  3. js精要之继承

    // 继承object.prototype的方法 // hasOwnProperty() //检查是否存在一个给定名字的自有属性 // propertyIsEnumerable() // 检查一个自有 ...

  4. Import Statements 导入语句

    Syntax of an Import Statement 导入语句的语法 An import statement allows clients to tell the engine which mo ...

  5. Maven入门,Maven项目的创建,nexus 2.x搭建私服以及Maven多模块项目创建

    maven的了解做一个总结,以便日后查阅, 若有不足之处,还望指出,学无止境 当然也能起到入门效果. 一,搭建maven私服 1.工具 a. Nexus 2.5.1-01 b. Maven 3.3.9 ...

  6. Vue2.0源码阅读笔记--生命周期

    一.Vue2.0的生命周期 Vue2.0的整个生命周期有八个:分别是 1.beforeCreate,2.created,3.beforeMount,4.mounted,5.beforeUpdate,6 ...

  7. 使用光盘iso实现Linux操作系统的自动安装部署

    前边写了一篇使用 PXE 的方式批量安装操作系统,不是任何时候任何地方都有环境来通过 PXE 方式来进行安装.如果此时需要通过光盘安装,默认的情况下是通过交互式方式进行安装,其实也可以通过 kicks ...

  8. loadrunner工作原理

  9. BZOJ 4089:[Sdoi2015]graft(SDOI 2015 Round 2 Day 2)

    别人家的神选系列,我只会做这道题QAQ 题目描述: 给定一颗树,加上k条边,将n个点染色,相邻两点不同,记颜色为i的又ti个,求$$\frac{\sum_{i=1}^{n} \frac{ti}{i}} ...

  10. js中可转bool为false的值

    number :0为false,其他都为true. string:""为false,其他字符串都为真,PS: " "空格为true,"false&qu ...