实现一个javaagent需要几步?

 最近在学习IAST技术,IAST的核心其实就是插桩,在java语言下,用到的技术就是javaagent,由于之前没有接触过javaaent,正好整理一下。

在介绍javaagent之前,我想有必要向大家介绍一下JVMTI,因为javaagent是基于这个技术实现的

JVMTI

JVMTI(JVM Tool Interface)是 Java 虚拟机所提供的 native 编程接口,JVMTI可以用来开发并监控JVM,可以查看JVM的内部状态,并控制JVM应用程序的执行。

JVMTI只是一套接口,我们要开发JVM工具就需要写一个Agent程序来使用这些接口。Agent程序其实就是一个C/C++语言编写的动态链接库。

注:这里提到的agent程序和javaagent不是同一概念

我们通过JVMTI开发好agent程序后,把程序编译成动态链接库,之后可以在jvm启动时指定加载运行该agent。

-agentlib:=

之后JVM启动后该agent程序就会开始工作。

而接下来要提到的Instrumention机制,也是通过实现了一个JVMTI的agent来完成的,这个agent的实现代码在libinstrument.so里(在BSD系统中叫做libinstrument.dylib),由于libinstrument.so是java内置的,所以不需要我们手动通过 -agentlib 参数指定就可以使用它。

这个动态链接库可以在{JAVA_HOME}/jre/lib 下找到,除此之外,还能看到和调试相关的agent实现——libjdwp.dylib

实现一个javaagent需要几步?_第1张图片

Instrumention 机制

有了Instrumention,我们就可以通过java语言编写一个javaagent来监控或者操作JVM了,比如对类进行插桩。

Instrumention支持的功能都在java.lang.instrument.Instrumentation接口中体现,而我们最关注的还是其中涉及到类转换相关的方法,比如addTransformer以及retransformClasses

实现一个javaagent需要几步?_第2张图片

当我们通过addTransformer添加了一个ClassFileTransformer之后,之后所有的类都会通过ClassFileTransformer.transform()方法进行转换,而具体怎么转换,我们可以通过重写transform方法进行自定义,对于已经加载的类,可以通过调用retransformClasses来重新触发这个Transformer的转换,而且Transformer是可以添加多个的,多个transformer会依次执行。

下面,我们来看一下怎么开发一个基于Instrumention的agent吧

开发一个Javaagent

开发一个javaagent需要几步呢?

  1. 创建一个包含premain()方法的类

  2. 创建一个实现ClassFileTransformer接口的Transfromer类

  3. 创建一个MANIFEST.MF文件,且这个文件的Premain-Class配置项必须设置为实现了premain方法的类的类名

  4. 将项目打包成jar包

然后我们就可以通过命令java -javaagent:agent.jar demo.jar来使用我们的javaagent了。

接下来,我们开始写代码,首先创建一个包含premain方法的类,其中premain方法需要严格按照下面两种格式的一种:

实现一个javaagent需要几步?_第3张图片

javaagent在执行时会首先查找第一个premain方法,如果找到了就不会执行第二个了,如果没有第一个,才回去执行第二个。

其实从premain方法的名字上也可以看出来,这个方法会先于main方法执行,实际上,它会在大多数类加载之前运行,这也是为什么它可以对类进行转换。

编写一个Agent类:

实现一个javaagent需要几步?_第4张图片

其中MyClassTransformer是我自定义的实现了ClassFileTransformer接口的类:

实现一个javaagent需要几步?_第5张图片

这个类中就实现了一个transform方法,我借助javaassist的

javassist.ClassPool  
javassist.CtClass
javassist.CtMethod

这三个类对org.example.Person类的getName方法的方法体进行了替换,我们看一下Person类原本的实现:

除了javaassist还可以使用asm对字节码进行修改,后者使用难度相对来说更大一点,但是性能更好,asm入门:https://github.com/dengshiwei/asm-module/blob/master/doc/blog/AOP 利器 ASM 基础入门.md

实现一个javaagent需要几步?_第6张图片

可以看到,原本的getName方法会打印tntaxin,而经过agent处理过后的getName应该会打印hello tntaxin, you are good!

接下来我们把javaagent打成jar包验证一下效果,不过,在这之前,不要忘了配置MANIFEST.MF文件

实现一个javaagent需要几步?_第7张图片

打包完成后,我们在IDEA中配置一下VM Options使用我们刚刚打包好的agent.jar

实现一个javaagent需要几步?_第8张图片

然后执行Person.main方法,输出如下:

实现一个javaagent需要几步?_第9张图片

至此,我们已经掌握了简单的javaagent的实现方法,不过上面这种javaagent需要在jvm启动前设置-javaagent参数,但是很多时候,我们想要在程序运行的过程中去插入agent,并修改其中的类。而正好,在Java6的新特性中支持通过attach的方式去加载agent

这种agent又要怎么实现呢?和之前的agent很像,我们需要创建一个实现以下两种方法中的一种的类

实现一个javaagent需要几步?_第10张图片

同样的,第一个agentmain方法优先级更高。之后要在META-INF/MAINIFEST.MF属性当中加入” Agent-Class”来指定拥有agentmain方法的类。

我们在之前的Agent类基础上添加agentmain方法:

实现一个javaagent需要几步?_第11张图片

然后打包该agent,之后再编写一个Test类去attach目标进程并加载这个agent

实现一个javaagent需要几步?_第12张图片

最后修改一下之前的Person类,确保它一直运行着:

实现一个javaagent需要几步?_第13张图片

接下来我们看下效果,先运行Person类,然后再运行Test类:

实现一个javaagent需要几步?_第14张图片

在没运行Test类之前一直输出着tntaxin,运行Test类将agent附加到进程后,输出内容变成了hello tntaxin, you are good!

整个动态加载agent修改字节码的时序图大概如下:

实现一个javaagent需要几步?_第15张图片

其他

在写这个demo的过程中遇到了一个错误:

Agent JAR loaded but agent failed to initialize

查资料发现是因为我的agent因为发生异常没有detach,导致我后面再次加载agent时和之前的agent冲突了,因为已经加载过了嘛,解决方案是修改Agent的类以及jar包名,然后重新加载,这样就不会冲突了。



实现一个javaagent需要几步?_第16张图片

 

你可能感兴趣的:(侦探工作笔记,java,开发语言,后端)