反向 Debug 瞭解一下?揭秘 Java DEBUG 的基本原理

来源:https://www.cnblogs.com/Jcloud/archive/2023/12/25/17925365.html
-Advertisement-
Play Games

Debug 的時候,都遇到過手速太快,直接跳過了自己想調試的方法、代碼的時候吧……一旦跳過,可能就得重新執行一遍,準備數據、重新啟動可能幾分鐘就過去了。 ...


Debug 的時候,都遇到過手速太快,直接跳過了自己想調試的方法、代碼的時候吧……

一旦跳過,可能就得重新執行一遍,準備數據、重新啟動可能幾分鐘就過去了。

Untitled.png

好在IDE 們都很強大,還給你後悔的機會,可以直接刪除某個 Stack Frame,直接返回到之前的狀態,確切的說是返回到之前的某個 Stack Frame,從而實現讓程式“逆向運行”。

Untitled 1.png

這個 Reset Frame 的能力,可不只是返回上一步,上 N 步也是可以的;選中你期望的那個幀,直接Reset Frame/Drop Frame,可以直接回到調用棧上的某個棧幀,時間反轉!

可惜這玩意也不是那麼萬能,畢竟是通過 stack pop 這種操作實現,實際上只是給調用棧棧頂的 N 個 frame pop 出來而已,還談不上是真正的“反向 DEBUG”。

相比之下, GDB 的Reverse Debugging就比較強大,真正的 “反向” DEBUG,逆向運行,實現回放。

所以吧在運行過程中,已經修改的數據,比如引用傳遞的方法參數、變數,一旦修改肯定回退不了,不然真的成時光機了。

這些亂七八糟的調試功能,都是基於 Java 內置的 Debug 體系來實現的。

JAVA DEBUG 體系

Java 提供了一個完整的 Debug 體系JPDA(Java Platform Debugger Architecture),這個 JPDA 架構體系由 3 部分組成:

  1. JVM TI- Java VM Tool Interface

  2. JDWP- Java Debug Wire Protocol

  3. JDI- Java Debug Interface

如果結合IDE 來看,那麼一個完整的 Debug 功能看起來就是這個樣子:

Untitled 2.png

解釋一下這個體系:

JVM TI 是一個 JVM 提供的一個調試介面,提供了一系列控制 JVM 行為的功能,比如分析、調試、監控、線程分析等等。也就是說,這個介面定義了一系列調試分析功能,而 JVM 實現了這個介面,從而提供調試能力。

不過吧,這個介面畢竟是 C++的,調用起來確實不方便,所以Java 還提供了 JDI 這麼個 Java 介面。

JDI 介面使用 JDWP 這個私有的應用層協議,通過 TCP 和目標 VM 的 JVMTI 介面進行交互。

也可以把簡單這個 JDWP 協議理解為 JSF/Dubbo 協議;相當於 IDE 里通過 JDI 這個 SDK,使用 JDWP 協議調用遠程 JVMTI 的 RPC 介面,來傳輸調試時的各種斷點、查看操作。

可能有人會問,搞什麼套殼!要什麼 JDWP,我直接 JVMTI 調試不是更香,鏈路越短性能越高!

當然可以,比如 Arthas 里的部分功能,就直接使用了 JVMTI 介面,要什麼 JDI!直接 JVMTI 乾就完了。

開個玩笑,Arthas 畢竟不是 Debug 工具,人家根本就不用 JDI 介面。而且 JVMTI 的能力也不只是斷點,它的功能非常多:

Untitled 3.png

左邊的功能類,提供了各種亂七八糟的功能,比如我們常用的添加一個斷點:

jvmtiError
SetBreakpoint(jvmtiEnv* env,
            jmethodID method,
            jlocation location)


右邊的事件類,可以簡單的理解為回調;還是拿斷點舉例,如果我用上面的 SetBreakpoint 添加了一個斷點,那麼當執行到該位置時,就會觸發這個事件:

void JNICALL
Breakpoint(jvmtiEnv *jvmti_env,
            JNIEnv* jni_env,
            jthread thread,
            jmethodID method,
            jlocation location)


JVMTI 的功能非常之多,而 JDI 只是實現了部分 JVMTI 的方法,所以某些專業的 Profiler 工具,可能會直接使用 JVMTI,從而實現更豐富的診斷分析功能。

遠程調試與本地調試

不知道大家有沒有留意過本地 Debug 啟動時的日誌:

Untitled 4.png

第一行是隱藏了後半段的啟動命令,展開後是這個樣子:

/path/to/java -agentlib:jdwp=transport=dt_socket,address=127.0.0.1:53631,suspend=y,server=n -javaagent:/path/to/jetbrains/debugger-agent.jar ...


第二行是一個 Connected 日誌,意思是使用 socket 連接到遠程 VM 的53631埠

上一段說到,IDE 通過 JDI 介面,使用 JDWP 協議和目標 VM 的 JVMTI 交互。這裡的 53631 埠,就是目標 JVM 暴露出的 JVM TI 的 server 埠。

而第一行里,IDEA 自動給我們加上了-agentlib:jdwp=transport=dt_socket,address=127.0.0.1:53631這麼一段,這個參數的意思就是,讓 jvm 以 53631 暴露 jdwp 協議

小知識,這個 agentlib 可不只是為 jvmti 提供的。它還可以讓 JVM 載入其他的 native lib包,直接“外掛”到你的 jvm 上,下麵是“外掛”的參數格式:

Untitled 5.png

所以吧,上面的描述其實不太嚴謹,更專業的說法是:

讓 JVM 載入 JDWP 這個 agent 庫,參數為transport=dt_socket,address=127.0.0.1:53631,這個 jdwp agent 庫以 53631 埠提供了 jdwp 協議的 server。只不過這個 jdwp 是jvm 內部的庫,不需要額外的 so/dylib/dll 文件。

如有需要,你完全可以弄個 “datupiao” 的 agentlib,“外掛”到這個 jvm 上,然後在這個 lib 里調用 JVMTI 介面,然後暴露個埠提供服務和遠程交互,實現自己的 jdwp!

可能某些老闆們註意到了,本地調試還要127.0.0.1走tcp 交互一遍,那遠程調試呢?

基於上面的解釋,本地調試和遠程調試真的沒啥區別!或者說,在目前 IDEA/Eclipse 的實現下,不存在本地調試,都是遠程!只不過一個是 127.0.0.1,一個是遠程的 IP 而已。

在本地調試時,IDEA 會自動給我們的 JVM 增加agent參數,隨機指定一個埠,然後通過 JDI 介面連接,代碼大概長這樣(JDI 的 SDK 在 JDK_HOME/lib/tools.jar ):

Map<String, Connector.Argument> env = connector.defaultArguments();
env.get("hostname").setValue(hostname);
env.get("port").setValue(port);

VirtualMachine vm = connector.attach(env);


瞅瞅, VirtualMachine 里的就這點方法,能力上比 JVMTI 還是差遠了

List<ReferenceType> classesByName(String className);

List<ReferenceType> allClasses();

void redefineClasses(Map<? extends ReferenceType, byte[]> classToBytes);

List<ThreadReference> allThreads();

void suspend();

void resume();

List<ThreadGroupReference> topLevelThreadGroups();

EventQueue eventQueue();

EventRequestManager eventRequestManager();

VoidValue mirrorOfVoid();

Process process();


再回來看看 IDEA 中獨立的遠程調試,配置好之後,紅框里的信息會提示你 ,遠程的 JVM 需增加這一段啟動參數,而且支持多個版本 JDK 的格式,CV 大法就能直接用。

Untitled 6.png

-agentlib 和 -javaagent

有些細心的同學可能發現了,IDEA 預設的啟動腳本里,同時配置了 -agentlib 和 -javaagent。

-javaagent:/path/to/jetbrains/debugger-agent.jar 


這個 debugger-agent吧,其實也沒幹啥事,只是對 JDK 內置的一些線程做了些增強,輔助 IDEA 的 debug 功能,支持一些非同步的調試。

Untitled 7.png

agentlib、javaagent 這倆兄弟,定位其實很像,都是載入自定義的代碼。

不過區別在於,agentlib 是載入 native lib,需要c/cpp 去寫,相當於外掛自己的代碼在 jvm 上,可以為所欲為,比如在 agentlib 里調用上面說的 JVMTI 。

而 javaagent 是用 java 寫的,可以直接用上層的 Instrumentation API,做一些類的增強轉換之類,這也是大多數 APM Agent、Profiler Agent實現的基本原理。

Arthas 的玩法

Arthas 的核心入口,其實還是 javaagent,支持靜態載入和動態載入兩種玩法。

靜態沒啥好說的,啟動腳本里增加一個-javaagent:/tmp/test/arthas-agent.jar,然後為所欲為。

動態的叫 attach,使用 Java 提供的VirtualMachine就可以實現運行時添加 -javaagent,效果一樣:

VirtualMachine virtualMachine = VirtualMachine.attach(virtualMachineDescriptor);
virtualMachine.loadAgent(agentPath, agentArgs);


這個 Agent 在 JVM 里啟動了一個TCP server,用於收發 Arthas Client 的各種 trace、watch 、Dashboard 等指令,然後通過 Instrumentation 增強Class 插入代碼、或者直接調用某些 Java API,實現各種功能。

註意到了嗎?Arthas 可以直接下載一個 jar 包,java -jar 就能連上。

其實吧,它這個直接啟動的 jar 包,是一個 boot 包,啟動之後把亂七八糟的 jar 都下載下來。接著動態 attach 的方式,連接到本機指定進程號的 JVM,然後再為所欲為。

在 3.5 版本之後,Arthas 還新增了一個vmtool命令,這個命令可以直接獲取記憶體中的指定對象實例。

$ vmtool --action getInstances --className java.lang.String --limit 10
@String[][
    @String[com/taobao/arthas/core/shell/session/Session],
    @String[com.taobao.arthas.core.shell.session.Session],
    @String[com/taobao/arthas/core/shell/session/Session],
    @String[com/taobao/arthas/core/shell/session/Session],
    @String[com/taobao/arthas/core/shell/session/Session.class],
    @String[com/taobao/arthas/core/shell/session/Session.class],
    @String[com/taobao/arthas/core/shell/session/Session.class],
    @String[com/],
    @String[java/util/concurrent/ConcurrentHashMap$ValueIterator],
    @String[java/util/concurrent/locks/LockSupport],
]


直接獲取記憶體對象,這玩意只靠 Instrumentation API 可做不到。Arthas 搞了個騷操作,直接 JNI 調用自定義 lib,用過 cpp 直接調用了 JVMTI 的 API,融合了 Instrumentation 和 JVMTI 的能力,這下是真的為所欲為了!

#include <stdio.h>
#include <jni.h>
#include <jni_md.h>
#include <jvmti.h>
#include "arthas_VmTool.h" // under target/native/javah/

static jvmtiEnv *jvmti;

...

extern "C"
JNIEXPORT jobjectArray JNICALL
Java_arthas_VmTool_getInstances0(JNIEnv *env, jclass thisClass, jclass klass, jint limit) {
    jlong tag = getTag();
    limitCounter.init(limit);
    jvmtiError error = jvmti->IterateOverInstancesOfClass(klass, JVMTI_HEAP_OBJECT_EITHER,
                                               HeapObjectCallback, &tag);
    if (error) {
        printf("ERROR: JVMTI IterateOverInstancesOfClass failed!%u\n", error);
        return NULL;
    }

    jint count = 0;
    jobject *instances;
    error = jvmti->GetObjectsWithTags(1, &tag, &count, &instances, NULL);
    if (error) {
        printf("ERROR: JVMTI GetObjectsWithTags failed!%u\n", error);
        return NULL;
    }

    jobjectArray array = env->NewObjectArray(count, klass, NULL);
    //添加元素到數組
    for (int i = 0; i < count; i++) {
        env->SetObjectArrayElement(array, i, instances[i]);
    }
    jvmti->Deallocate(reinterpret_cast<unsigned char *>(instances));
    return array;
}


總結

  1. Debug 基於 JDPA 體系

    1. IDE 直接接入 JDPA 體系中的 JDI 介面完成

    2. JDI 通過 JDWP 協議,調用遠程 VM 的 JVMTI 介面

    3. JDWP 是通過 agentlib 載入的,agentlib 算是一個 native 的靜態“外掛”介面

  2. javaagent 是 JAVA 層面的“外掛”介面,用過 Instrumentation API(Java)實現各種功能,主要用於APM、Profiler 工具

  3. 如果你想,在 javaagent 里調用功能更豐富的 JVMTI 也不是不行。

作者:京東保險 蔣信

來源:京東雲開發者社區 轉載請註明來源


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • C 語言中的條件和 if...else 語句 您已經學習過 C 語言支持數學中的常見邏輯條件: 小於:a < b 小於或等於:a <= b 大於:a > b 大於或等於:a >= b 等於:a == b 不等於:a != b 您可以使用這些條件來根據不同的決策執行不同的操作。 C 語言具有以下條件語 ...
  • Spring 可能成為您的所有企業應用程式的一站式商店。但是,Spring 是模塊化的,允許您挑選適用於您的模塊,而無需引入其他模塊。下麵的部分提供了 Spring Framework 中所有可用模塊的詳細信息。Spring Framework 提供了大約20個模塊,可以根據應用程式要求使用。 核心 ...
  • Spring 是用於企業 Java 應用程式開發的最流行的應用程式開發框架。全球數百萬開發人員使用 Spring Framework 創建高性能、易於測試和可重用的代碼。Spring Framework 是一個開源的 Java 平臺。它最初由 Rod Johnson 編寫,並於 2003 年 6 月 ...
  • 前言 在日常工作和學習中,有很多地方都需要發送HTTP請求,本文以Java為例,總結髮送HTTP請求的多種方式 HTTP請求實現過程: GET 創建遠程連接 設置連接方式(get、post、put…) 設置連接超時時間 設置響應讀取時間 發起請求 獲取請求數據 關閉連接 POST 創建遠程連接 設置 ...
  • 基於php的服裝商城的設計與實現 1.引言 隨著互聯網的普及和電子商務的快速發展,網路購物已成為人們日常生活的一部分。網路購物商城網站作為電子商務的重要平臺,具有便捷性、高效性和不受時空限制等優勢,越來越受到消費者的青睞。本文旨在設計和實現一個功能完善、操作簡便的網路購物商城網站,以滿足用戶和商家的 ...
  • 目錄概述定義實體類CarsizecarInfo造測試數據Spring BeanUtilsApache BeanUtilsCglib BeanCopierMapStruct性能測試深拷貝or淺拷貝 概述 眾所周知,java世界是由類構成的,各種各樣的類,提供各種各樣的作用,共同創造了一個個的java應 ...
  • Floyd判聯通(傳遞閉包) Floyd傳遞閉包顧名思義就是把判最短路的代碼替換成了判是否連通的代碼,它可以用來判斷圖中兩點是否連通。板子大概是這個樣的: for(int k=1; k<=n; k++){ for(int i=1; i<=n; i++){ for(int j=1; j<=n; j++ ...
  • 線性回歸是一種用於連續型分佈預測的機器學習演算法。其基本思想是通過擬合一個線性函數來最小化樣本數據和預測函數之間的誤差。 1. 概述 常見的線性回歸模型就是:\(f(x) = w_0+w_1x_1+w_2x_2+...+w_nx_n\)這樣的一個函數。其中 \((w_1,w_2,...w_n)\)是模 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...