首页 > 代码库 > 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: 8787

java7是个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 70

SimpleDebugger的运行很奇怪,第一次跑总会报错,重跑一下就好了,是个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写一个调试器