实现一个 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。
之后 JVM 启动后该 agent 程序就会开始工作。
而接下来要提到的 Instrumention 机制,也是通过实现了一个 JVMTI 的 agent 来完成的,这个 agent 的实现代码在 libinstrument.so 里(在 BSD 系统中叫做 libinstrument.dylib),由于 libinstrument.so 是 java 内置的,所以不需要我们手动通过 -agentlib 参数指定就可以使用它。
这个动态链接库可以在{JAVA_HOME}/jre/lib 下找到,除此之外,还能看到和调试相关的 agent 实现——libjdwp.dylib
Instrumention 机制
有了 Instrumention,我们就可以通过 java 语言编写一个 javaagent 来监控或者操作 JVM 了,比如对类进行插桩。
Instrumention 支持的功能都在 java.lang.instrument.Instrumentation 接口中体现,而我们最关注的还是其中涉及到类转换相关的方法,比如 addTransformer 以及 retransformClasses
当我们通过 addTransformer 添加了一个 ClassFileTransformer 之后,之后所有的类都会通过 ClassFileTransformer.transform()方法进行转换,而具体怎么转换,我们可以通过重写 transform 方法进行自定义,对于已经加载的类,可以通过调用 retransformClasses 来重新触发这个 Transformer 的转换,而且 Transformer 是可以添加多个的,多个 transformer 会依次执行。
下面,我们来看一下怎么开发一个基于 Instrumention 的 agent 吧
开发一个 Javaagent
开发一个 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 的
这三个类对 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 包名,然后重新加载,这样就不会冲突了。
版权声明: 本文为 InfoQ 作者【BUG侦探】的原创文章。
原文链接:【http://xie.infoq.cn/article/9e50d19f3a3af2f5b1b7b4c70】。文章转载请联系作者。
评论