JPDA工作流程

Posted by kyle on August 2, 2019

JPDA(Java Platform Debugger Architecture)简介

JPDA是为桌面系统开发环境调试而设计的一套调试体系,它由三个模块组成:

graph LR JPDA[JPDA -- Java平台调试架构] --> JVMTI[JVMTI -- Java虚拟机工具接口] JPDA --> JDWP[JDWP -- Java调试连线协议] JPDA --> JDI[JDI -- Java调试接口]

JVM TI(Java Virtual Machine Tools Interface)

定义了虚拟机必须提供的用于调试的服务。JVM TI由虚拟机实现,我们把虚拟机端/被调试进程端称为JPDA后端,而把调试进程端称为JPDA前端

JDWP(Java Debug Wire Protocol)

定义了被调试进程与调试器终端进程之间信息和请求的传输格式。在某些虚拟机中,JVM TI的实现存在一些问题,它们直接在虚拟机中实现了JDWP。在客户端,用Java编程语言以外的语言编写的应用程序可能不是使用JDI的最佳选择。

JDI(Java Debug Interface)

从用户代码级别定义了消息和请求。JDI提供了一个纯净的Java程序接口,用以调试Java应用程序。在JPDA中,JDI是调试器进程与被调试进程间的远程视图。它由前端实现。

JPDA工作流程

访问任何一个接口有两类活动:请求事件

  • 请求
    由调试器端发起,包括查询信息设置远程虚拟机/程序的状态设置调试状态

  • 事件
    由被调试端发起,指示远程虚拟机/程序的状态

一、查询信息

让我们以用户在IDE的栈视图中选择一个本地变量,并请求它的值的例子来说明JPDA的工作流程:

sequenceDiagram JDI->> JDWP: theStackFrame.getValue() JDWP->> JVMTI: 传送消息 JVMTI->> VM: jvmti->GetLocalInt() VM->> JVMTI: 返回结果 JVMTI->> JDWP: 返回消息 JDWP->> JDI: 返回消息

1、IDE使用JDI获取值,它将调用getValue方法:

    theStackFrame.getValue(theLocalVariable)

其中,theStackFrame是com.sun.jdi.StackFrame,theLocalVariable是com.sun.jdi.LocalVariable,都位于JDK lib目录下的tools.jar。

前端通过通信通道(比如Socket)向运行在后端的被调试进程发送查询请求。具体实现时,会按照JDWP的标准将发送的信息转化为字节流。

2、后端解析字节流,然后通过JVM TI将查询发送给虚拟机。假设我们所请求的值是整型,那么我们将调用如下JVM TI方法:

    error = jvmti->GetLocalInt(frame, slot, &intValue);

然后后端通过Socket返回响应报文,响应报文中便包含了intValue的值。当然,响应报文也会按照JDWP的标准格式化。

3、前端解析响应报文,返回getValue的值,继而IDE显示这个值。

二、设置调试状态

我们以设置断点为例。

sequenceDiagram JDI->> JDWP: 调用设置断点方法 JDWP->> JVMTI: 传送消息 JVMTI->> VM: 新建Breakpoint事件 JVMTI->> VM: 查询事件、事件入队 JVMTI->> JDWP: 返回事件队列 JDWP->> JDI: 得到事件,封装成BreakpointEvent

设置调试状态的流程与查询信息的流程基本一致。首先,我们需要调用JDI相应的方法,然后按照JDWP的标准发送命令,但JVM TI方法的调用有些不同。

此外,前端和后端做的不仅仅是来回推送数据,它们跟踪和调度活动并转换、过滤和缓存信息,因此断点请求与值查询处理完全不同,但是通信序列将是相同的。

那么当一个被调试的应用断点被触发时将会发生什么呢?这个时候事件就发挥它的作用了。虚拟机通过JVM TI发送事件。比如说,发起事件处理函数传递断点信息:

static void Breakpoint(jvmtiEnv *jvmti_env,
                       JNIEnv* jni_env, jthread thread,
                       jmethodID method, jlocation location) {
    ...
}

这个后端方法将会开启一个活动链,用来过滤是否是它感兴趣的事件消息。当捕获到它感兴趣的消息时,会将消息入队,然后按照JDWP标准定义的断点事件格式,通过Socket发送消息给前端。前端解码并处理事件,最终将生成对应的JDI事件。以断点事件为例,将会暴露成com.sun.jdi.event.BreakpointEvent。此时,IDE通过将事件移除事件队列的方法来获取它:

    theEventQueue.remove()

theEventQueue为com.sun.jdi.event.EventQueue。IDE将会利用JDI进行多次查询来合理更新断点状态。

参考资料:

Java Platform Debugger Architecture