Java字节码修改

Java字节码插桩

1、什么是字节码

Java中的字节码也就是通过java编译器产生的.class文件,Class文件作为虚拟机所执行的平台中立文件,内部结构设计的十分的清晰,每一个Class文件都对应着唯一一个类或接口的定义信息。每个Class文件都由以8位为单位的字节流组成,下面是一个Class文件中所包括的项,在Class文件中,各项按照严格顺序连续存放,中间没有任何填充或者对齐作为各项间的分隔符号。各个字段的含义可以自行查看。

ClassFile { 
    u4 magic; 
    u2 minor_version; 
    u2 major_version; 
    u2 constant_pool_count; 
    cp_info constant_pool[constant_pool_count-1]; 
    u2 access_flags; 
    u2 this_class; 
    u2 super_class; 
    u2 interfaces_count; 
    u2 interfaces[interfaces_count]; 
    u2 fields_count; 
    field_info fields[fields_count]; 
    u2 methods_count; 
    method_info methods[methods_count]; 
    u2 attributes_count; 
    attribute_info attributes[attributes_count]; 
}

2、字节码插桩

对字节码插桩就是修改由java编译器生成的class文件,目前比较常用且上手简单的字节码修改工具就是ASM,它是一个Java字节码层次的处理框架。它可以直接对class文件进行增删改的操作。

3、ASM介绍

3.1 ASM的设计模式

ASM 通过树这种数据结构来表示复杂的字节码结构,并利用 Push 模型来对树进行遍历,在遍历过程中对字节码进行修改。所谓的 Push 模型类似于简单的 Visitor 设计模式,因为需要处理字节码结构是固定的,所以只需要提供 Visitor 接口。

​ 访问者模式主要包含被访问的元素以及访问者两部分,元素一般是不同的类型,不同的访问者对于这些元素的操作一般也是不同的,访问者模式使得用户可以在不修改现有系统的情况下扩展系统的功能,为这些不同类型的元素增加新的操作。

3.2 实际流程

​ 首先,在ASM中提供了一个ClassReader类,这个类可以直接由字节数组或由 class 文件间接的获得字节码数据,它能正确的分析字节码,构建出抽象的树在内存中表示字节码。然后它会调用自己的accept方法,这个方法需要一个实现了 ClassVisitor接口的对象实例作为参数,按照一定的次序依次去调用ClassVisitor接口的各个方法,实现对class文件的解析和修改。ClassVisitor接口中的访问方法次序如下:

			classVisitor.visit(readInt(items[1] - 7), access, name, signature,superClass, interfaces);
            classVisitor.visitSource(sourceFile, sourceDebug);
            classVisitor.visitOuterClass(enclosingOwner, enclosingName,enclosingDesc);
            classVisitor.visitAnnotation(readUTF8(v, c);
            classVisitor.visitTypeAnnotation(context.typeRef,context.typePath, readUTF8(v, c);
            classVisitor.visitAttribute(attributes);
            classVisitor.visitInnerClass(readClass(v, c),readClass(v + 2, c), readUTF8(v + 4, c),readUnsignedShort(v + 6));
            classVisitor.visitField(access, name, desc,signature, value);
            classVisitor.visitMethod(context.access,context.name, context.desc, signature, exceptions);
            classVisitor.visitEnd();

​ 我们可以实现很多个ClassVisitor,每个访问者只需要关注一些简单的字节码操作即可,而无须关注字节码的字节偏移,可以减小耦合性。各个ClassVisitor通过职责链模式连接在一起,依次对字节码进行处理。

ASM 的最终的目的是生成可以被正常装载的 class 文件,因此其框架结构为客户提供了一个生成字节码的工具类 —— ClassWriter。它实现了 ClassVisitor接口,而且含有一个 toByteArray()函数,返回生成的字节码的字节流,将字节流写回文件即可生产调整后的 class 文件。ClassWriter可以接受两个参数(一个是ClassReader类,另一个表示自动计算方法的堆栈帧map)表示构造一个新的ClassWriter类并启用 "主要是添加 "字节码转换的优化功能;如果只接受一个参数(表示自动计算方法的堆栈帧map)就只是构造一个新的ClassWriter类。在toByteArray()函数中,它会计算class字节的真实大小以及字节偏移,这些都是可以自动计算的,使用者无需关注。ClassWriter一般都是作为职责链的终点,把所有的visit事件的先后调用,最终转换成字节码的位置调整和修改。

​ 整个生产/转换class文件的过程如下图所示,起点和终点分别是CLASSREADERCLASS文件解析器)和ClassWriterclass事件序列转换到class字节码),中间的过程由若干个自定义的事件过滤器组成。

在这里插入图片描述

​ 字节码转换的时序图如下所示。通过CLASSREADERaccept方法先通过一系列适配器的visitxx方法,在通过ClassWriter的对应访问方法,直到最后返回修改后的字节码。

在这里插入图片描述

3.3 demo

public class InstrumentClassVisitor extends ClassVisitor{
	
    public InstrumentClassVisitor(ClassVisitor cv) {
        //获取上一个职责链的访问者
        super(Opcodes.ASM8, cv);
    }
    //下面表示可以对类进行visitxxx并修改字节码
    @Override
    public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
    }
     @Override
    public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
}
		//by表示源字节
		ClassReader cr = new ClassReader(by);
        ClassWriter cw = new ClassWriter(cr, ClassWriter.COMPUTE_FRAMES);

        // 添加适配器,对类进行修改和过滤,可以添加多个适配器进行类修改,通过职责链模式连在一起即可
        InstrumentClassVisitor icv = new InstrumentClassVisitor(cw);

        // 实际执行插桩,第二个参数表示在解析字节码时的可选操作
		// ClassReader.EXPAND_FRAMES
		// ClassReader.SKIP_DEBUG
		// ClassReader.SKIP_CODE
		// ClassReader.SKIP_FRAMES
        cr.accept(icv, 0);
		//返回修改之后的字节码
        return cw.toByteArray();
//ClassReader生产者生产的class字节码bytes可以被ClassWriter直接消费
byte[] b1 = ...;
ClassWriter cw = new ClassWriter(0);
ClassReader cr = new ClassReader(b1);
cr.accept(cw, 0);
byte[] b2 = cw.toByteArray(); //这里的b2与b1表示同一个类且值一样

//ClassReader生产者生产的class字节码bytes可以先被继承自ClassVisitor的自定义类过滤,最后被ClassWriter消费

byte[] b1 = ...;
ClassWriter cw = new ClassWriter(0);
// cv forwards all events to cw
ClassVisitor cv = new ChangeVersionAdapter (cw) { };
ClassReader cr = new ClassReader(b1);
cr.accept(cv, 0);
byte[] b2 = cw.toByteArray(); //这里的b2与b1表示同一个类但值不一样

​ 到现在,我们可以根据自己的需求对class文件进行修改,但是这需要我们对一些jvm的指令有所了解。这里推荐可以直接使用IDEA的一个插件ASM Bytecode Outline,通过它我们可以就可以知道源码对应的asm代码怎么写,就可以很快实现需求。

​ 我们可以采用离线修改字节码的方式对类进行修改,同时也有在线修改的方式,比如premain,可以在启动的时候在类加载之前对类进行拦截并修改类(采用java代理的技术实现)。如果应用已经启动了,那么如何修改呢?比如正常的生产环境下,一般不会开启代理功能,但是在发生问题时,我们不希望停止应用就能够动态的去修改一些类的行为,以帮助排查问题,这在应用启动前是无法确定的。 为解决运行时启动代理类的问题,JavaSE6开始,提供了在应用程序的VM启动后在动态添加代理的方式,即agentmain方式。 与permain类似,agentmain方式同样需要提供一个agentjar

​ 不过如此设计的再运行时进行代理有个问题——如何在应用程序启动之后再开启代理程序呢?JDK6中提供了JavaTools API,其中AttachAPI可以满足这个需求。Attach API中的VirtualMachine代表一个运行中的VM。其提供了loadAgent()方法,可以在运行时动态加载一个代理jar

参考

ASM官网

Java虚拟机指令集

参考

职责链的访问顺序

public byte[] instrument(byte[] inb) {
        // 读取文件字节流
        ClassReader cr = new ClassReader(inb);
        ClassWriter cw = new ClassWriter(cr, ClassWriter.COMPUTE_FRAMES);
        
        SpecialLabelClassVisitor slcv = new SpecialLabelClassVisitor(cw);
        InstrumentClassVisitor icv = new InstrumentClassVisitor(slcv);
        ModifyExitClassVisitor mcv = new ModifyExitClassVisitor(icv);

        // 实际执行
        cr.accept(mcv, 0);
        return cw.toByteArray();
    }

实际上,对于上面这个例子而言。实际的访问顺序时slcv, icv, mcv ,职责链的终点时cw。
比如说对一个类进行访问的时候,我们自定义了三个访问者(slcv,icv,mcv),在对类进行访问的时候,asm会按照一定的顺序去调用访问方法。同时,在调用访问方法的时候,会按照职责链一个个去调用访问者的对应方法。
比如说对于visitMethod而言,它会先调用slcv的visitMethod,再调用icv的visitMethod,再调用mcv的visitMethod。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

meilidekcl

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值