概述
本文重点讲述javaagent的具体实现,因为它面向的是我们java程序员,而且agent都是用java编写的,不需要太多的c/c++编程基础,不过这篇文章里也会讲到JVMTIAgent(c实现的),因为javaagent的运行还是依赖于一个特殊的JVMTIAgent。
对于javaagent或许大家都听过,甚至使用过,常见的用法大致如下:
java -javaagent:myagent.jar=mode=test Test
我们通过-javaagent来指定我们编写的agent的jar路径(./myagent.jar)及要传给agent的参数(mode=test),这样在启动的时候这个agent就可以做一些我们想要它做的事了。
javaagent的主要的功能如下:
- 可以在加载class文件之前做拦截把字节码做修改
- 可以在运行期将已经加载的类的字节码做变更,但是这种情况下会有很多的限制,后面会详细说
- 还有其他的一些小众的功能
- 获取所有已经被加载过的类
- 获取所有已经被初始化过了的类(执行过了clinit方法,是上面的一个子集)
- 获取某个对象的大小
- 将某个jar加入到bootstrapclasspath里作为高优先级被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,在加载过程中就会调用Agent_OnAttach
函数。Agent_OnUnload
函数,在agent做卸载的时候调用,不过貌似基本上很少实现它。
其实我们每天都在和JVMTIAgent打交道,只是你可能没有意识到而已,比如我们经常使用eclipse等工具对java代码做调试,其实就利用了jre自带的jdwp agent来实现的,只是由于eclipse等工具在没让你察觉的情况下将相关参数(类似-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,也支持在运行期来动态来加载这个agent,其中启动时加载agent还可以通过类似-javaagent:myagent.jar
的方式来间接加载instrument agent,运行期动态加载agent依赖的是jvm的attach机制JVM Attach机制实现,通过发送load命令来加载agent。
instrument agent的核心数据结构如下:
struct _JPLISAgent {
JavaVM * mJVM; /* handle to the JVM */