参考文献
http://www.infoq.com/cn/articles/javaagent-illustrated
http://lovestblog.cn/blog/2014/06/18/jvm-attach/
原理
对于javaagent,或许大家都听过,甚至使用过,常见的用法大致如下:
java -javaagent:myagent.jar=mode=test Test
我们通过-javaagent来指定我们编写的agent的jar路径(./myagent.jar),以及要传给agent的参数(mode=test),在启动的时候这个agent就可以做一些我们希望的事了。
javaagent的主要功能如下:
- 可以在加载class文件之前做拦截,对字节码做修改
- 可以在运行期对已加载类的字节码做变更,但是这种情况下会有很多的限制,后面会详细说
还有其他一些小众的功能 - 获取所有已经加载过的类
- 获取所有已经初始化过的类(执行过clinit方法,是上面的一个子集)
- 获取某个对象的大小
- 将某个jar加入到bootstrap classpath里作为高优先级被bootstrapClassloader加载
- 将某个jar加入到classpath里供AppClassloard去加载
- 设置某些native方法的前缀,主要在查找native方法的时候做规则匹配
想象一下可以让程序按照我们预期的逻辑去执行,听起来是不是挺酷的。
JVMTI
JVMTI全称JVM Tool Interface,是JVM暴露出来的一些供用户扩展的接口集合。JVMTI是基于事件驱动的,JVM每执行到一定的逻辑就会调用一些事件的回调接口(如果有的话),这些接口可以供开发者扩展自己的逻辑。
比如最常见的,我们想在某个类的字节码文件读取之后、类定义之前修改相关的字节码,从而使创建的class对象是我们修改之后的字节码内容,那就可以实现一个回调函数赋给jvmtiEnv(JVMTI的运行时,通常一个JVMTIAgent对应一个jvmtiEnv,但是也可以对应多个)的回调方法集合里的ClassFileLoadHook
,这样在接下来的类文件加载过程中都会调用到这个函数中,大致实现如下:,
jvmtiEventCallbacks callbacks;
jvmtiEnv * jvmtienv = jvmti(agent);
jvmtiError jvmtierror;
memset(&callbacks, 0, sizeof(callbacks));
callbacks.ClassFileLoadHook = &eventHandlerClassFileLoadHook;
jvmtierror = (*jvmtienv)->SetEventCallbacks( jvmtienv,
&callbacks,
sizeof(callbacks));
JVMTIAgent
JVMTIAgent其实就是一个动态库,利用JVMTI暴露出来的一些接口来干一些我们想做、但是正常情况下又做不到的事情,不过为了和普通的动态库进行区分,它一般会实现如下的一个或者多个函数:
JNIEXPORT jint JNICALL
Agent_OnLoad(JavaVM *vm, char *options, void *reserved);
JNIEXPORT jint JNICALL
Agent_OnAttach(JavaVM* vm, char* options, void* reserved);
JNIEXPORT void JNICALL
Agent_OnUnload(JavaVM *vm);
Agent_OnLoad
函数,如果agent是在启动时加载的,也就是在vm参数里通过-agentlib来指定的,那在启动过程中就会去执行这个agent里的Agent_OnLoad函数。Agent_OnAttach
函数,如果agent不是在启动时加载的,而是我们先attach到目标进程上,然后给对应的目标进程发送load命令来加载,则在加载过程中会调用Agent_OnAttach函数。Agent_OnUnload
函数,在agent卸载时调用,不过貌似基本上很少实现它。
其实我们每天都在和JVMTIAgent打交道,只是你可能没有意识到而已,比如我们经常使用Eclipse等工具调试Java代码,其实就是利用JRE自带的jdwp agent实现的,只是IDEA等工具在没让你察觉的情况下将相关参数(类似-agentlib:jdwp=transport=dt_socket,suspend=y,address=localhost:61349
)自动加到程序启动参数列表里了,其中agentlib参数就用来跟要加载的agent的名字,比如这里的jdwp(不过这不是动态库的名字,JVM会做一些名称上的扩展,比如在Linux下会去找libjdwp.so的动态库进行加载,也就是在名字的基础上加前缀lib,再加后缀.so),接下来会跟一堆相关的参数,将这些参数传给Agent_OnLoad
或者Agent_OnAttach
函数里对应的options。
javaagent
说到javaagent,必须要讲的是一个叫做instrument的JVMTIAgent(Linux下对应的动态库是libinstrument.so),因为javaagent功能就是它来实现的,另外instrument agent还有个别名叫JPLISAgent(Java Programming Language Instrumentation Services Agent),这个名字也完全体现了其最本质的功能:就是专门为Java语言编写的插桩服务提供支持的。
instrument agent
instrument agent实现了Agent_OnLoad
和Agent_OnAttach
两方法,也就是说在使用时,agent既可以在启动时加载,也可以在运行时动态加载。其中启动时加载还可以通过类似-javaagent:myagent.jar的方式来间接加载instrument agent,运行时动态加载依赖的是JVM的attach机制(JVM Attach机制实现),通过发送load命令来加载agent。
instrument agent的核心数据结构如下:
struct _JPLISAgent {
JavaVM * mJVM; /* handle to the JVM */
JPLISEnvironment mNormalEnvironment; /* for every thing but retransform stuff */
JPLISEnvironment mRetransformEnvironment;/* for retransform stuff only */
jobject mInstrumentationImpl; /* handle to the Instrumentation instance */
jmethodID mPremainCaller; /* method on the InstrumentationImpl that does the premain stuff (cached to save lots of lookups) */
jmethodID mAgentmainCaller; /* method on the InstrumentationImpl for agents loaded via attach mechanism */
jmethodID mTransform; /* method on the InstrumentationImpl that does the class file transform */
jboolean mRedefineAvailable; /* cached answer to "does this agent support redefine" */
jboolean mRedefineAdded; /* indicates if can_redefine_classes capability has been added */
jboolean mNativeMethodPrefixAvailable; /* cached answer to "does this agent support prefixing" */
jboolean mNativeMethodPrefixAdded; /* indicates if can_set_native_method_prefix capability has been added */
char const * mAgentClassName; /* agent class name */
char const * mOptionsString; /* -javaagent options string */
};
struct _JPLISEnvironment {
jvmtiEnv * mJVMTIEnv; /* the JVM TI environment */
JPLISAgent * mAgent; /* corresponding agent */
jboolean mIsRetransformer; /* indicates if special environment */
};
这里解释一下几个重要项:
- mNormalEnvironment:主要提供正常的类transform及redefine功能。
- mRetransformEnvironment:主要提供类retransform功能。
- mInstrumentationImpl:这个对象非常重要,也是我们Java agent和JVM进行交互的入口,或许写过javaagent的人在写
premain
以及agentmain
方法的时候注意到了有个Instrumentation参数,该参数其实就是这里的对象。 - mPremainCaller:指向
sun.instrument.InstrumentationImpl.loadClassAndCallPremain
方法,如果agent是在启动时加载的,则该方法会被调用。 - mAgentmainCaller:指向
sun.instrument.InstrumentationImpl.loadClassAndCallAgentmain
方法,该方法在通过attach的方式动态加载agent的时候调用。 - mTransform:指向
sun.instrument.InstrumentationImpl.transform
方法。
mAgentClassName:在我们javaagent的MANIFEST.MF里指定的Agent-Class
。 - mOptionsString:传给agent的一些参数。
- mRedefineAvailable:是否开启了redefine功能,在javaagent的MANIFEST.MF里设置
Can-Redefine-Classes:true
。 - mNativeMethodPrefixAvailable:是否支持native方法前缀设置,同样在javaagent的MANIFEST.MF里设置
Can-Set-Native-Method-Prefix:true
。 - mIsRetransformer:如果在javaagent的MANIFEST.MF文件里定义了
Can-Retransform-Classes:true
,将会设置mRetransformEnvironment的mIsRetransformer为true。
在启动时加载instrument agent
正如前面“概述”里提到的方式,就是启动时加载instrument agent,具体过程都在InvocationAdapter.c
的Agent_OnLoad
方法里,这里简单描述下过程:
创建并初始化JPLISAgent
监听VMInit事件,在vm初始化完成之后做下面的事情:
1. 创建InstrumentationImpl对象
2. 监听ClassFileLoadHook事件
3. 调用InstrumentationImpl的loadClassAndCallPremain
方法,在这个方法里会调用javaagent里MANIFEST.MF里指定的Premain-Class
类的premain方法
4. 解析javaagent里MANIFEST.MF里的参数,并根据这些参数来设置JPLISAgent里的一些内容
5. 在运行时加载instrument agent
在运行时加载的方式,大致按照下面的方式来操作:
VirtualMachine vm = VirtualMachine.attach(pid);
vm.loadAgent(agentPath, agentArgs);
上面会通过JVM的attach机制来请求目标JVM加载对应的agent,过程大致如下:
创建并初始化JPLISAgent
解析javaagent里MANIFEST.MF里的参数
创建InstrumentationImpl对象
监听ClassFileLoadHook事件
调用InstrumentationImpl的loadClassAndCallAgentmain方法,在这个方法里会调用javaagent里MANIFEST.MF里指定的Agent-Class类的agentmain方法
实现
#include <jni.h>
#include <jvmti.h>
#include <stdint.h>
#include <stdio.h>
#include <string.h>
_Atomic(int32_t) count = ATOMIC_VAR_INIT(0);
JNIEXPORT jint JNICALL
Java_com_github_marschall_excount_ExceptionCounter_getCount(JNIEnv *env,
jobject thisObj) {
return atomic_load(&count);
}
JNIEXPORT jint JNICALL
Java_com_github_marschall_excount_ExceptionCounter_clearAndGetCount(JNIEnv *env,
jobject thisObj) {
return atomic_exchange(&count, 0);
}
void JNICALL ExceptionCallback(jvmtiEnv *jvmti, JNIEnv *env, jthread thread,
jmethodID method, jlocation location, jobject exception,
jmethodID catch_method, jlocation catch_location) {
atomic_fetch_add(&count , 1);
char* class_name;
jclass exception_class = (*env)->GetObjectClass(env, exception);
(*jvmti)->GetClassSignature(jvmti, exception_class, &class_name, NULL);
fprintf(stdout, "Exception: %s\n", class_name);
}
JNIEXPORT jint JNICALL Agent_OnLoad(JavaVM *vm, char *options, void *reserved) {
jvmtiEnv* jvmti;
jvmtiEventCallbacks callbacks;
jvmtiCapabilities capabilities;
if ((*vm)->GetEnv(vm, (void**)&jvmti, JVMTI_VERSION_1_0) != JNI_OK) {
fprintf(stderr, "GetEnv failed\n");
return -1;
}
memset(&capabilities, 0, sizeof(capabilities));
capabilities.can_generate_exception_events = 1;
if ((*jvmti)->AddCapabilities(jvmti, &capabilities) != JVMTI_ERROR_NONE) {
fprintf(stderr, "AddCapabilities failed\n");
return -1;
}
memset(&callbacks, 0, sizeof(callbacks));
callbacks.Exception = ExceptionCallback;
if ((*jvmti)->SetEventCallbacks(jvmti, &callbacks, sizeof(callbacks)) != JVMTI_ERROR_NONE) {
fprintf(stderr, "SetEventCallbacks failed\n");
return -1;
}
if ((*jvmti)->SetEventNotificationMode(jvmti, JVMTI_ENABLE, JVMTI_EVENT_EXCEPTION, NULL) != JVMTI_ERROR_NONE) {
fprintf(stderr, "SetEventNotificationMode failed\n");
return -1;
}
return 0;
}
/* JNIEXPORT jint JNICALL Agent_OnAttach(JavaVM* vm, char *options, void *reserved) */
JNIEXPORT void JNICALL Agent_OnUnload(JavaVM *vm) {
jvmtiEnv* jvmti;
jvmtiCapabilities capabilities;
if ((*vm)->GetEnv(vm, (void**)&jvmti, JVMTI_VERSION_1_0) != JNI_OK) {
fprintf(stderr, "GetEnv failed\n");
return;
}
memset(&capabilities, 0, sizeof(capabilities));
capabilities.can_generate_exception_events = 1;
if ((*jvmti)->RelinquishCapabilities(jvmti, &capabilities) != JVMTI_ERROR_NONE) {
fprintf(stderr, "RelinquishCapabilities failed\n");
}
}
编译
$JAVA_HOME/include/linux -Wall -fPIC excount.c -o excount.so
attach
java -cp $JAVA_HOME/lib/tools.jar:xxx/ec.jar xxx.ExceptionCounterAttacher -p pidToAttach -a xxx/excount.so