之前我们已经通过使用JDI的API写出了一个简单的调试器。那么这些API后面又是隐藏了什么样的实现机制?下面就通过源码来分析下。先贴上博主辛苦整理出来的一张图。
+----------+ | 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 | +----------+
接下来就按上图中的数字序号一步一步讲解下。
这一部分我们在前面的栗子已经说过了,API就是com.sun.jdi包,JDI实现有JDK自带的实现,也有HotSpotSA的实现。这里就不再细说了。
这个Protocol就是Java Debug Wire Protocol,也就是JDI实现用来跟目标VM进行数据传输的格式规范。让我们来看下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分为两步,
具体的协议细节参考官方文档,这里就不展开了。
还有一点值得说明的就是,JDWP只是规范了数据传输格式,具体的数据传输方式,例如是使用TCP还是UDP,使用哪个端口,这些都是不作约束的。
上面看到JDI实现会去跟目标VM建立TCP Socket,那么首先在目标VM就必须有人去监听那个TCP端口,这件事是谁来干的呢?就是由我们接下来要说的JDWP Transports来做的了。
通过官方文档Serviceability in the J2SE Repository可以找到对应的源码,
Technology | Source Location | Binary Location |
---|---|---|
JDWP Transports | jdk/src/share/transport jdk/src/solaris/transport jdk/src/windows/transport |
libdt_socket.so dt_socket.dll,dt_shmem.dll |
JDWP Agent | jdk/src/share/back | libjdwp.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; }
上述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又是如何被调用的呢?这便是下面两层干的事了。
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,例如上述的Agent_OnLoad方法,定义如下,
JNIEXPORT jint JNICALL Agent_OnLoad(JavaVM *vm, char *options, void *reserved);
最后,其实还没有搞清楚JVMTI是怎么样实现的,也就是HotSpotVM是在什么时候去调用的JVMTI API,这部分恐怕是要深入HotSpotVM的源码当中了,暂且搁置,留待以后再深入研究研究了。
原文地址:http://blog.csdn.net/kisimple/article/details/43512725