java字节码注入

BTrace 简要介绍

2016-10-28  本文已影响3138人  HelloArmin

BTrace是Java的安全可靠的动态跟踪工具。 他的工作原理是通过 instrument + asm 来对正在运行的java程序中的class类进行动态增强。

说他是安全可靠的,是因为它对正在运行的程序是只读的。也就是说,他可以插入跟踪语句来检测和分析运行中的程序,不允许对其进行修改。因此他存在一些限制:

这些限制其实是可以使用unsafe模式绕过。通过声明 *@BTrace(unsafe = true) annotation 并且以unsafe 模式-u运行btrace

实际使用非安全模式跟踪时,发现一个问题,一个进程如果被安全模式btrace探测过一次, 后面再使用非安全模式进行探测时非安全模式不生效。

安装并使用

运行BTrace

btrace <pid> <btrace-script>

也可以先使用btracec对btrace脚本继续预先编译再进行执行。

visualvm-plugin

btrace还提供了一个vaisualvm上的一个插件,可以执行btrace脚本。尝试了下,可以attach到本机的jvm进程上,但是远程主机的JVM进程不行。晚上有的说通过端口转发绑定的方式可以,但是还是没有试出来。

安装

运行jvisualvm.exe, 选择工具->插件->可用插件 选择 BTrace Workbench进行在线安装。

jvisualvm_btrace.jpg

在线不能安装的,也可以通过手动安装。先从java visualvm 插件中心找到相应版本的插件更新文件地址进行下载,从更新文件中获取所需要的插件包的nbm下载地址进行下载。最后在 插件->已下载tab页->添加插件

执行btrace脚本

选择需要监控的进程,右击 trace application

jvisualvm_btrace2.png

在btrace的工作台中直接编写脚本并执行

jvisualvm_btrace3.png

BTrace 脚本

先来看简单的BTrace脚本的使用,用于跟踪方式执行时间

@BTrace
public class TimeLogger {

  @TLS private static long startTime = 0;

  @OnMethod(clazz="org.springframework.data.redis.core.DefaultValueOperations", method="get")
  public static void startExecute(){
    startTime = timeNanos();
  }

  @OnMethod(clazz="org.springframework.data.redis.core.DefaultValueOperations", method="get",
                                            location=@Location(Kind.RETURN)
  )
  public static void endExecute(@Duration long duration){
    long time = timeNanos() - startTime;
    println(strcat("execute time(nanos): ", str(time)));
  }

}

跟踪方式执行时间程序的逻辑大致是: 在org.springframework.data.redis.core.DefaultValueOperations 对象执行get方法时,先记录一下当前时间,在get方法return时获取当前时间,从而获得方法执行所消耗的时间。值得注意的是,@TLS声明的变量是 ThreadLocal的, 每个线程都会有一份这个自己的startTime 变量。

执行以上的TimeLogger来看一下:

$ btrace 13778 TimeLogger.java 
execute time(nanos): 214692000
execute time(nanos): 1215000
execute time(nanos): 3210000

执行后,当被监控的程序运行了这些检查点的方法时,btrace会在控制台对执行时间进行输出。通过重定向符>也可以将输出重定向到文件.

API

@BTrace

声明这个类是个BTrace脚本.unsafe参数表示是否不安全的模式执行.

方法annotation

未声明注解的方法参数

未声明注解的方法参数的映射,根据探测点类型locaiton的不同而不同:

字段注解

参数annotation介绍

BTrace原理分析

BTrace 主要使用了 Instrumentation + ASM技术来实现对正在运行进程的探测。

基本实现逻辑

虚拟机其实提供了一个hook,那就是Instrumentation,可以将独立于应用程序的代理程序agent程序随着着应用程序一起启动或者attach挂载到正在运行中的应用程序。而在代理程序中可以对class进行修改或者重新定义。

btrace的agent程序在btrace-agent.jar,通过挂在vm = VirtualMachine.attach(pid); vm.loadAgent 进行挂载到正在运行中的java进程。 源码如下:

btrace-client.jar/com.sun.btrace.client.Main

    /**
     * Attach the BTrace client to the given Java process.
     * Loads BTrace agent on the target process if not loaded
     * already.
     */
    public void attach(String pid, String sysCp, String bootCp) throws IOException {

            String agentPath = "/btrace-agent.jar";
            String tmp = Client.class.getClassLoader().getResource("com/sun/btrace").toString();
            tmp = tmp.substring(0, tmp.indexOf("!"));
            tmp = tmp.substring("jar:".length(), tmp.lastIndexOf("/"));
            agentPath = tmp + agentPath;
            agentPath = new File(new URI(agentPath)).getAbsolutePath();
            attach(pid, agentPath, sysCp, bootCp);

    }


    /**
     * Attach the BTrace client to the given Java process.
     * Loads BTrace agent on the target process if not loaded
     * already. Accepts the full path of the btrace agent jar.
     * Also, accepts system classpath and boot classpath optionally.
     */
    public void attach(String pid, String agentPath, String sysCp, String bootCp) throws IOException {
        try {
            VirtualMachine vm = null;
             byte[] code = client.compile(fileName, classPath, includePath);   // 编译btrace脚本
            vm = VirtualMachine.attach(pid);

           // 此处 省略了诺干代码..
            vm.loadAgent(agentPath, agentArgs); // 挂在 agent 包后,会运行 com.sun.btrace.agent.Main.main 开启server socket后台线程,监听客户端的连接
            client.submit(fileName, code, btraceArgs, createCommandListener(client)); // 将btrace脚本编译后的字节码提交给 正在运行的agent的程序
           
    }

btrace-agent.jar/com.sun.btrace.agent.Main


Thread agentThread = new Thread(new Runnable() {
            @Override
            public void run() {
                BTraceRuntime.enter();
                try {
                    startServer();
                } finally {
                    BTraceRuntime.leave();
                }
            }
        });
        BTraceRuntime.initUnsafe();
        BTraceRuntime.enter();
        try {
            agentThread.setDaemon(true);
            if (isDebug()) debugPrint("starting agent thread");
            agentThread.start();
        } finally {
            BTraceRuntime.leave();
        }

        public static final int BTRACE_DEFAULT_PORT = 2020;

    //-- Internals only below this point
    private static void startServer() {
        int port = BTRACE_DEFAULT_PORT;
        String p = argMap.get("port");
        if (p != null) {
            try {
                port = Integer.parseInt(p);
            } catch (NumberFormatException exp) {
                error("invalid port assuming default..");
            }
        }
        ServerSocket ss;
        try {
            if (isDebug()) debugPrint("starting server at " + port);
            System.setProperty("btrace.port", String.valueOf(port));
            if (scriptOutputFile != null && scriptOutputFile.length() > 0) {
                System.setProperty("btrace.output", scriptOutputFile);
            }
            ss = new ServerSocket(port);
        } catch (IOException ioexp) {
            ioexp.printStackTrace();
            return;
        }

        while (true) {
            try {
                if (isDebug()) debugPrint("waiting for clients");
                Socket sock = ss.accept();
                if (isDebug()) debugPrint("client accepted " + sock);
                Client client = new RemoteClient(inst, sock);
                handleNewClient(client).get();
            } catch (RuntimeException | IOException | ExecutionException re) {
                if (isDebug()) debugPrint(re);
            } catch (InterruptedException e) {
                return;
            }
        }
    }

Instrumentation.ClassFileTransformer:定义了类加载前的预处理类,可以在这个类中对要加载的类的字节码做一些处理
Instrumentation.retransformClasses 对于已经加载的类触发重新加载类定义, 进行ClassFileTransformer转换处理

com.sun.btrace.agent.Main 在会启动一个socket后台线程与btrace client 进行交互. 客户端会将BTRACE 程序的提交到agent去,再通过字节码操作对class进行操作。

在被探测点上增加探测点动作是通过 在Instrumentation代理程序中通过修改字节码来谈价探测点动作。

上一篇 下一篇

猜你喜欢

热点阅读