JPDA(jaa platform debugger architecture)

版权声明:转载 请标注 出处 https://blog.csdn.net/youyou1543724847/article/details/84792301

参考文献:
https://www.ibm.com/developerworks/cn/java/j-lo-jpda1/index.html?ca=drs-
https://www.ibm.com/developerworks/cn/java/j-lo-jpda2/
http://www.ibm.com/developerworks/cn/java/j-lo-jpda3/index.html?ca=drs-
http://www.ibm.com/developerworks/cn/java/j-lo-jpda4/index.html?ca=drs-

https://docs.oracle.com/javase/7/docs/technotes/guides/jpda/architecture.html
https://docs.oracle.com/javase/8/docs/platform/jvmti/jvmti.html

1.概述

Java 程序都是运行在 Java 虚拟机上的,我们要调试 Java 程序,事实上就需要向 Java 虚拟机请求当前运行态的状态,并对虚拟机发出一定的指令,设置一些回调等等,那么 Java 的调试体系,就是虚拟机的一整套用于调试的工具和接口。
对于 Java 虚拟机接口熟悉的人来说,您一定还记得 Java 提供了两个接口体系,JVMPI(Java Virtual Machine Profiler Interface)和 JVMDI(Java Virtual Machine Debug Interface),而它们,以及在 Java SE 5 中准备代替它们的 JVMTI(Java Virtual Machine Tool Interface),都是Java 平台调试体系(Java Platform Debugger Architecture,JPDA)的重要组成部分。 Java SE 自 1.2.2 版就开始推出 Java 平台调试体系结构(JPDA)工具集,而从 JDK 1.3.x 开始,Java SDK 就提供了对 Java 平台调试体系结构的直接支持。顾名思义,这个体系为开发人员提供了一整套用于调试 Java 程序的 API,是一套用于开发 Java 调试工具的接口和协议。本质上说,它是我们通向虚拟机,考察虚拟机运行态的一个通道,一套工具。理解这一点对于学习 JPDA 非常重要。

换句话说,通过 JPDA 这套接口,我们就可以开发自己的调试工具。通过这些 JPDA 提供的接口和协议,调试器开发人员就能根据特定开发者的需求,扩展定制 Java 调试应用程序,开发出吸引开发人员使用的调试工具。前面我们提到的 IDE 调试工具都是基于 JPDA 体系开发的,区别仅仅在于它们可能提供了不同的图形界面、具有一些不同的自定义功能。另外,我们要注意的是,JPDA 是一套标准,任何的 JDK 实现都必须完成这个标准,因此,通过 JPDA 开发出来的调试工具先天具有跨平台、不依赖虚拟机实现、JDK 版本无关等移植优点,因此大部分的调试工具都是基于这个体系的。

1.1 架构

JVMTI ( Java VM Tool Interface):java 1.5推出的新接口。它通过接口的形式定义了VM可提供的调试服务;
JDWP(Java Debug Wire Protocol): java 调试通信协议。调试者与被调试者两者通信协议。
JDI (Java Debug Interface): 高层的Java语言接口,调试工具开发者可基于该接口开发自己的调试工具。
在这里插入图片描述

1.2 关于JPDA中各层说明

调试开发者可使用JPDA中各层接口进行开发。但是JDI是JPDA中最高层的,y也是使用起来最简单的,鼓励调试工具开发者使用该层接口开发。如果某个公司需要开发调试器,则可参考“JDI参看实现”。

1.2.1 VM

JVM实现了JVM TI接口。
关于JVM TI接口的说明见下章节。

1.2.2. back-end

负责和debugger(调试者)前端通信,并交消息传递给被调试的VM,并将消息返回给debugger(调试者)前端。两者通信通过JDWP(Java Debug Wire Protocol)。back-end和被调试的虚拟机通信通过 JVMTI ( Java Virtual Machine Debug Interface)通信
== 前后端的通信机制:==
通信包括两种机制,1)connector,2)transport(具体实现形式没有定义) 。其中connector是一个JDI对象。backend与front-end可通过conector建立通信连接。JPDA定义了三种connector:

  • (1) listening connectors: 前端监听back-end的连接请求;
  • (2)attaching connectors:前端主动的绑定到已经运行了的后端上;
    *(3)launching connectors:前端自己启动java vm,其中包括了被调试的java代码和back-end(所有的东西都在一起)

1.2.3. JDWP

JDWP(Java Debug Wire Protocol)是一个为 Java 调试而设计的一个通讯交互协议。
在 JPDA 体系中,作为前端(front-end)的调试者(debugger)进程和后端(back-end)的被调试程序(debuggee)进程之间的交互数据的格式就是由 JDWP 来描述的,它详细完整地定义了请求命令、回应数据和错误代码,保证了前端和后端的 JVMTI 和 JDI 的通信通畅。比如在 Sun 公司提供的实现中,它提供了一个名为== jdwp.dll(jdwp.so)的动态链接库文件==,这个动态库文件实现了一个 Agent,它会负责解析前端发出的请求或者命令,并将其转化为 JVMTI 调用,然后将 JVMTI 函数的返回值封装成 JDWP 数据发还给后端。
另外,这里需要注意的是 JDWP 本身并不包括传输层的实现,传输层需要独立实现,但是 JDWP 包括了和传输层交互的严格的定义,就是说,JDWP 协议虽然不规定我们是通过 EMS 还是快递运送货物的,但是它规定了我们传送的货物的摆放的方式。在 Sun 公司提供的 JDK 中,在传输层上,它提供了 socket 方式,以及在 Windows 上的 shared memory 方式。当然,传输层本身无非就是本机内进程间通信方式和远端通信方式,用户有兴趣也可以按 JDWP 的标准自己实现。

1.2.4 JDI

JDI(Java Debug Interface)是三个模块中最高层的接口,在多数的 JDK 中,它是由 Java 语言实现的。 JDI 由针对前端定义的接口组成,通过它,调试工具开发人员就能通过前端虚拟机上的调试器来远程操控后端虚拟机上被调试程序的运行,JDI 不仅能帮助开发人员格式化 JDWP 数据,而且还能为 JDWP 数据传输提供队列、缓存等优化服务。从理论上说,开发人员只需使用 JDWP 和 JVMTI 即可支持跨平台的远程调试,但是直接编写 JDWP 程序费时费力,而且效率不高。因此基于 Java 的 JDI 层的引入,简化了操作,提高了开发人员开发调试程序的效率。

1.2.5. front-end

front-end实现了JDI接口。

2. JVMTI:Java 虚拟机工具接口

JVMTI(Java Virtual Machine Tool Interface)即指 Java 虚拟机工具接口,它是一套由虚拟机直接提供的 native 接口,它处于整个 JPDA 体系的最底层,所有调试功能本质上都需要通过 JVMTI 来提供。通过这些接口,开发人员不仅调试在该虚拟机上运行的 Java 程序,还能查看它们运行的状态,设置回调函数,控制某些环境变量,从而优化程序性能。

JVMTI 并不一定在所有的 Java 虚拟机上都有实现,不同的虚拟机的实现也不尽相同。不过在一些主流的虚拟机中,比如 Sun 和 IBM,以及一些开源的如 Apache Harmony DRLVM 中,都提供了标准 JVMTI 实现

2.1 JVMTI与Agent的关系

JVM TI is implemented by HotSpot and allows a native code ‘agent’ to inspect and modify the state of the JVM.(参看http://openjdk.java.net/groups/hotspot/docs/Serviceability.html#battach 中的说明)
Agent 即 JVMTI 的客户端,它和执行 Java 程序的虚拟机运行在同一个进程上,通过调用 JVMTI 提供的接口和虚拟机交互,负责获取并返回当前虚拟机的状态或者转发控制命令。把 Agent 编译成一个动态链接库之后,我们就可以在 Java 程序启动的时候来加载它(启动加载模式),也可以在 Java 5 之后使用运行时加载(活动加载模式)。
总结:个人感觉,JVMTI一种操作JVM的规范与接口。开发者通过Agent的方式使用该套接口。同时Agent的逻辑运行机制依赖与JVMTI接口的实现。

2.2 Agent的工作过程

我们使用 JVMTI 的过程,主要是设置 JVMTI 环境监听虚拟机所产生的事件,以及在某些事件上加上我们所希望的回调函数。()

Dynamic Attach. This is a Sun private mechanism that allows an external process to start a thread in HotSpot that can then be used to launch an agent to run in that HotSpot, and to send information about the state of HotSpot back to the external process.

Agent 的主要功能是通过一系列的在虚拟机上设置的回调(callback)函数完成的,一旦某些事件发生,Agent 所设置的回调函数就会被调用,来完成特定的需求。

2.2.1 JVM启动时加载

Agent 是在 Java 虚拟机启动之时加载的,这个加载处于虚拟机初始化的早期,在这个时间点上:

  1. 所有的 Java 类都未被初始化;
  2. 所有的对象实例都未被创建;
    因而,没有任何 Java 代码被执行;但在这个时候,我们已经可以:
  3. 操作 JVMTI 的 Capability 参数;
  4. 使用系统参数;

2.2.2 JVM运行态加载

基于jvm attach机制。
关于dynamic attach: Dynamic Attach. This is a Sun private mechanism that allows an external process to start a thread in HotSpot that can then be used to launch an agent to run in that HotSpot, and to send information about the state of HotSpot back to the external process.(参见 http://openjdk.java.net/groups/hotspot/docs/Serviceability.html#battach )

attach是Sun的私有实现(并不是所有的jvm都提供该功能),该机制允许 外部进程 在JVM(该JVM指运行被监控、需要被操控的Java程序的JVM)中启动一个线程,该线程随后会启动加载agent,并且将本JVM的状态发送给 外部进程。

2.3 JVMTIAgent 示例

该Agent通过监听 JVMTI_EVENT_METHOD_ENTRY 事件,注册对应的回调函数来响应这个事件,来输出所有被调用函数名。

具体实现都在 MethodTraceAgent 这个类里提供。按照顺序,他会处理环境初始化、参数解析、注册功能、注册事件响应,每个功能都被抽象在一个具体的函数里。

MethodTraceAgent.h

#include "jvmti.h"

class AgentException 
{
 public:
	AgentException(jvmtiError err) {
		m_error = err;
	}

	char* what() const throw() { 
		return "AgentException"; 
	}

	jvmtiError ErrCode() const throw() {
		return m_error;
	}

 private:
	jvmtiError m_error;
};


class MethodTraceAgent 
{
 public:

	MethodTraceAgent() throw(AgentException){}

	~MethodTraceAgent() throw(AgentException);

	void Init(JavaVM *vm) const throw(AgentException);
        
	void ParseOptions(const char* str) const throw(AgentException);

	void AddCapability() const throw(AgentException);
        
	void RegisterEvent() const throw(AgentException);
    
	static void JNICALL HandleMethodEntry(jvmtiEnv* jvmti, JNIEnv* jni, jthread thread, jmethodID method);

 private:
	static void CheckException(jvmtiError error) throw(AgentException)
	{
		// 可以根据错误类型扩展对应的异常,这里只做简单处理
		if (error != JVMTI_ERROR_NONE) {
			throw AgentException(error);
		}
	}
    
	static jvmtiEnv * m_jvmti;
	static char* m_filter;
};

MethodTraceAgent.cpp

#include <iostream>

#include "MethodTraceAgent.h"
#include "jvmti.h"

using namespace std;

jvmtiEnv* MethodTraceAgent::m_jvmti = 0;
char* MethodTraceAgent::m_filter = 0;

MethodTraceAgent::~MethodTraceAgent() throw(AgentException)
{
    // 必须释放内存,防止内存泄露
    m_jvmti->Deallocate(reinterpret_cast<unsigned char*>(m_filter));
}

void MethodTraceAgent::Init(JavaVM *vm) const throw(AgentException){
    jvmtiEnv *jvmti = 0;
	jint ret = (vm)->GetEnv(reinterpret_cast<void**>(&jvmti), JVMTI_VERSION_1_0);
	if (ret != JNI_OK || jvmti == 0) {
		throw AgentException(JVMTI_ERROR_INTERNAL);
	}
	m_jvmti = jvmti;
}

void MethodTraceAgent::ParseOptions(const char* str) const throw(AgentException)
{
    if (str == 0)
        return;
	const size_t len = strlen(str);
	if (len == 0) 
		return;

  	// 必须做好内存复制工作
	jvmtiError error;
    error = m_jvmti->Allocate(len + 1,reinterpret_cast<unsigned char**>(&m_filter));
	CheckException(error);
    strcpy(m_filter, str);

    // 可以在这里进行参数解析的工作
	// ...
}

void MethodTraceAgent::AddCapability() const throw(AgentException)
{
    // 创建一个新的环境
    jvmtiCapabilities caps;
    memset(&caps, 0, sizeof(caps));
    caps.can_generate_method_entry_events = 1;
    
    // 设置当前环境
    jvmtiError error = m_jvmti->AddCapabilities(&caps);
	CheckException(error);
}
  
void MethodTraceAgent::RegisterEvent() const throw(AgentException)
{
    // 创建一个新的回调函数
    jvmtiEventCallbacks callbacks;
    memset(&callbacks, 0, sizeof(callbacks));
    callbacks.MethodEntry = &MethodTraceAgent::HandleMethodEntry;
    
    // 设置回调函数
    jvmtiError error;
    error = m_jvmti->SetEventCallbacks(&callbacks, static_cast<jint>(sizeof(callbacks)));
	CheckException(error);

	// 开启事件监听
	error = m_jvmti->SetEventNotificationMode(JVMTI_ENABLE, JVMTI_EVENT_METHOD_ENTRY, 0);
	CheckException(error);
}

void JNICALL MethodTraceAgent::HandleMethodEntry(jvmtiEnv* jvmti, JNIEnv* jni, jthread thread, jmethodID method)
{
	try {
        jvmtiError error;
        jclass clazz;
        char* name;
		char* signature;
        
		// 获得方法对应的类
        error = m_jvmti->GetMethodDeclaringClass(method, &clazz);
        CheckException(error);
        // 获得类的签名
        error = m_jvmti->GetClassSignature(clazz, &signature, 0);
        CheckException(error);
        // 获得方法名字
        error = m_jvmti->GetMethodName(method, &name, NULL, NULL);
        CheckException(error);
        
        // 根据参数过滤不必要的方法
		if(m_filter != 0){
			if (strcmp(m_filter, name) != 0)
				return;
		}			
		cout << signature<< " -> " << name << "(..)"<< endl;

        // 必须释放内存,避免内存泄露
        error = m_jvmti->Deallocate(reinterpret_cast<unsigned char*>(name));
		CheckException(error);
        error = m_jvmti->Deallocate(reinterpret_cast<unsigned char*>(signature));
		CheckException(error);

	} catch (AgentException& e) {
		cout << "Error when enter HandleMethodEntry: " << e.what() << " [" << e.ErrCode() << "]";
    }
}



Agent_OnLoad 函数会在 Agent 被加载的时候创建这个类,并依次调用上述各个方法,从而实现这个 Agent 的功能。Agent_OnUnload函数,在agent卸载时调用。

(通过在vm参数里 加上-agentlib,则会VM启动时,执行VM JVMTIAgent的Agent_OnLoad函数 )

(Agent_OnAttach函数:如果agent不是在启动时加载的,而是我们先attach到目标进程上,然后给对应的目标进程发送load命令来加载,则在加载过程中会调用Agent_OnAttach函数。)

Main.cpp

#include <iostream>

#include "MethodTraceAgent.h"
#include "jvmti.h"

using namespace std;

JNIEXPORT jint JNICALL Agent_OnLoad(JavaVM *vm, char *options, void *reserved)
{
    cout << "Agent_OnLoad(" << vm << ")" << endl;
    try{
        
        MethodTraceAgent* agent = new MethodTraceAgent();
		agent->Init(vm);
        agent->ParseOptions(options);
        agent->AddCapability();
        agent->RegisterEvent();
        
    } catch (AgentException& e) {
        cout << "Error when enter HandleMethodEntry: " << e.what() << " [" << e.ErrCode() << "]";
		return JNI_ERR;
	}
    
	return JNI_OK;
}

JNIEXPORT void JNICALL Agent_OnUnload(JavaVM *vm)
{
    cout << "Agent_OnUnload(" << vm << ")" << endl;
}

Agent 编译和运行

g++ -I${JAVA_HOME}/include/ -I${JAVA_HOME}/include/linux 
MethodTraceAgent.cpp Main.cpp -fPIC -shared -o libagent.so

用于测试Java程序,一个简单类:

public class MethodTraceTest{

	public static void main(String[] args){
		MethodTraceTest test = new MethodTraceTest();
		test.first();
		test.second();
	}
	
	public void first(){
		System.out.println("=> Call first()");
	}
	
	public void second(){
		System.out.println("=> Call second()");
	}
}

运行Java程序,并指定Agent

java -agentlib:Agent=first MethodTraceTest

当程序运行到到 MethodTraceTest 的 first 方法是,Agent 会输出这个事件。“ first ”是 Agent 运行的参数,如果不指定话,所有的进入方法的触发的事件都会被输出,如果读者把这个参数去掉再运行的话,会发现在运行 main 函数前,已经有非常基本的类库函数被调用了。

2.4 Agent 时序图

在这里插入图片描述

2.5 补充说明:Java Agent

我们通过-javaagent来指定我们编写的agent的jar路径(./myagent.jar),以及要传给agent的参数(mode=test),在启动的时候这个agent就可以做一些我们希望的事了。

javaagent的主要功能如下:

  • 可以在加载class文件之前做拦截,对字节码做修改
  • 可以在运行期对已加载类的字节码做变更,但是这种情况下会有很多的限制,后面会详细说
  • 还有其他一些小众的功能
    • 获取所有已经加载过的类
    • 获取所有已经初始化过的类(执行过clinit方法,是上面的一个子集)
    • 获取某个对象的大小
    • 将某个jar加入到bootstrap classpath里作为高优先级被bootstrapClassloader加载
    • 将某个jar加入到classpath里供AppClassloard去加载
    • 设置某些native方法的前缀,主要在查找native方法的时候做规则匹配

JavaAgent,必须要讲的是一个叫做instrumentJVMTIAgentLinux下对应的动态库是libinstrument.so),因为javaagent功能就是它来实现的,另外instrument agent还有个别名叫JPLISAgent(Java Programming Language Instrumentation Services Agent),这个名字也完全体现了其最本质的功能:就是专门为Java语言编写的插桩服务提供支持的。

instrument agent的核心数据结构如下:

struct _JPLISAgent {
    JavaVM *                mJVM;                   /* handle to the JVM */
    JPLISEnvironment        mNormalEnvironment;     /* for every thing but retransform stuff */
    JPLISEnvironment        mRetransformEnvironment;/* for retransform stuff only */
    jobject                 mInstrumentationImpl;   /* handle to the Instrumentation instance */
    jmethodID               mPremainCaller;         /* method on the InstrumentationImpl that does the premain stuff (cached to save lots of lookups) */
    jmethodID               mAgentmainCaller;       /* method on the InstrumentationImpl for agents loaded via attach mechanism */
    jmethodID               mTransform;             /* method on the InstrumentationImpl that does the class file transform */
    jboolean                mRedefineAvailable;     /* cached answer to "does this agent support redefine" */
    jboolean                mRedefineAdded;         /* indicates if can_redefine_classes capability has been added */
    jboolean                mNativeMethodPrefixAvailable; /* cached answer to "does this agent support prefixing" */
    jboolean                mNativeMethodPrefixAdded;     /* indicates if can_set_native_method_prefix capability has been added */
    char const *            mAgentClassName;        /* agent class name */
    char const *            mOptionsString;         /* -javaagent options string */
};

struct _JPLISEnvironment {
    jvmtiEnv *              mJVMTIEnv;              /* the JVM TI environment */
    JPLISAgent *            mAgent;                 /* corresponding agent */
    jboolean                mIsRetransformer;       /* indicates if special environment */
};

这里解释一下几个重要项:

  • mNormalEnvironment:主要提供正常的类transform及redefine功能。

  • mRetransformEnvironment:主要提供类retransform功能。

  • mInstrumentationImpl:这个对象非常重要,也是我们Java agent和JVM进行交互的入口,或许写过javaagent的人在写premain以及agentmain方法的时候注意到了有个Instrumentation参数,该参数其实就是这里的对象。

  • mPremainCaller:指向sun.instrument.InstrumentationImpl.loadClassAndCallPremain方法,如果agent是在启动时加载的,则该方法会被调用。

  • mAgentmainCaller:指向sun.instrument.InstrumentationImpl.loadClassAndCallAgentmain方法,该方法在通过attach的方式动态加载agent的时候调用。

  • mTransform:指向sun.instrument.InstrumentationImpl.transform方法。

  • mAgentClassName:在我们javaagent的MANIFEST.MF里指定的Agent-Class

  • mOptionsString:传给agent的一些参数。

  • mRedefineAvailable:是否开启了redefine功能,在javaagent的MANIFEST.MF里设置Can-Redefine-Classes:true

  • mNativeMethodPrefixAvailable:是否支持native方法前缀设置,同样在javaagent的MANIFEST.MF里设置Can-Set-Native-Method-Prefix:true

  • mIsRetransformer:如果在javaagent的MANIFEST.MF文件里定义了Can-Retransform-Classes:true,将会设置mRetransformEnvironment的mIsRetransformer为true。

2.5.1在启动时加载instrument agent

正如前面“概述”里提到的方式,就是启动时加载instrument agent,具体过程都在InvocationAdapter.cAgent_OnLoad方法里,这里简单描述下过程:

  • 创建并初始化JPLISAgent
  • 监听VMInit事件,在vm初始化完成之后做下面的事情:
  • 创建InstrumentationImpl对象
  • 监听ClassFileLoadHook事件
  • 调用InstrumentationImpl的loadClassAndCallPremain方法,在这个方法里会调用javaagent里MANIFEST.MF里指定的Premain-Class类的premain方法
    解析javaagent里MANIFEST.MF里的参数,并根据这些参数来设置JPLISAgent里的一些内容

2.5.2 在运行时加载instrument agent

上面会通过JVM的attach机制来请求目标JVM加载对应的agent,过程大致如下:

  • 创建并初始化JPLISAgent
  • 解析javaagent里MANIFEST.MF里的参数
  • 创建InstrumentationImpl对象
  • 监听ClassFileLoadHook事件
  • 调用InstrumentationImpl的loadClassAndCallAgentmain方法,在这个方法里会调用javaagent里MANIFEST.MF里指定的Agent-Class类的agentmain方法

在运行时加载instrument agent大致按照如下方式进行:

VirtualMachine vm = VirtualMachine.attach(pid);  (根据PID获取 运行被监控的JVM实例)
vm.loadAgent(agentPath, agentArgs); (通知该JVM去加载agent)

3.JDWP协议介绍

JDWP 是 Java Debug Wire Protocol 的缩写,它定义了调试器(debugger)和被调试的 Java 虚拟机(target vm)之间的通信协议。

这里首先要说明一下 debugger 和 target vm。Target vm 中运行着我们希望要调试的程序,它与一般运行的 Java 虚拟机没有什么区别,只是在启动时加载了 ==Agent JDWP ==从而具备了调试功能。而 debugger 就是我们熟知的调试器,它向运行中的 target vm 发送命令来获取 target vm 运行时的状态和控制 Java 程序的执行。Debugger 和 target vm 分别在各自的进程中运行,他们之间的通信协议就是 JDWP

JDWP 与其他许多协议不同,它仅仅定义了数据传输的格式,但并没有指定具体的传输方式。这就意味着一个 JDWP 的实现可以不需要做任何修改就正常工作在不同的传输方式上(在 JDWP 传输接口中会做详细介绍)。

JDWP 是语言无关的。理论上我们可以选用任意语言实现 JDWP。然而我们注意到,在 JDWP 的两端分别是 target vm 和 debugger。Target vm 端,JDWP 模块必须以 Agent library 的形式在 Java 虚拟机启动时加载,并且它必须通过 Java 虚拟机提供的 JVMTI 接口实现各种 debug 的功能,所以必须使用 C/C++ 语言编写。而 debugger 端就没有这样的限制,可以使用任意语言编写,只要遵守 JDWP 规范即可。JDI(Java Debug Interface)就包含了一个 Java 的 JDWP debugger 端的实现(JDI 将在该系列的下一篇文章中介绍),JDK 中调试工具 jdb 也是使用 JDI 完成其调试功能的

在这里插入图片描述

3.1 协议分析

JDWP 大致分为两个阶段:握手和应答。握手是在传输层连接建立完成后,做的第一件事:

Debugger 发送 14 bytes 的字符串“JDWP-Handshake”到 target Java 虚拟机

Target Java 虚拟机回复“JDWP-Handshake”

JDWP 的握手协议
在这里插入图片描述

握手完成,debugger 就可以向 target Java 虚拟机发送命令了。JDWP 是通过命令(command)和回复(reply)进行通信的,这与 HTTP 有些相似。JDWP 本身是无状态的,因此对 command 出现的顺序并不受限制。

JDWP 有两种基本的包(packet)类型:命令包(command packet)和回复包(reply packet)。

Debugger 和 target Java 虚拟机都有可能发送 command packet。Debugger 通过发送 command packet 获取 target Java 虚拟机的信息以及控制程序的执行。Target Java 虚拟机通过发送 command packet 通知 debugger 某些事件的发生,如到达断点或是产生异常。

Reply packet 是用来回复 command packet 该命令是否执行成功,如果成功 reply packet 还有可能包含 command packet 请求的数据,比如当前的线程信息或者变量的值。从 target Java 虚拟机发送的事件消息是不需要回复的。

还有一点需要注意的是,JDWP 是异步的:command packet 的发送方不需要等待接收到 reply packet 就可以继续发送下一个 command packet。

3.2 JDWP 传输接口(Java Debug Wire Protocol Transport Interface)

前面提到 JDWP 的定义是与传输层独立的,但如何使 JDWP 能够无缝的使用不同的传输实现,而又无需修改 JDWP 本身的代码? JDWP 传输接口(Java Debug Wire Protocol Transport Interface)为我们解决了这个问题。

JDWP 传输接口定义了一系列的方法用来定义 JDWP 与传输层实现之间的交互方式。首先传输层的必须以动态链接库的方式实现,并且暴露一系列的标准接口供 JDWP 使用。与 JNI 和 JVMTI 类似,访问传输层也需要一个环境指针(jdwpTransport),通过这个指针可以访问传输层提供的所有方法。

当 JDWP agent 被 Java 虚拟机加载后,JDWP 会根据参数去加载指定的传输层实现(Sun 的 JDK 在 Windows 提供 socket 和 share memory 两种传输方式,而在 Linux 上只有 socket 方式)。传输层实现的动态链接库实现必须暴露 jdwpTransport_OnLoad 接口,JDWP agent 在加载传输层动态链接库后会调用该接口进行传输层的初始化。

3.3 连接管理

连接管理接口主要负责连接的建立和关闭。一个连接为 JDWP 和 debugger 提供了可靠的数据流。Packet 被接收的顺序严格的按照被写入连接的顺序。

连接的建立是双向的,即 JDWP 可以主动去连接 debugger 或者 JDWP 等待 debugger 的连接。对于主动去连接 debugger,需要调用方法 Attach

在连接建立后,会立即进行握手操作,确保对方也在使用 JDWP。因此方法参数中分别指定了 attch 和握手的超时时间。

address 参数因传输层的实现不同而有不同的格式。对于 socket,address 是主机地址;对于 share memory 则是共享内存的名称。

JDWP 等待 debugger 连接的方式,首先需要调用 StartListening 方法。该方法将使 JDWP 处于监听状态,随后调用 Accept 方法接收连接

3.4 JDWP 的命令实现机制

下面将通过讲解一个 JDWP 命令的实例来介绍 JDWP 命令的实现机制。JDWP 作为一种协议,它的作用就在于充当了调试器与 Java 虚拟机的沟通桥梁。通俗点讲,调试器在调试过程中需要不断向 Java 虚拟机查询各种信息,那么 JDWP 就规定了查询的具体方式。

在 Java 6.0 中,JDWP 包含了 18 组命令集合,其中每个命令集合又包含了若干条命令。那么这些命令是如何实现的呢?下面我们先来看一个最简单的 VirtualMachine(命令集合 1)的 Version 命令,以此来剖析其中的实现细节。

因为 JDWP 在整个 JPDA 框架中处于相对底层的位置(在前两篇本系列文章中有具体说明),我们无法在现实应用中来为大家演示 JDWP 的单个命令的执行过程。在这里我们通过一个针对该命令的 Java 测试用例来说明。

CommandPacket packet = new CommandPacket( 
    JDWPCommands.VirtualMachineCommandSet.CommandSetID, 
    JDWPCommands.VirtualMachineCommandSet.VersionCommand); 
         
ReplyPacket reply = debuggeeWrapper.vmMirror.performCommand(packet); 
 
String description = reply.getNextValueAsString(); 
int    jdwpMajor   = reply.getNextValueAsInt(); 
int    jdwpMinor   = reply.getNextValueAsInt(); 
String vmVersion   = reply.getNextValueAsString(); 
String vmName      = reply.getNextValueAsString(); 
 
logWriter.println("description\t= " + description); 
logWriter.println("jdwpMajor\t= " + jdwpMajor); 
logWriter.println("jdwpMinor\t= " + jdwpMinor); 
logWriter.println("vmVersion\t= " + vmVersion); 
logWriter.println("vmName\t\t= " + vmName);

这里先简单介绍一下这段代码的作用。

首先,我们会创建一个 VirtualMachine 的 Version 命令的命令包实例 packet。你可能已经注意到,该命令包主要就是配置了两个参数 : CommandSetID 和 VersionComamnd,它们的值均为 1。表明我们想执行的命令是属于命令集合 1 的命令 1,即 VirtualMachine 的 Version 命令。

然后在 performCommand 方法中我们发送了该命令并收到了 JDWP 的回复包 reply。通过解析 reply,我们得到了该命令的回复信息。

description = Java 虚拟机 version 1.6.0 (IBM J9 VM, J2RE 1.6.0 IBM J9 2.4 Windows XP x86-32 
jvmwi3260sr5-20090519_35743 (JIT enabled, AOT enabled) 
J9VM - 20090519_035743_lHdSMr 
JIT  - r9_20090518_2017 
GC   - 20090417_AA, 2.4) 
jdwpMajor    = 1 
jdwpMinor    = 6 
vmVersion    = 1.6.0 
vmName       = IBM J9 VM

3.5 JDWP 的事件处理机制

前面介绍的 VirtualMachine 的 Version 命令过程非常简单,就是一个查询和信息返回的过程。在实际调试过程中,一个 JDI 的命令往往会有数条这类简单的查询命令参与,而且会涉及到很多更为复杂的命令。要了解更为复杂的 JDWP 命令实现机制,就必须介绍 JDWP 的事件处理机制。

在 Java 虚拟机中,我们会接触到许多事件,例如 VM 的初始化,类的装载,异常的发生,断点的触发等等。那么这些事件调试器是如何通过 JDWP 来获知的呢?下面,我们通过介绍在调试过程中断点的触发是如何实现的,来为大家揭示其中的实现机制。

在这里,我们任意调试一段 Java 程序,并在某一行中加入断点。然后,我们执行到该断点,此时所有 Java 线程都处于 suspend 状态。这是很常见的断点触发过程。为了记录在此过程中 JDWP 的行为,我们使用了一个开启了 trace 信息的 JDWP。虽然这并不是一个复杂的操作,但整个 trace 信息也有几千行。

可见,作为相对底层的 JDWP,其实际处理的命令要比想象的多许多。为了介绍 JDWP 的事件处理机制,我们挑选了其中比较重要的一些 trace 信息来说明:

[RequestManager.cpp:601] AddRequest: event=BREAKPOINT[2], req=48, modCount=1, policy=1 
[RequestManager.cpp:791] GenerateEvents: event #0: kind=BREAKPOINT, req=48 
[RequestManager.cpp:1543] HandleBreakpoint: BREAKPOINT events: count=1, suspendPolicy=1, 
                          location=0 
[RequestManager.cpp:1575] HandleBreakpoint: post set of 1 
[EventDispatcher.cpp:415] PostEventSet -- wait for release on event: thread=4185A5A0, 
                          name=(null), eventKind=2 
 
[EventDispatcher.cpp:309] SuspendOnEvent -- send event set: id=3, policy=1 
[EventDispatcher.cpp:334] SuspendOnEvent -- wait for thread on event: thread=4185A5A0, 
                          name=(null) 
[EventDispatcher.cpp:349] SuspendOnEvent -- suspend thread on event: thread=4185A5A0, 
                          name=(null) 
[EventDispatcher.cpp:360] SuspendOnEvent -- release thread on event: thread=4185A5A0, 
                          name=(null)

首先,调试器需要发起一个断点的请求,这是通过 JDWP 的 Set 命令完成的。在 trace 中,我们看到 AddRequest 就是做了这件事。可以清楚的发现,调试器请求的是一个断点信息(event=BREAKPOINT[2])。

在 JDWP 的实现中,这一过程表现为:在 Set 命令中会生成一个具体的 request, JDWP 的 RequestManager 会记录这个 request(request 中会包含一些过滤条件,当事件发生时 RequestManager 会过滤掉不符合预先设定条件的事件),并通过 JVMTI 的 SetEventNotificationMode 方法使这个事件触发生效(否则事件发生时 Java 虚拟机不会报告)。

当断点发生时,Java 虚拟机就会调用 JDWP 中预先定义好的处理该事件的回调函数。在 trace 中,HandleBreakpoint 就是我们在 JDWP 中定义好的处理断点信息的回调函数。它的作用就是要生成一个 JDWP 端所描述的断点事件来告知调试器(Java 虚拟机只是触发了一个 JVMTI 的消息)。

由于断点的事件在调试器申请时就要求所有 Java 线程在断点触发时被 suspend,那这一步由谁来完成呢?这里要谈到一个细节问题,HandleBreakpoint 作为一个回调函数,其执行线程其实就是断点触发的 Java 线程。

显然,我们不应该由它来负责 suspend 所有 Java 线程。

原因很简单,我们还有一步工作要做,就是要把该断点触发信息返回给调试器。如果我们先返回信息,然后 suspend 所有 Java 线程,这就无法保证在调试器收到信息时所有 Java 线程已经被 suspend。

反之,先 Suspend 了所有 Java 线程,谁来负责发送信息给调试器呢?

为了解决这个问题,我们通过 JDWP 的 EventDispatcher 线程来帮我们 suspend 线程和发送信息实现的过程是,我们让触发断点的 Java 线程来 PostEventSet(trace 中可以看到),把生成的 JDWP 事件放到一个队列中,然后就开始等待。由 EventDispatcher 线程来负责从队列中取出 JDWP 事件,并根据事件中的设定,来 suspend 所要求的 Java 线程并发送出该事件

在这里,我们在事件触发的 Java 线程和 EventDispatcher 线程之间添加了一个同步机制,当事件发送出去后,事件触发的 Java 线程会把 JDWP 中的该事件删除,到这里,整个 JDWP 事件处理就完成了。

3.6 小节

我们在调试 Java 程序的时候,往往需要对虚拟机内部的运行状态进行观察和调试,JDWP Agent 就充当了调试器与 Java 虚拟机的沟通桥梁。它的工作原理简单来说就是对于 JDWP 命令的处理和事件的管理。由于 JDWP 在 JPDA 中处于相对底层的位置,调试器发出一个 JDI 指令,往往要通过很多 JDWP 命令来完成。

4. JDI :Java 调试接口

JDI(Java Debug Interface)是 JPDA 三层模块中最高层的接口,定义了调试器(Debugger)所需要的一些调试接口。基于这些接口,调试器可以及时地了解目标虚拟机的状态,例如查看目标虚拟机上有哪些类和实例等。另外,调试者还可以控制目标虚拟机的执行,例如挂起和恢复目标虚拟机上的线程,设置断点等。

目前,大多数的 JDI 实现都是通过 Java 语言编写的。比如,Java 开发者再熟悉不过的 Eclipse IDE,它的调试工具相信大家都使用过。它的两个插件 org.eclipse.jdt.debug.ui 和 org.eclipse.jdt.debug 与其强大的调试功能密切相关,其中 org.eclipse.jdt.debug.ui 是 Eclipse 调试工具界面的实现,而 org.eclipse.jdt.debug 则是 JDI 的一个完整实现。

4.1 JDI 工作方式

工作方式:

  1. 首先,调试器(Debuuger)通过 Bootstrap 获取唯一的虚拟机管理器。虚拟机管理器将在第一次被调用时初始化可用的链接器。一般地,调试器会默认地采用启动型链接器进行链接。
  2. 调试器调用链接器的 launch () 来启动目标程序,并完成调试器与目标虚拟机的链接
  3. 当链接完成后,调试器与目标虚拟机便可以进行双向通信了。
  4. 调试器将用户的操作转化为调试命令,命令通过链接被发送到前端运行目标程序的虚拟机上;然后,目标虚拟机根据接受的命令做出相应的操作,将调试的结果发回给后端的调试器;最后,调试器可视化数据信息反馈给用户。

从功能上,可以将== JDI 分成三个部分:数据模块,链接模块,以及事件请求与处理模块==。

  • 数据模块负责调试器和目标虚拟机上的数据建模;
  • 链接模块建立调试器与目标虚拟机的沟通渠道;
  • 事件请求与处理模块提供调试器与目标虚拟机交互方式

猜你喜欢

转载自blog.csdn.net/youyou1543724847/article/details/84792301