背景
週五下班回家,在公司班車上以爲無聊,看了下btrace的源碼(本身反編譯)。 一些關於btrace的基本內容,能夠看下我早起的一篇記錄:btrace記憶 html
上一篇主要介紹的是btrace的一些基本使用以及api,這裏我想從btrace源碼自己進行下介紹。至於btrace的優點,能用來幹些什麼,本身上他的官網看下或者google一下,花個半小時就能明白了。 java
至於爲何會去反編譯查看btrace源碼,主要是會在部門整個關於btrace的分享。同時btrace的相關技術文檔缺少,javadoc不少時候說的不明不白,做者也沒有提供源碼開源,因此就有了此次的分享。 web
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
你們能夠先去預備一下知識。 api
Btrace的大致設計
下面來看一個Btrace的設計圖: 數組
說明: 服務器
1. BtraceClient : 爲咱們使用的btrace的本地api,通常咱們使用的bin/btrace會在本地啓動一個btrace jvm,其內部使用了Java Complier Api, JVMTI技術,以及建立了一個socket。 oracle
- 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的包結構: app
- btrace-client.jar
- btrace-boot.jar
- btrace-agent.jar
btrace-client 異步
通常咱們一般直接使用的命令,好比: jvm
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的使用能帶來一些幫助!!
有問題和交流,歡迎站內聯繫