反向Debug了解一下?揭秘Java DEBUG的基本原理
作者:京东云开发者-京东保险 蒋信
链接:https://my.oschina.net/u/4090830/blog/10388524
Debug 的时候,都遇到过手速太快,直接跳过了自己想调试的方法、代码的时候吧……
一旦跳过,可能就得重新执行一遍,准备数据、重新启动可能几分钟就过去了。
好在 IDE 们都很强大,还给你后悔的机会,可以直接删除某个 Stack Frame,直接返回到之前的状态,确切的说是返回到之前的某个 Stack Frame,从而实现让程序 “逆向运行”。
这个 Reset Frame 的能力,可不只是返回上一步,上 N 步也是可以的;选中你期望的那个帧,直接 Reset Frame/Drop Frame,可以直接回到调用栈上的某个栈帧,时间反转!
可惜这玩意也不是那么万能,毕竟是通过 stack pop 这种操作实现,实际上只是给调用栈栈顶的 N 个 frame pop 出来而已,还谈不上是真正的 “反向 DEBUG”。
相比之下, GDB 的 Reverse Debugging 就比较强大,真正的 “反向” DEBUG,逆向运行,实现回放。
所以吧在运行过程中,已经修改的数据,比如引用传递的方法参数、变量,一旦修改肯定回退不了,不然真的成时光机了。
这些乱七八糟的调试功能,都是基于 Java 内置的 Debug 体系来实现的。
JAVA DEBUG 体系
Java 提供了一个完整的 Debug 体系 JPDA(Java Platform Debugger Architecture),这个 JPDA 架构体系由 3 部分组成:
JVM TI- Java VM Tool Interface
JDWP- Java Debug Wire Protocol
JDI- Java Debug Interface
如果结合 IDE 来看,那么一个完整的 Debug 功能看起来就是这个样子:
解释一下这个体系:
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 的能力也不只是断点,它的功能非常多:
左边的功能类,提供了各种乱七八糟的功能,比如我们常用的添加一个断点:
jvmtiError
SetBreakpoint(jvmtiEnv* env,
jmethodID method,
jlocation location)
右边的事件类,可以简单的理解为回调;还是拿断点举例,如果我用上面的 SetBreakpoint 添加了一个断点,那么当执行到该位置时,就会触发这个事件:
void JNICALL
Breakpoint(jvmtiEnv *jvmti_env,
JNIEnv* jni_env,
jthread thread,
jmethodID method,
jlocation location)
远程调试与本地调试
/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 ...
-agentlib:jdwp=transport=dt_socket,address=127.0.0.1:53631
这么一段,这个参数的意思就是,让 jvm 以 53631 暴露 jdwp 协议transport=dt_socket,address=127.0.0.1:53631
,这个 jdwp agent 库以 53631 端口提供了 jdwp 协议的 server。只不过这个 jdwp 是 jvm 内部的库,不需要额外的 so/dylib/dll 文件。agent
参数,随机指定一个端口,然后通过 JDI 接口连接,代码大概长这样(JDI 的 SDK 在 JDK_HOME/lib/tools.jar ):Map<String, Connector.Argument> env = connector.defaultArguments();
env.get("hostname").setValue(hostname);
env.get("port").setValue(port);
VirtualMachine vm = connector.attach(env);
瞅瞅, VirtualMachine 里的就这点方法,能力上比 JVMTI 还是差远了
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();
-agentlib 和 -javaagent
-javaagent:/path/to/jetbrains/debugger-agent.jar
Arthas 的玩法
-javaagent:/tmp/test/arthas-agent.jar
,然后为所欲为。VirtualMachine
就可以实现运行时添加 -javaagent,效果一样:VirtualMachine virtualMachine = VirtualMachine.attach(virtualMachineDescriptor);
virtualMachine.loadAgent(agentPath, agentArgs);
$ 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],
]
#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;
}
总结
Debug 基于 JDPA 体系
IDE 直接接入 JDPA 体系中的 JDI 接口完成
JDI 通过 JDWP 协议,调用远程 VM 的 JVMTI 接口
JDWP 是通过 agentlib 加载的,agentlib 算是一个 native 的静态 “外挂” 接口
javaagent 是 JAVA 层面的 “外挂” 接口,用过 Instrumentation API(Java)实现各种功能,主要用于 APM、Profiler 工具
如果你想,在 javaagent 里调用功能更丰富的 JVMTI 也不是不行。
END
这里有最新开源资讯、软件更新、技术干货等内容
微信扫码关注该文公众号作者