Java Instrumentation简介
整理ibm.com,不完善
插装&ASM
介绍
插桩技术是在保证目标程序原有逻辑完整的情况下,在特定的位置插入代码段,从而收集程序运行时的动态上下文信息
目前基于插桩技术实现Java程序的动态交互安全监测已经有一些实现形式,如RASP,IAST。在Java中插桩通过Instrument以及字节码操作工具(如:ASM,Javassist,Byte Buddy等)实现
相关知识
Instrumentation简介
Java SE 5 引入了静态Instument的概念,利用它我们可以构建一个独立于应用程序的代理程序(Agent),用力啊检测和协助运行在JVM上的程序,这样的特性实际上提供了一种虚拟机级别支持的AOP实现方式,如此无需对应用程序做升级改动,就能实现某些AOP的功能
在Java SE 6 ,instrumentation包被赋予更大的功能包括:启动后的instrument,本地代码(native code)instument,以及动态改变classpath等
Instrumentation基本功能和用法
在 Instrumentation 的实现当中,存在一个 JVMTI 的代理程序,通过调用 JVMTI 当中 Java 类相关的函数来完成 Java 类的动态操作
使用步骤:
-
编写premain函数
-
编写Java类,包含以下两个方法
public static void premain(String agentArgs, Instrumentation inst); //[1] public static void premain(String agentArgs); //[2]
-
其中,[1]的优先级比[2]高,将会被优先([1] [2]同时存在时[2]将会被忽略)
-
在premain函数中,开发者可以对类进行各种操作。java.lang.instrument.Instrumentation 是 instrument 包中定义的一个核心接口,集中了所有的功能方法
-
agentArgs时premain函数得到的程序参数,随同"- javaagent"一起传入
-
-
jar文件打包
- 将这个Java类打包成一个jar文件,同时在manifest属性中加入
Primain-Class
来指定步骤一中编写的有oremain的Java类
- 将这个Java类打包成一个jar文件,同时在manifest属性中加入
-
运行
java -javaagent:jar 文件位置 [= 传入 primain的参数]
建立一个Transformer类:
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.security.ProtectionDomain;
class Transformer implements ClassFileTransformer {
public static final String classNumberReturns2 = "TransClass.class.2";
public static byte[] getBytesFromFile(String fileName) {
try {
// precondition
File file = new File(fileName);
InputStream is = new FileInputStream(file);
long length = file.length();
byte[] bytes = new byte[(int) length];
// Read in the bytes
int offset = 0;
int numRead = 0;
while (offset <bytes.length
&& (numRead = is.read(bytes, offset, bytes.length - offset)) >= 0) {
offset += numRead;
}
if (offset < bytes.length) {
throw new IOException("Could not completely read file "
+ file.getName());
}
is.close();
return bytes;
} catch (Exception e) {
System.out.println("error occurs in _ClassTransformer!"
+ e.getClass().getName());
return null;
}
}
public byte[] transform(ClassLoader l, String className, Class<?> c,
ProtectionDomain pd, byte[] b) throws IllegalClassFormatException {
if (!className.equals("TransClass")) {
return null;
}
return getBytesFromFile(classNumberReturns2);
}
}
此类实现了ClassFileTransformer接口,其中getBytesFromFile方法根据二进制流字符,此处transform转化方法不进行详细举例
public class Premain {
public static void premain(String agentArgs, Instrumentation inst)
throws ClassNotFoundException, UnmodifiableClassException {
inst.addTransformer(new Transformer());
}
}
可以看到,addTransformer
方法并没有指定要转化哪一个类。转换发生在permain函数执行之后,main函数执行之前,这时状态一个类,transform方法就会执行一次
所以在transform方法(Transformer类)中,使用className.equals("TransClass")判断当前的类是否需要转换
Java SE 6 的新特性:虚拟机启动后的动态instrument
在 Java SE 5 当中,开发者只能在 premain 当中进行处理
Java SE 6 针对这种状况做出了改进,开发者可以在 main 函数开始执行以后,再启动自己的 Instrumentation 程序。
类似于permain函数,开发者可以编写一个含有agentmain
函数的Java类
public static void agentmain (String agentArgs, Instrumentation inst); // [1]
public static void agentmain (String agentArgs); // [2]
同样1
的优先级高,agentArgs和Inst的用法与permain相同。开发者可以在agentmain中进行类的各种操作。其中agentArgs和Inst的用法跟premain相同
与Premian-不同的是,agentmain需要在main函数运行后才启动
此时,绑定的时机就成了问题。此时可以使用Java SE 6 中提供的Attach API
Attach API
Attach API不是Java的标准API,是Sun公司提供的一套拓展API,用来向目标JVM附着
代理工具程序。可以用此方便的监控JVM,运行外加程序
主要包含两部分:
VirtualMachine
代表一个Java虚拟机,即程序需要监控的目标虚拟机,提供了 JVM 枚举,Attach 动作和 Detach 动作等等VirtualMachineDescriptor
则是一个描述虚拟机的容器类,配合 VirtualMachine 类完成各种功能
TransClass类和Transformer类的代码不变。含有 main 函数的 TestMainInJar 代码为:
public class TestMainInJar {
public static void main(String[] args) throws InterruptedException {
System.out.println(new TransClass().getNumber());
int count = 0;
while (true) {
Thread.sleep(500);
count++;
int number = new TransClass().getNumber();
System.out.println(number);
if (3 == number || count >= 10) {
break;
}
}
}
}
含有 agentmain 的 AgentMain 类的代码为:
import java.lang.instrument.ClassDefinition;
import java.lang.instrument.Instrumentation;
import java.lang.instrument.UnmodifiableClassException;
public class AgentMain {
public static void agentmain(String agentArgs, Instrumentation inst)
throws ClassNotFoundException, UnmodifiableClassException,
InterruptedException {
inst.addTransformer(new Transformer (), true);
inst.retransformClasses(TransClass.class);
System.out.println("Agent Main Done");
}
}
其中,retransformClasses 是 Java SE 6 里面的新方法,它跟 redefineClasses 一样,可以批量转换类定义,多用于 agentmain 场合
Jar文件跟Permain那个例里面的Jar文件类似,也是把 main 和 agentmain 的类,TransClass,Transformer 等类放在一起,打包为TestInstrument1.jar
。而 Jar 文件当中的 Manifest 文件为 :
Manifest-Version: 1.0
Agent-Class: AgentMain
为运行Attach API,可以写要给控制程序来模拟监控程序
import com.sun.tools.attach.VirtualMachine;
import com.sun.tools.attach.VirtualMachineDescriptor;
……
// 一个运行 Attach API 的线程子类
static class AttachThread extends Thread {
private final List<VirtualMachineDescriptor> listBefore;
private final String jar;
AttachThread(String attachJar, List<VirtualMachineDescriptor> vms) {
listBefore = vms; // 记录程序启动时的 VM 集合
jar = attachJar;
}
public void run() {
VirtualMachine vm = null;
List<VirtualMachineDescriptor> listAfter = null;
try {
int count = 0;
while (true) {
listAfter = VirtualMachine.list();
for (VirtualMachineDescriptor vmd : listAfter) {
if (!listBefore.contains(vmd)) {
// 如果 VM 有增加,我们就认为是被监控的 VM 启动了
// 这时,我们开始监控这个 VM
vm = VirtualMachine.attach(vmd);
break;
}
}
Thread.sleep(500);
count++;
if (null != vm || count >= 10) {
break;
}
}
vm.loadAgent(jar);
vm.detach();
} catch (Exception e) {
ignore
}
}
}
……
public static void main(String[] args) throws InterruptedException {
new AttachThread("TestInstrument1.jar", VirtualMachine.list()).start();
}
运行时,可以首先运行上面这个启动新线程的main函数,然后在5s内(仅模拟简单JVM监控过程),启动方式如下
java – javaagent:TestInstrument2.jar – cp TestInstrument2.jar TestMainInJar
程序会首先在屏幕上打印出1,然后打印出2,表示agentmain已经被Attach API 成功附着到JVM上了,代理程序生效。