JVMTM Tool Interface:JVM源码分析之jav

作者: 光剑书架上的书 | 来源:发表于2020-05-08 10:31 被阅读0次

    JVMTM Tool Interface:JVM源码分析之javaagent原理完全解读

    概述

    本文重点讲述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加入到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实现的,只是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既可以在启动时加载,也可以在运行时动态加载。其中启动时加载还可以通过类似-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.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
    • 解析javaagent里MANIFEST.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的实现

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

    Class Redefine的实现

    类重新定义,这是Instrumentation提供的基础功能之一,主要用在已经被加载过的类上,想对其进行修改,要做这件事,我们必须要知道两个东西,一个是要修改哪个类,另外一个是想将那个类修改成怎样的结构,有了这两个信息之后就可以通过InstrumentationImpl下面的redefineClasses方法操作了:

    public void redefineClasses(ClassDefinition[]   definitions) throws  ClassNotFoundException {
    
            if (!isRedefineClassesSupported()) {
    
                throw new UnsupportedOperationException("redefineClasses is not supported in this environment");
    
            }
    
            if (definitions == null) {
    
                throw new NullPointerException("null passed as 'definitions' in redefineClasses");
    
            }
    
            for (int i = 0; i < definitions.length; ++i) {
    
                if (definitions[i] == null) {
    
                    throw new NullPointerException("element of 'definitions' is null in redefineClasses");
    
                }
    
            }
    
            if (definitions.length == 0) {
    
                return; // short-circuit if there are no changes requested
    
            }
    
            redefineClasses0(mNativeAgent, definitions);
    
        }
    
    

    在JVM里对应的实现是创建一个VM_RedefineClasses的VM_Operation,注意执行它的时候会stop-the-world:

    jvmtiError
    
    JvmtiEnv::RedefineClasses(jint class_count, const jvmtiClassDefinition* class_definitions) {
    
    //TODO: add locking
    
      VM_RedefineClasses op(class_count, class_definitions, jvmti_class_load_kind_redefine);
    
      VMThread::execute(&op);
    
      return (op.check_error());
    
    } /* end RedefineClasses */
    

    这个过程我尽量用语言来描述清楚,不详细贴代码了,因为代码量实在有点大:

    • 挨个遍历要批量重定义的jvmtiClassDefinition
    • 然后读取新的字节码,如果有关注ClassFileLoadHook事件的,还会走对应的transform来对新的字节码再做修改
    • 字节码解析好,创建一个klassOop对象
    • 对比新老类,并要求如下:
      • 父类是同一个
      • 实现的接口数也要相同,并且是相同的接口
      • 类访问符必须一致
      • 字段数和字段名要一致
      • 新增的方法必须是private static/final的
      • 可以删除修改方法
    • 对新类做字节码校验
    • 合并新老类的常量池
    • 如果老类上有断点,那都清除掉
    • 对老类做JIT去优化
    • 对新老方法匹配的方法的jmethodId做更新,将老的jmethodId更新到新的method上
    • 新类的常量池的holer指向老的类
    • 将新类和老类的一些属性做交换,比如常量池,methods,内部类
    • 初始化新的vtable和itable
    • 交换annotation的method、field、paramenter
    • 遍历所有当前类的子类,修改他们的vtable及itable

    上面是基本的过程,总的来说就是只更新了类里的内容,相当于只更新了指针指向的内容,并没有更新指针,避免了遍历大量已有类对象对它们进行更新所带来的开销。

    Class Retransform的实现

    retransform class可以简单理解为回滚操作,具体回滚到哪个版本,这个需要看情况而定,下面不管那种情况都有一个前提,那就是javaagent已经要求要有retransform的能力了:

    • 如果类是在第一次加载的的时候就做了transform,那么做retransform的时候会将代码回滚到transform之后的代码
    • 如果类是在第一次加载的的时候没有任何变化,那么做retransform的时候会将代码回滚到最原始的类文件里的字节码
    • 如果类已经加载了,期间类可能做过多次redefine(比如被另外一个agent做过),但是接下来加载一个新的agent要求有retransform的能力了,然后对类做redefine的动作,那么retransform的时候会将代码回滚到上一个agent最后一次做redefine后的字节码

    我们从InstrumentationImpl的retransformClasses方法参数看猜到应该是做回滚操作,因为我们只指定了class:

        public void retransformClasses(Class<?>[] classes) {
    
            if (!isRetransformClassesSupported()) {
    
                throw new UnsupportedOperationException( "retransformClasses is not supported in this environment");
    
            }
    
            retransformClasses0(mNativeAgent, classes);
    
        }
    

    不过retransform的实现其实也是通过redefine的功能来实现,在类加载的时候有比较小的差别,主要体现在究竟会走哪些transform上,如果当前是做retransform的话,那将忽略那些注册到正常的TransformerManager里的ClassFileTransformer,而只会走专门为retransform而准备的TransformerManager的ClassFileTransformer,不然想象一下字节码又被无声无息改成某个中间态了。

    private:
    
      void post_all_envs() {
    
        if (_load_kind != jvmti_class_load_kind_retransform) {
    
          // for class load and redefine,
    
          // call the non-retransformable agents
    
          JvmtiEnvIterator it;
    
          for (JvmtiEnv* env = it.first(); env != NULL; env = it.next(env)) {
    
            if (!env->is_retransformable() && env->is_enabled(JVMTI_EVENT_CLASS_FILE_LOAD_HOOK)) {
    
              // non-retransformable agents cannot retransform back,
    
              // so no need to cache the original class file bytes
    
              post_to_env(env, false);
    
            }
    
          }
    
        }
    
        JvmtiEnvIterator it;
    
        for (JvmtiEnv* env = it.first(); env != NULL; env = it.next(env)) {
    
          // retransformable agents get all events
    
          if (env->is_retransformable() && env->is_enabled(JVMTI_EVENT_CLASS_FILE_LOAD_HOOK)) {
    
            // retransformable agents need to cache the original class file
    
            // bytes if changes are made via the ClassFileLoadHook
    
            post_to_env(env, true);
    
          }
    
        }
    
      }
    
    

    javaagent的其他小众功能

    javaagent除了做字节码上面的修改之外,其实还有一些小功能,有时候还是挺有用的

    • 获取所有已经被加载的类:Class[] getAllLoadedClasses();
    • 获取所有已经初始化了的类: Class[] getInitiatedClasses(ClassLoader loader);
    • 获取某个对象的大小: long getObjectSize(Object objectToSize);
    • 将某个jar加入到bootstrap classpath里优先其他jar被加载: void appendToBootstrapClassLoaderSearch(JarFile jarfile);
    • 将某个jar加入到classpath里供appclassloard去加载:void appendToSystemClassLoaderSearch(JarFile jarfile);
    • 设置某些native方法的前缀,主要在找native方法的时候做规则匹配: void setNativeMethodPrefix(ClassFileTransformer transformer, String prefix)。

    Debug的实现原理

    IDEA 的 Debug 能查看断点的上下文环境,更神奇的是我可以在断点处使用它的 Evaluate 功能直接执行某些命令,进行一些计算或改变当前变量。

    很多时候,开发者都是“面向 Debug 开发”。

    但 Java 是静态语言,运行之前是要先进行编译的,难道我写的这些代码是被实时编译又”注入”到我正在 Debug 的服务里了吗?

    反射、字节码、Btrace , Java 的 ASM 框架和 JVM TI 接口。

    Java 代码都是要被编译成字节码后才能放到 JVM 里执行的,而字节码一旦被加载到虚拟机中,就可以被解释执行。

    字节码文件(.class)就是普通的二进制文件,它是通过 Java 编译器生成的。而只要是文件就可以被改变,如果我们用特定的规则解析了原有的字节码文件,对它进行修改或者干脆重新定义,这不就可以改变代码行为了么。

    Java 生态里有很多可以动态生成字节码的技术,像 BCEL、Javassist、ASM、CGLib 等,它们各有自己的优势。有的使用复杂却功能强大、有的简单确也性能些差。

    ASM 框架

    ASM 是它们中最强大的一个,使用它可以动态修改类、方法,甚至可以重新定义类,连 CGLib 底层都是用 ASM 实现的。

    当然,它的使用门槛也很高,使用它需要对 Java 的字节码文件有所了解,熟悉 JVM 的编译指令。虽然我对 JVM 的字节码语法不熟,但有大神开发了可以在 IDEA 里查看字节码的插件:ASM Bytecode Outline ,在要查看的类文件里右键选择 Show bytecode Outline 即可以右侧的工具栏查看我们要生成的字节码。对照着示例,我们就可以很轻松地写出操作字节码的 Java 代码了。

    而切到 ASMified 标签栏,我们甚至可以直接获取到 ASM 的使用代码。

    常用方法

    在 ASM 的代码实现里,最明显的就是访问者模式,ASM 将对代码的读取和操作都包装成一个访问者,在解析 JVM 加载到的字节码时调用。

    ClassReader 是 ASM 代码的入口,通过它解析二进制字节码,实例化时它时,我们需要传入一个 ClassVisitor,在这个 Visitor 里,我们可以实现 visitMethod()/visitAnnotation() 等方法,用以定义对类结构(如方法、字段、注解)的访问方法。

    而 ClassWriter 接口继承了 ClassVisitor 接口,我们在实例化类访问器时,将 ClassWriter “注入” 到里面,以实现对类写入的声明。

    Instrument


    介绍

    字节码是修改完了,可是 JVM 在执行时会使用自己的类加载器加载字节码文件,加载后并不会理会我们做出的修改,要想实现对现有类的修改,我们还需要搭配 Java 的另一个库 instrument

    instrument 是 JVM 提供的一个可以修改已加载类文件的类库。1.6以前,instrument 只能在 JVM 刚启动开始加载类时生效,之后,instrument 更是支持了在运行时对类定义的修改。

    使用

    要使用 instrument 的类修改功能,我们需要实现它的 ClassFileTransformer 接口定义一个类文件转换器。它唯一的一个 transform() 方法会在类文件被加载时调用,在 transform 方法里,我们可以对传入的二进制字节码进行改写或替换,生成新的字节码数组后返回,JVM 会使用 transform 方法返回的字节码数据进行类的加载。

    JVM TI


    定义完了字节码的修改和重定义方法,但我们怎么才能让 JVM 能够调用我们提供的类转换器呢?这里又要介绍到 JVM TI 了。

    介绍

    JVM TI(JVM Tool Interface)JVM 工具接口是 JVM 提供的一个非常强大的对 JVM 操作的工具接口,通过这个接口,我们可以实现对 JVM 多种组件的操作,从JVMTM Tool Interface 这里我们认识到 JVM TI 的强大,它包括了对虚拟机堆内存、类、线程等各个方面的管理接口。

    JVM TI 通过事件机制,通过接口注册各种事件勾子,在 JVM 事件触发时同时触发预定义的勾子,以实现对各个 JVM 事件的感知和反应。

    Agent

    Agent 是 JVM TI 实现的一种方式。我们在编译 C 项目里链接静态库,将静态库的功能注入到项目里,从而才可以在项目里引用库里的函数。我们可以将 agent 类比为 C 里的静态库,我们也可以用 C 或 C++ 来实现,将其编译为 dll 或 so 文件,在启动 JVM 时启动。

    这时再来思考 Debug 的实现,我们在启动被 Debug 的 JVM 时,必须添加参数 -agentlib:jdwp=transport=dt_socket,suspend=y,address=localhost:3333,而 -agentlib 选项就指定了我们要加载的 Java Agent,jdwp 是 agent 的名字,在 linux 系统中,我们可以在 jre 目录下找到 jdwp.so 库文件。

    Java 的调试体系 jdpa 组成,从高到低分别为 jdi->jdwp->jvmti,我们通过 JDI 接口发送调试指令,而 jdwp 就相当于一个通道,帮我们翻译 JDI 指令到 JVM TI,最底层的 JVM TI 最终实现对 JVM 的操作。

    使用

    JVM TI 的 agent 使用很简单,在启动 agent 时添加 -agent 参数指定我们要加载的 agent jar包即可。

    而要实现代码的修改,我们需要实现一个 instrument agent,它可以通过在一个类里添加 premain()agentmain() 方法来实现。而要实现 1.6 以上的动态 instrument 功能,实现 agentmain 方法即可。

    在 agentmain 方法里,我们调用 Instrumentation.retransformClasses() 方法实现对目标类的重定义。

    另外往一个正在运行的 JVM 里动态添加 agent,还需要用到 JVM 的 attach 功能,Sun 公司的 tools.jar 包里包含的 VirtualMachine 类提供了 attach 一个本地 JVM 的功能,它需要我们传入一个本地 JVM 的 pid, tools.jar 可以在 jre 目录下找到。

    agent生成

    另外,我们还需要注意 agent 的打包,它需要指定一个 Agent-Class 参数指定我们的包括 agentmain 方法的类,可以算是指定入口类吧。

    此外,还需要配置 MANIFEST.MF 文件的一些参数,允许我们重新定义类。如果你的 agent 实现还需要引用一些其他类库时,还需要将这些类库都打包到此 jar 包中,下面是我的 pom 文件配置。

        <build>
            <plugins>
                <plugin>
                    <groupId>org.apache.maven.plugins</groupId>
                    <artifactId>maven-assembly-plugin</artifactId>
                    <configuration>
                        <archive>
                            <manifestEntries>
                                <Agent-Class>asm.TestAgent</Agent-Class>
                                <Can-Redefine-Classes>true</Can-Redefine-Classes>
                                <Can-Retransform-Classes>true</Can-Retransform-Classes>
                                <Manifest-Version>1.0</Manifest-Version>
                                <Permissions>all-permissions</Permissions>
                            </manifestEntries>
                        </archive>
                        <descriptorRefs>
                            <descriptorRef>jar-with-dependencies</descriptorRef>
                        </descriptorRefs>
                    </configuration>
                </plugin>
            </plugins>
        </build>
    
    

    另外在打包时需要使用 mvn assembly:assembl 命令生成 jar-with-dependencies 作为 agent。

    代码实现


    我在测试时写了一个用以上技术实现了一个简单的字节码动态修改的 Demo。

    被修改的类

    TransformTarget 是要被修改的目标类,正常执行时,它会三秒输出一次 “hello”。

    public class TransformTarget {
        public static void main(String[] args) {
            while (true) {
                try {
                    Thread.sleep(3000L);
                } catch (Exception e) {
                    break;
                }
                printSomething();
            }
        }
    
        public static void printSomething() {
            System.out.println("hello");
        }
    
    }
    
    

    Agent

    Agent 是执行修改类的主体,它使用 ASM 修改 TransformTarget 类的方法,并使用 instrument 包将修改提交给 JVM。

    入口类,也是代理的 Agent-Class。

    public class TestAgent {
        public static void agentmain(String args, Instrumentation inst) {
            inst.addTransformer(new TestTransformer(), true);
            try {
                inst.retransformClasses(TransformTarget.class);
                System.out.println("Agent Load Done.");
            } catch (Exception e) {
                System.out.println("agent load failed!");
            }
        }
    }
    
    

    执行字节码修改和转换的类。

    public class TestTransformer implements ClassFileTransformer {
    
        public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
            System.out.println("Transforming " + className);
            ClassReader reader = new ClassReader(classfileBuffer);
            ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_FRAMES);
            ClassVisitor classVisitor = new TestClassVisitor(Opcodes.ASM5, classWriter);
            reader.accept(classVisitor, ClassReader.SKIP_DEBUG);
            return classWriter.toByteArray();
        }
    
        class TestClassVisitor extends ClassVisitor implements Opcodes {
            TestClassVisitor(int api, ClassVisitor classVisitor) {
                super(api, classVisitor);
            }
    
            @Override
            public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
                MethodVisitor mv = super.visitMethod(access, name, desc, signature, exceptions);
                if (name.equals("printSomething")) {
                    mv.visitCode();
                    Label l0 = new Label();
                    mv.visitLabel(l0);
                    mv.visitLineNumber(19, l0);
                    mv.visitFieldInsn(Opcodes.GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
                    mv.visitLdcInsn("bytecode replaced!");
                    mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
                    Label l1 = new Label();
                    mv.visitLabel(l1);
                    mv.visitLineNumber(20, l1);
                    mv.visitInsn(Opcodes.RETURN);
                    mv.visitMaxs(2, 0);
                    mv.visitEnd();
                    TransformTarget.printSomething();
                }
                return mv;
            }
        }
    }
    
    

    Attacher

    使用 tools.jar 里方法将 agent 动态加载到目标 JVM 的类。

    public class Attacher {
        public static void main(String[] args) throws AttachNotSupportedException, IOException, AgentLoadException, AgentInitializationException {
    
            VirtualMachine vm = VirtualMachine.attach("34242"); // 目标 JVM pid
            vm.loadAgent("/path/to/agent.jar");
        }
    }
    
    

    这样,先启动 TransformTarget 类,获取到 pid 后将其传入 Attacher 里,并指定 agent jar,将 agent attach 到 TransformTarget 中,原来输出的 “hello” 就变成我们想要修改的 “bytecode replaced!” 了。

    参考链接

    https://www.cnblogs.com/zhenbianshu/p/10210597.html
    https://docs.oracle.com/javase/7/docs/platform/jvmti/jvmti.html
    https://www.cnblogs.com/beautiful-code/p/6424931.html


    Kotlin开发者社区

    专注分享 Java、 Kotlin、Spring/Spring Boot、MySQL、redis、neo4j、NoSQL、Android、JavaScript、React、Node、函数式编程、编程思想、"高可用,高性能,高实时"大型分布式系统架构设计主题。

    相关文章

      网友评论

        本文标题:JVMTM Tool Interface:JVM源码分析之jav

        本文链接:https://www.haomeiwen.com/subject/ozoaghtx.html