入侵JVM?Java Agent原理浅析和实践(中)

入侵JVM?Java Agent原理浅析和实践(中),第1张

入侵JVM?Java Agent原理浅析和实践(中)

声明:本文首发于京东零售技术公众号,为博主本人撰写投稿。

JVM运行时Agent

在JDK1.6版本中,SUN更进一步,提供了可以在JVM运行时代理的能力,和启动时代理类似,只需要满足:

  • JAR包的MANIFEST.MF清单文件中定义Agent-Class属性,指定一个类,加入Can-Redefine-Classes 和 Can-Retransform-Classes 选项。
  • JAR包中包含清单文件中定义的这个类,类中包含agentmain方法,方法逻辑可以自己实现

运行时Agent可以在JVM运行时动态的修改某个类的字节码,然后JVM会重定义这个类**(不需要创建新的类加载器)**,但是为了保证JVM的正常运行,新定义的类相较于原来的类需要满足:

  1. 父类是同一个
  2. 实现的接口数也要相同,并且是相同的接口
  3. 类访问符必须一致
  4. 字段数和字段名要一致
  5. 新增或删除的方法必须是private static/final的
  6. 可以修改方法内部代码

运行时Agent需要借助JVM的Attach机制,简单来说就是JVM提供的一种通信机制,JVM中会存在一个Attach Listener线程,监听其他JVM的attach请求,其通信方式基于socket,JVM Attach机制大体流程图如下:

SUN在JDK中提供了Attach机制的Java语言工具包(com.sun.tools.attach),方便开发者使用Java语言进行 *** 作,这里我们使用其中提供的loadAgent方法实现运行中agent的能力。

public class AttachUtil {

    public static void main(String[] args) throws IOException, AgentLoadException, AgentInitializationException, AttachNotSupportedException {

        // 获取运行中的JVM列表
        List vmList = VirtualMachine.list();
        // 需要agent的jar包路径
        String agentJar = "xxxx/agent-test.jar";
        for (VirtualMachineDescriptor vmd : vmList) {
            // 找到测试的JVM
            if (vmd.displayName().endsWith("WorkerMain")) {
                // attach到目标ID的JVM上
                VirtualMachine virtualMachine = VirtualMachine.attach(vmd.id());
                // agent指定jar包到已经attach的JVM上
                virtualMachine.loadAgent(agentJar);
                virtualMachine.detach();
            }
        }
    }

同时对之前启动时Agent的代码进行改写:

public class AgentMain {

    // JVM启动时agent
    public static void premain(String args, Instrumentation inst) {
        agent0(args, inst);
    }

    // JVM运行时agent
    public static void agentmain(String args, Instrumentation inst) {
        agent0(args, inst);
    }

    public static void agent0(String args, Instrumentation inst) {
        System.out.println("agent is running!");
        inst.addTransformer(new ClassFileTransformer() {
            @Override
            public byte[] transform(ClassLoader loader, String className, Class classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) {
                // 打印transform的类名
                System.out.println(className);
                return classfileBuffer;
            }
        },true);

        try {
            // 找到WorkerMain类,对其进行重定义
            Class c = Class.forName("test.WorkerMain");
            inst.retransformClasses(c);
        } catch (Exception e) {
            System.out.println("error!");
        }
    }
}

这里我们也没有对字节码进行修改,还是直接返回原本的字节码。运行AttachUtil类,在目标JVM运行时完成了对其中test.WorkerMain 类的重新定义(虽然并没有修改字节码)。

下面从JDK源码层面对整个流程进行浅析:

当AttachUtil的loadAgent方法调用时,目标JVM会调用自身的Agent_OnAttach方法,这个方法和之前提到的Agent_OnLoad 方法类似,会进行Agent JAR包的解析,不同的是Agent_OnAttach方法会直接注册ClassFileLoadHook事件回调函数,然后执行agentmain方法添加类转换器。

需要注意的是我们在Java代码里调用了Instrumentation#retransformClasses(Class...)方法,追踪代码可以发现最终调用了一个native方法,而这个native方法的实现则在jdk的srcshareinstrumentJPLISAgent.c类中,最终retransformClasses会调用到JVMTI的RetransformClasses方法,这里由于JVM源码实现非常复杂,感兴趣的同学可以自行阅读(hotspot源码路径srcsharevmprimsjvmtiEnv.cpp),简单来说在这个方法里,JVM会触发ClassFileLoadHook事件回调完成类字节码的转换,并完成虚拟机内已经加载的类字节码的热替换。

至此,在JVM运行时悄无声息的完成了类的重定义,不得不佩服JDK开发者的高超手段。

欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/zaji/4668668.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-11-06
下一篇 2022-11-06

发表评论

登录后才能评论

评论列表(0条)

保存