java插桩简介

什么是插桩


最简单的理解就是通过某种策略,在源代码的基础上替换或者插入另外一些代码。

应用场景


方法监控耗时、性能分析、获取运行时信息、动态调试、热修复。

常用框架


  1. javaassist

特点:简单,性能比asm低
是一个开源的分析、编辑和创建java字节码的类库。性能消耗较⼤大,使⽤用容易。

  1. asm

特点:复杂,性能高,一般更为常用
是一个轻量及java字节码操作框架,直接涉及到JVM底层的操作和指令,性能高,功能丰富

  1. BCEL

这是Apache Software Fundation的jakarta项目的一部分。BCEL它可以让你深入JVM汇编语言进行类的操作的细节。

插桩原理

java 5之后新加了一个包java.lang.instrument,基本上大多数的动态修改类都是在此基础上实现的
(基于jvmti,小部分可能直接使用jvmti实现)
它把 Java 的 instrument 功能从本地代码中解放出来,使之可以用 Java 代码的方式解决问题。

Instrumentation 的基本功能

java.lang.instrument包的具体实现,依赖于 JVMTI
入口类是 java.lang.instrument的 Instrumentation
JVMTI(Java Virtual Machine Tool Interface)是一套由 Java 虚拟机提供的,为 JVM 相关的工具提供的本地编程接口集合。
Instrumentation 的最大作用,就是类定义动态改变和操作。

Instrumentation 用法

使用Instrumentation 代理,主要分为以下几个步骤

编写 premain 函数

编写一个 Java 类,包含如下两个方法当中的任何一个

public static void premain(String agentArgs, Instrumentation inst);  [1]
public static void premain(String agentArgs); [2]

其中[1]执行的优先级是高于[2]的。

在这个permain函数中,开发者是可以对加载的类进行各种操作。

agentArgs 是 premain 函数得到的程序参数,随同 “– javaagent”一起传入。与 main 函数不同的是,这个参数是一个字符串而不是一个字符串数组,如果程序参数有多个,程序将自行解析这个字符串。

Inst 是一个 java.lang.instrument.Instrumentation 的实例,由 JVM 自动传入。
java.lang.instrument.Instrumentation 是 instrument 包中定义的一个接口,也是这个包的核心部分
集中了其中几乎所有的功能方法,例如类定义的转换和操作等等

jar 文件打包

将这个 Java 类打包成一个 jar 文件
并在其中的 manifest 属性当中加入" Premain-Class"来指定步骤 1 当中编写的那个带有 premain 的 Java 类。

运行

用如下方式运行带有 Instrumentation 的 Java 程序:

java -javaagent:jar 文件的位置 [= 传入 premain 的参数 ]

实际例子(打印以下加载的类)

  1. 首先定义 MANIFEST.MF 文件
Manifest-Version: 1.0
Can-Redefine-Classes: true
Can-Retransform-Classes: true
Premain-Class: qunar.tc.Main
  1. 创建一个Premain-Class 指定的类,类中包含 premain 方法:
    public class Main {

  public static void premain(String agentArgs, Instrumentation inst) {
    System.out.println("agentArgs : " + agentArgs);
    inst.addTransformer(new ClassFileTransformer() {
      @Override
      public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,
          ProtectionDomain protectionDomain, byte[] classfileBuffer)
          throws IllegalClassFormatException {
        System.out.println("premain load Class     :" + className);
        return classfileBuffer;
      }
    }, true);
  }
}

3 使用maven或者gradle打包成jar,包含刚刚定义的MANIFEST.MF 文件和包含premain函数的类。
4 使用参数 -javaagent:/jar包路径=[agentArgs 参数] 启动目标程序。
运行之后可以看到打印了许多加载的类。

和插桩有什么联系

我们可以看到获取了一个Instrumentation实例,这个是最重要的一个类。

常用的api

  1. void addTransformer(ClassFileTransformer transformer, boolean canRetransform);
    增加一个Class 文件的转换器,转换器用于改变 Class 二进制流的数据,参数 canRetransform 设置是否允许重新转换。

  2. void redefineClasses(ClassDefinition... definitions) hrows ClassNotFoundException, UnmodifiableClassException;
    在类加载之前,重新定义 Class 文件,ClassDefinition 表示对一个类新的定义,如果在类加载之后,需要使用 retransformClasses 方法重新定义。

  3. boolean removeTransformer(ClassFileTransformer transformer)
    删除一个类转换器

  4. void retransformClasses(Class<?>... classes) throws UnmodifiableClassException
    在类加载之后,重新定义 Class。这个很重要,该方法是1.6 之后加入的,事实上,该方法是 update 了一个类。

  5. void appendToBootstrapClassLoaderSearch(JarFile jarfile)
    添加jar文件到BootstrapClassLoader中

  6. void appendToSystemClassLoaderSearch(JarFile jarfile)
    添加jar 文件到 system class loader.

  7. Class[] getAllLoadedClasses()
    获取加载的所有类数组。

与其结合的类是ClassFileTransformer接口。
这个接口只有一个方法

byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer)

通过这个接口就可以将原本的类字节码替换,从而实现想要的目标。

attach方法

插桩的jar包已经弄好,接下来就是需要attach目标JVM,让目标JVM加载此jar包,从而实现插桩
主要包括三种方法

  1. 静态的attach,侵入大,需要目标JVM指定javaagent参数
  2. 动态attach,侵入小,目标JVM完全无感知
  3. 直接使用JVMTI接口

指定javaagent参数

启动java程序时,指定agent所在的lib
java -javaagent:agent.jar Main

动态attach

使用VirtualMachine进行attach

  public static void main(String[] args) throws Exception
  {  
    VirtualMachine vm = null;  
    String agentjarpath = "agent.jar"; //agentjar路径  
    vm = VirtualMachine.attach("1024");//目标JVM的进程ID(PID)  
    vm.loadAgent(agentjarpath, "This is Args to the Agent.");  
    vm.detach();  
  }

直接使用jvmti

前两种方法都是基于JVMTI提供的Java接口,JVMTI是底层实现,所以是可以通过编写c代码实现插桩

怎样修改代码

上面都是讲怎么样实现可以动态加载代码
那么怎样才能动态修改class文件,这时候就需要asm了
因为class文件是及其复杂的,手动修改是基本不能实现的

基本实现

使用asm+ClassFileTransformer类就可以实现修改原class文件。

asm core 包使用

主要是使用asm的core包实现修改类。

ClassVisitor 类

asm使用visiter模式修改类

ClassReader 类

这个类会将 .class 文件读入到 ClassReader 中的字节数组中
它的 accept 方法接受一个 ClassVisitor 实现类,并按照顺序调用 ClassVisitor 中的方法

ClassWriter 类

ClassWriter 是一个 ClassVisitor 的子类,是和 ClassReader 对应的类
ClassReader 是将 .class 文件读入到一个字节数组中,ClassWriter 是将修改后的类的字节码内容以字节数组的形式输出。

2.4 MethodVisitor & AdviceAdapter

MethodVisitor 是一个抽象类,当 ASM 的 ClassReader 读取到 Method 时就转入 MethodVisitor 接口处理。
AdviceAdapter 是 MethodVisitor 的子类,使用 AdviceAdapter 可以更方便的修改方法的字节码。

操作流程

需要创建一个 ClassReader 对象,将 .class 文件的内容读入到一个字节数组中
然后需要一个 ClassWriter 的对象将操作之后的字节码的字节数组回写
需要事件过滤器 ClassVisitor。在调用 ClassVisitor 的某些方法时会产生一个新的 XXXVisitor 对象,当我们需要修改对应的内容时只要实现自己的 XXXVisitor 并返回就可以了

总结

主要是使用Instrumentation接口+asm实现了动态修改类,从而实现插桩。
实现步骤 :

  1. 获取Instrumentation对象
    两种方法

方法一

启动应用时,指定javaagent参数,将包含premain方法的agent包指定。
局限较大,必须启动应用程序时指定agent包。

方法二

使用VirtualMachine进行attach,优点是对应用无侵入。

  1. 修改class文件

增加一个ClassFileTransformer类,使用asm修改字节码,实现修改逻辑
使用premain传入的Instrumentation实例的addTransformer方法,添加上一步骤的ClassFileTransformer
调用Instrumentation实例的retransformClasses方法,类重新加载,实现插桩

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。
禁止转载,如需转载请通过简信或评论联系作者。