`
lijingyao8206
  • 浏览: 219270 次
  • 性别: Icon_minigender_2
  • 来自: 杭州
社区版块
存档分类
最新评论

ASM系列四 利用Method 组件动态注入方法逻辑

阅读更多

        这篇继续结合例子来深入了解下Method组件动态变更方法字节码的实现。通过前面一篇,知道ClassVisitor visitMethod()方法可以返回一个MethodVisitor的实例。那么我们也基本可以知道,同ClassVisitor改变类成员一样,MethodVIsistor如果需要改变方法成员,注入逻辑,也可以通过继承MethodVisitor,来编写一个MethodXXXAdapter来实现对于方法逻辑的注入。通过下面的两个例子来介绍下无状态注入和有状态注入方法逻辑的实现。例子主要参考官方文档介绍,大家按照这个思路可以扩展更多种场景的应用。

 

    一、无状态注入

 

        先看一个例子,也是比较常见的一种场景,我们需要给下面这个类的所有方法注入一个计时的逻辑。

    源码如下:

 

package asm.core.methord;

/**
 * Created by yunshen.ljy on 2015/6/29.
 */
public class Time {
    public void myCount() throws Exception {
        int i = 5;
        int j = 10;
        System.out.println(j - i);
    }

    public void myDeal() {
        try {
            int[] myInt = { 1, 2, 3, 4, 5 };
            int f = myInt[10];
            System.out.println(f);
        } catch (ArrayIndexOutOfBoundsException e) {
            e.printStackTrace();
        }
    }
}

         我们目标的class 字节码如下:

 

 

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//

package asm.core.methord;

public class Time {
    public static long timer;

    public Time() {
    }

    public void myCount() throws Exception {
        timer -= System.currentTimeMillis();
        byte i = 5;
        byte j = 10;
        System.out.println(j - i);
        timer += System.currentTimeMillis();
    }

    public void myDeal() {
        timer -= System.currentTimeMillis();

        try {
            int[] e = new int[]{1, 2, 3, 4, 5};
            int f = e[10];
            System.out.println(f);
        } catch (ArrayIndexOutOfBoundsException var3) {
            var3.printStackTrace();
        }

        timer += System.currentTimeMillis();
    }
}

         通过查看字节码结构可以知道,首先我们需要增加一个fieldTime类。然后在除了构造器以外的方法注入计时逻辑的字节码。我们先以第一个方法myCount()为例,用javap工具查看字节码信息如下:

 

 

public void myCount() throws java.lang.Exception;
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=7, locals=3, args_size=1
         0: getstatic     #18                 // Field timer:J
         3: invokestatic  #24                 // Method java/lang/System.currentTimeMillis:()J
         6: lsub
         7: putstatic     #18                 // Field timer:J
        10: iconst_5
        11: istore_1
        12: bipush        10
        14: istore_2
        15: getstatic     #28                 // Field java/lang/System.out:Ljava/io/PrintStream;
        18: iload_2
        19: iload_1
        20: isub
        21: invokevirtual #34                 // Method java/io/PrintStream.println:(I)V
        24: getstatic     #18                 // Field timer:J
        27: invokestatic  #24                 // Method java/lang/System.currentTimeMillis:()J
        30: ladd
        31: putstatic     #18                 // Field timer:J
        34: return
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
           10      25     0  this   Lasm/core/methord/Time;
           12      23     1     i   I
           15      20     2     j   I
      LineNumberTable:
        line 8: 10
        line 9: 12
        line 10: 15
        line 11: 24
    Exceptions:
      throws java.lang.Exception

         从方法的偏移量0 7 是我们的  timer -= System.currentTimeMillis();对应的字节码实现。24 31 timer += System.currentTimeMillis();的字节码实现。基本可以判定,我们需要再方法刚进入的时候先生成timer -= System.currentTimeMillis();的字节码,然后在方法返回return 指令或者是athrow指令之前生成timer += System.currentTimeMillis()的字节码。

 

 

       timer += System.currentTimeMillis()我们可以通过visitCode(方法开始是通过此方法的调用)方法中添加ASM提供的字节码指令生成的几个方法来实现:

 

@Override
        public void visitCode() {
            mv.visitCode();
            mv.visitFieldInsn(Opcodes.GETSTATIC, owner, "timer", "J");
            mv.visitMethodInsn(Opcodes.INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false);
            mv.visitInsn(Opcodes.LSUB);
            mv.visitFieldInsn(Opcodes.PUTSTATIC, owner, "timer", "J");
        }

       timer -= System.currentTimeMillis()需要通过visitInsn(int opcode)方法来完成,遍历所有的操作码来判断我们当前的指令是否是return 或者athrow 。如果是那么前插入我们需要的指令,再继续调用下一层mv.visitInsn(opcode)。代码如下:

 

 

 @Override
        public void visitInsn(int opcode) {
            if ((opcode >= Opcodes.IRETURN && opcode <= Opcodes.RETURN) || opcode == Opcodes.ATHROW) {
                mv.visitFieldInsn(Opcodes.GETSTATIC, owner, "timer", "J");
                mv.visitMethodInsn(Opcodes.INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false);
                mv.visitInsn(Opcodes.LADD);
                mv.visitFieldInsn(Opcodes.PUTSTATIC, owner, "timer", "J");
            }
            mv.visitInsn(opcode);
        }

       那么最后还剩下,需要在class中生成一个timer的属性,如前面ClassVisitor的介绍一样,需要在ClassVisitor 的适配子类中的visitEnd()方法中插入我们的FieldVisitor

 

 

@Override
    public void visitEnd() {
        if (!isInterface) {
            FieldVisitor fv = cv.visitField(Opcodes.ACC_PUBLIC + Opcodes.ACC_STATIC, "timer", "J", null, null);
            if (fv != null) {
                fv.visitEnd();
            }
        }
        cv.visitEnd();
    }

       至此,我们的字节码已经创建和生成完毕,为了健壮性考虑,我们只要再加上是否是Interface的判断,因为接口是没有方法实现体的,并且还要判断,构造器方法中不添加timer计时逻辑。这里我们把需要注入逻辑的Classname通过参数owner传递给MethodVisitor。整体Adapter方法如下:

 

 

package asm.core.methord;

import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.FieldVisitor;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;

/**
 * Created by yunshen.ljy on 2015/6/29.
 */
public class AddTimerAdapter extends ClassVisitor {
    private String owner;
    private boolean isInterface;

    public AddTimerAdapter(ClassVisitor cv) {
        super(Opcodes.ASM4, cv);
    }

    @Override
    public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
        cv.visit(version, access, name, signature, superName, interfaces);
        owner = name;
        isInterface = (access & Opcodes.ACC_INTERFACE) != 0;
    }

    @Override
    public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
        MethodVisitor mv = cv.visitMethod(access, name, desc, signature, exceptions);
        if (!isInterface && mv != null && !name.equals("<init>")) {
            mv = new AddTimerMethodAdapter(mv);
        }
        return mv;
    }

    @Override
    public void visitEnd() {
        if (!isInterface) {
            FieldVisitor fv = cv.visitField(Opcodes.ACC_PUBLIC + Opcodes.ACC_STATIC, "timer", "J", null, null);
            if (fv != null) {
                fv.visitEnd();
            }
        }
        cv.visitEnd();
    }

    class AddTimerMethodAdapter extends MethodVisitor {
        public AddTimerMethodAdapter(MethodVisitor mv) {
            super(Opcodes.ASM4, mv);
        }

        @Override
        public void visitCode() {
//            mv.visitCode();
            mv.visitFieldInsn(Opcodes.GETSTATIC, owner, "timer", "J");
            mv.visitMethodInsn(Opcodes.INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false);
            mv.visitInsn(Opcodes.LSUB);
            mv.visitFieldInsn(Opcodes.PUTSTATIC, owner, "timer", "J");
        }

        @Override
        public void visitInsn(int opcode) {
            if ((opcode >= Opcodes.IRETURN && opcode <= Opcodes.RETURN) || opcode == Opcodes.ATHROW) {
                mv.visitFieldInsn(Opcodes.GETSTATIC, owner, "timer", "J");
                mv.visitMethodInsn(Opcodes.INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false);
                mv.visitInsn(Opcodes.LADD);
                mv.visitFieldInsn(Opcodes.PUTSTATIC, owner, "timer", "J");
            }
            mv.visitInsn(opcode);
        }

        @Override
        public void visitMaxs(int maxStack, int maxLocals) {
            // 手动挡需要计算栈空间,这里两个long型变量的操作需要4个slot
            mv.visitMaxs(maxStack + 4, maxLocals);
        }

    }
}

   二、有状态注入

 

 

        这里的有状态是相对于无状态来说的。刚才的例子中是对于方法绝对偏移量的一种逻辑注入。简单来说就是注入的逻辑不依赖前一个指令的操作或者指令的参数。对于Class文件的所有方法都是相同的逻辑注入。但是如果考虑一种情况,那就是当前需要注入的字节码指令依赖于前面指令的执行结果状态。那么我们就必须存储前面这个指令的状态。

 

       下面这个例子来源于自官方文档中的举例。考虑如下方法:

 

public void myCount(){
        int i = 5;
        int j = 10;
        System.out.println(j - i);
        System.out.println(j + i);
        System.out.println(j + 0);
}

       这里我们知道j+0 的输出结果都是j。那么这里的例子中尝试要让上面的代码去掉+0 操作,也就是需要变成如下的方法:

 

 

public void myCount(){
        byte i = 5;
        byte j = 10;
        System.out.println(j - i);
        System.out.println(j + i);
        System.out.println(j);
 }

      通过查看原方法的字节码信息如下:

 

 

         0: iconst_5
         1: istore_1
         2: bipush        10
         4: istore_2
         5: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
         8: iload_2
         9: iload_1
        10: isub
        11: invokevirtual #3                  // Method java/io/PrintStream.println:(I)V
        14: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
        17: iload_2
        18: iload_1
        19: iadd
        20: invokevirtual #3                  // Method java/io/PrintStream.println:(I)V
        23: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
        26: iload_2
        27: iconst_0
        28: iadd
        29: invokevirtual #3                  // Method java/io/PrintStream.println:(I)V
        32: return

         可以发现iadd 指令是iconst_0 的后置指令。但是我们不能单纯得判断当前字节码指令时iadd或者iconst_0 就直接remove。当然remove的实现方式MethodVisitor ClassVisitor的适配器实现方式相近,都是通过不继续调用mv.visitInsn(opcode);方法的方式。但这里我们需要标记iconst_0指令的状态。iconst_0指令执行时标记一个状态,在下一条指令执行的时候判断状态值,如果下一条命令是iadd那么就直接return掉方法来移除指令。官方实现非常的优雅,这里加了一些注释,方便理解实现。

package asm.core.methord;

import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;

/**
 * Created by yunshen.ljy on 2015/7/1.
 */
public class RemoveAddZeroAdapter extends MethodVisitor {
    private static int SEEN_ICONST_0 = 1;
    protected final static int SEEN_NOTHING = 0;
    protected int state;
    public RemoveAddZeroAdapter(MethodVisitor mv) {
        super(Opcodes.ASM4, mv);
    }

    @Override
    public void visitInsn(int opcode) {
        // 是否检测到前一个指令是ICONST_0
        if (state == SEEN_ICONST_0) {
            // 并且当前指令时iadd
            if (opcode == Opcodes.IADD) {
                // 重新初始化指令状态
                state = SEEN_NOTHING;
                // 移除指令序列
                return;
            }
        }
        visitInsn();
        // 如果当前指令是ICONST_0 记录指令状态,并且直接返回(移除)
        if (opcode == Opcodes.ICONST_0) {
            state = SEEN_ICONST_0;
            return;
        }
        // 继续访问下一条指令
        mv.visitInsn(opcode);
    }

    protected void visitInsn() {
        // 如果最后访问的是SEEN_ICONST_0指令,那么还原指令(因为刚才被移除了)
        if (state == SEEN_ICONST_0) {
            mv.visitInsn(Opcodes.ICONST_0);
        }
        state = SEEN_NOTHING;
    }

}

   这里再补充一下,我们不需要处理StacckMapFrame 以及像前一部分需要计算局部变量表和操作数栈的size,那是因为我们没有增加额外的属性,并且示例中也没有无条件跳转语句等,需要验证的操作。但如果我们要实现更复杂的情况,还需要覆盖visitMaxs方法、visitFrame visitLable方法等。(保证移除指令不会影响其他指令的正常跳转,需要调用visitInsn()方法)

       其实我个人觉得,处理有状态的字节码指令移除、添加、转移还是需要注意各种字节码指令的情况。字节码指令的顺序,上下文,栈信息对于编写一段健壮的ASM 逻辑注入代码都非常关键。有时候其实还是建议先去把注入前后情况的class文件分析一遍,再进行编码。

 

 

 

 

 

0
0
分享到:
评论
2 楼 jzzwy 2017-11-13  
分析的挺好,比官方文档易懂
1 楼 haohaokan123 2015-08-10  
膜拜中!!新手值得收藏的中文API帮助,新手且英文不好的福音,全是中文翻译的,一个很不错的中文API帮助
需要的朋友自己去看看。www.apicx.com

相关推荐

    ASM操作字节码,动态生成Java类class文件

    在`visitMethod`方法中,我们需要在方法开始前(`visitCode`之前)插入切面逻辑,然后继续遍历原方法的字节码,最后在方法结束(`visitEnd`)时添加切面逻辑。 4. 在方法的字节码流中,使用`visitInsn`系列方法插入...

    [字节码系列]ObjectWeb ASM构建Method Monitor

    【字节码系列】ObjectWeb ASM构建Method Monitor 在Java世界中,字节码是运行在JVM(Java虚拟机)上的程序表示形式。它是一种中间语言,允许开发者对Java代码进行低级别的操作,比如性能优化、动态代理、代码混淆等...

    ASM注入器工具

    ASM注入器工具是一种专门设计用于调试和远程注入CALL指令的实用程序。在IT行业中,ASM(Assembly Language)注入是高级编程技术的一种,它涉及到在运行时修改或插入汇编代码到另一个进程,以改变其行为或功能。这种...

    ASM函数监听实现(二)之打印注入函数的参数值

    这篇博客"ASM函数监听实现(二)之打印注入函数的参数值"深入探讨了如何利用ASM库来监控并记录函数调用时的参数值,这对于调试、日志记录以及性能分析等场景非常有用。 首先,我们来看`asmAopClassAdapter.java`。...

    ASM函数监听实现(三)拦截注入函数的参数值 (函数执行前)

    为了在函数执行前拦截参数值,我们需要在`visitInsn`方法中插入自定义逻辑,这通常是在调用`invokeXXX`系列方法之前。 接下来,我们要创建一个`ClassAdapter`,这是ASM提供的一种简化类改造的工具。通过继承`Class...

    远程线程注入asm代码

    在本例中,我们探讨的是如何使用汇编(ASM)代码进行远程线程注入,实现“Hello World”级别的操作。汇编语言是计算机编程的底层语言,它直接对应于机器指令,提供了对硬件的直接控制。 首先,理解远程线程注入的...

    Android-Android无痕埋点框架使用ASM插桩实现

    在Android无痕埋点中,ASM插桩的原理是利用Java字节码注入技术,在运行时动态插入埋点代码。当用户执行特定操作时,如点击按钮或加载页面,ASM会在对应的函数调用前后插入埋点代码,记录用户的行为并上报至服务器。...

    15 - ASM之方法Frame - 简书1

    "ASM之方法Frame" ASM(Abstract Syntax Method)是一种Java bytecode manipulation library,提供了对Java字节码的解析、修改和生成等功能。本文将详细介绍ASM之方法Frame的结构和工作原理。 JVM Architecture ...

    cglib-2.2.jar asm-tree.jar asm-commons.jar asm.jar

    【标题】"cglib-2.2.jar asm-tree.jar asm-commons.jar asm.jar" 提供的是一组用于Java编程的库,它们主要用于实现动态代理和字节码操作。 【描述】"cglib动态代理模式jar包 cglib-2.2.jar asm-tree.jar asm-...

    oracle不使用oracleasm的包配置ASM磁盘配置方法

    ### Oracle 不使用 OracleASM 的包配置 ASM 磁盘配置方法 #### 概述 在 Oracle 数据库系统中,自动存储管理(ASM)是用于管理数据库文件的一种高性能、高可用性的解决方案。通常情况下,ASM 依赖于 Oracle 提供的 ...

    Android-埋点计时Gradle插件利用ASM插入字节码

    在这个插件中,ASM被用来在运行时动态插入代码,记录并打印指定类或注解方法的执行时间。 插件的使用流程大致如下: 1. 配置:在项目的build.gradle文件中引入并配置该Gradle插件,指定需要监控的包名或注解。 2. ...

    ASM4手册中文版.pdf.zip

    ASM4是中国Java开发者常用的一款字节码操作框架ASM的第四个主要版本,它主要用于动态生成和分析Java字节码。ASM是一个低级别的库,可以直接操作和生成类的字节码,这在创建编译器、代码分析工具以及运行时代码修改等...

    logback日志级别动态切换的终极方案(Java ASM使用).doc

    3. 编写Java Agent,利用ASM在应用程序启动时注入代码,动态修改Logger类,使其支持动态日志级别设置和过滤功能。 4. 配置Maven的jar插件,指定agent-class为包含Agent的主类,并设置premain-class属性。 使用ASM...

    asm动态代理jar包

    asm动态代理jar包,是java开发的好的工具类

    ASM4.2 DEMO

    ASM是一个强大的Java字节码操作框架,主要用于动态生成和修改Java类和方法。在ASM 4.2这个DEMO中,我们将深入探讨ASM的核心概念、功能以及如何利用它来实现动态编程。 首先,ASM的核心是它的类访问器和类适应器机制...

    asm动态生成class,并且动态加载执行

    NULL 博文链接:https://name327.iteye.com/blog/1554558

    ASM4使用指南 ASM GUIDE

    ASM4使用指南是为帮助用户掌握该工具的使用方法而编写的详细教程。以下是对ASM4及其使用的一些核心知识点的详细说明: 1. **ASM4介绍**:ASM4是一款强大的建模工具,它支持对复杂软件系统进行抽象建模,通过图形化...

    asm-guide.rar

    5. **动态代理**:利用ASM,开发者可以轻松创建动态代理类,实现方法调用的拦截和增强,这对于实现事务管理、权限控制等功能具有重要意义。 6. **JVM行为研究**:ASM为开发者提供了直接与JVM字节码交互的手段,有助...

    asm操作指南(中文)

    - **性能提升**:利用ASM的字节码增强技术可以在反射调用时显著提升性能,接近直接调用的速度。 - **代码生成**:ASM可以用于自动生成Java类,特别适用于需要动态生成大量代码的情况。 - **代码优化**:通过对字节码...

    asm 6.0 工具集

    这套工具包括了ASM、ASM-Util、ASM-TREE和ASM-ANALYSIS等组件,每个都有其特定的功能和用途。 **ASM库**是核心部分,提供低级别的API来生成和解析Java字节码。ASM库允许开发者直接操作字节码,创建和修改类,甚至在...

Global site tag (gtag.js) - Google Analytics