拿常规的hook和event callback技术进行类比可以按照如下通俗理解:
关于JVM TI技术,官方文档的解释如下:
The JVM tool interface (JVM TI) is a native programming interface for use by tools. It provides both a way to inspect the state and to control the execution of applications running in the Java virtual machine (JVM). JVM TI supports the full breadth of tools that need access to JVM state, including but not limited to:
Note: JVM TI was introduced at JDK 5.0. JVM TI replaced the Java Virtual Machine Profiler Interface (JVMPI) and the Java Virtual Machine Debug Interface (JVMDI) which, as of JDK 6, are no longer provided.
JVMTI(Java Virtual Machine Tool Interface)是一套由 Java 虚拟机提供的,为 JVM 相关的工具提供的 Native 接口集合。JVMTI 是从 Java SE 5 开始引入,整合和取代了以前使用的 Java Virtual Machine Profiler Interface (JVMPI) 和 the Java Virtual Machine Debug Interface (JVMDI),而在 Java SE 6 中,JVMPI 和 JVMDI 已经消失了。
JVMTI 提供了一套”代理”程序机制,可以支持第三方工具程序以代理的方式连接和访问 JVM,并利用 JVMTI 提供的丰富的编程接口,完成很多跟 JVM 相关的功能。从 Java SE 5 开始,可以使用 Java 的Instrumentation 接口(java.lang.instrument)来编写 Agent。无论是通过 Native 的方式还是通过 Java Instrumentation 接口的方式来编写 Agent,它们的工作都是借助 JVMTI 来进行完成。
JVMTI和Instumentation API的作用很相似,都是一套 JVM 操作和监控的接口,且都需要通过 agent 来启动:
Instumentation API 可以支持 Java 语言实现 agent 功能,但 Instumentation API 本身是基于 JVMTI 实现的,JVMTI 功能比 Instumentation API 更强大,它支持:
Java agent 是基于 JVMTI 实现,核心部分是 ClassFileLoadHook和TransFormClassFile。
在字节码文件加载的时候,会触发ClassFileLoadHook事件,该事件调用TransFormClassFile,通过经由Instrumentation 的 addTransformer 注册的方法完成整体的字节码修改。
对于已加载的类,需要调用retransformClass函数,然后经由redefineClasses函数,在读取已加载的字节码文件后,若该字节码文件对应的类关注了ClassFileLoadHook事件,则调用ClassFileLoadHook事件。后续流程与类加载时字节码替换一致。
参考链接:
https://docs.oracle.com/javase/8/docs/technotes/guides/jvmti/
关于Java Interface Instrumentation技术,官方文档的解释如下:
public interface Instrumentation
This class provides services needed to instrument Java programming language code.
Instrumentation is the addition of byte-codes to methods for the purpose of gathering data to be utilized by tools. Since the changes are purely additive, these tools do not modify application state or behavior.
Examples of such benign tools include
There are two ways to obtain an instance of the Instrumentation
interface:
When a JVM is launched in a way that indicates an agent class. In that case an Instrumentation
instance is passed to the premain
method of the agent class.
When a JVM provides a mechanism to start agents sometime after the JVM is launched. In that case an Instrumentation
instance is passed to the agentmain
method of the agent code.
Once an agent acquires an Instrumentation
instance, the agent may call methods on the instance at any time.
JDK™5.0中引入包java.lang.instrument。 该包提供了一个Java编程API,可以用来开发增强Java应用程序的工具,例如监视它们或收集性能信息。 使用 Instrumentation,开发者可以构建一个独立于应用程序的代理程序(Agent),用来监测和协助运行在 JVM 上的程序,甚至能够替换和修改某些类的定义。有了这样的功能,开发者就可以实现更为灵活的运行时虚拟机监控和 Java 类操作了,这样的特性实际上提供了一种虚拟机级别支持的 AOP 实现方式,使得开发者无需对 JDK 做任何升级和改动,就可以实现某些 AOP(AOP核心理念是定义切入点(pointcut)以及通知(advice)。程序控制流中所有匹配该切入点的连接点(joinpoint)都将执行这段通知代码)的功能了。
在 Java SE 6 里面,instrumentation 包被赋予了更强大的功能,所有这些新的功能,都使得 instrument 包的功能更加丰富,从而使 Java 语言本身更加强大,包括:
这些改变,意味着 Java 具有了更强的动态控制、解释能力,它使得 Java 语言变得更加灵活多变。
java.lang.instrument包的具体实现,依赖于 JVMTI。在 Instrumentation 的实现当中,存在一个 JVMTI 的代理程序,通过调用 JVMTI 当中 Java 类相关的函数来完成 Java 类的动态操作。
Instrumentation 的最大作用,就是类定义动态改变和操作。在 Java SE 5 及其后续版本当中,开发者可以在一个普通 Java 程序(带有 main 函数的 Java 类)运行时,通过 –javaagent参数指定一个特定的 jar 文件(包含 Instrumentation 代理)来启动 Instrumentation 的代理程序。
Instrumentation 的一些主要方法如下:
public interface Instrumentation { /** * 注册一个Transformer,从此之后的类加载都会被Transformer拦截。 * Transformer可以直接对类的字节码byte[]进行修改 */ void addTransformer(ClassFileTransformer transformer); /** * 对JVM已经加载的类重新触发类加载。使用的就是上面注册的Transformer。 * retransformClasses可以修改方法体,但是不能变更方法签名、增加和删除方法/类的成员属性 */ void retransformClasses(Class<?>... classes) throws UnmodifiableClassException; /** * 获取一个对象的大小 */ long getObjectSize(Object objectToSize); /** * 将一个jar加入到bootstrap classloader的 classpath里 */ void appendToBootstrapClassLoaderSearch(JarFile jarfile); /** * 获取当前被JVM加载的所有类对象 */ Class[] getAllLoadedClasses(); }
其中最常用的方法是addTransformer(ClassFileTransformer transformer),这个方法可以在类加载时做拦截,对输入的类的字节码进行修改,其参数是一个ClassFileTransformer接口,定义如下:
public interface ClassFileTransformer { /** * 传入参数表示一个即将被加载的类,包括了classloader,classname和字节码byte[] * 返回值为需要被修改后的字节码byte[] */ byte[] transform( ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException; }
addTransformer方法配置之后,后续的类加载都会被Transformer拦截。对于已经加载过的类,可以执行retransformClasses来重新触发这个Transformer的拦截。类加载的字节码被修改后,除非再次被retransform,否则不会恢复。
在运行时,我们可以通过Instrumentation的redefineClasses方法进行类重定义,在redefineClasses方法上有一段注释需要特别注意:
* The redefinition may change method bodies, the constant pool and attributes. * The redefinition must not add, remove or rename fields or methods, change the * signatures of methods, or change inheritance. These restrictions maybe be * lifted in future versions. The class file bytes are not checked, verified and installed * until after the transformations have been applied, if the resultant bytes are in * error this method will throw an exception.
这里面提到,我们不可以增加、删除或者重命名字段和方法,改变方法的签名或者类的继承关系。认识到这一点很重要,当我们通过 ASM 获取到增强的字节码之后,如果增强后的字节码没有遵守这些规则,那么调用redefineClasses方法来进行类的重定义就会失败。
参考链接:
https://docs.oracle.com/javase/8/docs/api/java/lang/instrument/Instrumentation.html
Java agent 动态加载是通过 Attach API 实现。
Attach 机制是 JVM 提供一种 JVM 进程间通信的能力,能让一个进程传命令给另外一个进程,并让它执行内部的一些操作。
日常很多工作都是通过 Attach API 实现的,例如:
由于是进程间通讯,那代表着使用 Attach API 的程序需要是一个独立的 Java 程序,通过 attach 目标进程,与其进行通讯。
下面的代码表示了向进程 pid 为 1234 的 JVM 发起通讯,加载一个名为 agent.jar 的 Java agent。
// VirtualMachine等相关Class位于JDK的tools.jar VirtualMachine vm = VirtualMachine.attach("1234"); // 1234表示目标JVM进程pid try { vm.loadAgent(".../javaagent.jar"); // 指定agent的jar包路径,发送给目标进程 } finally { // attach 动作的相反的行为,从 JVM 上面解除一个代理 vm.detach(); }
vm.loadAgent 之后,相应的 agent 就会被目标 JVM 进程加载,并执行 agentmain() 方法。
Linux 系统为例。当 external process(attach 发起的进程)执行 VirtualMachine.attach 时,需要通过操作系统提供的进程通信方法,例如信号、socket,进行握手和通信。其具体内部实现流程如下所示:
上面提到了两个文件:
VirtualMachine.attach 动作类似 TCP 创建连接的三次握手,目的就是搭建 attach 通信的连接。而后面执行的操作,例如 vm.loadAgent,其实就是向这个 socket 写入数据流,接收方 target VM 会针对不同的传入数据来做不同的处理。
Javassist (JAVA programming ASSISTant) 是在 Java 中编辑字节码的类库,它使 Java 程序能够在运行时定义一个新类,并在 JVM 加载时修改类文件。
与其他类似的字节码编辑器不同, Javassist 提供了两个级别的 API:
ASM是一个java字节码操纵框架,它能被用来动态生成类或者增强既有类的功能。ASM 可以直接产生二进制 class 文件,也可以在类被加载入 Java 虚拟机之前动态改变类行为。Java class 被存储在严格格式定义的 .class文件里,这些类文件拥有足够的元数据来解析类中的所有元素:类名称、方法、属性以及 Java 字节码(指令)。ASM从类文件中读入信息后,能够改变类行为,分析类信息,甚至能够根据用户要求生成新类。
ASM框架中的核心类有以下几个:
参考链接:
https://central.sonatype.com/artifact/org.ow2.asm/asm/7.0-beta?smo=true https://codeleading.com/article/62733319970/
参考链接:
https://www.51cto.com/article/722419.html https://www.jianshu.com/p/1b07a8a6a475 https://learn.lianglianglee.com/%E4%B8%93%E6%A0%8F/%E6%B7%B1%E5%85%A5%E6%8B%86%E8%A7%A3Java%E8%99%9A%E6%8B%9F%E6%9C%BA/33%20%20Java%20Agent%E4%B8%8E%E5%AD%97%E8%8A%82%E7%A0%81%E6%B3%A8%E5%85%A5.md https://leokongwq.github.io/2017/12/21/java-agent-instrumentation.html https://www.cnblogs.com/liboware/p/12497621.html
主流的 JVM 都提供了Instrumentation的实现,但是鉴于Instrumentation的特殊功能,并不适合直接提供在 JDK 的runtime里,而更适合出现在 Java 程序的外层,以上帝视角在合适的时机出现。因此如果想使用Instrumentation功能,拿到 Instrumentation 实例,我们必须通过 Java agent。
Java agent是一种特殊的 Java 程序(Jar 文件),它是Instrumentation的客户端。与普通 Java 程序通过 main 方法启动不同,agent 并不是一个可以单独启动的程序,而必须依附在一个 Java 应用程序(JVM)上,与它运行在同一个进程中,通过Instrumentation API与虚拟机交互。
Java agent与Instrumentation密不可分,二者也需要在一起使用。因为Instrumentation的实例会作为参数注入到Java agent的启动方法中。
Java agent 以 jar 包的形式部署在 JVM 中,jar 文件的 manifest 需要指定 agent 的类名。根据不同的启动时机,agent 类需要实现不同的方法(二选一)。
premain
agentmain
JVM 启动时加载
[1] public static void premain(String agentArgs, Instrumentation inst);
[2] public static void premain(String agentArgs);
JVM 将首先寻找[1],如果没有发现[1],再寻找[2]。
JVM 运行时加载
[1] public static void agentmain(String agentArgs, Instrumentation inst);
[2] public static void agentmain(String agentArgs);
与 premain()一致,JVM 将首先寻找[1],如果没有发现[1],再寻找[2]。
Java agent 的这些功能都是通过 JVMTI agent,也就是 C agent 来实现的。JVMTI 是一个事件驱动的工具实现接口,通常,我们会在 C agent 加载后的入口方法Agent_OnLoad/Agent_OnAttach处注册各个事件的钩子(hook)方法,当 Java 虚拟机触发了这些事件时,便会调用对应的钩子方法。例如:
Java agent 的包先会被加入到 system class path 中,然后 agent 的类会被system calss loader(默认AppClassLoader)所加载,和应用代码的真实 classLoader 无关。例如:
该类加载逻辑非常重要,在使用 Java agent 时如果遇到ClassNotFoundException、NoClassDefFoundError,很大可能就是与该加载逻辑有关。
Java agent 支持静态加载和动态加载。
静态加载,即 JVM 启动时加载,对应的是 premain() 方法。通过 vm 启动参数-javaagent将 agent jar 挂载到目标 JVM 程序,随目标 JVM 程序一起启动。
Java 虚拟机并不限制 Java agent 的数量。你可以在 java 命令后附上多个-javaagent参数。
-javaagent格式:"-javaagent:<jarpath>[=<option>]"。
java -javaagent:agent1.jar=key1=value1&key2=value2 -javaagent:agent2.jar -jar Test.jar
premain()方法会在程序 main 方法执行之前被调用,此时大部分 Java 类都没有被加载("大部分"是因为,agent 类本身和它依赖的类还是无法避免的会先加载的),是一个对类加载埋点做手脚(addTransformer)的好机会。
如果此时 premain 方法执行失败或抛出异常,那么 JVM 的启动会被终止。
agent 中的 class 由 system calss loader(默认AppClassLoader)加载,premain() 方法会调用 Instrumentation API,然后 Instrumentation API 调用 JVMTI,在需要加载的类需要被加载时,会回调 JVMTI,然后回调 Instrumentation API,触发 ClassFileTransformer.transform()。
可以通过Instrumentation来注册类加载事件的拦截器。该拦截器需要实现ClassFileTransformer接口,并重写其中的transform方法
当方法返回之后,Java 虚拟机会使用所返回的 byte 数组,来完成接下来的类加载工作;如果transform方法返回 null 或者抛出异常,那么 Java 虚拟机将使用原来的 byte 数组完成类加载工作。
基于这一类加载事件的拦截功能,我们可以实现字节码注入(bytecode instrumentation),往正在被加载的类中插入额外的字节码
下面是一次 ClassFileTransformer.transform()执行时的方法调用栈,
transform:38, MethodAgentMain$1 (demo) transform:188, TransformerManager (sun.instrument) transform:428, InstrumentationImpl (sun.instrument) defineClass1:-1, ClassLoader (java.lang) defineClass:760, ClassLoader (java.lang) defineClass:142, SecureClassLoader (java.security) defineClass:467, URLClassLoader (java.net) access$100:73, URLClassLoader (java.net) run:368, URLClassLoader$1 (java.net) run:362, URLClassLoader$1 (java.net) doPrivileged:-1, AccessController (java.security) findClass:361, URLClassLoader (java.net) loadClass:424, ClassLoader (java.lang) loadClass:331, Launcher$AppClassLoader (sun.misc) loadClass:357, ClassLoader (java.lang) checkAndLoadMain:495, LauncherHelper (sun.launcher)
可以看到
下面是精简后的 ClassLoader.load() 核心代码:
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { synchronized (getClassLoadingLock(name)) { // 判断是否已经加载过了,如果没有,则进行load // First, check if the class has already been loaded Class<?> c = findLoadedClass(name); if (c == null) { if (c == null) { // If still not found, then invoke findClass in order // to find the class. long t1 = System.nanoTime(); // findClass()内部最终会调用 Java agent 中 ClassFileTransformer.transform() c = findClass(name); // this is the defining class loader; record the stats sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0); sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1); sun.misc.PerfCounter.getFindClasses().increment(); } } if (resolve) { resolveClass(c); } return c; } }
ClassFileTransformer.transform() 中可以对指定的类进行增强,我们可以选择代码生成库修改指定的类的字节码,对类进行增强,比如
动态加载,即 JVM 启动后的任意时间点(即运行时),通过Attach API动态地加载 Java agent,对应的是 agentmain() 方法。
对于 VM 启动后加载的Java agent,其agentmain()方法会在加载之时立即执行。如果agentmain执行失败或抛出异常,JVM 会忽略掉错误,不会影响到正在 running 的 Java 程序。
一般 agentmain() 中会编写如下步骤:
redefine和retransform针对的是已加载的类,并要求用户传入所要redefine或者retransform的类实例。
参考链接:
https://docs.oracle.com/javase/8/docs/platform/jvmti/jvmti.html
创建如下结构文件
PreDemo.java内容为:
import java.lang.instrument.Instrumentation; public class PreDemo { /** * 该方法在main方法之前运行,与main方法运行在同一个JVM中 * 并被同一个System ClassLoader装载 * 被统一的安全策略(security policy)和上下文(context)管理 */ public static void premain(String agentOps, Instrumentation inst) { System.out.println("====premain1 execute===="); System.out.println(agentOps); } /** * 如果不存在 premain(String agentOps, Instrumentation inst) * 则会执行 premain(String agentOps) */ public static void premain(String agentOps) { System.out.println("====premain2 execute===="); System.out.println(agentOps); } }
MANIFEST.MF内容为:
Manifest-Version: 1.0 Premain-Class: PreDemo Can-Redefine-Classes: true
打包agent代码为一个独立的jar包,
cd /Users/zhenghan/Projects/JavaAgent_Demo/src # 编译成.class 文件 javac PreDemo.java # 加入META-INF/MANIFEST.MF,打包 jar -cvfm JavaAgent_Demo.jar META-INF/MANIFEST.MF PreDemo.class
写一个简单的hello world,用于演示premian注入特定java应用。
创建如下结构文件
HelloWorldMain.java内容,
// Press Shift twice to open the Search Everywhere dialog and type `show whitespaces`, // then press Enter. You can now see whitespace characters in your code. public class HelloWorldMain { public static void main(String[] args) { // Press Opt+Enter with your caret at the highlighted text to see how // IntelliJ IDEA suggests fixing it. System.out.printf("Hello and welcome!"); // Press Ctrl+R or click the green arrow button in the gutter to run the code. for (int i = 1; i <= 5; i++) { // Press Ctrl+D to start debugging your code. We have set one breakpoint // for you, but you can always add more by pressing Cmd+F8. System.out.println("i = " + i); } } }
MANIFEST.MF 文件内容,
Manifest-Version: 1.0 Main-Class: HelloWorldMain Can-Redefine-Classes: true
打包代码为一个独立的jar包,
cd /Users/zhenghan/Projects/HelloWorld/src # 编译成.class 文件 javac HelloWorldMain.java # 加入META-INF/MANIFEST.MF,打包 jar -cvfm HelloWorld.jar META-INF/MANIFEST.MF HelloWorldMain.class
通过 -javaagent 参数来指定我们的Java代理包,-javaagent 这个参数的个数是不限的,如果指定了多个,则会按指定的先后执行,执行完各个 agent 后,才会执行主程序的 main 方法。
cd /Users/zhenghan/Projects
java -javaagent:JavaAgent_Demo/src/JavaAgent_Demo.jar=javaagent_args -jar HelloWorld/src/HelloWorld.jar
整个流程大致如下:
创建如下结构文件
agantmainDemo.java内容为:
import java.lang.instrument.Instrumentation; import java.io.*; public class agantmainDemo { public static void agentmain(String agentArgs, Instrumentation inst) { for (int i = 0; i < 10; i++) { System.out.println("hello I`m agentMain!!!"); } } public static void premain(String args, Instrumentation inst) throws Exception { System.out.println("Pre Args:" + args); Class[] classes = inst.getAllLoadedClasses(); for (Class clazz : classes) { System.out.println(clazz.getName()); } } }
MANIFEST.MF内容为:
Manifest-Version: 1.0 Agent-Class: agantmainDemo Premain-Class: agantmainDemo Can-Redine-Classes: true Can-Retransform-Classes: true
打包代码为一个独立的jar包,
cd /Users/zhenghan/Projects/JavaAgent_Demo/src # 编译成.class 文件 javac agantmainDemo.java # 加入META-INF/MANIFEST.MF,打包 jar -cvfm JavaAgent_Demo.jar META-INF/MANIFEST.MF agantmainDemo.class
HelloWorldMain.java内容,
import java.lang.Thread; // Press Shift twice to open the Search Everywhere dialog and type `show whitespaces`, // then press Enter. You can now see whitespace characters in your code. public class HelloWorldMain { public static void main(String[] args) { // Press Opt+Enter with your caret at the highlighted text to see how // IntelliJ IDEA suggests fixing it. System.out.printf("Hello and welcome!"); // Press Ctrl+R or click the green arrow button in the gutter to run the code. for (int i = 1; i <= 999999; i++) { // Press Ctrl+D to start debugging your code. We have set one breakpoint // for you, but you can always add more by pressing Cmd+F8. System.out.println("i = " + i); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } } }
MANIFEST.MF 文件内容,
Manifest-Version: 1.0 Main-Class: HelloWorldMain Can-Redefine-Classes: true
打包代码为一个独立的jar包,
cd /Users/zhenghan/Projects/HelloWorld/src # 编译成.class 文件 javac HelloWorldMain.java # 加入META-INF/MANIFEST.MF,打包 jar -cvfm HelloWorld.jar META-INF/MANIFEST.MF HelloWorldMain.class
官方为了实现启动后加载jar包,提供了Attach API。
Attach API 很简单,只有 2 个主要的类,都在 com.sun.tools.attach 包里面。着重关注的是VitualMachine这个类。
VirtualMachine字面意义表示一个Java 虚拟机,也就是程序需要监控的目标虚拟机,提供了获取系统信息、 loadAgent,Attach 和 Detach 等方法,可以实现的功能可以说非常之强大 。
下面列几个这个类提供的方法:
public abstract class VirtualMachine { // 获得当前所有的JVM列表 public static List<VirtualMachineDescriptor> list() { ... } // 根据pid连接到JVM public static VirtualMachine attach(String id) { ... } // 断开连接 public abstract void detach() {} // 加载agent,agentmain方法靠的就是这个方法 public void loadAgent(String agent) { ... } }
根据提供的api,可以写出一个attacher,代码如下:
import com.sun.tools.attach.AgentInitializationException; import com.sun.tools.attach.AgentLoadException; import com.sun.tools.attach.AttachNotSupportedException; import com.sun.tools.attach.VirtualMachine; import java.io.IOException; public class attacherDemo { public static void main(String[] args) throws IOException, AttachNotSupportedException, AgentLoadException, AgentInitializationException { String id = args[0]; String jarName = args[1]; System.out.println("id ==> " + id); System.out.println("jarName ==> " + jarName); VirtualMachine virtualMachine = VirtualMachine.attach(id); virtualMachine.loadAgent(jarName); virtualMachine.detach(); System.out.println("ends"); } }
MANIFEST.MF内容为:
Manifest-Version: 1.0 Main-Class: attacherDemo
打包代码为一个独立的jar包,
cd /Users/zhenghan/Projects/JavaAgent_Demo/src # 编译成.class 文件 # 加入tool.jar外部依赖,指定本地系统中tool.jar路径 javac -cp ".:/Library/Java/JavaVirtualMachines/jdk1.8.0_291.jdk/Contents/Home/lib/*" attacherDemo.java # 加入META-INF/MANIFEST.MF,打包 jar -cvfm JavaAgent_Attach_Demo.jar META-INF/MANIFEST.MF attacherDemo.class
启动被注入进程HelloWorld.jar,
获取被注入进程HelloWorld.jar的pid,
启动JavaAgent_Attach_Demo.jar,将JavaAgent_Demo.jar注入到目标进程HelloWorld.jar中,
cd /Users/zhenghan/Projects/JavaAgent_Demo/src jar tf "/Users/zhenghan/Projects/JavaAgent_Demo/src/JavaAgent_Demo.jar" java -Xbootclasspath/a:/Library/Java/JavaVirtualMachines/jdk1.8.0_291.jdk/Contents/Home/lib/tools.jar -jar "/Users/zhenghan/Projects/JavaAgent_Demo/src/JavaAgent_Attach_Demo.jar" 86149 "/Users/zhenghan/Projects/JavaAgent_Demo/src/JavaAgent_Demo.jar"
整个过程的流程图大致如下图所示:
Instrumentation是JVMTIAgent(JVM Tool Interface Agent)的一部分。Java agent通过这个类和目标JVM进行交互,从而达到修改数据的效果。
下面列出这个类的一些方法,更加详细的介绍和方法,可以参照官方文档。
public interface Instrumentation { // 增加一个 Class 文件的转换器,转换器用于改变 Class 二进制流的数据,参数 canRetransform 设置是否允许重新转换。在类加载之前,重新定义 Class 文件,ClassDefinition 表示对一个类新的定义,如果在类加载之后,需要使用 retransformClasses 方法重新定义。addTransformer方法配置之后,后续的类加载都会被Transformer拦截。对于已经加载过的类,可以执行retransformClasses来重新触发这个Transformer的拦截。类加载的字节码被修改后,除非再次被retransform,否则不会恢复。 void addTransformer(ClassFileTransformer transformer); // 删除一个类转换器 boolean removeTransformer(ClassFileTransformer transformer); // 在类加载之后,重新定义 Class。这个很重要,该方法是1.6 之后加入的,事实上,该方法是 update 了一个类。 void retransformClasses(Class<?>... classes) throws UnmodifiableClassException; // 判断目标类是否能够修改。 boolean isModifiableClass(Class<?> theClass); // 获取目标已经加载的类。 @SuppressWarnings("rawtypes") Class[] getAllLoadedClasses(); ...... }
修改之前写的agantmainDemo.java:
import java.lang.instrument.Instrumentation; import java.io.*; public class agantmainDemo { public static void agentmain(String agentArgs, Instrumentation inst) { Class[] classes = inst.getAllLoadedClasses(); try { FileOutputStream fileOutputStream = new FileOutputStream(new File("/tmp/classesInfo")); for (Class aClass : classes) { String result = "class ==> " + aClass.getName() + "\n\t" + "Modifiable ==> " + (inst.isModifiableClass(aClass) ? "true" : "false") + "\n"; fileOutputStream.write(result.getBytes()); } fileOutputStream.close(); } catch (IOException fnfe) { System.out.println(fnfe); } finally { // } } public static void premain(String args, Instrumentation inst) throws Exception { System.out.println("Pre Args:" + args); Class[] classes = inst.getAllLoadedClasses(); for (Class clazz : classes) { System.out.println(clazz.getName()); } } }
重新attach到某个JVM,在/tmp/classesInfo文件中有如下信息:
得到了目标JVM上所有已经加载的类,并且知道了这些类能否被修改。
除此之外,Java agent 还提供了一套 instrumentation 机制,允许应用程序拦截类加载事件,并且更改该类的字节码。
接下来来讲讲如何使用addTransformer()和retransformClasses()进行类加载劫持和重新。
首先看一下这两个方法的声明:
public interface Instrumentation { // 增加一个 Class 文件的转换器,转换器用于改变 Class 二进制流的数据,参数 canRetransform 设置是否允许重新转换。在类加载之前,重新定义 Class 文件,ClassDefinition 表示对一个类新的定义,如果在类加载之后,需要使用 retransformClasses 方法重新定义。addTransformer方法配置之后,后续的类加载都会被Transformer拦截。对于已经加载过的类,可以执行retransformClasses来重新触发这个Transformer的拦截。类加载的字节码被修改后,除非再次被retransform,否则不会恢复。 void addTransformer(ClassFileTransformer transformer); // 删除一个类转换器 boolean removeTransformer(ClassFileTransformer transformer); // 在类加载之后,重新定义 Class。这个很重要,该方法是1.6 之后加入的,事实上,该方法是 update 了一个类。 void retransformClasses(Class<?>... classes) throws UnmodifiableClassException; ...... }
在addTransformer()方法中,有一个参数ClassFileTransformer transformer。这个参数将帮助我们完成字节码的修改工作。
这是一个接口,它提供了一个transform方法:
public interface ClassFileTransformer { default byte[] transform( ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) { .... } }
简单概括一下:
在开始修改字节码之前,我们先来看一下通过addTransformer和retransformClasses,对目标类进行劫持重加载,并打印出被劫持类的字节码。
为了方便演示修改特定class的字节码,修改HelloWorldMain.java内容,增加了对hello类的调用,
// Press Shift twice to open the Search Everywhere dialog and type `show whitespaces`, // then press Enter. You can now see whitespace characters in your code. public class HelloWorldMain { public static void main(String[] args) { // Press Opt+Enter with your caret at the highlighted text to see how // IntelliJ IDEA suggests fixing it. System.out.printf("Hello and welcome!"); hello h2 = new hello(); h2.hello(); System.out.println("ends..."); } }
新建一个hello.java文件,
import java.lang.Thread; // hello.java public class hello { public void hello() { System.out.println("hello world"); // Press Ctrl+R or click the green arrow button in the gutter to run the code. for (int i = 1; i <= 999999; i++) { // Press Ctrl+D to start debugging your code. We have set one breakpoint // for you, but you can always add more by pressing Cmd+F8. System.out.println("i = " + i); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } } }
打包代码为一个独立的jar包,
cd /Users/zhenghan/Projects/HelloWorld/src # 编译成.class 文件 javac HelloWorldMain.java # 加入META-INF/MANIFEST.MF,打包 jar -cvfm HelloWorld.jar META-INF/MANIFEST.MF HelloWorldMain.class hello.class
修改之前写的agantmainDemo.java:
import java.io.*; import java.lang.instrument.Instrumentation; import java.lang.instrument.ClassFileTransformer; import java.lang.instrument.IllegalClassFormatException; import java.lang.instrument.Instrumentation; import java.security.ProtectionDomain; public class agantmainDemo { public static void agentmain(String agentArgs, Instrumentation inst) { Class[] classes = inst.getAllLoadedClasses(); // 判断类是否已经加载 for (Class aClass : classes) { if (aClass.getName().equals(TransformerDemo.editClassName)) { // 添加 Transformer inst.addTransformer(new TransformerDemo(), true); // 触发 Transformer try { inst.retransformClasses(aClass); } catch (Exception e) { } } } } }
新建一个TransformerDemo.java文件
import java.io.*; import java.lang.instrument.Instrumentation; import java.lang.instrument.ClassFileTransformer; import java.lang.instrument.IllegalClassFormatException; import java.lang.instrument.Instrumentation; import java.security.ProtectionDomain; // TransformerDemo.java // 如果在使用过程中找不到javassist包中的类,那么可以使用URLCLassLoader+反射的方式调用 public class TransformerDemo implements ClassFileTransformer { // 只需要修改这里就能修改别的函数 public static final String editClassName = "hello"; public static final String editVNClassName = new String(editClassName).replaceAll("\\.", "\\/");; public static final String editMethodName = "hello"; @Override public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException { // 判断类名是否为目标类名 if (!className.equals(this.editVNClassName)) { return classfileBuffer; } System.out.printf("Loaded %s: 0x%X%X%X%X\n", className, classfileBuffer[0], classfileBuffer[1], classfileBuffer[2], classfileBuffer[3]); return null; } }
在上面这段代码中,
基于这一类加载事件的拦截功能,我们可以实现字节码注入(bytecode instrumentation)等更复杂的功能,例如往正在被加载的类中插入额外的字节码。
打包编译为独立jar文件,
cd /Users/zhenghan/Projects/JavaAgent_Demo/src
# 编译成.class 文件,引入本地包含javassist.jar的lib路径
javac -cp ".:./*" agantmainDemo.java
# 加入META-INF/MANIFEST.MF,打包
jar -cvfm JavaAgent_Demo.jar META-INF/MANIFEST.MF agantmainDemo.class TransformerDemo.class
attach到HelloWorld目标进程,
ps -ef | grep HelloWorld
# 501 96170 86164 0 11:47AM ttys001 0:04.26 /usr/bin/java -jar HelloWorld.jar
cd /Users/zhenghan/Projects/JavaAgent_Demo/src
java -Xbootclasspath/a:/Library/Java/JavaVirtualMachines/jdk1.8.0_291.jdk/Contents/Home/lib/tools.jar:/Users/zhenghan/Projects/JavaAgent_Demo/src/javassist.jar -jar "/Users/zhenghan/Projects/JavaAgent_Demo/src/JavaAgent_Attach_Demo.jar" "96170" "/Users/zhenghan/Projects/JavaAgent_Demo/src/JavaAgent_Demo.jar"
接下来我们不满足于仅仅劫持重加载目标类,我们更进一步看看如何自定义修改目标类的字节码逻辑,具体的做法还需要用到另一个工具——javassist。
agantmainDemo.java保持不变,TransformerDemo.java文件修改为,
import java.io.*; import java.lang.instrument.Instrumentation; import java.lang.instrument.ClassFileTransformer; import java.lang.instrument.IllegalClassFormatException; import java.lang.instrument.Instrumentation; import java.security.ProtectionDomain; import javassist.*; import javassist.ClassPath; // TransformerDemo.java // 如果在使用过程中找不到javassist包中的类,那么可以使用URLCLassLoader+反射的方式调用 public class TransformerDemo implements ClassFileTransformer { // 只需要修改这里就能修改别的函数 public static final String editClassName = "hello"; public static final String editVNClassName = new String(editClassName).replaceAll("\\.", "\\/");; public static final String editMethodName = "hello"; @Override public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException { // 判断类名是否为目标类名 if (!className.equals(this.editVNClassName)) { return classfileBuffer; } try { ClassPool cp = ClassPool.getDefault(); cp.appendClassPath("/Users/zhenghan/Projects/JavaAgent_Demo/src/"); if (classBeingRedefined != null) { ClassClassPath ccp = new ClassClassPath(classBeingRedefined); cp.insertClassPath(ccp); } CtClass ctc = cp.get(this.editClassName); CtMethod method = ctc.getDeclaredMethod(this.editMethodName); String source = "{System.out.println(\"hello transformer\");}"; method.setBody(source); byte[] bytes = ctc.toBytecode(); ctc.writeFile("/Users/zhenghan/Projects/JavaAgent_Demo/src/dump"); ctc.detach(); return bytes; } catch (Exception e){ e.printStackTrace(); } return null; } }
下载javassist.jar到当前编译目录,
agantmainDemo.java保持不变
import java.io.*; import java.lang.instrument.*; public class agantmainDemo { public static void agentmain(String agentArgs, Instrumentation inst) { Class[] classes = inst.getAllLoadedClasses(); // 判断类是否已经加载 for (Class aClass : classes) { if (aClass.getName().equals(TransformerDemo.editClassName)) { // 添加 Transformer inst.addTransformer(new TransformerDemo(), true); // 触发 Transformer try { inst.retransformClasses(aClass); } catch (Exception e) { } } } } }
TransformerDemo.java文件修改为,
import java.io.*; import java.lang.instrument.*; import java.security.ProtectionDomain; import org.objectweb.asm.*; import org.objectweb.asm.tree.*; // TransformerDemo.java public class TransformerDemo implements ClassFileTransformer, Opcodes { // 只需要修改这里就能修改别的函数 public static final String editClassName = "hello"; public static final String editVNClassName = new String(editClassName).replaceAll("\\.", "\\/");; public static final String editMethodName = "hello"; @Override public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException { // 判断类名是否为目标类名 if (!className.equals(this.editVNClassName)) { return classfileBuffer; } ClassReader cr = new ClassReader(classfileBuffer); ClassNode classNode = new ClassNode(ASM7); cr.accept(classNode, ClassReader.SKIP_FRAMES); for (MethodNode methodNode : classNode.methods) { if (editMethodName.equals(methodNode.name)) { InsnList instrumentation = new InsnList(); instrumentation.add(new FieldInsnNode(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;")); instrumentation.add(new LdcInsnNode("Hello, Instrumentation!")); instrumentation .add(new MethodInsnNode(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false)); methodNode.instructions.insert(instrumentation); } } ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES | ClassWriter.COMPUTE_MAXS); classNode.accept(cw); return cw.toByteArray(); } }
上面代码使用ClassReader读取所传入的 byte 数组,并将其转换成ClassNode。然后我们将遍历ClassNode中的MethodNode节点,也就是该类中的构造器和方法。
当遇到名字为"hello"的方法时,我们会在方法的入口处注入System.out.println("Hello, Instrumentation!");。
打包编译为独立jar文件,
cd /Users/zhenghan/Projects/JavaAgent_Demo/src # 编译成.class 文件,引入本地包含javassist.jar的lib路径 javac -cp ".:./*" agantmainDemo.java # 加入META-INF/MANIFEST.MF,打包 jar -cvfm JavaAgent_Demo.jar META-INF/MANIFEST.MF agantmainDemo.class TransformerDemo.class
运行结果如下所示,
ps -ef | grep HelloWorld # 501 96170 86164 0 11:47AM ttys001 0:04.26 /usr/bin/java -jar HelloWorld.jar cd /Users/zhenghan/Projects/JavaAgent_Demo/src java -Xbootclasspath/a:/Library/Java/JavaVirtualMachines/jdk1.8.0_291.jdk/Contents/Home/lib/tools.jar:/Users/zhenghan/Projects/JavaAgent_Demo/src/asm.jar:/Users/zhenghan/Projects/JavaAgent_Demo/src/asm-tree.jar -jar "/Users/zhenghan/Projects/JavaAgent_Demo/src/JavaAgent_Attach_Demo.jar" "13680" "/Users/zhenghan/Projects/JavaAgent_Demo/src/JavaAgent_Demo.jar"
Java agent 还提供了另外两个功能redefine和retransform。这两个功能针对的是已加载的类,并要求用户传入所要redefine或者retransform的类实例。
参考链接:
https://www.cnblogs.com/yyhuni/p/15371534.html https://github.com/DanielJyc/premain-simple-demo/tree/master https://blog.csdn.net/21aspnet/article/details/81252656 https://toforu.com/blog/article/1062 https://www.118pan.com/o14107 https://xz.aliyun.com/t/9450 https://www.cnblogs.com/rickiyang/p/11336268.html https://www.cnblogs.com/rickiyang/p/11368932.html https://central.sonatype.com/artifact/org.ow2.asm/asm-tree/7.0-beta/versions?smo=true https://central.sonatype.com/artifact/org.ow2.asm/asm/7.0-beta/versions?smo=true https://zhuanlan.zhihu.com/p/340541234 https://ggball.top/pages/a1a9db/#%E7%B2%BE%E9%80%89%E8%AF%84%E8%AE%BA