如何编写自己的Java / Scala调试器
通過這篇文章,我們將探討Java / Scala調試器的編寫和工作方式。 諸如Windows的WinDbg或Linux / Unix的gdb之類的本機調試器通過操作系統直接提供給它們的鉤子來獲取其強大功能,以監視和操縱外部進程的狀態。 JVM充當OS之上的抽象層,它提供了自己的獨立體系結構來調試字節碼。
該框架及其API是完全開放的,有文檔的和可擴展的,這意味著您可以輕松編寫自己的調試器。 該框架的當前設計由兩個主要部分組成-JDWP協議和JVMTI API層。 每種方法都有其自己的一套優點和最佳用例。
JDWP協議
Java調試器有線協議通常用于在網絡上使用二進制消息在調試器和被調試進程之間傳遞請求和接收事件(例如線程狀態或異常的更改)。 該體系結構背后的概念是在兩者之間建立盡可能多的隔離。 這是為了減少讓調試器在目標代碼運行時改變其執行的海森堡效應(物理學家維爾納,而不是友好的Meth Cooking Walt )。
從目標進程中刪除盡可能多的調試器邏輯也有助于確保已調試VM狀態的更改(例如“停止世界” GC或OutOfMemoryErrors)不會影響調試器本身。 為了簡化操作,JDK附帶了JDI (Java調試器接口),該接口提供了協議的完整調試器端實現,并具有連接,分離,監視和操縱目標VM的狀態的能力。
例如,該協議與Eclipse的調試器使用的協議相同。 如果查看在IDE調試時傳遞給Java進程的命令行參數,您會注意到Eclipse傳遞給它的其他參數(-agentlib:jdwp = transport = dt_socket,…)來啟用JVM調試,并且還會建立發送請求和事件的端口。
JVMTI API
現代JVM調試器體系結構中的第二個關鍵組件是一組本機API,涵蓋了與JVM操作相關的廣泛領域,稱為JVM工具接口 (即JVMTI)。 與JDWP不同,JVMTI被設計為一組C / C ++ API,并且具有JVM動態加載利用API提供的命令的預編譯庫(例如.dll或.so)的機制。
這種方法與JDWP的不同之處在于,它實際上在目標進程內部執行調試器。 這增加了調試器在性能和穩定性方面影響應用程序代碼的可能性。 但是,關鍵優勢在于能夠以近乎實時的方式直接與JVM交互。
由于JVMTI提供了一組功能強大的低級API集,所以我認為有必要進一步深入研究并解釋其工作原理,以及可以使用它進行哪些出色的工作。 可通過JDK隨附的jvmti.h獲得API標頭。
編寫調試器庫
編寫自己的調試器需要使用C ++創建本機OS庫。 在這種情況下,您的“主要”功能看起來像–
JNIEXPORT jint JNICALL Agent_OnLoad(JavaVM *vm, char *options, void*)當您的調試器代理由JVM加載時,該函數將由JVM調用。 傳遞給您的日益重要的JavaVM指針將為您提供與JVM對話所需的一切。 它引入了JavaVM :: GetEnv方法中可用的jvmtiEnv類,該類使您能夠通過功能和事件的概念與JVMTI層進行交互。
JVMTI功能
編寫調試器的關鍵方面之一是要特別注意調試器代碼對目標進程的影響。 這對于本機調試器庫尤其重要,在該庫中,代碼與應用程序非常緊密地運行。 為了幫助您更好地控制調試器如何影響代碼的執行,JVMTI規范引入了功能概念。
在編寫調試器時,您可以提前告訴JVM您打算使用哪些API命令或事件集(即設置斷點,掛起線程等)。 這使JVM可以提前為此做準備,并使您可以更好地控制調試器的運行時開銷。 這種方法還使來自不同供應商的JVM能夠以編程方式告訴您整個JVMTI規范中當前支持哪些API命令。
并非所有能力都是平等的 。 某些功能的性能開銷相對較小。 其他有趣的,如can_generate_exception_events接收回調時異常的代碼拋出,或用于接收回調時獲取鎖定can_generate_monitor_events,付出更高的成本。 原因是它們阻止JVM在JIT編譯期間完全優化代碼,并可能迫使JVM在運行時進入解釋模式。
其他功能,例如can_generate_field_modification_events用于在設置目標對象字段(即設置監視)時接收通知,其成本甚至更高,從而使代碼執行速度大大降低。 即使JVM支持同時加載多個本機庫,HotSpot中的某些功能(例如can_suspend用于掛起和恢復線程)也只能一次聲明一個庫。
構建Takipi的生產調試器時,我們面臨的最困難的部分之一就是提供類似的功能,而又不會產生這種開銷(在以后的文章中會介紹更多)。
設置回調 。 收到一組功能后,下一步就是設置回調,JVM將調用這些回調以讓您知道實際發生的時間。 這些回調中的每一個都會提供有關發生的事件的相當深的信息。 例如,對于異常回調,此信息將包括引發異常的字節碼位置,線程,異常對象以及是否以及將在何處捕獲該異常。
void JNICALL 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 Native Interface代碼,則應該很熟悉)。 提供了諸如GetMethodName和GetClassSignature之類的附加功能,以幫助您從類的常量池中獲取實際的符號名稱。 然后,您可以使用它們以可讀格式記錄數據或將其呈現在UI中,就像我們每天在IDE中看到的那樣。
附加調試器
編寫調試器庫后,下一步就是將其附加到JVM。 有幾種方法可以做到–
1.連接JDWP 。 如果要編寫基于JDWP的調試器,則需要以– agentlib:jdwp = transport = dt_socket,suspend = y,address = localhost:<port>的形式向調試對象添加啟動參數以通過線路啟用調試。 這些參數詳細說明了調試器和目標(在本例中為套接字)之間的通信形式,以及是否以掛起模式啟動調試對象。
2.附加JVMTI庫 。 JVM通過傳遞給debuggee進程并指向您的庫在磁盤上的位置的agentpath命令行參數加載JVMTI庫。
另一種方法是將代理程序命令行參數附加到全局JAVA_TOOL_OPTIONS環境變量,該環境變量將由每個新JVM拾取,并且其值會自動附加到其現有參數列表中。
3.遠程連接 。 附加調試器的另一種方法是使用遠程附加API 。 這個簡單而強大的API使您可以將代理附加到正在運行的JVM進程,而無需使用任何命令行參數啟動它們。 不利的一面是您將無法使用通常需要的某些功能,例如can_generate_exception_events ,因為這些功能僅在VM啟動時才需要-遺憾的是,調試器有些麻煩了。
您可以下載Takipi的生產調試器,以在此處查看其中的一些方法。
翻譯自: https://www.javacodegeeks.com/2013/09/how-to-write-your-own-java-scala-debugger.html
創作挑戰賽新人創作獎勵來咯,堅持創作打卡瓜分現金大獎總結
以上是生活随笔為你收集整理的如何编写自己的Java / Scala调试器的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: linux网卡查看命令(linux网卡查
- 下一篇: 常见的DDos攻击(ddos基本攻击)