背景
周五下班回家,在公司班車上覺得無聊,看了下btrace的源碼(自己反編譯)。 一些關于btrace的基本內容,可以看下我早起的一篇記錄:btrace記憶
上一篇主要介紹的是btrace的一些基本使用以及api,這里我想從btrace源碼本身進行下介紹。至于btrace的優勢,能用來干些什么,自己上他的官網看下或者google一下,花個半小時就能明白了。
至于為什么會去反編譯查看btrace源碼,主要是會在部門整個關于btrace的分享。同時btrace的相關技術文檔缺乏,javadoc很多時候說的不明不白,作者也沒有提供源碼開源,所以就有了這次的分享。
Btrace涉及相關技術
- asm
- instrument http://download.oracle.com/javase/6/docs/api/java/lang/instrument/package-summary.html
- JVM TI(java tool api) http://download.oracle.com/javase/6/docs/jdk/api/attach/spec/com/sun/tools/attach/VirtualMachine.html
- 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
一般我們通常直接使用的命令,比如:
Java代碼

- 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文件
Java代碼

- this.compiler = ToolProvider.getSystemJavaCompiler();
- this.stdManager = this.compiler.getStandardFileManager(null, null, null);
-
-
- Verifier btraceVerifier = new Verifier(this.unsafe); //指定了btrace特定的語法校驗器
- attach方法: 調用VirtualMachine.attach(pid);vm.loadAgent(agentPath, agentArgs);動態加載btrace-agent.jar包
Java代碼

- 傳遞給agent程序的幾個參數:
- debug=true
- unsafe=true
- dumpClass=true
- dumpDir=xx
- trackRetransforms=true ##是否記錄instrument行為
- bootClassPath= xx ##agent.jar使用
- systemClassPath =xx ##agent.jar使用
- probeDescPath=xx
- submit方法: 調用提交對應的instrument指令,并傳遞對應code的byte[]
Java代碼

- this.sock = new Socket("localhost", this.port);
- this.oos = new ObjectOutputStream(this.sock.getOutputStream());
- ...
-
- WireIO.write(this.oos, new InstrumentCommand(code, args));
幾點說明:
* 在調用了attach方法后,會通過btrace-agent.jar中的com.sun.btrace.agent.Main啟動一個ServerSocket
Java代碼

- int port = 2020;
- String p = (String)argMap.get("port");
- ....
- ServerSocket ss;
- try {
- (isDebug()) debugPrint(new StringBuilder().append("starting server at ").append(port).toString());
- System.setProperty("btrace.port", String.valueOf(port));
- if ((scriptOutputFile != null) && (scriptOutputFile.length() > 0)) {
- System.setProperty("btrace.output", scriptOutputFile);
- }
- ss = new ServerSocket(port);
- } catch(Exception e) ....
-
-
- while (true)
- {
- if (!isDebug()) continue; debugPrint("waiting for clients");
- Socket sock = ss.accept();
- if (!isDebug()) continue; debugPrint(new StringBuilder().append("client accepted ").append(sock).toString());
- Client client = new RemoteClient(inst, sock);
- handleNewClient(client);
- continue;
- }
* 所以在submit中,會通過一個本地socket進行連接server,并提交相應的Btrace.java中的監控代碼(這時應該是編譯后的字節碼).
3. com.sun.btrace.compiler.Verifier btrace自定義的語法校驗器
Java代碼

- 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后的一些參數
Java代碼

- bootClassPath=xx.jar //需要動態增加的jar
- systemClassPath=xx.jar
- noServer=true/false //是否啟動server socket
- debug=true/false
- unsafe=true/false
- dumpClasses=true/false
- dumpDir=路徑
- trackRetransforms=true/false
- probeDescPath=路徑
- stdout=true/false
- script=文件
- scriptdir=路徑
- 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實現代碼監控增強處理的核心邏輯
可以直接調用:
Java代碼

- 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啟動:
Java代碼

- java -Xshare:off -javaagent:${BTRACE_HOME}/build/btrace-agent.jar=dumpClasses=false,debug=false,unsafe=false,probeDescPath=.,noServer=true,script=$1
-
- 具體的參數見上面的代碼分析
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改寫過后的類:
Java代碼

- public InstrumentServer(String ip, String port)
- {
- $btrace$com$agapple$btrace$Instrumentor$InstrumentTracer$bufferMonitor(this);
- this.ip = ip;
- this.port = port;
- }
-
- private static void $btrace$com$agapple$btrace$Instrumentor$InstrumentTracer$bufferMonitor(@Self Object arg0)
- {
- if (!BTraceRuntime.enter(InstrumentTracer.runtime)) return; try { Field ipField = BTraceUtils.field("com.agapple.btrace.Instrumentor.InstrumentServer", "ip");
- Field portField = BTraceUtils.field("com.agapple.btrace.Instrumentor.InstrumentServer", "port");
-
- String ip = (String)BTraceUtils.get(ipField, self);
- String port = (String)BTraceUtils.get(portField, self);
- BTraceUtils.println(BTraceUtils.strcat(BTraceUtils.strcat(BTraceUtils.strcat("ip : ", BTraceUtils.str(ip)), " port : "), BTraceUtils.str(port)));
- BTraceRuntime.leave(); return; } catch (Throwable localThrowable) { BTraceRuntime.handleException(localThrowable);
- }
- }
注意其中的if (!BTraceRuntime.enter(InstrumentTracer.runtime)) return;
再看一下BTraceRuntime中對應方法的實現:
Java代碼

- private volatile boolean disabled;
-
- public static boolean enter(BTraceRuntime current)
- {
- if (current.disabled) return false;
- return map.enter(current);
- }
每次執行你的監控代碼之前會先進行一個判斷,判斷當前是否處于監控中。你的客戶端發起了exit指令后,該方法判斷false,直接return。
所以btrace使用退出后會讓你的代碼多走了一個方法調用+一個對象屬性判斷,所以說影響還是非常的少
5. btrace諸多的使用限制,你必須得知道:
Java代碼

- can not create new objects.
- can not create new arrays.
- can not throw exceptions.
- can not catch exceptions.
- 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.
- (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.
- 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).
- can not have outer, inner, nested or local classes.
- can not have synchronized blocks or synchronized methods.
- can not have loops (for, while, do..while)
- can not extend arbitrary class (super class has to be java.lang.Object)
- can not implement interfaces.
- can not contains assert statements.
- 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的使用能帶來一些幫助!!
有問題和交流,歡迎站內聯系