您好!
欢迎来到京东云开发者社区
登录
首页
博文
课程
大赛
工具
用户中心
开源
首页
博文
课程
大赛
工具
开源
更多
用户中心
开发者社区
>
博文
>
反向 Debug 了解一下?揭秘 Java DEBUG 的基本原理
分享
打开微信扫码分享
点击前往QQ分享
点击前往微博分享
点击复制链接
反向 Debug 了解一下?揭秘 Java DEBUG 的基本原理
ji****
2023-12-21
IP归属:北京
167浏览
> 点赞再看,养成习惯 Debug 的时候,都遇到过手速太快,直接跳过了自己想调试的方法、代码的时候吧…… 一旦跳过,可能就得重新执行一遍,准备数据、重新启动可能几分钟就过去了。 ![Untitled.png](https://s3.cn-north-1.jdcloud-oss.com/shendengbucket1/2023-11-19-12-08oleiHOrAlh8kTie.png) 好在IDE 们都很强大,还给你后悔的机会,可以直接删除某个 Stack Frame,直接返回到之前的状态,确切的说是返回到之前的某个 Stack Frame,从而实现让程序“逆向运行”。 ![Untitled 1.png](https://s3.cn-north-1.jdcloud-oss.com/shendengbucket1/2023-11-19-12-08tZjMjyHSbNdpAu8.png) 这个 Reset Frame 的能力,可不只是返回上一步,上 N 步也是可以的;选中你期望的那个帧,直接Reset Frame/Drop Frame,可以直接回到调用栈上的某个栈帧,时间反转! 可惜这玩意也不是那么万能,毕竟是通过 stack pop 这种操作实现,实际上只是给调用栈栈顶的 N 个 frame pop 出来而已,还谈不上是真正的“反向 DEBUG”。 相比之下, GDB 的 [Reverse Debugging](https://www.sourceware.org/gdb/news/reversible.html) 就比较强大,真正的 “反向” DEBUG,逆向运行,实现回放。 所以吧在运行过程中,已经修改的数据,比如引用传递的方法参数、变量,一旦修改肯定回退不了,不然真的成时光机了。 这些乱七八糟的调试功能,都是基于 Java 内置的 Debug 体系来实现的。 ## JAVA DEBUG 体系 Java 提供了一个完整的 Debug 体系 **JPDA** (Java Platform Debugger Architecture),这个 JPDA 架构体系由 3 部分组成: 1. [JVM TI](https://docs.oracle.com/javase/8/docs/technotes/guides/jvmti/index.html) - Java VM Tool Interface 2. [JDWP](https://docs.oracle.com/javase/8/docs/technotes/guides/jpda/jdwp-spec.html) - Java Debug Wire Protocol 3. [JDI](https://docs.oracle.com/javase/8/docs/jdk/api/jpda/jdi/index.html) - Java Debug Interface 如果结合IDE 来看,那么一个完整的 Debug 功能看起来就是这个样子: ![Untitled 2.png](https://s3.cn-north-1.jdcloud-oss.com/shendengbucket1/2023-11-19-12-08129VuqLlh19HeCExh.png) 解释一下这个体系: JVM TI 是一个 JVM 提供的一个调试接口,提供了一系列控制 JVM 行为的功能,比如分析、调试、监控、线程分析等等。也就是说,这个接口定义了一系列调试分析功能,而 JVM 实现了这个接口,从而提供调试能力。 不过吧,这个接口毕竟是 C++的,调用起来确实不方便,所以Java 还提供了 JDI 这么个 Java 接口。 JDI 接口使用 JDWP 这个私有的应用层协议,通过 TCP 和目标 VM 的 JVMTI 接口进行交互。 也可以把简单这个 JDWP 协议理解为 JSF/Dubbo 协议;相当于 IDE 里通过 JDI 这个 SDK,使用 JDWP 协议调用远程 JVMTI 的 RPC 接口,来传输调试时的各种断点、查看操作。 可能有人会问,搞什么套壳!要什么 JDWP,我直接 JVMTI 调试不是更香,链路越短性能越高! 当然可以,比如 Arthas 里的部分功能,就直接使用了 JVMTI 接口,要什么 JDI!直接 JVMTI 干就完了。 开个玩笑,Arthas 毕竟不是 Debug 工具,人家根本就不用 JDI 接口。而且 JVMTI 的能力也不只是断点,它的功能非常多: ![Untitled 3.png](https://s3.cn-north-1.jdcloud-oss.com/shendengbucket1/2023-11-19-12-0819od43PdyVrEOjtTP.png) 左边的功能类,提供了各种乱七八糟的功能,比如我们常用的添加一个断点: ```c jvmtiError SetBreakpoint(jvmtiEnv* env, jmethodID method, jlocation location) ``` 右边的事件类,可以简单的理解为回调;还是拿断点举例,如果我用上面的 SetBreakpoint 添加了一个断点,那么当执行到该位置时,就会触发这个事件: ```c void JNICALL Breakpoint(jvmtiEnv *jvmti_env, JNIEnv* jni_env, jthread thread, jmethodID method, jlocation location) ``` JVMTI 的功能非常之多,而 JDI 只是实现了部分 JVMTI 的方法,所以某些专业的 Profiler 工具,可能会直接使用 JVMTI,从而实现更丰富的诊断分析功能。 ## 远程调试与本地调试 不知道大家有没有留意过本地 Debug 启动时的日志: ![Untitled 4.png](https://s3.cn-north-1.jdcloud-oss.com/shendengbucket1/2023-11-19-12-08UAIUfwqu8mWHWTH.png) 第一行是隐藏了后半段的启动命令,展开后是这个样子: ```c /path/to/java -agentlib:jdwp=transport=dt_socket,address=127.0.0.1:53631,suspend=y,server=n -javaagent:/path/to/jetbrains/debugger-agent.jar ... ``` 第二行是一个 Connected 日志,意思是使用 socket 连接到远程 VM 的53631端口 上一段说到,IDE 通过 JDI 接口,使用 JDWP 协议和目标 VM 的 JVMTI 交互。这里的 53631 端口,就是目标 JVM 暴露出的 JVM TI 的 server 端口。 而第一行里,IDEA 自动给我们加上了 `-agentlib:jdwp=transport=dt_socket,address=127.0.0.1:53631` 这么一段,这个参数的意思就是,让 jvm 以 53631 暴露 jdwp 协议 小知识,这个 agentlib 可不只是为 jvmti 提供的。它还可以让 JVM 加载其他的 native lib包,直接“外挂”到你的 jvm 上,下面是“外挂”的参数格式: ![Untitled 5.png](https://s3.cn-north-1.jdcloud-oss.com/shendengbucket1/2023-11-19-12-08Vy0912rKBUuOBMye.png) 所以吧,上面的描述其实不太严谨,更专业的说法是: 让 JVM 加载 JDWP 这个 agent 库,参数为`transport=dt_socket,address=127.0.0.1:53631` ,这个 jdwp agent 库以 53631 端口提供了 jdwp 协议的 server。只不过这个 jdwp 是jvm 内部的库,不需要额外的 so/dylib/dll 文件。 如有需要,你完全可以弄个 “datupiao” 的 agentlib,“外挂”到这个 jvm 上,然后在这个 lib 里调用 JVMTI 接口,然后暴露个端口提供服务和远程交互,实现自己的 jdwp! 可能某些老板们注意到了,本地调试还要127.0.0.1走tcp 交互一遍,那远程调试呢? 基于上面的解释,本地调试和远程调试真的没啥区别!或者说,在目前 IDEA/Eclipse 的实现下,不存在本地调试,都是远程!只不过一个是 127.0.0.1,一个是远程的 IP 而已。 在本地调试时,IDEA 会自动给我们的 JVM 增加 `agent` 参数,随机指定一个端口,然后通过 JDI 接口连接,代码大概长这样(JDI 的 SDK 在 JDK_HOME/lib/tools.jar ): ```c Map<String, Connector.Argument> env = connector.defaultArguments(); env.get("hostname").setValue(hostname); env.get("port").setValue(port); VirtualMachine vm = connector.attach(env); ``` 瞅瞅, VirtualMachine 里的就这点方法,能力上比 JVMTI 还是差远了 ```java List<ReferenceType> classesByName(String className); List<ReferenceType> allClasses(); void redefineClasses(Map<? extends ReferenceType, byte[]> classToBytes); List<ThreadReference> allThreads(); void suspend(); void resume(); List<ThreadGroupReference> topLevelThreadGroups(); EventQueue eventQueue(); EventRequestManager eventRequestManager(); VoidValue mirrorOfVoid(); Process process(); ``` 再回来看看 IDEA 中独立的远程调试,配置好之后,红框里的信息会提示你 ,远程的 JVM 需增加这一段启动参数,而且支持多个版本 JDK 的格式,CV 大法就能直接用。 ![Untitled 6.png](https://s3.cn-north-1.jdcloud-oss.com/shendengbucket1/2023-11-19-12-09aZdTrWaGxcoBVtI.png) ## -agentlib 和 -javaagent 有些细心的同学可能发现了,IDEA 默认的启动脚本里,同时配置了 -agentlib 和 -javaagent。 ```bash -javaagent:/path/to/jetbrains/debugger-agent.jar ``` 这个 debugger-agent吧,其实也没干啥事,只是对 JDK 内置的一些线程做了些增强,辅助 IDEA 的 debug 功能,支持一些异步的调试。 ![Untitled 7.png](https://s3.cn-north-1.jdcloud-oss.com/shendengbucket1/2023-11-19-12-09uMEzNf19FBA12z11N.png) agentlib、javaagent 这俩兄弟,定位其实很像,都是加载自定义的代码。 不过区别在于,agentlib 是加载 native lib,需要c/cpp 去写,相当于外挂自己的代码在 jvm 上,可以为所欲为,比如在 agentlib 里调用上面说的 JVMTI 。 而 javaagent 是用 java 写的,可以直接用上层的 Instrumentation API,做一些类的增强转换之类,这也是大多数 APM Agent、Profiler Agent实现的基本原理。 ## Arthas 的玩法 Arthas 的核心入口,其实还是 javaagent,支持静态加载和动态加载两种玩法。 静态没啥好说的,启动脚本里增加一个`-javaagent:/tmp/test/arthas-agent.jar`,然后为所欲为。 ![Untitled 8.png](https://s3.cn-north-1.jdcloud-oss.com/shendengbucket1/2023-11-19-12-09qhDVmrcEXHVntGR.png) 动态的叫 attach,使用 Java 提供的 `VirtualMachine` 就可以实现运行时添加 -javaagent,效果一样: ```java VirtualMachine virtualMachine = VirtualMachine.attach(virtualMachineDescriptor); virtualMachine.loadAgent(agentPath, agentArgs); ``` 这个 Agent 在 JVM 里启动了一个TCP server,用于收发 Arthas Client 的各种 trace、watch 、Dashboard 等指令,然后通过 Instrumentation 增强Class 插入代码、或者直接调用某些 Java API,实现各种功能。 注意到了吗?Arthas 可以直接下载一个 jar 包,java -jar 就能连上。 其实吧,它这个直接启动的 jar 包,是一个 boot 包,启动之后把乱七八糟的 jar 都下载下来。接着动态 attach 的方式,连接到本机指定进程号的 JVM,然后再为所欲为。 在 3.5 版本之后,Arthas 还新增了一个 ****vmtool**** 命令,这个命令可以直接获取内存中的指定对象实例。 ```java $ vmtool --action getInstances --className java.lang.String --limit 10 @String[][ @String[com/taobao/arthas/core/shell/session/Session], @String[com.taobao.arthas.core.shell.session.Session], @String[com/taobao/arthas/core/shell/session/Session], @String[com/taobao/arthas/core/shell/session/Session], @String[com/taobao/arthas/core/shell/session/Session.class], @String[com/taobao/arthas/core/shell/session/Session.class], @String[com/taobao/arthas/core/shell/session/Session.class], @String[com/], @String[java/util/concurrent/ConcurrentHashMap$ValueIterator], @String[java/util/concurrent/locks/LockSupport], ] ``` 直接获取内存对象,这玩意只靠 Instrumentation API 可做不到。Arthas 搞了个骚操作,直接 JNI 调用自定义 lib,用过 cpp 直接调用了 JVMTI 的 API,融合了 Instrumentation 和 JVMTI 的能力,这下是真的为所欲为了! ```c #include <stdio.h> #include <jni.h> #include <jni_md.h> #include <jvmti.h> #include "arthas_VmTool.h" // under target/native/javah/ static jvmtiEnv *jvmti; ... extern "C" JNIEXPORT jobjectArray JNICALL Java_arthas_VmTool_getInstances0(JNIEnv *env, jclass thisClass, jclass klass, jint limit) { jlong tag = getTag(); limitCounter.init(limit); jvmtiError error = jvmti->IterateOverInstancesOfClass(klass, JVMTI_HEAP_OBJECT_EITHER, HeapObjectCallback, &tag); if (error) { printf("ERROR: JVMTI IterateOverInstancesOfClass failed!%u\n", error); return NULL; } jint count = 0; jobject *instances; error = jvmti->GetObjectsWithTags(1, &tag, &count, &instances, NULL); if (error) { printf("ERROR: JVMTI GetObjectsWithTags failed!%u\n", error); return NULL; } jobjectArray array = env->NewObjectArray(count, klass, NULL); //添加元素到数组 for (int i = 0; i < count; i++) { env->SetObjectArrayElement(array, i, instances[i]); } jvmti->Deallocate(reinterpret_cast<unsigned char *>(instances)); return array; } ``` ## 总结 1. Debug 基于 JDPA 体系 1. IDE 直接接入 JDPA 体系中的 JDI 接口完成 2. JDI 通过 JDWP 协议,调用远程 VM 的 JVMTI 接口 3. JDWP 是通过 agentlib 加载的,agentlib 算是一个 native 的静态“外挂”接口 2. javaagent 是 JAVA 层面的“外挂”接口,用过 Instrumentation API(Java)实现各种功能,主要用于APM、Profiler 工具 3. 如果你想,在 javaagent 里调用功能更丰富的 JVMTI 也不是不行。
上一篇:【行云流水线】满足你对工作流编排的一切幻想~skr
下一篇:记录一次RPC服务有损上线的分析过程
ji****
文章数
7
阅读量
1656
作者其他文章
01
GPTs 初体验 - 1 分钟就能创建一个自己的 ChatGPT?
点赞再看,养成习惯就在 11.10 号早上,ChatGPT 已经偷摸的把 GPTs 功能,开放给所有尊贵的 Plus 用户了。随着这波的功能开放,界面也是改了不少。点击左侧的 Explore 或者左下角的用户处,就可以直接进入新的 GPTs 功能:这里可以看到我们自己创建的 GPT,下面呢还有 OpenAI 官方出品的 GPTs :不过这些官方的,目前看起来更像是一个个的提示词包,貌似没啥惊艳的,
01
完蛋!我被 Out of Memory 包围了!
先点赞再看,养成好习惯是极致魅惑、洒脱自由的 Java heap space?是知性柔情、温婉大气的 GC overhead limit exceeded?是纯真无邪、活泼可爱的 Metaspace?如果以上不是你的菜,那还有……刁蛮任性,无迹可寻的 CodeCache!性感火辣、心思细腻的 Direct Memory高贵冷艳,独爱你一人的 OOM Killer!总有一款,能让你钟情!BUG 选择
01
从头到尾说一次 Spring 事务管理(器)
先点赞再看,养成好习惯事务管理,一个被说烂的也被看烂的话题,还是八股文中的基础股之一。本文会从设计角度,一步步的剖析 Spring 事务管理的设计思路(都会设计事务管理器了,还能玩不转?)为什么需要事务管理?先看看如果没有事务管理器的话,如果想让多个操作(方法/类)处在一个事务里应该怎么做:// MethodA:public void methodA(){ Connection connecti
01
别再纠结线程池池大小、线程数量了,哪有什么固定公式
可能很多人都看到过一个线程数设置的理论:CPU 密集型的程序 - 核心数 + 1I/O 密集型的程序 - 核心数 * 2不会吧,不会吧,真的有人按照这个理论规划线程数?线程数和CPU利用率的小测试抛开一些操作系统,计算机原理不谈,说一个基本的理论(不用纠结是否严谨,只为好理解):一个CPU核心,单位时间内只能执行一个线程的指令那么理论上,我一个线程只需要不停的执行指令,就可以跑满一个核心的利用率。
ji****
文章数
7
阅读量
1656
作者其他文章
01
GPTs 初体验 - 1 分钟就能创建一个自己的 ChatGPT?
01
完蛋!我被 Out of Memory 包围了!
01
从头到尾说一次 Spring 事务管理(器)
01
别再纠结线程池池大小、线程数量了,哪有什么固定公式
添加企业微信
获取1V1专业服务
扫码关注
京东云开发者公众号