背景

周五下班回家,在公司班车上觉得无聊,看了下btrace的源码(自己反编译)。 一些关于btrace的基本内容,可以看下我早起的一篇记录:btrace记忆

上一篇主要介绍的是btrace的一些基本使用以及api,这里我想从btrace源码本身进行下介绍。至于btrace的优势,能用来干些什么,自己上他的官网看下或者google一下,花个半小时就能明白了。

至于为什么会去反编译查看btrace源码,主要是会在部门整个关于btrace的分享。同时btrace的相关技术文档缺乏,javadoc很多时候说的不明不白,作者也没有提供源码开源,所以就有了这次的分享。

Btrace涉及相关技术

  1. asm
  2. instrument   http://download.oracle.com/javase/6/docs/api/java/lang/instrument/package-summary.html
  3. JVM TI(java tool api)  http://download.oracle.com/javase/6/docs/jdk/api/attach/spec/com/sun/tools/attach/VirtualMachine.html
  4. Java Compiler Api   http://download.oracle.com/javase/6/docs/api/javax/tools/package-summary.html

大家可以先去预备一下知识。

Btrace的大体设计

下面来看一个Btrace的设计图:

说明:

1. BtraceClient : 为我们使用的btrace的本地api,一般我们使用的bin/btrace会在本地启动一个btrace jvm,其内部使用了Java Complier Api, JVMTI技术,以及创建了一个socket。

  • Java Complier Api:动态的将我们传递的监控的java源文件动态编译成.class文件
  • JVMTI: 主要是利用了java 1.6之后的VirtaulMachine技术,动态的attach到一个已启动的jvm上,为他去启动一个BtraceAgent。该Agent会为BtraceClient启动一个server socket进行通讯。(多进程之间的通讯)
  • 本地socket: BtraceClient和BtraceAgent之间的数据通讯,比如生成的.class发送到BtraceAgent,还有一些Event事件等等。BtraceAgent同样可以将服务端print()的数据通过socket的方式回传给BtraceClient进行打印
2. BtraceAgent:为我们在目标jvm上植入的btrace agent实现。主要是Instrumentation技术, asm字节码处理技术。
  • BtraceAgent的启动可以有两种方式: BtaceClient动态attach后进行启动, 另一种就是在目标jvm启动之前添加agent参数进行启动。
  • BtraceAgent会启动一个server socket,与BtraceClient客户端进行交互,客户端可以将监控的.class文件通过socket发送,同样也可以在jvm启动时直接指定对应本地的.class做为监控脚本。
  • BtraceAgent在接受到监控指令后,会遍历当前所有已经加载的class类,挨个进行匹配检查,并生成相应的监控字节码(监控方法)。

btrace的包结构:

  • btrace-client.jar
  • btrace-boot.jar
  • btrace-agent.jar

btrace-client

   一般我们通常直接使用的命令,比如:

  1. bin/btrace $pid Btrace.java

都是直接调用了btrace-client包中的代码。

几个核心类介绍:

1. com.sun.btrace.client.Main (btrace的启动入口)

  • 调用Client进行complier(Java Compiler Api)和attach(JVM TI)处理

2. com.sun.btrace.client.Client

  • compiler方法 : 调用了Java Complier Api进行动态编译你的Btrace.java文件
    1. this.compiler = ToolProvider.getSystemJavaCompiler();
    2. this.stdManager = this.compiler.getStandardFileManager(null, null, null);
    3. Verifier btraceVerifier = new Verifier(this.unsafe); //指定了btrace特定的语法校验器
  • attach方法: 调用VirtualMachine.attach(pid);vm.loadAgent(agentPath, agentArgs);动态加载btrace-agent.jar包
    1. 传递给agent程序的几个参数:
    2. debug=true
    3. unsafe=true
    4. dumpClass=true
    5. dumpDir=xx
    6. trackRetransforms=true  ##是否记录instrument行为
    7. bootClassPath= xx ##agent.jar使用
    8. systemClassPath =xx ##agent.jar使用
    9. probeDescPath=xx
  • submit方法: 调用提交对应的instrument指令,并传递对应code的byte[]
    1. this.sock = new Socket("localhost", this.port);
    2. this.oos = new ObjectOutputStream(this.sock.getOutputStream());
    3. ...
    4. WireIO.write(this.oos, new InstrumentCommand(code, args));

几点说明:

*   在调用了attach方法后,会通过btrace-agent.jar中的com.sun.btrace.agent.Main启动一个ServerSocket

  1. int port = 2020;
  2. String p = (String)argMap.get("port");
  3. ....
  4. ServerSocket ss;
  5. try {
  6. (isDebug()) debugPrint(new StringBuilder().append("starting server at ").append(port).toString());
  7. System.setProperty("btrace.port", String.valueOf(port));
  8. if ((scriptOutputFile != null) && (scriptOutputFile.length() > 0)) {
  9. System.setProperty("btrace.output", scriptOutputFile);
  10. }
  11. ss = new ServerSocket(port);
  12. } catch(Exception e) ....
  13. while (true)
  14. {
  15. if (!isDebug()) continue; debugPrint("waiting for clients");
  16. Socket sock = ss.accept();
  17. if (!isDebug()) continue; debugPrint(new StringBuilder().append("client accepted ").append(sock).toString());
  18. Client client = new RemoteClient(inst, sock);
  19. handleNewClient(client);
  20. continue;
  21. }

*  所以在submit中,会通过一个本地socket进行连接server,并提交相应的Btrace.java中的监控代码(这时应该是编译后的字节码).

3. com.sun.btrace.compiler.Verifier  btrace自定义的语法校验器

    1. Boolean value = this.unsafe ? Boolean.TRUE : (Boolean)ct.accept(new VerifierVisitor(this), null);  // 注意下unsafe的判断

4.  com.sun.btrace.compiler.VerifierVisitor (具体的一些检查规则)

  • visitBinary  String字符串的+限制
  • visitClass  class的检查,不允许有父类,不允许有接口类,不允许非static变量,必须有Btrace @标签
  • visitDoWhileLoop 不允许do while循环
  • visitForLoop 不允许for循环
  • visitMethod 必须为static public ,不允许出现synchronized标记
  • visitNewArray 不允许出现new Array
  • visitNewClass 不允许出现new 对象
  • visitReturn 不允许有返回值
  • visitSynchronized 不允许有同步快
  • visitThrow visitTry 不允许有try catch的动作
  • visitOther 除上面允许之外的,不允许有其他的

说明:

*  看完Verifier和VerifierVisitor后,相信大家都应该明白了Btrace所谓的诸多限制,只是针对.java需要动态编译。如果我们预先生成.class文件,Btrace在1.2版本中并不会作类型合法性检查。(在将code发送给btrace-agent后,会在目标的jvm内部进行一次简单的Btrace语法检查,具体见后面Btrace-agent介绍)

5. com.sun.btrace.comm.XXX  Btrace的各种command指令

btrace-agent

大致了解了Client类中的attach和submit方法后,相信也能猜到对应agent的一些设计。简单的看一下

1. com.sun.btrace.agent.Main 为attach上之后agent的总入口,会调用agentmain()方法

  • main方法: 首先解析参数,然后会启动一个agentThread(Daemon线程)
  • parseArgs : 对应的参数解析,客户端在attach时,提交给agent后的一些参数
    1. bootClassPath=xx.jar  //需要动态增加的jar
    2. systemClassPath=xx.jar
    3. noServer=true/false //是否启动server socket
    4. debug=true/false
    5. unsafe=true/false
    6. dumpClasses=true/false
    7. dumpDir=路径
    8. trackRetransforms=true/false
    9. probeDescPath=路径
    10. stdout=true/false
    11. script=文件
    12. scriptdir=路径
    13. scriptOutputFile=文件路径特别注意下相比于Btrace-client提交的参数中,多了几个script,scriptdir等参数,允许在Client调用服务端一个指定的Btrace script文件进行处理
  • loadBTraceScript : 装载指定的script(注意是script和scriptDir中指定的script),必须是.class文件,会调用FileClient进行处理,最后调用handleNewClient进行统一处理,最后调用handleNewClient进行统一instrument处理。
  • startServer : main启动的agentThread会调用该方法,这里会启动一个serversocket,和btrace-client的客户端socket进行通讯,使用RemoteClient,最后调用handleNewClient进行统一instrument处理。
  • handleNewClient : 启动一个异步线程进行class Transformer,根据提交的byte[] code进行类文件重写

说明:

* 目前instrument进行字节码重写时,会重新load所有的class进行处理。(Btrace可以使用正则,父类的方式进行匹配,只能是挨个Class进行处理,看下是否有匹配的OnMethod)

* 相比于btrace-client提交过来的参数中,btrace-agent支持的参数中多了几个script,scriptdir等,允许在Client调用服务端一个指定的Btrace script文件进行处理,注意这里的script必须是编译后的.class文件。和通过socket提交的btrace在处理上没有太大的差异。

2. com.sun.btrace.agent.RemoteClient / FileClient : (RemoteClient为通过socket提交的script , FileClient为script和scriptDir指定的script文件)

  • 两者的不同无非就是对应的结果输出方式不同,一个是传回给客户端,另一个是直接终端输出
  • 会调用Client.loadClass()进行btrace数据解析,主要是解析对应的OnMethod和OnProbe数据。

3. com.sun.btrace.agent.ProbeDescriptorLoader

  • 会解析对应Btrace script中出现的@OnProbe,解析xml文件中对应的@OnMethod信息

4. com.sun.btrace.agent.Client:  (RemoteClient和FileClient的共同父类)

  • instument中的Transformer的实现类
  • loadClass()方法: btrace script脚本解析
  • verify 首先进行class的校验, 调用Verity类进行检查(可手工执行:java com.sun.btrace.runtime.Verifier <.class file>)
  • runtime.defineClass(codeBuf); 使用反射重新装载class byte
  • transform: 核心的方法(isBTraceClass(cname)) || (isSensitiveClass(cname)) 过滤btrace内部类,已经一些Object,ThreadLocal,sun/reflect类)
  • instrument方法 : 方法中调用asm的ClassReader进行class对象解析,并设置Instrumentor进行Class对象处理

5.  com.sun.btrace.runtime.Instrumentor : 是Btrace实现代码监控增强处理的核心逻辑

可以直接调用:

  1. java com.sun.btrace.runtime.Instrumentor <btrace-class> <target-class>]

Btrace的几点总结

1. btrace支持的监控方式

  • 本地jvm监控:目前大多数都是用的是btrace和监控的目标java是在同一机器上
  • 远程jvm监控:需要在远程服务器启动时添加btrace-agent.jar,需要重写btrace客户端,完成和serversocket建立通讯,完成btrace script发送监控。

    1. VirtualMachine动态attach不支持远程操作,所以无法动态的进行agent添加。
2. btrace支持的jdk版本
  • java 1.4以及之前 : 不支持,Instrument在jdk 1.5之后才出现。
  • java 1.5 : 必须手动在jvm启动时添加btrace-agent.jar,因为VirtualMachine是在jdk 1.6之后才出现。
  • java 1.6 : 推荐使用
agent启动: 
  1. java -Xshare:off -javaagent:${BTRACE_HOME}/build/btrace-agent.jar=dumpClasses=false,debug=false,unsafe=false,probeDescPath=.,noServer=true,script=$1
  2. 具体的参数见上面的代码分析

3. btrace的支持的script方式有多种。

  • client上的.java文件

    会进行动态编译,会有比较多的语法限制,btrace一堆的你不能做的事
  • client上的.class文件

    没什么好讲的,自己写Btrace script时导入btrace-client.jar,写好后生成一个.class文件,再通过btrace pid Btrace.class进行启动。
  • remote上的.class文件

    1. 修改btrace-client中的Client类,支持script和scriptDir的一些参数提交。

    2. 在remote机器上放置对应的btrace.class文件

4. btrace的使用是否会对java进程造成影响?(影响是肯定的,不过影响不大)

装载时的影响:

  • btrace每次使用,都会重新load所有的class。当然如果OnMethod不匹配,是不会被重新装载。所以跟你的OnMethod的匹配规则很有关系,如果使用+java.lang.Object。那就死定了。
退出后的影响:
  • btrace监控每次退出后,原先所有的class都不会被恢复,你的所有的监控代码依然一直在运行
抓取了下btrace改写过后的类:
  1. public InstrumentServer(String ip, String port)
  2. {
  3. $btrace$com$agapple$btrace$Instrumentor$InstrumentTracer$bufferMonitor(this);
  4. this.ip = ip;
  5. this.port = port;
  6. }
  7. private static void $btrace$com$agapple$btrace$Instrumentor$InstrumentTracer$bufferMonitor(@Self Object arg0)
  8. {
  9. if (!BTraceRuntime.enter(InstrumentTracer.runtime)) return; try { Field ipField = BTraceUtils.field("com.agapple.btrace.Instrumentor.InstrumentServer", "ip");
  10. Field portField = BTraceUtils.field("com.agapple.btrace.Instrumentor.InstrumentServer", "port");
  11. String ip = (String)BTraceUtils.get(ipField, self);
  12. String port = (String)BTraceUtils.get(portField, self);
  13. BTraceUtils.println(BTraceUtils.strcat(BTraceUtils.strcat(BTraceUtils.strcat("ip : ", BTraceUtils.str(ip)), " port : "), BTraceUtils.str(port)));
  14. BTraceRuntime.leave(); return; } catch (Throwable localThrowable) { BTraceRuntime.handleException(localThrowable);
  15. }
  16. }
注意其中的if (!BTraceRuntime.enter(InstrumentTracer.runtime)) return;
再看一下BTraceRuntime中对应方法的实现:
  1. private volatile boolean disabled;
  2. public static boolean enter(BTraceRuntime current)
  3. {
  4. if (current.disabled) return false;
  5. return map.enter(current);
  6. }
每次执行你的监控代码之前会先进行一个判断,判断当前是否处于监控中。你的客户端发起了exit指令后,该方法判断false,直接return。
所以btrace使用退出后会让你的代码多走了一个方法调用+一个对象属性判断,所以说影响还是非常的少


5. btrace诸多的使用限制,你必须得知道:

  1. can not create new objects.
  2. can not create new arrays.
  3. can not throw exceptions.
  4. can not catch exceptions.
  5. can not make arbitrary instance or static method calls - only the public static methods of com.sun.btrace.BTraceUtils class or methods declared in the same program may be called from a BTrace program.
  6. (pre 1.2) can not have instance fields and methods. Only static public void returning methods are allowed for a BTrace class. And all fields have to be static.
  7. can not assign to static or instance fields of target program's classes and objects. But, BTrace class can assign to it's own static fields ("trace state" can be mutated).
  8. can not have outer, inner, nested or local classes.
  9. can not have synchronized blocks or synchronized methods.
  10. can not have loops (for, while, do..while)
  11. can not extend arbitrary class (super class has to be java.lang.Object)
  12. can not implement interfaces.
  13. can not contains assert statements.
  14. can not use class literals.

说明:

补充说明:

  • 正因为btrace有这诸多的限制,才可以让我们的监控代码可以更加的放心,这也正是btrace能普及的一个很重要的原因。
  • 不得不说的一个点:对String的"+"限制使用,让我们使用起来很不爽,不过还好在btrace 1.2之后,作者提供了一个StringBuilder。相比于strcat已经好用多了

6. btrace对string字符串的处理

  • 可以参看总结3,突破对应的限制。不是非常建议,因为总结4中提出即使btrace client退出后,服务端一直会运行btrace script。所以一旦有写的动作,会是一个长期持续的过程
  • btrace 1.2 release说明中,已经提到增加了StringBuilder进行字符串处理,至少比先前的strcat使用上已经方便很多了。具体查看:http://kenai.com/jira/browse/BTRACE-38

7. btrace的相关源码:

8. btrace中对OnMethod的Location使用上,以及一些annotation使用不明确,可以查看:http://kenai.com/projects/btrace/sources/hg/content/src/share/classes/com/sun/btrace/runtime/Instrumentor.java

说明: self, ProbeClassName , ProbeMethodName 在任何的Kind中都支持,所以就不在每个表格中赘述。

Kind Where.BEFORE Where.AFTER
ARRAY_GET 数组长度(int) , 数组类型(type) @return , 数组长度(int) , 数组类型(type)
ARRAY_SET 原始数组类型(type) , 数组长度(int) , 目标数组类型(type) @return,原始数组类型(type) , 数组长度(int) , 目标数组类型(type)
CALL 方法参数 , @TargetInstance , @TargetMethodOrField 方法参数, @return , @TargetInstance , @TargetMethodOrField
CATCH 异常类型(type) 异常类型(type)
CHECKCAST  转型的目标类型 转型的目标类型
ENTRY 方法参数 方法参数 
ERROR 异常类型(throwable type) 异常类型(throwable type)
FIELD_GET @TargetInstance,@TargetMethodOrField @TargetInstance,@TargetMethodOrField,@return 
FIELD_SET fldValueIndex,@TargetInstance,@TargetMethodOrField fldValueIndex,@TargetInstance,@TargetMethodOrField 
INSTANCEOF 转型的目标类型 转型的目标类型 
LINE 行数  行数 
NEW 对象类名  @return
NEWARRAY 数组内部对象类名,类名 数组内部对象类名,类名, @return 
RETURN 参数,@return , @Duration
SYNC_ENTRY sync对象 sync对象
SYNC_EXIT sync对象 sync对象
THROW 异常类型 异常类型

最后

花了多个小时时间整理了这份blog,希望能给大家理解btrace,掌握btrace的使用能带来一些帮助!!

btrace一些你不知道的事(源码入手)的更多相关文章

  1. 从源码入手,一文带你读懂Spring AOP面向切面编程

    之前<零基础带你看Spring源码--IOC控制反转>详细讲了Spring容器的初始化和加载的原理,后面<你真的完全了解Java动态代理吗?看这篇就够了>介绍了下JDK的动态代 ...

  2. [源码分析] 从源码入手看 Flink Watermark 之传播过程

    [源码分析] 从源码入手看 Flink Watermark 之传播过程 0x00 摘要 本文将通过源码分析,带领大家熟悉Flink Watermark 之传播过程,顺便也可以对Flink整体逻辑有一个 ...

  3. 使用CEF(三)— 从CEF官方Demo源码入手解析CEF架构与CefApp、CefClient对象

    在上文<使用CEF(2)- 基于VS2019编写一个简单CEF样例>中,我们介绍了如何编写一个CEF的样例,在文章中提供了一些代码清单,在这些代码清单中提到了一些CEF的定义的类,例如Ce ...

  4. debug模式开启会做哪些事(源码分析)

    以往开发中不管是django框架下开发还是其它框架下开发, 只知道在开发阶段要开启debug模式, 却一直没有深究它会我们做哪些事, 今天使用tornado时偶然看到源码中写的很清楚,故写下来加深印象 ...

  5. 从ReentrantLock源码入手看锁的实现

    写这篇确实挺伤脑筋的,是按部就班一行一行读,但是我想这么写估计很多没有接触过的可能就劝退了,很容易出现的一种现象就是看了后面忘了前面,而且很容易看了一行代码就一层层往下钻,这样不仅容易打击看源码的积极 ...

  6. 从源码入手探究一个因useImperativeHandle引起的Bug

    今天本来正在工位上写着一段很普通的业务代码,将其简化后大致如下: function App(props: any) { // 父组件 const subRef = useRef<any>( ...

  7. [源码分析] 从实例和源码入手看 Flink 之广播 Broadcast

    [源码分析] 从实例和源码入手看 Flink 之广播 Broadcast 0x00 摘要 本文将通过源码分析和实例讲解,带领大家熟悉Flink的广播变量机制. 0x01 业务需求 1. 场景需求 对黑 ...

  8. Vue2.1.7源码学习

    原本文章的名字叫做<源码解析>,不过后来想想,还是用“源码学习”来的合适一点,在没有彻底掌握源码中的每一个字母之前,“解析”就有点标题党了.建议在看这篇文章之前,最好打开2.1.7的源码对 ...

  9. 源码级深度理解 Java SPI

    作者:vivo 互联网服务器团队- Zhang Peng SPI 是一种用于动态加载服务的机制.它的核心思想就是解耦,属于典型的微内核架构模式.SPI 在 Java 世界应用非常广泛,如:Dubbo. ...

  10. 从源码浅析MVC的MvcRouteHandler、MvcHandler和MvcHttpHandler

    熟悉WebForm开发的朋友一定都知道,Page类必须实现一个接口,就是IHttpHandler.HttpHandler是一个HTTP请求的真正处理中心,在HttpHandler容器中,ASP.NET ...

随机推荐

  1. 【图文安装教程】在docker中安装kibana

    在上一篇中,我们已经在docker里面安装了ES. kibana可以给我们提供一个elasticsearch的可视化界面,便于我们学习. 所以,本篇咱们就在docker里面安装kibana图文教程: ...

  2. 如何排查线上w3wp.exe CPU高的问题,使用到了WinDbg、Visual studio来分析IIS进程池的.dmp文件

    最近发现服务器上某个web站点老是CPU很高,该站点部署在IIS上,我IIS上有多个站点,每个站点一个进程池,每个进程池取名都是根据站点来取的,所以很容易看出哪个站点吃掉的CPU,该站点已运行十几年, ...

  3. 【YashanDB知识库】yasdb jdbc驱动集成BeetISQL中间件,业务(java)报autoAssignKey failure异常

    问题现象 BeetISQL中间件版本:2.13.8.RELEASE 客户在调用BeetISQL提供的api向yashandb的表中执行batch insert并将返回sequence设置到传入的jav ...

  4. Java实现英语作文单词扫盲程序

    来自背英语四级单词的突发奇想: 是否可以通过Java语言实现一个随机抽取作文中单词进行复习的程序. 首先,展示下成果: 点击查看代码 package Demo; import java.util.Ar ...

  5. 从0开始计算机体系结构的学习(一):FGPA预备知识与Vivado环境搭建

    引入与预备知识 什么是FPGA? FPGA(Field-Programmable Gate Array,现场可编程门阵列)是一种集成电路(IC),其硬件功能可以通过用户在现场编程来定义.与传统的ASI ...

  6. C# 中的 AEAD_AES_256_GCM

    注意:AEAD_AES_256_GCM Key的长度必须是32位,nonce的长度必须是12位,附加数据有可能为空值.AEAD_AES_128_GCM Key的长度必须是16位,nonce的长度必须是 ...

  7. Dockerfile定制镜像(FROM?RUN ?WORKDIR ?ADD & COPY指令)(七)

    一.Dockerfile 镜像的定制实际上就是定制镜像的每一层所添加的配置.文件等信息,实际上当我们在一个容器中添加或者修改了一些文件后,我们可以通过docker commit命令来生成一个新的镜像, ...

  8. CSP2024-S 游记

    9-21 今天考完了初赛,明显感觉数学门槛变高了一些,有高中数学知识才能保证看得懂题意,只是苦了小学和初中同学,看数据参加人数还涨了50%,权当拉低分数线了吧.用小图灵估分70.应该是稳过.

  9. go语言中变量的作用域

    Go 语言中的变量作用域规则决定了变量在程序的哪些部分是可见的和可以访问的.理解这些规则对于编写清晰.维护性高的代码非常重要.下面是一个系统性的解释. 变量的作用域类型 包级作用域: 包级作用域的变量 ...

  10. 鲲鹏(ARM64)+麒麟(Kylin v10)离线部署 KubeSphere

    作者:社区用户-天行1st 本文将详细介绍,如何基于鲲鹏 CPU(ARM64) 和操作系统 Kylin V10 SP2/SP3,利用 KubeKey 制作 KubeSphere 和 Kubernete ...