如何编写属于自己的Java / Scala的调试器

译者:赖辉强  原文地址

在本帖中,我们将探讨Java和Scala的调试器是如何编写和工作的;系统自带的调试器,例如:Windows中的WinDbg或者是Linux/Unix中的gdb,会获取操作系统直接提供给他们的链接入口来启动,从而指导和操作外部程序的状态。工作在操作系统顶部抽象层的Java虚拟机对字节码的调试有独立的处理架构。

这个调试的框架和APIs具有全开源、文档化、可扩展的特点,这意味着你可以轻松毫无顾忌的编写自己的调试器。该框架当前的设计由两大部分构成—JDWP协议和JVMTI API层。其中每个都有一系列使性能最佳的优点和使用案例。(你也可以阅读这篇文档:深入 JAVA 调试体系

JDWP协议

JDWP(Java Debugger Wire Protocol)是用来在调试和被调试程序之间通过二进制信息来传递请求和接收事件的(例如:线程中的状态或者异常的变化),这些活动通常是网络上进行。这个架构背后的理念是在两个程序之间尽可能的解耦。旨在减少由编译器更改目标代码在运行期的执行所带来的海森堡效应(Werner是位德国物理学家,不是你喜欢的那个厨师Werner)。

从目标程序中移除多数调试逻辑操作,对检测被调试的虚拟机中状态的改变会有所帮助(例如:GC or OutOfMemoryErrors),这些逻辑是不会调试本身的。为了更加简便,JDK自带了JDI(java调试接口),该接口提供了全面的调试的协议实现,以及对一个目标虚拟机状态的完备的操作能力,包括:连接、断开、指导、处理。

Eclipse的编译器使用的就是JDWP协议,IDE( Integrated Development Environment )调试JAVA程序时,如果你查看当时传递给该程序的命令行参数,你会发现Eclipse会传递额外的参数(-agentlib:jdwp=transport=dt_socket,…)给程序来启动java虚拟机调试,同时也将确定发送请求和事件的端口。

JVMTI编程接口

一系列的原生API是现代JVM中的第二个关键组件,这些API涵盖了广泛关于JVM操作的领域,其中为人所熟知的是 JVM Tooling Interface (i.e. JVMTI)。与JDWP不同的是,JVMTI设计时提供了一系列C/C++ 版的API和一种为JVM动态地加载预编译的库文件(如:.dull等)的机制,而这些库文件会使用由API提供的命令。

JVMTI的使用方式不同于JDWP,实际上,它是在目标程序内执行编译器。这种方式使调试器同时在性能和稳定性方面改善程序代码更加得心应手。然而,最关键的优势是这样一种能够几乎是实时直接地和JVM交互的能力。

从JVMTI提供了一系列功能强、易入门的API中可以看出,JVMTI乐于去深入探究并分析自身的工作原理以及同通过用该些API所能完成的功能。你可以从JDK自带的JVMTI中获取API标头。

编写调试器类库

编写自己的调试器需要用C++创建本地的操作系统类库。你的主方法应该如下:

JNIEXPORT jint JNICALL Agent_OnLoad(JavaVM *vm,char*options,void*)

当JVM加载调试代理器的同时,它会调用该方法。传递的Java虚拟机指针是至关重要的,它会提供所有你需要跟JVM打交道的砝码。该指针可以从java虚拟机中引入jvmtiEnv类;你可以使用GetEnv方法利用capabilities (特性)和events(事件)的概念与JVMTI层进行交互。

JVMTI 特性

编写调试器时,最关键的一方面是你对在目标程序中的调试器代码的功能有清晰的认识,特别是运行代码的本地调试器类库和运行程序联系紧密时。为了你更好的控制你的调试器去影响代码的执行,因此JVMTI详解中引入了capabilities(特性)的概念。

当编写自己的编译器时,你可以事先通知java虚拟机你一系列打算使用的API命令或者事件(例如:设置断点、中断线程)。这能够使JVM可以预先为这些命令或者事件做好准备,同时,让你更好的掌控调试器运行期的开销。这种方式也使得出自不同制造商的JVM能够以程序设计的方式告诉你那些API的命令可以在整个JVMTI详解中得以支持。

特性的性能是大不相同的。有些特性使用的性能开销较低,但是有些较有意思的特性则是相反,例如:在代码中抛出异常来接收回调的特性—can_generate_exception_events或者是需要加锁来接收回调的特性—can_generate_monitor_events。原因在于这些特性会在 JIT全范围的编译时阻碍JVM优化代码,与此同时,迫使JVM在运行期降到解释模式。

其它一些特性,如:每当设定一个目标对象域时,用来接收通知的特性—can_generate_field_modification_events,会产生更大的开销,导致代码运行极慢。尽管JVM支持同时加载多个本地类库,遗憾是一些 HotSpot的特性,如:用来挂起和唤醒线程的特性—can_suspend,只能每次地被一个类库调用。

当我们搭建Takipi’s production debugger时,我们需攻坚的问题之一是提供类似的特性且不能引起大的开销(在之后的版本中更是这样)。

设置回调。一旦你接收到一系列的特性后,你随即设置好会被JVM调用的回调,这会让你知道实际发生过的操作。每个回调都将会完全地提供关于已经发生过的事件的深层次信息。举个例子,一则异常回调信息会包括抛出异常的字节码位置、线程、异常对象、异常是否将被捕获以及将被捕获的位置。

voidJNICALL ExceptionCallback(jvmtiEnv *jvmti,JNIEnv *jni, jthread thread, jmethodID method,

jlocation location, jobject exception,jmethodID catch_method, jlocation catch_location)

值得注意的是特性的开销通常分为两个部分,第一部分开销来自驱动它工作,因为它需要使JIT编译器不同地编译事务,从而能够访问代码。另外一部分来自当你启用一个回调功能时,此时这会引起JVM在执行期选择低性能的执行路径,通过这些路径,特性可以访问代码,期间压缩和传递重要数据会产生额外的开销

断点和检查。你的编译器能够提供熟知的用来检查在运行期所处的特定状态的特性,如:SetBreakpoint,通知JVM通过某个具体的字节码来中断执行,或者每当某个区域更改时,通过设置SetFieldModificationWatch中断执行。针对这点,你可以使用其它一些补充性的功能,例如:GetStackTrace 和GetThreadInfo ,用来知晓当前你所在代码中的位置并报告当前位置。

大多数JVMTI 的功能都涉及到一些使用抽象处理的类和方法,较为熟知的是jmethodID 和 jclass(如果你曾经编写过java本地接口代码,这是)。其中也提供了额外的一些功能,如:GetMethodName 和 GetClassSignature,来帮助你从类常量池中获取实际的符号名。之后,你就可以使用这些符号名以可读的方式记录为日志文件或者以界面的方式展示它们,就如同日常在IDE中所见的一样。

连接调试器

一旦你已经开始编写调试器类库,你的下一步任务就是将它连接到JVM上,下面是几种连接的方式:

1. 连接JDWP。倘若你编写的是一个以JDWP为基础的调试器,你需要向被调试对象增加一个启动参数,就可以在线上进行调试,该参数的形式如下:agentlib:jdwp=transport=dt_socket,suspend=y,address=localhost:<port>。这些参数详细反映了调试器和目标程序之间传递信息的方式,以及在挂起模式中是否启用被调试对象。

2. 连接JVMTI 类库.通过传递给被调试程序的代理路径命令行,同时指向类库所在硬盘上的位置,此时,JVM将会加载JVMTI类库。

另外一种可行的方式是:将命令行参数追加到全局环境变量JAVA_TOOL_OPTIONS 后面,每个新的JVM会接收该变量,并且该变量的值会自动地追加到现存参数列表之后。

3. 远程连接.还有一种通过使用远程连接API来连接调试器的方式,使用这个简单而功能强大的API能够在没有使用任何命令行参数来开始程序的情况下连接代理器来运行JVM程序。这个的不足在于你不能获取通常本可以获得的特性,如:can_generate_exception_events,因为这些特性只能在虚拟机启动时获取。 

时间: 2024-09-19 09:35:24

如何编写属于自己的Java / Scala的调试器的相关文章

编写 unix和 windows的 Scala 脚本

编写 unix和 windows的 Scala 脚本 今天在看<Scala 编程>的时候看到附录了,里面提到了怎么在 unix 和 windows 下面编写 scala 脚本. 之前我也一直想用 scala 来在 unix 下写一些脚本,代替 shell,因为我对 shell 说实话不是很熟悉. 先直接给出一个可以正常的运行的例子把: #!/bin/sh exec scala "$0" "$@" !# 1.to(10).foreach(println)

java/scala数组去重??????

问题描述 java/scala数组去重?????? 求教,如何对Array((String,Array(People)))这种类型的数组进行去除重复? 谢谢 解决方案 你要去什么重复,是字典的key还是后面的people 判断重复的依据又是什么,搞清楚这两个问题,才好解决.

唱吧DevOps的落地,微服务CI/CD的范本技术解读----最大的难点并不是实际业务代码的编写,而是服务的监控和调试以及容器的编排

1.业务架构:从单体式到微服务 K歌亭是唱吧的一条新业务线,旨在提供线下便捷的快餐式K歌方式,用户可以在一个电话亭大小的空间里完成K歌体验.K歌亭在客户端有VOD.微信和Web共三个交互入口,业务复杂度较高,如长连接池服务.用户系统服务.商户系统.增量更新服务.ERP等.对于服务端的稳定性要求也很高,因为K歌亭摆放地点不固定,很多场所的运营活动会造成突发流量. 为了快速开发上线,K歌亭项目最初采用的是传统的单体式架构,但是随着时间的推移,需求的迭代速度变得很快,代码冗余变多,经常会出现牵一发动全

jsoup v1.5.1发布 一款Java的HTML解析器

Java 程序在解析 HTML 文档时,相信大家都接触过 htmlparser 这个开源项目,我曾经在 IBM DW 上发表过两篇关于 htmlparser 的文章,分别是:从HTML中攫取你所需的信息 和扩展 HTMLhttp://www.aliyun.com/zixun/aggregation/33959.html">Parser 对自定义标签的处理能力.但现在我已经不再使用 htmlparser 了,原因是 htmlparser 很少更新,但最重要的是有了 jsoup . jsoup

jsoup v1.6.0发布 一款Java的HTML解析器

jsoup 是一款 Java 的HTML 解析器,可直接解析某个URL地址.HTML文本内容.它提供了一套非常省力的API,可通过DOM,CSS以及类似于JQuery的操作方法来取出和操作数据.其他方面的改进请看发行说明. jsoup 1.6.0发行说明: jsoup is a Java library for working with real-world HTML. It provides a very convenient API for extracting and manipulati

java-基于JAVA,身份证阅读器二次开发接口如何调用?

问题描述 基于JAVA,身份证阅读器二次开发接口如何调用? 身份证阅读器已经有了,我现在做了一个网站(JAVA语言开发的),网站后台需要调用身份证信息,录入身份证信息,如何调用这个身份证阅读器的二次开发接口?是身份证阅读器厂商提供二次开发接口吗?还是如何做?没有做过,不知如何下手...求大神帮忙!感激不尽!!!

swing-想用Java开发电子书阅读器,其Swing/AWT控件如何获取当前阅读进度?

问题描述 想用Java开发电子书阅读器,其Swing/AWT控件如何获取当前阅读进度? 想用java开发一个桌面版的电子书同步阅读器.可是能力有限,在获取阅读进度模块遇到了技术瓶颈,不知道如何获取当前进度,望大神们指点一二.我用的是swing里的JTextArea,貌似没有相关方法,只能获取光标位置,但光标又无法随屏幕滚动,所以就纠结了.不知有没有相关的开源项目?或者换用其他语言?还是说有什么巧法?

java 音乐播放器-java设计音乐播放器,如何实现暂停后继续播放?

问题描述 java设计音乐播放器,如何实现暂停后继续播放? 做课程设计,想采用JAVA语言做个音乐播放器 音乐停止后再点播放按钮时总是从开始的位置播放的 求大神们支招如何实现从暂停位置继续播放? 实现过程中应用了第三方JAR包,javazoom.jlgui.*:这个也没多少文档,也不知道里面有实现的方法没 还有如何实现对音量的调控? 解决方案 http://bbs.csdn.net/topics/390328293

多功能电表通信协议调试器里面的转化在java中转化怎么实现?

问题描述 多功能电表通信协议调试器里面的转化在java中转化怎么实现? 将多功能协议表的调试器方法加到java中实现手机超控,里面的转化怎么实现,求大神给讲讲.. 解决方案 可以用java调用外部DLL方式使用,或者直接解析串口数据