閱讀815 返回首頁    go 阿裏雲 go 技術社區[雲棲]


如何編寫屬於自己的Java / Scala的調試器

在本帖中,我們將探討Java和Scala的調試器是如何編寫和工作的;係統自帶的調試器,例如:Windows中的WinDbg或者是Linux/Unix中的gdb,會獲取操作係統直接提供給他們的鏈接入口來啟動,從而指導和操作外部程序的狀態。工作在操作係統頂部抽象層的Java虛擬機對字節碼的調試有獨立的處理架構。

這個調試的框架和APIs具有全開源、文檔化、可擴展的特點,這意味著你可以輕鬆毫無顧忌的編寫自己的調試器。該框架當前的設計由兩大部分構成—JDWP協議和JVMTI API層。其中每個都有一係列使性能最佳的優點和使用案例。(你也可以閱讀這篇文檔:深入 JAVA 調試體係

JDWP協議

JDWP(Java Debugger Wire Protocol)是用來在調試和被調試程序之間通過二進製信息來傳遞請求和接收事件的(例如:線程中的狀態或者異常的變化),這些活動通常是網絡上進行。這個架構背後的理念是在兩個程序之間盡可能的解耦。旨在減少由編譯器更改目標代碼在運行期的執行所帶來的海森堡效應(Werner是位德國物理學家,不是你喜歡的那個廚師Werner)。

從目標程序中移除多數調試邏輯操作,對檢測被調試的虛擬機中狀態的改變會有所幫助(例如:GC or OutOfMemoryErrors),這些邏輯是不會調試本身的。為了更加簡便,JDK自帶了JDI(java調試接口),該接口提供了全麵的調試的協議實現,以及對一個目標虛擬機狀態的完備的操作能力,包括:連接、斷開、指導、處理。

Eclipse的編譯器使用的就是JDWP協議,IDE( Integrated Development Environment )調試JAVA程序時,如果你查看當時傳遞給該程序的命令行參數,你會發現Eclipse會傳遞額外的參數(-agentlib:jdwp=transport=dt_socket,…)給程序來啟動java虛擬機調試,同時也將確定發送請求和事件的端口。

JVMTI編程接口

一係列的原生API是現代JVM中的第二個關鍵組件,這些API涵蓋了廣泛關於JVM操作的領域,其中為人所熟知的是 JVM Tooling Interface (i.e. JVMTI)。與JDWP不同的是,JVMTI設計時提供了一係列C/C++ 版的API和一種為JVM動態地加載預編譯的庫文件(如:.dull等)的機製,而這些庫文件會使用由API提供的命令。

JVMTI的使用方式不同於JDWP,實際上,它是在目標程序內執行編譯器。這種方式使調試器同時在性能和穩定性方麵改善程序代碼更加得心應手。然而,最關鍵的優勢是這樣一種能夠幾乎是實時直接地和JVM交互的能力。

從JVMTI提供了一係列功能強、易入門的API中可以看出,JVMTI樂於去深入探究並分析自身的工作原理以及同通過用該些API所能完成的功能。你可以從JDK自帶的JVMTI中獲取API標頭。

編寫調試器類庫

編寫自己的調試器需要用C++創建本地的操作係統類庫。你的主方法應該如下:

JNIEXPORT jint JNICALL Agent_OnLoad(JavaVM *vm,char*options,void*)

當JVM加載調試代理器的同時,它會調用該方法。傳遞的Java虛擬機指針是至關重要的,它會提供所有你需要跟JVM打交道的砝碼。該指針可以從java虛擬機中引入jvmtiEnv類;你可以使用GetEnv方法利用capabilities (特性)和events(事件)的概念與JVMTI層進行交互。

JVMTI 特性

編寫調試器時,最關鍵的一方麵是你對在目標程序中的調試器代碼的功能有清晰的認識,特別是運行代碼的本地調試器類庫和運行程序聯係緊密時。為了你更好的控製你的調試器去影響代碼的執行,因此JVMTI詳解中引入了capabilities(特性)的概念。

當編寫自己的編譯器時,你可以事先通知java虛擬機你一係列打算使用的API命令或者事件(例如:設置斷點、中斷線程)。這能夠使JVM可以預先為這些命令或者事件做好準備,同時,讓你更好的掌控調試器運行期的開銷。這種方式也使得出自不同製造商的JVM能夠以程序設計的方式告訴你那些API的命令可以在整個JVMTI詳解中得以支持。

特性的性能是大不相同的。有些特性使用的性能開銷較低,但是有些較有意思的特性則是相反,例如:在代碼中拋出異常來接收回調的特性—can_generate_exception_events或者是需要加鎖來接收回調的特性—can_generate_monitor_events。原因在於這些特性會在 JIT全範圍的編譯時阻礙JVM優化代碼,與此同時,迫使JVM在運行期降到解釋模式。

其它一些特性,如:每當設定一個目標對象域時,用來接收通知的特性—can_generate_field_modification_events,會產生更大的開銷,導致代碼運行極慢。盡管JVM支持同時加載多個本地類庫,遺憾是一些 HotSpot的特性,如:用來掛起和喚醒線程的特性—can_suspend,隻能每次地被一個類庫調用。

當我們搭建Takipi’s production debugger時,我們需攻堅的問題之一是提供類似的特性且不能引起大的開銷(在之後的版本中更是這樣)。

設置回調。一旦你接收到一係列的特性後,你隨即設置好會被JVM調用的回調,這會讓你知道實際發生過的操作。每個回調都將會完全地提供關於已經發生過的事件的深層次信息。舉個例子,一則異常回調信息會包括拋出異常的字節碼位置、線程、異常對象、異常是否將被捕獲以及將被捕獲的位置。

voidJNICALL ExceptionCallback(jvmtiEnv *jvmti,JNIEnv *jni, jthread thread, jmethodID method,

jlocation location, jobject exception,jmethodID catch_method, jlocation catch_location)

值得注意的是特性的開銷通常分為兩個部分,第一部分開銷來自驅動它工作,因為它需要使JIT編譯器不同地編譯事務,從而能夠訪問代碼。另外一部分來自當你啟用一個回調功能時,此時這會引起JVM在執行期選擇低性能的執行路徑,通過這些路徑,特性可以訪問代碼,期間壓縮和傳遞重要數據會產生額外的開銷

斷點和檢查。你的編譯器能夠提供熟知的用來檢查在運行期所處的特定狀態的特性,如:SetBreakpoint,通知JVM通過某個具體的字節碼來中斷執行,或者每當某個區域更改時,通過設置SetFieldModificationWatch中斷執行。針對這點,你可以使用其它一些補充性的功能,例如:GetStackTrace 和GetThreadInfo ,用來知曉當前你所在代碼中的位置並報告當前位置。

大多數JVMTI 的功能都涉及到一些使用抽象處理的類和方法,較為熟知的是jmethodID 和 jclass(如果你曾經編寫過java本地接口代碼,這是)。其中也提供了額外的一些功能,如:GetMethodName 和 GetClassSignature,來幫助你從類常量池中獲取實際的符號名。之後,你就可以使用這些符號名以可讀的方式記錄為日誌文件或者以界麵的方式展示它們,就如同日常在IDE中所見的一樣。

連接調試器

一旦你已經開始編寫調試器類庫,你的下一步任務就是將它連接到JVM上,下麵是幾種連接的方式:

1. 連接JDWP。倘若你編寫的是一個以JDWP為基礎的調試器,你需要向被調試對象增加一個啟動參數,就可以在線上進行調試,該參數的形式如下:agentlib:jdwp=transport=dt_socket,suspend=y,address=localhost:<port>。這些參數詳細反映了調試器和目標程序之間傳遞信息的方式,以及在掛起模式中是否啟用被調試對象。

2. 連接JVMTI 類庫.通過傳遞給被調試程序的代理路徑命令行,同時指向類庫所在硬盤上的位置,此時,JVM將會加載JVMTI類庫。

另外一種可行的方式是:將命令行參數追加到全局環境變量JAVA_TOOL_OPTIONS 後麵,每個新的JVM會接收該變量,並且該變量的值會自動地追加到現存參數列表之後。

3. 遠程連接.還有一種通過使用遠程連接API來連接調試器的方式,使用這個簡單而功能強大的API能夠在沒有使用任何命令行參數來開始程序的情況下連接代理器來運行JVM程序。這個的不足在於你不能獲取通常本可以獲得的特性,如:can_generate_exception_events,因為這些特性隻能在虛擬機啟動時獲取。

最後更新:2017-05-23 15:36:58

  上一篇:go  SOAP-Simple Object Access Protocol(簡單對象訪問協議)
  下一篇:go  gcc的 “-fpack-struct” 編譯選項導致程序core dump的分析