Java Platform Debugger Architecture


Java 平台调试器架构

概述

Java 平台调试器架构(JPDA)由两个接口(JVM TI 和 JDI)、一个协议(JDWP)以及两个将它们联系在一起的软件组件(后端和前端)组成。JVM TI 的意图是多方面的:

背景

参见 Java 平台调试器架构

模块化

下面讨论了 JPDA 的模块化结构的详细信息。在每种情况下,描述了标准 JPDA 的使用方式。描述了参考实现,并讨论了接口的替代实现和客户端。

JVM TI 模块化

Java 虚拟机工具接口(JVM TI)描述了虚拟机(VM)提供的功能,以允许调试在该 VM 下运行的 Java 编程语言应用程序。在 JPDA 中,JVM TI 由 VM 实现,客户端是 JPDA 后端。在 JPDA 的参考实现中,JVM TI 由 Java HotSpot VM 实现,客户端是后端的参考实现,作为一个本地共享库(jdwp.so、jdwp.dll 等)随 JDK 一起提供。

除了 Java HotSpot VM 之外,许多其他 VM 实现了 JVM TI。后端的参考实现已被移植到其他平台。除了后端,还有其他 JVM TI 客户端,尤其是用于允许调试本地和 Java 编程语言代码的应用程序的代理,因此需要本机级别的控制和信息。我们知道没有干净的后端实现,尽管这是可能的 - 也是很多工作。

JDWP 模块化

Java 调试线协议(JDWP)描述了调试信息和请求在被调试程序和调试器之间的格式。在 JPDA 中,前端(在调试器进程中)和后端(在被调试程序进程中)之间有一个通信通道 - 在该通道上流动的数据格式由 JDWP 描述。在 JPDA 的参考实现中,后端的参考实现(上文)提供了该通道的被调试程序端,而前端的参考实现(JDK 中的 Java 编程语言组件,位于 tools.jar 中)提供了调试器端。

JVM TI 在某些 VM 中实现起来有问题。在这些 VM 中,JDWP 直接实现。在客户端方面,除了使用 JDI 的 Java 编程语言之外的应用程序可能不是使用 JDI 的最佳候选。有些人选择成为 JDWP 的客户端。

JDI 模块化

Java 调试接口(JDI)为调试 Java 编程语言应用程序提供了一个纯 Java 编程语言接口。在 JPDA 中,JDI 是调试器进程中虚拟机的远程视图,在被调试程序进程中实现。它由前端(上文)实现,而调试器类似的应用程序(IDE、调试器、跟踪器、监控工具等)是客户端。

JDI 可以由具有应用程序静态视图的系统实现。它可以由具有与 JDWP/前端完全不同的机制的系统实现。

演练

上面讨论了接口的各种使用方式。本节将详细讨论标准完整的 JPDA 如何工作。示例详细介绍了特定调用和代码。理解这些并不重要 - 它们只是为了使示例更具体。

在每个接口上有两类活动:请求和事件。请求源自调试器端,包括信息查询、设置远程 VM/应用程序中的状态更改以及设置调试状态。事件源自被调试程序端,表示远程 VM/应用程序中的状态更改。

让我们通过一个示例来演练。用户在 IDE 中的堆栈视图中点击一个局部变量,请求其值。IDE 使用 JDI 获取该值,特别是调用 getValue 方法,例如:

    theStackFrame.getValue(theLocalVariable)

其中 theStackFrame 是一个 com.sun.jdi.StackFrame,而 theLocalVariable 是一个 com.sun.jdi.LocalVariable

然后,前端通过通信通道(比如一个套接字)将此查询发送到在被调试程序进程中运行的后端。它通过按照 JDWP 的规定将其格式化为字节流来发送它。特别是,它发送一个 GetValues 命令(字节值:1)在 StackFrame 命令集(字节值:16)中,后跟线程 ID、帧 ID 等。

后端解析字节流并将查询发送到 VM 通过 JVM TI。特别是,假设请求的值是一个整数,将进行以下 JVM TI 函数调用:

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

后端通过套接字发送回一个响应数据包,其中将包括 intValue 的值,并且将根据 JDWP 进行格式化。前端解析响应数据包并将值作为 getValue 方法调用的值返回。IDE 然后显示该值。

更改调试状态的请求以类似的方式处理。例如,设置断点的请求经过相同的步骤 - 尽管调用的 JDI 方法、发送的 JDWP 命令以及调用的 JVM TI 函数是不同的。此外,前端和后端不仅仅是将数据来回传递,它们还跟踪和安排活动,并转换、过滤和缓存信息,因此设置断点请求将被处理得与获取值查询完全不同 - 但通信顺序将是相同的。

当被调试的应用程序最终触发此断点时会发生什么?这就是事件发挥作用的地方。虚拟机通过 JVM TI 接口发送一个事件。特别是,它调用事件处理函数传递断点:

后端已将事件处理函数设置为:

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

此后端函数启动了一系列活动,过滤事件以查看其是否有趣,将其排队,并按照为断点事件定义的 JDWP 格式通过套接字发送。前端解码和处理事件,最终生成一个 JDI 事件。特别是,JDI 事件将其显示为一个 com.sun.tools.jdi.event.BreakpointEvent。IDE 然后通过从事件队列中移除它来获取事件:

    theEventQueue.remove()

其中 theEventQueue 是一个 com.sun.jdi.event.EventQueue。IDE 可能会通过在 JDI 上进行多次查询调用来更新其显示。

移植

每个虚拟机实现都需要自己的 JVM TI 实现 - JVM TI 实现必须深入挖掘 VM 数据结构,并必须在 VM 实现中设置钩子以获取事件。在没有 JVM TI 支持的 VM 中添加 JVMDT 是一项重大工作。根据 VM 的复杂性和可选 JVM TI 的数量,这可能是一个三到十二个月的项目。将具有 JVM TI 支持的 VM 移植到新平台主要是将 VM 的非 JVM TI 部分移植 - JVM TI 添加了相对较少的工作。

后端的参考实现通常可以在不改变源代码或几行代码的情况下移植到新平台,然后重新编译。要在同一平台上使用新的 VM,后端的二进制通常应该可以工作 - 尽管它不是 Java 编程语言代码,所以你永远不知道。请注意,许可问题不在本文档的讨论范围内。

前端实现是用 Java 编程语言编写的,可以在任何平台或 VM 上运行。但是,连接器代码具有可能需要为某些系统扩展功能。例如,前端的参考实现包括一个假定使用 Java SE 约定启动虚拟机的启动器。JDI 的使用者可以配置任何启动器语法,但通常调试器应用程序会倾向于将此留给 JDI 实现。如果需要不同类型的通信通道(例如串行),则需要使用 JDK 5.0 中引入的 服务提供程序接口 进行添加。