首页 > 代码库 > JPDA(一):使用JDI写一个调试器
JPDA(一):使用JDI写一个调试器
Debugging规范
简单来说,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 | \ |--------------|
具体的就不再赘述,可参考下面的参考资料。
JDK&SA的JNI实现
假设我们要开发一个调试工具,那我们只需要使用front-end的JDI的API就可以完成。JDI的API在com.sun.jdi包下,相当于是JDI的接口规范了。除了JDK自带的实现外,我在HotSpot的SA中也发现了一个实现。他俩的实现分别是在com.sun.tools.jdi包下和sun.jvm.hotspot.jdi包下。
我们来稍微看下在代码结构上这两个实现的差异,diff -u,
blues@ubuntu:~/Projects/jdk$ tree src/share/classes/com/sun/tools/jdi/ > ~/jdi_jdk blues@ubuntu:~/Projects/hotspot/agent$ tree src/share/classes/sun/jvm/hotspot/jdi/ > ~/jdi_sa blues@ubuntu:~$ diff -u jdi_jdk jdi_sa --- jdi_jdk 2015-01-31 01:29:49.995433328 +0800 +++ jdi_sa 2015-01-31 01:32:21.231773459 +0800 @@ -1,5 +1,4 @@ -src/share/classes/com/sun/tools/jdi/ -├── AbstractLauncher.java +src/share/classes/sun/jvm/hotspot/jdi ├── ArrayReferenceImpl.java ├── ArrayTypeImpl.java ├── BaseLineInfo.java @@ -12,80 +11,51 @@ ├── ClassLoaderReferenceImpl.java ├── ClassObjectReferenceImpl.java ├── ClassTypeImpl.java -├── CommandSender.java ├── ConcreteMethodImpl.java ├── ConnectorImpl.java ├── DoubleTypeImpl.java ├── DoubleValueImpl.java -├── EventQueueImpl.java -├── EventRequestManagerImpl.java -├── EventSetImpl.java ├── FieldImpl.java ├── FloatTypeImpl.java ├── FloatValueImpl.java -├── GenericAttachingConnector.java -├── GenericListeningConnector.java ├── IntegerTypeImpl.java ├── IntegerValueImpl.java ├── InterfaceTypeImpl.java -├── InternalEventHandler.java -├── JDWPException.java ├── JNITypeParser.java +├── JVMTIThreadState.java ├── LineInfo.java -├── LinkedHashMap.java ├── LocalVariableImpl.java ├── LocationImpl.java -├── LockObject.java ├── LongTypeImpl.java ├── LongValueImpl.java -├── META-INF -│ └── services -│ ├── com.sun.jdi.connect.Connector -│ └── com.sun.jdi.connect.spi.TransportService ├── MethodImpl.java ├── MirrorImpl.java ├── MonitorInfoImpl.java ├── NonConcreteMethodImpl.java ├── ObjectReferenceImpl.java -├── ObsoleteMethodImpl.java -├── Packet.java -├── PacketStream.java ├── PrimitiveTypeImpl.java ├── PrimitiveValueImpl.java -├── ProcessAttachingConnector.java -├── RawCommandLineLauncher.java ├── ReferenceTypeImpl.java -├── resources -│ ├── jdi_ja.properties -│ ├── jdi.properties -│ └── jdi_zh_CN.properties +├── SACoreAttachingConnector.java +├── SADebugServerAttachingConnector.java +├── SADebugServer.java +├── SAJDIClassLoader.java +├── SAPIDAttachingConnector.java ├── SDE.java ├── ShortTypeImpl.java ├── ShortValueImpl.java -├── SocketAttachingConnector.java -├── SocketListeningConnector.java -├── SocketTransportService.java ├── StackFrameImpl.java ├── StratumLineInfo.java ├── StringReferenceImpl.java -├── SunCommandLineLauncher.java -├── TargetVM.java -├── ThreadAction.java ├── ThreadGroupReferenceImpl.java -├── ThreadListener.java ├── ThreadReferenceImpl.java ├── TypeComponentImpl.java ├── TypeImpl.java ├── ValueContainer.java ├── ValueImpl.java ├── VirtualMachineImpl.java -├── VirtualMachineManagerImpl.java -├── VirtualMachineManagerService.java -├── VMAction.java -├── VMListener.java ├── VMModifiers.java -├── VMState.java ├── VoidTypeImpl.java └── VoidValueImpl.java -3 directories, 85 files +0 directories, 58 files
JDI实例
下面直接通过一个例子来对JDI有一个更直观的认识。写一个简单的调试器,对目标Java进程打断点,并且输出一个目标进程的变量(这其实就已经是我们平常调试程序要做的事情了不是吗^_^)。
目标进程(debugee)的代码,我们调试的目标就是输出满足条件的变量i,
package me.kisimple.just4fun; import java.util.Random; /** * Created by blues on 31/01/15. */ public class Main { public static void main(String[] args) throws Exception{ Random random = new Random(); while(true) { int i = random.nextInt(1000); if(i % 10 == 0) { // System.out.println(i); doNothing(); Thread.sleep(5000); } } } private static void doNothing() { } }
调试器(debuger)的代码,
package me.kisimple.just4fun; import com.sun.jdi.*; import com.sun.jdi.connect.Connector; import com.sun.jdi.event.*; import com.sun.jdi.request.BreakpointRequest; import com.sun.jdi.request.EventRequestManager; import com.sun.tools.jdi.SocketAttachingConnector; import sun.jvm.hotspot.jdi.SAPIDAttachingConnector; import java.util.List; import java.util.Map; /** * Created by blues on 31/01/15. */ public class SimpleDebugger { private static final String CLASS_NAME = "me.kisimple.just4fun.Main"; private static final int LINE = 16; private static final String VAR_NAME = "i"; public static void main(String[] args) { List<Connector> connectors = Bootstrap.virtualMachineManager().allConnectors(); SocketAttachingConnector sac = null; SAPIDAttachingConnector sapac = null; for (Connector connector : connectors) { if(connector instanceof SocketAttachingConnector) { sac = (SocketAttachingConnector)connector; } else if(connector instanceof SAPIDAttachingConnector) { sapac = (SAPIDAttachingConnector)connector; } } boolean sa = System.getProperty("kis.jdi.sa") != null; try { // 1. 使用不同SP提供的connector attach到目标VM上面 VirtualMachine vm = null; if(sa) { if(args.length != 1) { // usage(); return; } if(sapac != null) { Map<String, Connector.Argument> defaultArguments = sapac.defaultArguments(); Connector.Argument pidArg = defaultArguments.get("pid"); // SAPIDAttachingConnector#ARG_PID pidArg.setValue(args[0]); vm = sapac.attach(defaultArguments); System.out.println("using sa..."); System.out.println(vm.allThreads()); } } else { if(sac != null) { Map<String, Connector.Argument> defaultArguments = sac.defaultArguments(); Connector.Argument hostArg = defaultArguments.get("hostname"); // SocketAttachingConnector#ARG_HOST Connector.Argument portArg = defaultArguments.get("port"); // SocketAttachingConnector#ARG_PORT hostArg.setValue("localhost"); portArg.setValue("8787"); vm = sac.attach(defaultArguments); } } // process = vm.process(); if(vm == null) { return; } // 2. 发送请求告诉目标VM我们需要关心哪些事件 EventRequestManager requestManager = vm.eventRequestManager(); List<ReferenceType> referenceTypes = vm.classesByName(CLASS_NAME); List<Location> locations = referenceTypes.get(0).locationsOfLine(LINE); BreakpointRequest breakpointRequest = requestManager.createBreakpointRequest(locations.get(0)); breakpointRequest.enable(); // 3. 事件监听以及处理 EventQueue eventQueue = vm.eventQueue(); boolean disconnected = false; while(true) { if(disconnected) break; EventSet eventSet = eventQueue.remove(); EventIterator eventIterator = eventSet.eventIterator(); while(eventIterator.hasNext()) { Event event = eventIterator.nextEvent(); if(event instanceof BreakpointEvent) { System.out.println("Reach line " + LINE + " of " + CLASS_NAME); BreakpointEvent breakpointEvent = (BreakpointEvent) event; ThreadReference threadReference = breakpointEvent.thread(); StackFrame stackFrame = threadReference.frame(0); LocalVariable localVariable = stackFrame .visibleVariableByName(VAR_NAME); Value value = http://www.mamicode.com/stackFrame.getValue(localVariable);>上面注释已经给出了一般使用JDI API的三个步骤,从中也可以看出其实不同的JDI实现,差异主要在于Connector的实现。Connector是通过ServiceLoader机制来加载的,加载的代码在VirtualMachineManagerImpl中,
protected VirtualMachineManagerImpl() { /* * Create a top-level thread group */ ThreadGroup top = Thread.currentThread().getThreadGroup(); ThreadGroup parent = null; while ((parent = top.getParent()) != null) { top = parent; } mainGroupForJDI = new ThreadGroup(top, "JDI main"); /* * Load the connectors */ ServiceLoader<Connector> connectorLoader = ServiceLoader.load(Connector.class, Connector.class.getClassLoader()); Iterator<Connector> connectors = connectorLoader.iterator(); while (connectors.hasNext()) { Connector connector; try { connector = connectors.next(); } catch (ThreadDeath x) { throw x; } catch (Exception x) { System.err.println(x); continue; } catch (Error x) { System.err.println(x); continue; } addConnector(connector); } /* * Load any transport services and encapsulate them with * an attaching and listening connector. */ ServiceLoader<TransportService> transportLoader = ServiceLoader.load(TransportService.class, TransportService.class.getClassLoader()); Iterator<TransportService> transportServices = transportLoader.iterator(); while (transportServices.hasNext()) { TransportService transportService; try { transportService = transportServices.next(); } catch (ThreadDeath x) { throw x; } catch (Exception x) { System.err.println(x); continue; } catch (Error x) { System.err.println(x); continue; } addConnector(GenericAttachingConnector.create(transportService)); addConnector(GenericListeningConnector.create(transportService)); } // no connectors found if (allConnectors().size() == 0) { throw new Error("no Connectors loaded"); } // Set the default launcher. In order to be compatible // 1.2/1.3/1.4 we try to make the default launcher // "com.sun.jdi.CommandLineLaunch". If this connector // isn't found then we arbitarly pick the first connector. // boolean found = false; List<LaunchingConnector> launchers = launchingConnectors(); for (LaunchingConnector lc: launchers) { if (lc.name().equals("com.sun.jdi.CommandLineLaunch")) { setDefaultConnector(lc); found = true; break; } } if (!found && launchers.size() > 0) { setDefaultConnector(launchers.get(0)); } }那么这两个实现又分别提供了什么Connector呢?看下各自的jar包就知道了,如下图,
tools.jar与sa-jdi.jar中的com.sun.jdi.connect.Connector文件的内容分别是,
# List all Sun provided connector providers here. If there # are providers that are only available on a particular OS # then prefix the line with #[OS] and they will automatically # uncommented by the build process - see make/jpda/front/Makefile. # com.sun.tools.jdi.SunCommandLineLauncher com.sun.tools.jdi.RawCommandLineLauncher com.sun.tools.jdi.SocketAttachingConnector com.sun.tools.jdi.SocketListeningConnector com.sun.tools.jdi.SharedMemoryAttachingConnector com.sun.tools.jdi.SharedMemoryListeningConnector com.sun.tools.jdi.ProcessAttachingConnector# SA JDI Connectors sun.jvm.hotspot.jdi.SACoreAttachingConnector sun.jvm.hotspot.jdi.SADebugServerAttachingConnector sun.jvm.hotspot.jdi.SAPIDAttachingConnector在我们的SimpleDebugger中分别使用了JDK的SocketAttachingConnector与SA的SAPIDAttachingConnector,看类名很容易就知道,一个是通过socket来连接,一个则是通过pid。接下来就跑一跑程序看下。
作为debugee,运行Main需要加上-agentlib:jdwp=transport=dt_socket,server=y,address=8787这个VM option,这样才能通过socket的方式来连接。
$ java7 -agentlib:jdwp=transport=dt_socket,server=y,address=8787 me.kisimple.just4fun.Main Listening for transport dt_socket at address: 8787java7是个alias,alias java7=‘/usr/lib/jvm/java-7-openjdk-i386/bin/java‘。jps看了下pid是5156。
$ java7 me.kisimple.just4fun.SimpleDebugger java.lang.IndexOutOfBoundsException: Index: 0, Size: 0 at java.util.ArrayList.rangeCheck(ArrayList.java:635) at java.util.ArrayList.get(ArrayList.java:411) at java.util.Collections$UnmodifiableList.get(Collections.java:1211) at me.kisimple.just4fun.SimpleDebugger.main(SimpleDebugger.java:71) $ java7 me.kisimple.just4fun.SimpleDebugger Reach line 16 of me.kisimple.just4fun.Main The local variable i is 930 Reach line 16 of me.kisimple.just4fun.Main The local variable i is 350 Reach line 16 of me.kisimple.just4fun.Main The local variable i is 70SimpleDebugger的运行很奇怪,第一次跑总会报错,重跑一下就好了,是个bug?暂时没去管它。
上面的输出是使用了JDK的SocketAttachingConnector的结果。是的我们已经写出了一个很简单的调试器^_^ 下面看下SA的SAPIDAttachingConnector,通过传入了一个system property来选择使用它,当然还要把debugee的pid传过去,
$ java7 -Dkis.jdi.sa=true me.kisimple.just4fun.SimpleDebugger 5156 java.io.IOException at sun.jvm.hotspot.jdi.SAPIDAttachingConnector.attach(SAPIDAttachingConnector.java:126) at me.kisimple.just4fun.SimpleDebugger.main(SimpleDebugger.java:47) Caused by: java.lang.reflect.InvocationTargetException at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57) at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) at java.lang.reflect.Method.invoke(Method.java:606) at sun.jvm.hotspot.jdi.SAPIDAttachingConnector.createVirtualMachine(SAPIDAttachingConnector.java:87) at sun.jvm.hotspot.jdi.SAPIDAttachingConnector.attach(SAPIDAttachingConnector.java:111) ... 1 more Caused by: sun.jvm.hotspot.debugger.DebuggerException: sun.jvm.hotspot.debugger.DebuggerException: Can't attach to the process at sun.jvm.hotspot.debugger.linux.LinuxDebuggerLocal$LinuxDebuggerLocalWorkerThread.execute(LinuxDebuggerLocal.java:152) at sun.jvm.hotspot.debugger.linux.LinuxDebuggerLocal.attach(LinuxDebuggerLocal.java:268) at sun.jvm.hotspot.HotSpotAgent.attachDebugger(HotSpotAgent.java:602) at sun.jvm.hotspot.HotSpotAgent.setupDebuggerLinux(HotSpotAgent.java:565) at sun.jvm.hotspot.HotSpotAgent.setupDebugger(HotSpotAgent.java:338) at sun.jvm.hotspot.HotSpotAgent.go(HotSpotAgent.java:313) at sun.jvm.hotspot.HotSpotAgent.attach(HotSpotAgent.java:157) at sun.jvm.hotspot.jdi.VirtualMachineImpl.createVirtualMachineForPID(VirtualMachineImpl.java:222) ... 7 more Caused by: sun.jvm.hotspot.debugger.DebuggerException: Can't attach to the process at sun.jvm.hotspot.debugger.linux.LinuxDebuggerLocal.attach0(Native Method) at sun.jvm.hotspot.debugger.linux.LinuxDebuggerLocal.access$100(LinuxDebuggerLocal.java:51) at sun.jvm.hotspot.debugger.linux.LinuxDebuggerLocal$1AttachTask.doit(LinuxDebuggerLocal.java:259) at sun.jvm.hotspot.debugger.linux.LinuxDebuggerLocal$LinuxDebuggerLocalWorkerThread.run(LinuxDebuggerLocal.java:127)报错了,猜测是权限问题(对比下JDK的JDI实现则没有权限问题),sudo看看,
$ sudo java7 -Dkis.jdi.sa=true me.kisimple.just4fun.SimpleDebugger 5156 [sudo] password for blues: Exception in thread "main" java.lang.NoClassDefFoundError: com/sun/jdi/Bootstrap at me.kisimple.just4fun.SimpleDebugger.main(SimpleDebugger.java:24) Caused by: java.lang.ClassNotFoundException: com.sun.jdi.Bootstrap at java.net.URLClassLoader$1.run(URLClassLoader.java:366) at java.net.URLClassLoader$1.run(URLClassLoader.java:355) at java.security.AccessController.doPrivileged(Native Method) at java.net.URLClassLoader.findClass(URLClassLoader.java:354) at java.lang.ClassLoader.loadClass(ClassLoader.java:425) at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:308) at java.lang.ClassLoader.loadClass(ClassLoader.java:358) ... 1 more还是不行,环境变量的问题,我是在.bashrc中export CLASSPATH的,sudo之后,应该是一个新的bash。那再换个笨点的方式执行,
$ sudo su root # export CLASSPATH=.:/usr/lib/jvm/java-7-openjdk-i386/jre/lib/rt.jar:/usr/lib/jvm/java-7-openjdk-i386/lib/tools.jar:/usr/lib/jvm/java-7-openjdk-i386/lib/sa-jdi.jar: # /usr/lib/jvm/java-7-openjdk-i386/bin/java -Dkis.jdi.sa=true me.kisimple.just4fun.SimpleDebugger 5156 using sa... [instance of java.lang.Thread(name='JDWP Transport Listener: dt_socket', id=0), instance of java.lang.Thread(name='JDWP Event Helper Thread', id=1), instance of java.lang.Thread(name='Signal Dispatcher', id=2), instance of java.lang.ref.Finalizer$FinalizerThread(name='Finalizer', id=3), instance of java.lang.ref.Reference$ReferenceHandler(name='Reference Handler', id=4), instance of java.lang.Thread(name='main', id=5)] com.sun.jdi.VMCannotBeModifiedException at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method) at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:57) at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45) at java.lang.reflect.Constructor.newInstance(Constructor.java:526) at java.lang.Class.newInstance(Class.java:379) at sun.jvm.hotspot.jdi.VirtualMachineImpl.throwNotReadOnlyException(VirtualMachineImpl.java:281) at sun.jvm.hotspot.jdi.VirtualMachineImpl.eventRequestManager(VirtualMachineImpl.java:524) at me.kisimple.just4fun.SimpleDebugger.main(SimpleDebugger.java:67)前面已经成功输出了debugee当前的所有线程(有点像jstack的功能了)。但是后面报错又是咋回事?看了看官网文档和源码,
The VirtualMachine object returned by this connector‘s attach() method is ‘read-only‘. This means that the method:vm.canBeModified() will return false, and that the JDI client should not call any JDI methods that are defined to throw a VMCannotBeModifiedException in this case.
public EventQueue eventQueue() { throwNotReadOnlyException("VirtualMachine.eventQueue()"); return null; } public EventRequestManager eventRequestManager() { throwNotReadOnlyException("VirtualMachineImpl.eventRequestManager()"); return null; }是的,也就是说SA的JDI实现,对debugee的操作是read only的,没有办法去断点,所以如果要实现debuger还是用JDK的JDI实现吧。
最后,列出上述的两点差异,
- SA的实现有安全校验,需要sudo,而JDK的实现则没有;
- SA的实现对debugee都是只读的,像断点这样的更改操作是不支持的,而JDK的实现则都可以;
参考资料
- 深入Java调试体系系列
- http://docs.oracle.com/javase/8/docs/technotes/guides/jpda/
- http://docs.oracle.com/javase/8/docs/technotes/guides/jpda/conninv.html
JPDA(一):使用JDI写一个调试器