最近在学习IAST技术,IAST的核心其实就是插桩,在java语言下,用到的技术就是javaagent,由于之前没有接触过javaaent,正好整理一下。
在介绍javaagent之前,我想有必要向大家介绍一下JVMTI,因为javaagent是基于这个技术实现的
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
有了Instrumention,我们就可以通过java语言编写一个javaagent来监控或者操作JVM了,比如对类进行插桩。
Instrumention支持的功能都在java.lang.instrument.Instrumentation接口中体现,而我们最关注的还是其中涉及到类转换相关的方法,比如addTransformer以及retransformClasses
当我们通过addTransformer添加了一个ClassFileTransformer之后,之后所有的类都会通过ClassFileTransformer.transform()方法进行转换,而具体怎么转换,我们可以通过重写transform方法进行自定义,对于已经加载的类,可以通过调用retransformClasses来重新触发这个Transformer的转换,而且Transformer是可以添加多个的,多个transformer会依次执行。
下面,我们来看一下怎么开发一个基于Instrumention的agent吧
开发一个javaagent需要几步呢?
创建一个包含premain()方法的类
创建一个实现ClassFileTransformer接口的Transfromer类
创建一个MANIFEST.MF文件,且这个文件的Premain-Class配置项必须设置为实现了premain方法的类的类名
将项目打包成jar包
然后我们就可以通过命令java -javaagent:agent.jar demo.jar来使用我们的javaagent了。
接下来,我们开始写代码,首先创建一个包含premain方法的类,其中premain方法需要严格按照下面两种格式的一种:
javaagent在执行时会首先查找第一个premain方法,如果找到了就不会执行第二个了,如果没有第一个,才回去执行第二个。
其实从premain方法的名字上也可以看出来,这个方法会先于main方法执行,实际上,它会在大多数类加载之前运行,这也是为什么它可以对类进行转换。
编写一个Agent类:
其中MyClassTransformer是我自定义的实现了ClassFileTransformer接口的类:
这个类中就实现了一个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
可以看到,原本的getName方法会打印tntaxin,而经过agent处理过后的getName应该会打印hello tntaxin, you are good!
接下来我们把javaagent打成jar包验证一下效果,不过,在这之前,不要忘了配置MANIFEST.MF文件
打包完成后,我们在IDEA中配置一下VM Options使用我们刚刚打包好的agent.jar
然后执行Person.main方法,输出如下:
至此,我们已经掌握了简单的javaagent的实现方法,不过上面这种javaagent需要在jvm启动前设置-javaagent参数,但是很多时候,我们想要在程序运行的过程中去插入agent,并修改其中的类。而正好,在Java6的新特性中支持通过attach的方式去加载agent
这种agent又要怎么实现呢?和之前的agent很像,我们需要创建一个实现以下两种方法中的一种的类
同样的,第一个agentmain方法优先级更高。之后要在META-INF/MAINIFEST.MF属性当中加入” Agent-Class”来指定拥有agentmain方法的类。
我们在之前的Agent类基础上添加agentmain方法:
然后打包该agent,之后再编写一个Test类去attach目标进程并加载这个agent
最后修改一下之前的Person类,确保它一直运行着:
接下来我们看下效果,先运行Person类,然后再运行Test类:
在没运行Test类之前一直输出着tntaxin,运行Test类将agent附加到进程后,输出内容变成了hello tntaxin, you are good!
整个动态加载agent修改字节码的时序图大概如下:
在写这个demo的过程中遇到了一个错误:
Agent JAR loaded but agent failed to initialize
查资料发现是因为我的agent因为发生异常没有detach,导致我后面再次加载agent时和之前的agent冲突了,因为已经加载过了嘛,解决方案是修改Agent的类以及jar包名,然后重新加载,这样就不会冲突了。