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分为两步,
- debugger发送14个字节,也就是JDWP-Handshake,给debuggee;
- debuggee发送同样的14个字节回应;
具体的协议细节参考官方文档,这里就不展开了。
还有一点值得说明的就是,JDWP只是规范了数据传输格式,具体的数据传输方式,例如是使用TCP还是UDP,使用哪个端口,这些都是不作约束的。
JDWP Transports
上面看到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;
}
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的实现
最后,其实还没有搞清楚JVMTI是怎么样实现的,也就是HotSpotVM是在什么时候去调用的JVMTI API,这部分恐怕是要深入HotSpotVM的源码当中了,暂且搁置,留待以后再深入研究研究了。
今天就先到这了,have fun la ^_^