java debug初探

JPDA、JDI、JDWP傻傻分不清楚

关于Java debug架构,有一堆相关的名词。其中JPDA是整个debug架构的缩写:Java Platform Debugger Architecture,
整个架构可以从JPDA文档最开头了解到:

Components                         Debugger Interfaces

               /    |--------------|
              /     |     VM       |
debuggee ----(      |--------------|  <------- JVMTI - Java VM Tool Interface
              \     |   back-end   |
               \    |--------------|
               /           |
comm channel -(            |  <--------------- JDWP - Java Debug Wire Protocol
               \           |
                    |--------------|
                    | front-end    |
                    |--------------|  <------- JDI - Java Debug Interface
                    |      UI      |
                    |--------------|

从图中可以看出,

  • JDI: Java Debug Interface,作为整个debug架构的客户端API,封装了一些Java API用于debug。包括连接JVM,断点等等功能。
  • JDWP:Java Debug Wire Protocol,它实际是一个传输协议,定义了debug客户端和被debug的JVM之间的通信协议。具体协议参见文档
  • JVMTI:前文已经介绍过,这是JVM提供的扩展接口。

JDI如何工作

JDI定义了一堆接口,用于实现整个Debug功能。JDI接口文档在:https://docs.oracle.com/javase/8/docs/jdk/api/jpda/jdi/index.html

JDI中最核心的的类是VirtualMachine,提供连接目标机器debug端口,以及一堆支持的指令。注意tools.jar中包含2个同名对象,JDI相关的在jdi包中。

JDI使用

public class TempAgent {
    static VirtualMachine vm;
    public static void main(String[] args) throws IOException, IllegalConnectorArgumentsException {
        attach();

        //获取对象
        obtainTempTestObj();
    }

    private static void obtainTempTestObj() {
        List<ReferenceType> referenceTypes = vm.classesByName("TempTest");
        for (ReferenceType referenceType : referenceTypes) {
            List<ObjectReference> instances = referenceType.instances(0L);
            for (ObjectReference instance : instances) {
                System.out.println(instance);
            }
        }
    }

    private static void attach() throws IOException, IllegalConnectorArgumentsException {
        // 一、取得连接器
        VirtualMachineManager vmm = Bootstrap.virtualMachineManager();
        List<AttachingConnector> connectors = vmm.attachingConnectors();
        SocketAttachingConnector sac = null;
        for (AttachingConnector ac : connectors) {
            if (ac instanceof SocketAttachingConnector) {
                sac = (SocketAttachingConnector) ac;
                break;
            }
        }
        if (sac == null) {
            System.out.println("JDI error");
            return;
        }

        // 二、连接到远程虚拟器
        Map<String, Argument> arguments = sac.defaultArguments();
        Connector.Argument hostArg = (Connector.Argument) arguments.get("hostname");
        Connector.Argument portArg = (Connector.Argument) arguments.get("port");

        // hostArg.setValue("127.0.0.1");
        portArg.setValue(String.valueOf(8800));

        vm = sac.attach(arguments);
        vm.process();
    }
}

这里通过JDI接口,实现了通过debug端口连接到目标JVM,并获取一个类的对象引用。这里我们来看下实际获取对象引用的方式。

JDI实现

JDI实现代码在 jdk/src/share/classes/com/sun/tools/jdi/VirtualMachineImpl.java 类中,
对于本文关注功能(获得对象引用),代码在jdk/src/share/classes/com/sun/tools/jdi/ReferenceTypeImpl.java

其中获取引用的代:

public List<ObjectReference> instances(long maxInstances) {
    if (!vm.canGetInstanceInfo()) {
        throw new UnsupportedOperationException(
            "target does not support getting instances");
    }

    if (maxInstances < 0) {
        throw new IllegalArgumentException("maxInstances is less than zero: "
                                          + maxInstances);
    }
    int intMax = (maxInstances > Integer.MAX_VALUE)?
        Integer.MAX_VALUE: (int)maxInstances;
    // JDWP can't currently handle more than this (in mustang)

    try {
        return Arrays.asList(
            (ObjectReference[])JDWP.ReferenceType.Instances.
                    process(vm, this, intMax).instances);
    } catch (JDWPException exc) {
        throw exc.toJDIException();
    }
}

这里可以看见,所有实际操作,都在JDWP这个类中。但是如果直接搜索这个类名,可以发现JDK源码中不包含这个类的源码。
实际这个类是构建时生成的,生成规则在jdk/make/gensrc/GensrcJDWP.gmk中:

$(JDK_OUTPUTDIR)/gensrc/com/sun/tools/jdi/JDWP.java: $(JDWP_SPEC_FILE)
        $(MKDIR) -p $(@D)
        $(MKDIR) -p $(JDK_OUTPUTDIR)/gensrc_jdwp_headers
        $(RM) $@ $(JDK_OUTPUTDIR)/gensrc_jdwp_headers/JDWPCommands.h
        $(ECHO) $(LOG_INFO) Creating JDWP.java and JDWPCommands.h from jdwp.spec
        $(TOOL_JDWPGEN) $< -jdi $@ -include $(JDK_OUTPUTDIR)/gensrc_jdwp_headers/JDWPCommands.h

其中TOOL_JDWPGEN变量在Tools.gmk文件中定义:

TOOL_JDWPGEN = $(JAVA) -cp $(JDK_OUTPUTDIR)/btclasses build.tools.jdwpgen.Main

实际会通过JDWP协议和目标JVM交互。

目标JVM实现

每个JDI接口,基本上都有一个对应的实现。所有JDWP后端实现都在jdk/src/share/back目录中。

对于获取引用的功能,实现在ReferenceTypeImpl.c文件中:

static jboolean
instances(PacketInputStream *in, PacketOutputStream *out)
{
    jint maxInstances;
    jclass clazz;
    JNIEnv *env;

    if (gdata->vmDead) {
        outStream_setError(out, JDWP_ERROR(VM_DEAD));
        return JNI_TRUE;
    }

    env = getEnv();
    clazz = inStream_readClassRef(env, in);
    maxInstances = inStream_readInt(in);
    if (inStream_error(in)) {
        return JNI_TRUE;
    }

    WITH_LOCAL_REFS(env, 1) {
        jvmtiError   error;
        ObjectBatch  batch;

        error = classInstances(clazz, &batch, maxInstances);
        if (error != JVMTI_ERROR_NONE) {
            outStream_setError(out, map2jdwpError(error));
        } else {
            int kk;
            jbyte typeKey;

            (void)outStream_writeInt(out, batch.count);
            if (batch.count > 0) {
                /*
                 * They are all instances of this class and will all have
                 * the same typeKey, so just compute it once.
                 */
                typeKey = specificTypeKey(env, batch.objects[0]);

                for (kk = 0; kk < batch.count; kk++) {
                  jobject inst;

                  inst = batch.objects[kk];
                  (void)outStream_writeByte(out, typeKey);
                  (void)outStream_writeObjectRef(env, out, inst);
                }
            }
            jvmtiDeallocate(batch.objects);
        }
    } END_WITH_LOCAL_REFS(env);

    return JNI_TRUE;
}

这里最核心的函数调用是classInstances,这个函数在util.c文件中。

/* Get instances for one class */
jvmtiError
classInstances(jclass klass, ObjectBatch *instances, int maxInstances)
{
    ClassInstancesData data;
    jvmtiHeapCallbacks heap_callbacks;
    jvmtiError         error;
    jvmtiEnv          *jvmti;

    /* Check interface assumptions */

    if (klass == NULL) {
        return AGENT_ERROR_INVALID_OBJECT;
    }

    if ( maxInstances < 0 || instances == NULL) {
        return AGENT_ERROR_ILLEGAL_ARGUMENT;
    }

    /* Initialize return information */
    instances->count   = 0;
    instances->objects = NULL;

    /* Get jvmti environment to use */
    jvmti = getSpecialJvmti();
    if ( jvmti == NULL ) {
        return AGENT_ERROR_INTERNAL;
    }

    /* Setup data to passed around the callbacks */
    data.instCount    = 0;
    data.maxInstances = maxInstances;
    data.objTag       = (jlong)1;
    data.error        = JVMTI_ERROR_NONE;

    /* Clear out callbacks structure */
    (void)memset(&heap_callbacks,0,sizeof(heap_callbacks));

    /* Set the callbacks we want */
    heap_callbacks.heap_reference_callback = &cbObjectTagInstance;

    /* Follow references, no initiating object, just this class, all objects */
    error = JVMTI_FUNC_PTR(jvmti,FollowReferences)
                 (jvmti, 0, klass, NULL, &heap_callbacks, &data);
    if ( error == JVMTI_ERROR_NONE ) {
        error = data.error;
    }

    /* Get all the instances now that they are tagged */
    if ( error == JVMTI_ERROR_NONE ) {
        error = JVMTI_FUNC_PTR(jvmti,GetObjectsWithTags)
                      (jvmti, 1, &(data.objTag), &(instances->count),
                       &(instances->objects), NULL);
        /* Verify we got the count we expected */
        if ( data.instCount != instances->count ) {
            error = AGENT_ERROR_INTERNAL;
        }
    }
    /* Dispose of any special jvmti environment */
    (void)JVMTI_FUNC_PTR(jvmti,DisposeEnvironment)(jvmti);
    return error;
}

除了开头一些准备工作,这里实际的调用使用了jvmti的FollowReferencesGetObjectsWithTags
两个函数。第一个函数用于在堆中标记期望的对象,第二个函数从堆中将所有做了标记的对象取出来。

因此,实际上JDWP后端,最终通过jvmti实现了一个agent,通过jvmti的API对外提供服务。

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据