Java Platform Debugger Architecture 实现浅析

Java Platform Debugger Architecture(JPDA)简单来说就是Java提供的一套用于开发Java调试工具的规范,任何的JDK实现都需要实现这个规范。JPDA是一个Architecture,它包括了三个不同层次的规范,如下图,

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

具体的就不再赘述,可参考官方文档

今天我们通过源码来看看JPDA是怎么实现的。先贴上博主辛苦整理出来的一张图。

               +----------+
               | debugger |
               +----------+
                    ||
#1 ----->           || JDI API
                    \/
               +----------+
#2 ----->      | JDI Impl |
               +----------+
                    ||
#3 ----->           || Protocol
                    \/
           +-----------------+
#4 ----->  | JDWP Transports |
           |  (JDWPTI Impl)  | (libdt_socket.so)
           +-----------------+
                    /\
#5 ----->           || JDWPTI API(jdwpTransport.h)
                    ||
             +--------------+
#6 ----->    |  JDWP Agent  |
             | (JVMTI Impl) | (libjdwp.so)
             +--------------+
                    /\
#7 ----->           || JVMTI API(jvmti.h)
                    ||
               +----------+
               | debuggee |
               +----------+

接下来就按上图中的数字序号一步一步讲解下。

JDI API&Impl

假设我们要开发一个调试工具,那我们只需要使用front-end的JDI的API就可以完成。JDI的API在com.sun.jdi包下,相当于是JDI的接口规范了。

有规范就会有实现,JDI的实现有JDK自带的实现,也有HotSpot SA的实现。这里就不展开了。

感兴趣的朋友可以自己开发简单的debugger来熟悉这些API。

Protocol

这个Protocol就是Java Debug Wire Protocol,也就是JDI实现用来跟目标VM进行数据传输的格式规范。让我们来看下JDK的JDI Connector实现,SocketAttachingConnector的源码。当我们调用SocketAttachingConnector#attach时,最后会调用SocketTransportService#attach

    /**
     * Attach to the specified address with optional attach and handshake
     * timeout.
     */
    public Connection attach(String address, long attachTimeout, long handshakeTimeout)
        throws IOException {

        if (address == null) {
            throw new NullPointerException("address is null");
        }
        if (attachTimeout < 0 || handshakeTimeout < 0) {
            throw new IllegalArgumentException("timeout is negative");
        }

        int splitIndex = address.indexOf(':');
        String host;
        String portStr;
        if (splitIndex < 0) {
            host = InetAddress.getLocalHost().getHostName();
            portStr = address;
        } else {
            host = address.substring(0, splitIndex);
            portStr = address.substring(splitIndex+1);
        }

        int port;
        try {
            port = Integer.decode(portStr).intValue();
        } catch (NumberFormatException e) {
            throw new IllegalArgumentException(
                "unable to parse port number in address");
        }


        // open TCP connection to VM
        InetSocketAddress sa = new InetSocketAddress(host, port);
        Socket s = new Socket();
        try {
            s.connect(sa, (int)attachTimeout);
        } catch (SocketTimeoutException exc) {
            try {
                s.close();
            } catch (IOException x) { }
            throw new TransportTimeoutException("timed out trying to establish connection");
        }

        // handshake with the target VM
        try {
            handshake(s, handshakeTimeout);
        } catch (IOException exc) {
            try {
                s.close();
            } catch (IOException x) { }
            throw exc;
        }

        return new SocketConnection(s);
    }

所以就是根据我们传进来的IP跟Port跟目标VM建立了一个TCP Socket了。后续所有的通讯都是基于这个Socket。而JDWP就是在这个Socket上面进行传输的数据格式,看下handshake方法,

    void handshake(Socket s, long timeout) throws IOException {
        s.setSoTimeout((int)timeout);

        byte[] hello = "JDWP-Handshake".getBytes("UTF-8");
        s.getOutputStream().write(hello);

        byte[] b = new byte[hello.length];
        int received = 0;
        while (received < hello.length) {
            int n;
            try {
                n = s.getInputStream().read(b, received, hello.length-received);
            } catch (SocketTimeoutException x) {
                throw new IOException("handshake timeout");
            }
            if (n < 0) {
                s.close();
                throw new IOException("handshake failed - connection prematurally closed");
            }
            received += n;
        }
        for (int i=0; i<hello.length; i++) {
            if (b[i] != hello[i]) {
                throw new IOException("handshake failed - unrecognized message from target VM");
            }
        }

        // disable read timeout
        s.setSoTimeout(0);
    }

发送的"JDWP-Handshake"就是协议里面规定的。在连接建立之后,发送数据包之前,debugger跟debuggee必须要有一个handshake的过程,handshake分为两步,

  1. debugger发送14个字节,也就是JDWP-Handshake,给debuggee;
  2. debuggee发送同样的14个字节回应;

具体的协议细节参考官方文档,这里就不展开了。

还有一点值得说明的就是,JDWP只是规范了数据传输格式,具体的数据传输方式,例如是使用TCP还是UDP,使用哪个端口,这些都是不作约束的。

JDWP Transports

上面看到JDI实现会去跟目标VM建立TCP Socket,那么首先在目标VM就必须有人去监听那个TCP端口,这件事是谁来干的呢?就是由我们接下来要说的JDWP Transports来做的了。

通过官方文档Serviceability in the J2SE Repository可以找到对应的源码,

TechnologySource LocationBinary Location
JDWP Transportsjdk/src/share/transport
jdk/src/solaris/transport
jdk/src/windows/transport
libdt_socket.so
dt_socket.dll,dt_shmem.dll
JDWP Agentjdk/src/share/backlibjdwp.so
jdwp.dll

上面所说的建立TCP连接的代码是在share/transport/socket/socketTransport.c中,

static jdwpTransportError JNICALL
socketTransport_startListening(jdwpTransportEnv* env, const char* address,
                               char** actualAddress)
{
    struct sockaddr_in sa;
    int err;

    memset((void *)&sa,0,sizeof(struct sockaddr_in));
    sa.sin_family = AF_INET;

    /* no address provided */
    if ((address == NULL) || (address[0] == '\0')) {
        address = "0";
    }

    err = parseAddress(address, &sa, INADDR_ANY);
    if (err != JDWPTRANSPORT_ERROR_NONE) {
        return err;
    }

    serverSocketFD = dbgsysSocket(AF_INET, SOCK_STREAM, 0);
    if (serverSocketFD < 0) {
        RETURN_IO_ERROR("socket creation failed");
    }

    err = setOptions(serverSocketFD);
    if (err) {
        return err;
    }

    err = dbgsysBind(serverSocketFD, (struct sockaddr *)&sa, sizeof(sa));
    if (err < 0) {
        RETURN_IO_ERROR("bind failed");
    }

    err = dbgsysListen(serverSocketFD, 1);
    if (err < 0) {
        RETURN_IO_ERROR("listen failed");
    }

    {
        char buf[20];
        int len = sizeof(sa);
        jint portNum;
        err = dbgsysGetSocketName(serverSocketFD,
                               (struct sockaddr *)&sa, &len);
        portNum = dbgsysNetworkToHostShort(sa.sin_port);
        sprintf(buf, "%d", portNum);
        *actualAddress = (*callback->alloc)((int)strlen(buf) + 1);
        if (*actualAddress == NULL) {
            RETURN_ERROR(JDWPTRANSPORT_ERROR_OUT_OF_MEMORY, "out of memory");
        } else {
            strcpy(*actualAddress, buf);
        }
    }

    return JDWPTRANSPORT_ERROR_NONE;
}

JDWPTI API

上述JDWP Transports里的方法都是要提供给目标VM进行调用的,为此,这一层又有一个API规范,JDWP Transport Interface,具体API请查看官方文档说明,还有源码jdwpTransport.h,这里还是举上述建立连接的栗子,API如下,

jdwpTransportError
StartListening(jdwpTransportEnv* env, const char* address, char** actualAddress);

而在socketTransport.c中直接将socketTransport_startListening函数指针赋值给了StartListening,

JNIEXPORT jint JNICALL
jdwpTransport_OnLoad(JavaVM *vm, jdwpTransportCallback* cbTablePtr,
                     jint version, jdwpTransportEnv** result)
{
    if (version != JDWPTRANSPORT_VERSION_1_0) {
        return JNI_EVERSION;
    }
    if (initialized) {
        /*
         * This library doesn't support multiple environments (yet)
         */
        return JNI_EEXIST;
    }
    initialized = JNI_TRUE;
    jvm = vm;
    callback = cbTablePtr;

    /* initialize interface table */
    interface.GetCapabilities = &socketTransport_getCapabilities;
    interface.Attach = &socketTransport_attach;
    interface.StartListening = &socketTransport_startListening;
    interface.StopListening = &socketTransport_stopListening;
    interface.Accept = &socketTransport_accept;
    interface.IsOpen = &socketTransport_isOpen;
    interface.Close = &socketTransport_close;
    interface.ReadPacket = &socketTransport_readPacket;
    interface.WritePacket = &socketTransport_writePacket;
    interface.GetLastError = &socketTransport_getLastError;
    *result = &single_env;

    /* initialized TLS */
    tlsIndex = dbgsysTlsAlloc();
    return JNI_OK;
}

那么这些API又是如何被调用的呢?这便是下面两层干的事了。

JDWP Agent

JVM提供了一个JVMTI机制使得我们可以写一些动态链接库在目标VM里面运行,并与目标VM进行交互。而上述JDWPTI的调用也正是通过这一手段来完成。要运行的动态链接库称为Agent,需要使用-agentlib:<libname>=<options>选项来告诉JVM我们要使用哪些Agent。通常我们都是这样来使用JDWP Agent的,

-agentlib:jdwp=transport=dt_socket,server=y,address=8787

Linux下会使用libjdwp.so这个动态链接库,它的源码在share/back中。

JVM启动的时候会去调用Agent的Agent_OnLoad方法,JDWP Agent对该方法的实现在debugInit.c中,这个方法中会去解析我们传进来的transport=dt_socket,server=y,address=8787,解析的代码在parseOptions中,解析出来的dt_socket会被作为name参数传到loadTransportLibrary方法中,

static void *
loadTransportLibrary(const char *libdir, const char *name)
{
    void *handle;
    char libname[MAXPATHLEN+2];
    char buf[MAXPATHLEN*2+100];
    const char *plibdir;

    /* Convert libdir from UTF-8 to platform encoding */
    plibdir = NULL;
    if ( libdir != NULL ) {
        int  len;

        len = (int)strlen(libdir);
        (void)(gdata->npt->utf8ToPlatform)(gdata->npt->utf,
            (jbyte*)libdir, len, buf, (int)sizeof(buf));
        plibdir = buf;
    }

    /* Construct library name (simple name or full path) */
    dbgsysBuildLibName(libname, sizeof(libname), plibdir, name);
    if (strlen(libname) == 0) {
        return NULL;
    }

    /* dlopen (unix) / LoadLibrary (windows) the transport library */
    handle = dbgsysLoadLibrary(libname, buf, sizeof(buf));
    return handle;
}

而这个dt_socket动态链接库正是我们上面所说的JDWP Transports。在JDWP Agent的Agent_OnLoad方法启动后,会去调用dt_socket动态链接库中的那些JDWPTI API。还是看建立连接的部分,代码在transport_startTransport方法,

jdwpError
transport_startTransport(jboolean isServer, char *name, char *address,
                         long timeout)
{
    jvmtiStartFunction func;
    jdwpTransportEnv *trans;
    char threadName[MAXPATHLEN + 100];
    jint err;
    jdwpError serror;

    /*
     * If the transport is already loaded then use it
     * Note: We're assuming here that we don't support multiple
     * transports - when we do then we need to handle the case
     * where the transport library only supports a single environment.
     * That probably means we have a bag a transport environments
     * to correspond to the transports bag.
     */
    if (transport != NULL) {
        trans = transport;
    } else {
        serror = loadTransport(name, &trans);
        if (serror != JDWP_ERROR(NONE)) {
            return serror;
        }
    }

    if (isServer) {

        char *retAddress;
        char *launchCommand;
        TransportInfo *info;
        jvmtiError error;
        int len;
        char* prop_value;

        info = jvmtiAllocate(sizeof(*info));
        if (info == NULL) {
            return JDWP_ERROR(OUT_OF_MEMORY);
        }
        info->name = jvmtiAllocate((int)strlen(name)+1);
        (void)strcpy(info->name, name);
        info->address = NULL;
        info->timeout = timeout;
        if (info->name == NULL) {
            serror = JDWP_ERROR(OUT_OF_MEMORY);
            goto handleError;
        }
        if (address != NULL) {
            info->address = jvmtiAllocate((int)strlen(address)+1);
            (void)strcpy(info->address, address);
            if (info->address == NULL) {
                serror = JDWP_ERROR(OUT_OF_MEMORY);
                goto handleError;
            }
        }

        info->transport = trans;

        err = (*trans)->StartListening(trans, address, &retAddress);
        if (err != JDWPTRANSPORT_ERROR_NONE) {
            printLastError(trans, err);
            serror = JDWP_ERROR(TRANSPORT_INIT);
            goto handleError;
        }

        /*
         * Record listener address in a system property
         */
        len = (int)strlen(name) + (int)strlen(retAddress) + 2; /* ':' and '\0' */
        prop_value = (char*)jvmtiAllocate(len);
        strcpy(prop_value, name);
        strcat(prop_value, ":");
        strcat(prop_value, retAddress);
        setTransportProperty(getEnv(), prop_value);
        jvmtiDeallocate(prop_value);


        (void)strcpy(threadName, "JDWP Transport Listener: ");
        (void)strcat(threadName, name);

        func = &acceptThread;
        error = spawnNewThread(func, (void*)info, threadName);
        if (error != JVMTI_ERROR_NONE) {
            serror = map2jdwpError(error);
            goto handleError;
        }

        launchCommand = debugInit_launchOnInit();
        if (launchCommand != NULL) {
            serror = launch(launchCommand, name, retAddress);
            if (serror != JDWP_ERROR(NONE)) {
                goto handleError;
            }
        } else {
            if ( ! gdata->quiet ) {
                TTY_MESSAGE(("Listening for transport %s at address: %s",
                    name, retAddress));
            }
        }
        return JDWP_ERROR(NONE);

handleError:
        jvmtiDeallocate(info->name);
        jvmtiDeallocate(info->address);
        jvmtiDeallocate(info);
    } else {
        /*
         * Note that we don't attempt to do a launch here. Launching
         * is currently supported only in server mode.
         */

        /*
         * If we're connecting to another process, there shouldn't be
         * any concurrent listens, so its ok if we block here in this
         * thread, waiting for the attach to finish.
         */
         err = (*trans)->Attach(trans, address, timeout, 0);
         if (err != JDWPTRANSPORT_ERROR_NONE) {
             printLastError(trans, err);
             serror = JDWP_ERROR(TRANSPORT_INIT);
             return serror;
         }

         /*
          * Start the transport loop in a separate thread
          */
         (void)strcpy(threadName, "JDWP Transport Listener: ");
         (void)strcat(threadName, name);

         func = &attachThread;
         err = spawnNewThread(func, (void*)trans, threadName);
         serror = map2jdwpError(err);
    }
    return serror;
}

JVMTI API

JVMTI机制规范了一套标准的API,例如上述的Agent_OnLoad方法,定义如下,

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

具体还是参考官方文档还有源码jvmti.h

JVMTI的实现

最后,其实还没有搞清楚JVMTI是怎么样实现的,也就是HotSpotVM是在什么时候去调用的JVMTI API,这部分恐怕是要深入HotSpotVM的源码当中了,暂且搁置,留待以后再深入研究研究了。

 

今天就先到这了,have fun la ^_^

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值