Java agent
启动时加载的 JavaAgent是JDK1.5 之后引入的新特性,此特性为用户提供了在 JVM 将字节码文件读入内存之后,JVM使用对应的字节流在Java堆中生成一个Class对象之前,用户可以对其字节码进行修改的能力,从而 JVM也将会使用用户修改过之后的字节码进行 Class 对象的创建。
JVM Tool Interface
JVMTI是JVM暴露出来的一些供用户进行自定义扩展的接口集合,每当jvm执行到一些特定的逻辑的时间,就会去进行触发这些回调接口,用户就恰好可以在此回调接口之中做一些自定义逻辑。而对于此次所要描述的JavaAgent也恰恰是基于 JVMTI 的,JPLISAgent就是用作实现 javaagent功能的动态库。
JPLISAgent
JPLISAgent 实现了 Agent_OnLoad 方法,Agent_OnLoad 方法也就是整个启动时加载的 JavaAgent 的入口方法,后续也会说明整个运行流程。
如何使用
虽然大多数同学可能已经使用过 JavaAgent 了,但是为了下面原理的平滑过渡,我这里还是大概写一下使用:
premain 启动类
编写一个含有以下 premain 函数的类
[1] public static void premain(String agentArgs, Instrumentation instrumentation);
[2] public static void premain(String agentArgs);
上面的两个方法只需要实现一个即可,且[1]的优先级是高于[2]的,即如果上面的两个方法同时出现,则只会执行[1]方法
agentArgs 是跟随javaagent:xx.jar=yyy 传入的 yyy 字符串
instrumentation 是一个 java.lang.instrument.Instrumentation 实例,由本地方法实例化并由 jvm 自动传入。此类是 JavaAgent 的核心类。
public class Agent {
public static void premain(String args, Instrumentation inst){
System.out.println("Hi, This is a agent!");
//将类转换器添加到此`agent`的`instrumentation`实例之中
inst.addTransformer(new TestTransformer());
}
}
类转换器
类转换器的作用主要是在某个类的字节码被JVM读入之后,在Java堆上创建Class对象之前,JVM会遍历所有的 instrumentation 实例并执行其中的所有的 ClassFileTransformer 的 transform 方法,其中关于启动时加载的 javaAgent 重点需要关注的入参:
className:当前类的限定类名
classfileBuffer:当前类的以 byte 数组呈现的字节码数据(可能跟 class 文件的数据不一致,因为此处的 byte 数据是此类最新的字节码数据,即此数据可能是原始字节码数据被其他增强方法增强之后的自己买数据)
public class TestTransformer implements ClassFileTransformer {
@Override
public byte[] transform(ClassLoader classLoader, String className, Class<?> classBeingRedefined,
ProtectionDomain protectionDomain, byte[] classfileBuffer) {
//进行对应类字节码的操作,并返回新字节码数据的byte数组,如果返回null,
//则代码不对此字节码作任何操作
return null;
}
}
MAINIFEST.MF
Manifest-Version: 1.0
Premain-Class: test.Agent
实现原理
上面关于 JavaAgent 的使用有涉及到这么几个关键字:
premain,Instrumentation,ClassFileTransformer,MAINIFEST.MF 中的 Premain-Class
- 对于启动命令添加的-javaagent=xx.jar 如果有多个,加载顺序是从前往后,且每一个-javaagent 都是独立的。
- 以下的加载流程仅仅只是针对其中的一个 javaagent 描述的。
- javaAgent 的入口方法就是 InvocationAdapter.c 中的 Agent_OnLoad 方法。经过查看openjdk源码,发现如下注释
/*
* This will be called once for every -javaagent on the command line.
* Each call to Agent_OnLoad will create its own agent and agent data.
*/
每个-javaagent 都会有其自己的agent和agent数据,且每一个 javaagent 都会调用一次 Agent_OnLoad 方法就会被调用一次,且每一次的调用都是独立的。
在 Agent_OnLoad 方法中主要做的事情有下面三个:
- 1)初始化一个 JPLISAgent 对象,并给此对象设置 VMInit事件的回调函数 eventHandlerVMInit.
- 2)找到 jvm 启动参数中-javaagent:xx.jar=yyy 中的 xx.jar 文件添加到 classpath 之中,并获取 yyy
- 3)找到 xx.jar 包中的 MAINIFEST.MF 中定义的 premainClass 并作为此 Agent 的入口
- 4)并将premainClass和 yyy 设置到步骤 1 初始化的 JPLISAgent 对象之中。
当VMInit 事件完成以后,会回调InvocationAdapter.c 中的 eventHandlerVMInit 方法,eventHandlerVMInit方法主要做的事情有下面:
- 1)实例化一个 InstrumentationImpl 对象,jvm 并依借此对象与 java 代码进行交互。
- 2)通过 JNI 执行 MAINIFEST.MF 中定义的类中的 premain 方法(我们上面的例子之中在 premain 方法中给 Instrumentation 对象添加了一个 ClassFileTransformer)
- 3)去除 JPLISAgent 对象中的 VMInit 回调函数,转而设置一个 ClassFileLoadHook 事件的回调函数。
当ClassFileLoadHook事件(在字节码文件被 jvm 读入之后,在 Class 对象创建之前)完成后,进行触发 eventHandlerClassFileLoadHook,此方法主要做的事情有下面几件:
- 1)进行调用 InstrumentationImpl 对象中的 mTransform 方法,而对于 mTransform 方法,最终会调用到我们在 Agent 的 premain 方法中给 Instrumentation 增加的 ClassFileTransformer。
此时,JVM 会通过 JNI 调用 java 代码,对应的类就是 sun.instrument.InstrumentationImpl 类之中,而对应于 mTransform 的方法就是 byte[] transform(ClassLoader var1, String var2, Class<?> var3, ProtectionDomain var4, byte[] var5, boolean var6)
方法是上述中 c 代码调用的地方,其中的 var5 是对应当前文件的字节码数据,如果此接口返回的数据为 null,则认为 transform 方法并未对此 class 文件有过修改,如果返回的数据不为 null,则会使用返回的新字节码作为 jvm 中此类最新的字节码并进行下一个 Javaagent 的处理或者创建 Class 对象进行链接以及初始化。
其中对于 c 代码通过 JNI 反射调用 java 代码的方法声明都在 JPLISAgent.h 类中可以看见
struct _JPLISAgent;
typedef struct _JPLISAgent JPLISAgent;
typedef struct _JPLISEnvironment JPLISEnvironment;
/*
constants for class names and methods names and such
these all must stay in sync with Java code & interfaces
*/
#define JPLIS_INSTRUMENTIMPL_CLASSNAME "sun/instrument/InstrumentationImpl"
#define JPLIS_INSTRUMENTIMPL_CONSTRUCTOR_METHODNAME "<init>"
#define JPLIS_INSTRUMENTIMPL_CONSTRUCTOR_METHODSIGNATURE "(JZZ)V"
#define JPLIS_INSTRUMENTIMPL_PREMAININVOKER_METHODNAME "loadClassAndCallPremain"
#define JPLIS_INSTRUMENTIMPL_PREMAININVOKER_METHODSIGNATURE "(Ljava/lang/String;Ljava/lang/String;)V"
#define JPLIS_INSTRUMENTIMPL_AGENTMAININVOKER_METHODNAME "loadClassAndCallAgentmain"
#define JPLIS_INSTRUMENTIMPL_AGENTMAININVOKER_METHODSIGNATURE "(Ljava/lang/String;Ljava/lang/String;)V"
#define JPLIS_INSTRUMENTIMPL_TRANSFORM_METHODNAME "transform"
#define JPLIS_INSTRUMENTIMPL_TRANSFORM_METHODSIGNATURE \
"(Ljava/lang/ClassLoader;Ljava/lang/String;Ljava/lang/Class;Ljava/security/ProtectionDomain;[BZ)[B"
对于启动时加载的 JavaAgent 涉及到的 c 代码主要为:
其中涉及到的 c 代码在/src/share/instrument 目录下的 JPLISAgent.c,PLISAgent.h,InvocationAdapter.c。
涉及到的 java 代码为 sun.instrument.InstrumentationImpl。
javaAgent 提供了一些接口可以让我们在某些特定的时机进行对于 java 字节码的操作,在不改变代码的前提下,修改最终载入内存的字节码。但是由于直接操作字节码需要对 java 的字节码底层有较深入的研究。所以一些能帮助我们不需要了解字节码底层也能修改字节码的工具就诞生了。其中较为流行的字节码操作工具有 byteBuddy 和 javassist 等等。下一章我们主要去学习如何篡改class字节码。