Java技术专题-探针Agent原理分析(上篇)

研究背景

Agent都是用Java编写的,不需要太多的 C/C++ 编程基础,会讲到JVMTIAgent(C 实现的),因为 javaagent 的运行还是依赖于一个特殊的JVMTIAgent


常见的用法大致如下:

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里sun.msc.AppClassLoader去加载
    • 设置某些 native 方法的前缀,主要在查找 native 方法的时候做规则匹配

   让程序按照我们预期的逻辑去执行,听起来是不是挺酷的。

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 其实就是一个动态库,利用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 实现的,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,必须要讲的是一个叫做 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:指向启动方法,如果agent是在启动时加载的,则该方法会被调用。
    sun.instrument.InstrumentationImpl.loadClassAndCallPremain
  • 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.cAgent_OnLoad方法里,这里简单描述下过程:


创建并初始化JPLISAgent

  • 监听VMInit事件,在vm初始化完成之后做下面的事情:
    • 创建InstrumentationImpl对象
    • 监听 ClassFileLoadHook 事件
    • 调用 InstrumentationImpl 的loadClassAndCallPremain方法,在这个方法里会调用 javaagent 里 MANIFEST.MF 里指定的Premain-Class类的premain方法。
    • 解析javaagent里MANIFEST.MF里的参数,并根据这些参数来设置JPLISAgent 里的一些内容
    • 在运行时加载instrument agent

在运行时加载的方式,大致按照下面的方式来操作:

VirtualMachine vm = VirtualMachine.attach(pid); 
vm.loadAgent(agentPath, agentArgs);

上面会通过 JVM 的 attach 机制来请求目标 JVM 加载对应的 agent,过程大致如下:

创建并初始化 JPLISAgent

  • 解析javaagentMANIFEST.MF 里的参数
  • 创建InstrumentationImpl 对象
  • 监听ClassFileLoadHook 事件
  • 调用InstrumentationImpl 的 loadClassAndCallAgentmain 方法,在这个方法里会调用 javaagent 里 MANIFEST.MF 里指定的 Agent-Class 类的 agentmain 方法

instrument agent 的 ClassFileLoadHook 回调实现


不管是启动时还是运行时加载的instrument agent,都关注着同一个 jvmti 事件——ClassFileLoadHook这个事件是在读取字节码文件之后回调时用的,这样可以对原来的字节码做修改

void JNICALL

eventHandlerClassFileLoadHook(  jvmtiEnv *              jvmtienv,
                                JNIEnv *                jnienv,
                                jclass                  class_being_redefined,
                                jobject                 loader,
                                const char*             name,
                                jobject                 protectionDomain,
                                jint                    class_data_len,
                                const unsigned char*    class_data,
                                jint*                   new_class_data_len,
                                unsigned char**         new_class_data) {

    JPLISEnvironment * environment  = NULL;

    environment = getJPLISEnvironment(jvmtienv);

    /* if something is internally inconsistent (no agent), just silently return without touching the buffer */

    if ( environment != NULL ) {

        jthrowable outstandingException = preserveThrowable(jnienv);
        transformClassFile( environment->mAgent,
                            jnienv,
                            loader,
                            name,
                            class_being_redefined,
                            protectionDomain,
                            class_data_len,
                            class_data,
                            new_class_data_len,
                            new_class_data,
                            environment->mIsRetransformer);

        restoreThrowable(jnienv, outstandingException);
    }
}

先根据jvmtiEnv取得对应的JPLISEnvironment,因为上面我已经说到其实有两个JPLISEnvironment(并且有两个jvmtiEnv),其中一个是专门做retransform的。


而另外一个用来做其他事情,根据不同的用途,在注册具体的
ClassFileTransformer 时也是分开的,对于作为retransform用的 ClassFileTransformer,我们会注册到一个单独的 TransformerManager 里。


接着调用transformClassFile方法,由于函数实现比较长,这里就不贴代码了,大致意思就是调用 InstrumentationImpl 对象的 transform 方法,根据最后那个参数来决定选哪个TransformerManager里的 ClassFileTransformer对象们做transform操作。

private byte[]
    transform(  ClassLoader         loader,
                String              classname,
                Class               classBeingRedefined,
                ProtectionDomain    protectionDomain,
                byte[]              classfileBuffer,
                boolean             isRetransformer) {

        TransformerManager mgr = isRetransformer?

                                        mRetransfomableTransformerManager :
                                        mTransformerManager;

        if (mgr == null) {

            return null; // no manager, no transform

        } else {

            return mgr.transform(   loader,
                                    classname,
                                    classBeingRedefined,
                                    protectionDomain,
                                    classfileBuffer);

        }

    }


  public byte[]

    transform(  ClassLoader         loader,
                String              classname,
                Class               classBeingRedefined,
                ProtectionDomain    protectionDomain,
                byte[]              classfileBuffer) {

        boolean someoneTouchedTheBytecode = false;
        TransformerInfo[]  transformerList = getSnapshotTransformerList();
        byte[]  bufferToUse = classfileBuffer;

        // order matters, gotta run 'em in the order they were added

        for ( int x = 0; x < transformerList.length; x++ ) {

            TransformerInfo         transformerInfo = transformerList[x];
            ClassFileTransformer    transformer = transformerInfo.transformer();
            byte[]                  transformedBytes = null;

            try {

                transformedBytes = transformer.transform(   loader,
                                                            classname,
                                                            classBeingRedefined,
                                                            protectionDomain,
                                                            bufferToUse);

            }

            catch (Throwable t) {

                // don't let any one transformer mess it up for the others.
                // This is where we need to put some logging. What should go here? FIXME

            }


            if ( transformedBytes != null ) {
                someoneTouchedTheBytecode = true;
                bufferToUse = transformedBytes;
            }

        }


        // if someone modified it, return the modified buffer.
        // otherwise return null to mean "no transforms occurred"

        byte [] result;

        if ( someoneTouchedTheBytecode ) {
            result = bufferToUse;
        }
        else {
            result = null;
        }

        return result;

    }   

以上是最终调到的 java 代码,可以看到已经调用到我们自己编写的 javaagent 代码里了,我们一般是实现一个 ClassFileTransformer 类,然后创建一个对象注册到对应的 TransformerManager 里。

这里说的 class transform 其实是狭义的,主要是针对第一次类文件加载时就要求被 transform 的场景,在加载类文件的时候发出 ClassFileLoad 事件,然后交给 instrumenat agent 来调用 javaagent 里注册的 ClassFileTransformer 实现字节码的修改。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 201,924评论 5 474
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,781评论 2 378
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 148,813评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,264评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,273评论 5 363
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,383评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,800评论 3 393
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,482评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,673评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,497评论 2 318
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,545评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,240评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,802评论 3 304
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,866评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,101评论 1 258
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,673评论 2 348
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,245评论 2 341