Java调试原理

246 阅读14分钟

Java调试

Java调试的方法

JDB:

JDB(Java Debugger)是 Java 平台提供的一个命令行调试工具,用于调试 Java 程序。它允许开发者在程序运行时进行多种调试操作,如设置断点、单步执行、查看变量值、评估表达式等(每一种操作视为一种事件,返 回固定的数据,当然是通过Socket)

Logger:

Logger 是一种用于记录程序运行时信息的工具,通常用于生成日志文件,以便于调试、监控和审计。

JPDA原理:

Java Platform Debugger Architecture (JPDA)

总所周知Java程序是运行在JVM上的,调试java程序,事实上就是向虚拟机请求其当前的运行状态等,JPDA就是虚拟机提供的一整套用于java调试的工具和接口。

(JPDA 的全称是Java Platform Debugger Architecture)

image.png 基础概念:

JVM TI - Java VM Tool InterfaceDefines the debugging services a VM provides.

JDWP - Java Debug Wire ProtocolDefines the communication between Debugee and debugger processes.

JDI - Java Debug InterfaceDefines a high-level Java language interface which tool developers can easily use to write remote debugger applications.

JVMTI概述及使用:

JVMTI其实就是一套由虚拟机直接提供的本地代码接口,包含了调试、监听、线程分析及覆盖率分析等接口(接口定义可以参考jdk中的jvmti.h文件),它处于整个JPDA体系的最底层。基于JVMTI这些强大的接口,可以实现java调试器、java运行态测试以及分析工具等。对于主流的java虚拟机,都有提供标准的JVMTI实现,实现过程比较复杂,这里就不赘述它的实现。

一般我们可以通过采用建立一个Agent(调试代理)的方式来使用JVMTI,其显著的特征就是通过设置回调函数的方式,从java虚拟机上得到当前运行态信息,并做出自己的相应的操作,抑或操作虚拟机的运行态,以达到一些特定的目的(如优化程序性能)。把Agent编译成一个动态链接库后,就可以在java程序启动的时候(增加启动参数agentlib/ agentpath)来加载它(java5之后可以使用运行时加载)。

以启动时加载为例,动态库加载后,虚拟机会寻找Agent的入口函数,定义如下:

image.png 在该函数中,虚拟机传入了一个JavaVM指针和命令行参数options,由javaVM,可以获得jvmtiEnv指针,而通过 jvmtiEnv可以获取所有的JVMTI函数。通过options可以做一些初始化操作(如初始化连接方式等)。

image.png 假如我们需要某个类的字节码文件读取之后,类定义和初始化之前能修改相关的字节码,从而使创建的class对象是我们修改之后的字节码内容,就需要写一个HandleClassFileLoadHook函数,然后在Agent_OnLoad函数里为jvmtiEventCallbacks设定对应的函数指针。

image.png 这样在接下来的虚拟机运行中,所有类的字节码文件在读取时都会进入HandleClassFileLoadHook函数,可以在HandleClassFileLoadHook函数中进行修改字节码等操作。

JDWP协议:

JDWP(Java Debug Wire Protocol)定义了调试者和被调试者之间的通讯协议。在java程序中,调试者和被调试者运行在各自的java虚拟机上,被调试者(Target JVM)在启动时会加载一个Agent,该Agent里实现了各种方法,使用JVMTI函数,从而具备了调试功能,调试者(Debugger)与被调试者(Target JVM)建立连接后,向其发送命令来获取其运行时的状态和控制java程序的运行,发送命令和获取应答用的就是JDWP协议。

JDWP通讯可以分为两个阶段:握手和应答。

握手是在传输层建立连接后做的第一件事,Debugger发送字符串“JDWP-Handshake”到Target JVM,后者同样回复“JDWP-Handshake”则表示握手完成,通信正常。握手完成后,Debugger就可以向Target JVM发送命令了。

通信机制

调试器与被调试JVM之间需要通过一定的方式进行通信,通信的机制主要包括两部分

1.  连接器(Connector)

2.  通信方式(Transport)

连接器是指调试器与被调试 JVM 之间的一个连接,JPDA 在 JDI 这一层面实现了连接器。

通信方式是指调试器与被调试 JVM 之间的数据交换方式和通信报文格式,JPDA 在 JDWP 中定义了报文规范。

Packet结构

JDWP的数据包分为:

● 命令包(command packet)

● 回复包(reply packet)

Packet分为包头(header)和数据(data)两部分组成。包头部分的结构和长度是固定的,而数据部分的长度是可变的。command packet和reply packet的包头长度相同,都是11个bytes

image.png

● Length 是整个packet 的长度,包括length 部分。因为包头的长度是固定的11 bytes,所以如果一个command packet 没有数据部分,则length 的值就是 11。

● Id 是一个唯一值,用来标记和识别reply 所属的command。Reply packet 与它所回复的command packet 具有相同的Id,异步的消息就是通过Id 来配对识别的。

● Flags 目前对于command packet 值始终是 0。

● Command Set 相当于一个command 的分组,一些功能相近的command 被分在同一个Command Set 中。Command Set 的值被划分为3 个部分:0-63: 从debugger 发往target Java 虚拟机的命令。

○ 64 – 127: 从target Java 虚拟机发往debugger 的命令。

○ 128 – 256: 预留的自定义和扩展命令。

Reply packet包头结构如图四所示,Length和Id作用与command packet中的一样。Flags对于reply packet值是固定值 0x80。Error Code用来表示被回复的命令是否被正确执行了。0表示正确,非0表示执行错误。

image.png

● Length、Id 作用与command packet 中的一样。

● Flags 目前对于reply packet 值始终是 0x80。我们可以通过Flags 的值来判断接收到的packet 是command 还是 reply。

● Error Code 用来表示被回复的命令是否被正确执行了。零表示正确,非零表示执行错误。

● Data 的内容和结构依据不同的command 和reply 都有所不同。比如请求一个对象成员变量值的command,它的data 中就包含该对象的id 和成员变量的id 。而reply 中则包含该成员变量的值。

注意:

Debugger和Target JVM都可以发送命令包。Debugger通过发送命令包获取Target JVM的信息以及控制程序的执行。Target JVM通过发送命令包通知Debugger诸如断点或者异常等事件的发生。

连接管理:

JDWP传输接口JDWP本身是与传输层独立的,但JDWP提供了一套传输接口,它定义了一系列的方法用来定义JDWP与传输层之间的交互方式。传输层必须以动态链接库的方式实现,并且暴露一系列的标准接口供JDWP使用。

当JDWP agent被Java虚拟机加载后,JDWP会根据参数去加载指定的传输层实现(例如我们最常见的Sun的JDK提供了socket方式)。传输层的实现必须暴露jdwpTransport_OnLoad接口,JDWP agent在加载传输层动态链接库后会调用该接口进行传输层的初始化。接口定义如下:

image.png

和一般的连接方式类似,JDWP可以主动去连接debugger,也可以等待debugger的连接。对于主动去连接debugger,需要调用Attach方法,其定义如下:

image.png

对于JDWP等待debugger连接的方式,首先要调用其StartListening()方法,定义如下:

image.png

该方法将使JDWP处于监听状态,随后调用Accept()方法接收连接:

image.png

根据传入参数确定采用主动连接方式还是等待连接方式,无论是哪种连接方式,在连接建立后,会立即进行握手操作。握手完成后可以通过IO操作进行应答。

IO操作:

I/O 操作接口主要是负责从传输层读写packet 。

有 ReadPacket 和 WritePacket 两个方法:

jdwpTransportError 
ReadPacket(jdwpTransportEnv* env, jdwpPacket* packet) 
jdwpTransportError 
WritePacket(jdwpTransportEnv* env, const jdwpPacket* packet)

其结构jdwpPacket 与我们开始提到的JDWP packet 结构一致,定义如下:

typedef struct { 
    jint len;        // packet length 
    jint id;         // packet id 
    jbyte flags;     // value is 0 
    jbyte cmdSet;    // command set 
    jbyte cmd;       // command in specific command set 
    jbyte *data;     // data carried by packet 
} jdwpCmdPacket; 
 
typedef struct { 
    jint len;        // packet length 
    jint id;         // packet id 
    jbyte flags;     // value 0x80 
    jshort errorCode;    // error code 
    jbyte *data;     // data carried by packet 
} jdwpReplyPacket; 
 
typedef struct jdwpPacket { 
    union { 
        jdwpCmdPacket cmd; 
        jdwpReplyPacket reply; 
    } type; 
} jdwpPacket;

JDWP命令机制

JDWP内部命令实现机制如图五所示,JDWP接收和发送的包都会在 TransportManager进行处理。TransportManager的主要作用就是负责解析接收到的 JDWP的command packet以及将reply packet在发送前进行打包。对于接收到的command packet,TransportManager处理后转给PacketDispatcher,进一步封装后会继续转到CommandDispatcher。CommandDispatcher会根据命令中提供的命令组号和命令号创建一个CommandHandler来处理JDWP命令。CommandHandler才是真正执行JDWP命令的类,每个JDWP命令都有一个相对应的CommandHandler的子类,当接收到某个命令时,就会创建对应子类的实例,调用对应的JVMTI方法进行处理。

image.png

Java调试接口(JDI)

JDI主要包含了一套针对调试者定义的接口,通过这套接口,开发人员就能通过前端虚拟机上的调试器(如eclipse调试器)来远程监控/控制后端虚拟机上被调试程序的运行,它处于JPDA体系的最高层,eclipse的jdt.debug就是一个完整的JDI实现。

JDI工作方式

● 高层次抽象:JDI 提供了一组高层次的类和接口,使得开发者可以方便地访问和操作 Java 应用程序的状态,而不需要直接处理底层的 JDWP 协议。

● 与 JDWP 的关系:JDI 是建立在 JDWP(Java Debug Wire Protocol)之上的。JDWP 是一个低层次的协议,负责在调试器和 JVM 之间传输调试信息。JDI 则提供了一个更易于使用的接口,封装了 JDWP 的复杂性。

● 事件处理:JDI 支持事件驱动的编程模型,调试器可以注册对特定事件的监听(如断点命中、异常抛出等),并在事件发生时接收通知。

链接是Debugger与Target JVM之间交互的渠道,一个调试器可以链接多个目标虚拟机,但一个目标虚拟机最多只能链接一个调试器。链接是由链接器(Connector)生成的,不同的链接器有着不同的实现方式。JDI中定义了三种链接器接口,分别是依附型链接器(AttachingConnector)、监听型链接器(ListeningConnector)和启动型链接器(LaunchingConnector)。

首先,Debugger通过Bootstrap获取唯一的虚拟机管理器。

在调试过程中,实际使用的链接器必须实现其中一种接口,而在虚拟机管理器中就提供了各种连接器的实现。

根据调试器在链接过程中扮演的角色,也可以将链接方式划分为主动链接和被动链接:

● 主动链接表示调试器主动地向目标虚拟机发起链接。

● 被动链接表示调试器将被动地等待或者监听由目标虚拟机发起的链接。

以我们调试一个Test类的main方法为例,这是一个典型的被动链接:

首先调试器调用虚拟管理器的listeningConnectors ()方法获取所有的监听型链接器实例connector;根据socket传输方式选择对应的socket链接器,调用链接器的startListening() 方法让链接器进入监听状态;终端用户以-agentlib:jdwp=transport=dt_socket,suspend=y,address=localhost:port参数启动target

JVM(即启动时加载jdwp Agent,transport=dt_socket 表示采用socket连接方式, suspend=y参数表示虚拟机启动后挂起,等待连接建立后再执行,address是socket连接地址),然后调用链接器的accept()方法等待接受目标虚拟机的链接,链接成功后进行握手,确保通信正常,该方法返回目标虚拟机的实例。

自此Debugger和Target JVM便可以进行双向通信了。通过VirtualMachine负责发送命令和接收回应,Debugger将用户的操作按JDWP协议转化为调试命令发送到前端Target JVM上,经由JDWP的调试机制,将调试的结果按JDWP协议发回给Debugger;最后,Debugger解析后将可视化数据信息反馈给用户。

对于其他的链接方式,虚拟机管理器都提供了相对应的链接器,按照指定的链接方式完成链接后均可获得目标虚拟机实例。

JDI数据模块

上面介绍到的VirtualMachine接口如图六所示,该接口提供了方法可以用来直接或间接地获取target JVM上所有的数据和状态信息,也可以挂起、恢复、终止目标虚拟机。

image.png

VirtualMachine接口继承自Mirror接口,JDI中几乎所有其他接口都继承于它。镜像机制是将目标虚拟机上的所有数据、类型、域、方法、事件、状态和资源,以及调试器发向目标虚拟机的事件请求等都映射成Mirror对象。例如,在目标虚拟机上,对象实例被映射成ObjectReference镜像,基本类型的值(如float等)被映射成PrimitiveValue(如FloatValue等)。被调试的目标程序的运行状态信息被映射到StackFrame镜像中,被调试的目标虚拟机则被映射成VirtualMachine镜像等。调用Mirror实例virtualMachine()可以获取其虚拟机信息

image.png

这样,通过virtualMachine实例可以方便的获取JVM当前状态或者控制JVM。

理解:

● JVMTI

Java VM Tool Interface 定义的是一系列跟调试相关的接口,由 VM 实现这些接口。我们所说的程序暂停下来,确实是 JVM 运行代码的时候停在了有断点的地方,那么 JVM 必然提供一种联络方式,让别人告诉它断点在哪个类的第几行。这里的联络方式,就是 JVMTI(JVM Tool Interface),这是 JVM 提供的一个类似钩子的机制,通过 JVMTI,可以指挥 JVM 执行某些操作,例如停在断点处,也可以在 JVM 运行过程中,发生某些事件时,通过钩子通知外部感兴趣的人。

那么谁可以跟钩子通信呢,并不是说任何人只要有兴趣就行的。JVM 要求它必须是一个 JVMTI Agent,Java 在不同的操作系统中,都已经内置了本地 JVMTI Agent。在 Windows 系统中,这个 JVMTI Agent 就是一个 DLL 文件,在类 Unix 的操作系统中,则是一个 SO 文件。JVMTI Agent 与 JVM 是运行在同一个机器上同一个进程内的。

● JDWP

当想要利用 JVMTI 让 JVM 做一些事的时候,那么就要先要与 Agent 通信,由它代为传话。因此,JVMTI Agent 内置了一个称之为“通信后端”的模块,用来接收外部的请求。

想要与 JVMTI Agent 通信的第三方,就要先与通信后端通信,通信就意味着必然有一个通信协议的存在,这个协议就是 JDWP(Java Debug Wire Protocol) 协议。

● JDI

Java 程序员经常使用的 eclipse、idea 这样的 IDE 来 debug 程序的时候,就是以 JDWP 协议与目标 JVM 的 JVMTI Agent 通信的。考虑到 JDWP 协议的实现比较繁琐,Java 官方也在 com.sun.jdi 这个 package 中实现了一个叫做 JDI(Java Debug Interface)基础库,JDI 实现了 JDWP 协议,将与 JVMTI Agent 通信的细节封装为一个又一个 Java API,方便第三方与 JVMTI Agent 通信,与 JVMTI Agent 的通信后端相对应,JDI 包含了一个通信前端模块,负责 JDWP 协议的转换以及消息的发送和接收。

(Soon。。。)

源码步骤:

参考:

Java-JDWP协议探究 | Vgbhfive's Blog

Java 调试技术 JPDA 架构解读JPDA 概览 JPDA 的全称是 Java Platform Debugger - 掘金