java Debug我爱编程

java debug 体系-jdi

2018-07-24  本文已影响48人  链人成长chainerup

      JDI属于JPDA中最上层接口。定义了调试器(Debugger)所需要的一些调试接口。基于这些接口,调试器可以及时地了解目标虚拟机的状态,例如查看目标虚拟机上有哪些类和实例等。另外,调试者还可以控制目标虚拟机的执行,例如挂起和恢复目标虚拟机上的线程,设置断点等。

0、工作方式

      首先,调试器(Debuuger)通过 Bootstrap 获取唯一的虚拟机管理器。
虚拟机管理器将在第一次被调用时初始化可用的链接器。一般地,调试器会默认地采用启动型链接器进行链接。
      然后,调试器调用链接器的 launch () 来启动目标程序,并完成调试器与目标虚拟机的链接。
      当链接完成后,调试器与目标虚拟机便可以进行双向通信了。调试器将用户的操作转化为调试命令,命令通过链接被发送到前端运行目标程序的虚拟机上;然后,目标虚拟机根据接受的命令做出相应的操作,将调试的结果发回给后端的调试器;最后,调试器可视化数据信息反馈给用户。

1、模块划分

      通过上面的描述,我们可以将jdi分成3部分: 数据模块、连接模块、事件处理模块。

1.1 数据模块

      jdi的数据模块,主要就是Mirror机制。Mirror 接口是JDI最底层的接口,JDI中几乎所有其他接口都继承于它。Mirror 机制是将目标虚拟机上的所有数据、类型、域、方法、事件、状态和资源,以及调试器发向目标虚拟机的事件请求等都映射成 Mirror 对象。

例如,在目标虚拟机上,已装载的类被映射成 ReferenceType 镜像,对象实例被映射成 ObjectReference 镜像,基本类型的值(如 float 等)被映射成 PrimitiveValue(如 FloatValue 等)。被调试的目标程序的运行状态信息被映射到 StackFrame 镜像中,在调试过程中所触发的事件被映射成 Event 镜像(如 StepEvent 等),调试器发出的事件请求被映射成 EventRequest 镜像(如 StepRequest 等),被调试的目标虚拟机则被映射成 VirtualMachine 镜像。但是,JDI 并不保证目标虚拟机上的每份信息和资源都只有唯一的镜像与之对应,这是由 JDI 的具体实现所决定的。例如,目标虚拟机上的某个事件有可能存在多个 Event 镜像与之对应,例如 BreakpointEvent 等。

      Mirror 实例或是由调试器创建,或是由目标虚拟机创建,调用 Mirror 实例 virtualMachine() 可以获取其虚拟机信息。该接口提供了一套方法,可以用来直接或间接地获取目标虚拟机上所有的数据和状态信息,也可以挂起、恢复、终止目标虚拟机。

1.2 连接模块

      连接是调试器与目标虚拟机之间交互的渠道,一次连接可以由调试器发起,也可以由被调试的目标虚拟机发起。一个调试器可以连接多个目标虚拟机,但一个目标虚拟机最多只能连接一个调试器。下面的例子中就讲了一种常见的连接方式: 由调试器启动目标虚拟机的连接方式。也可以在虚拟机处于运行状态时,采用attach的方式连接到目标虚拟机(我们平时用的Intellij 用的就是这种方式)。

1.3 事件处理模块

      主要在com.sun.jdi.event 和 com.sun.jdi.request 包中。

      当然了,Debugger发送给Target VM的所有事件请求,不一定Target VM 都感兴趣。因此JDI提供了事件的过滤机制,来删选出最终真正要发送给Target VM的事件。

  • Debugger调用Target VM的 eventQueue() 和 eventRequestManager() 分别获取唯一的 EventQueue 实例和 EventRequestManager 实例.
  • Debugger通过 EventRequestManager 的 createXXXRequest() 创建需要的事件请求,并添加过滤器和设置挂起策略.
  • targetVM 上某个事件触发且匹配上eventRequest , 则将event放入对应的eventSet.
  • targetVM 上的EventQueue 管理这些eventSet, 按照FIFO原则发送给Debugger.
  • Debugger通过第一步获取到的EventQueue实例 获取来自Target VM的事件响应。

一句话概括就是 EventRequest总是由Debugger发向Target VM ,而当请求与目标虚拟机上发生事件匹配,则事件会被归到EventSet中,EventSet会被Target VM的EventQueue所管理,并且按照FIFO原则发送到Debugger

2、一个例子

(1)首先来个测试

public class HelloWorld {
    public static void main(String[] args) {
        String str = "Hello world!";
        System.out.println(str);
    }
}

(2)JDI agent的例子

import java.util.List;
import java.util.Map;
import com.sun.jdi.*;
import com.sun.jdi.connect.*;
import com.sun.jdi.event.*;
import com.sun.jdi.request.*;
/**
 * Created by zhangpeng48 on 2018/7/16.
 */
public class MethodTrace {
    private static VirtualMachine vm;
    private static Process process;
    private static EventRequestManager eventRequestManager;
    private static EventQueue eventQueue;
    private static EventSet eventSet;
    private static boolean vmExit = false;
    //write your own testclass
    private static String className = "HelloWorld";

    public static void main(String[] args) throws Exception {
        System.out.println("begin....");
        launchDebugee();
        registerEvent();

        processDebuggeeVM();

        // Enter event loop
        eventLoop();

        destroyDebuggeeVM();

    }

    public static void launchDebugee() {
        LaunchingConnector launchingConnector = Bootstrap
                .virtualMachineManager().defaultConnector();

        // Get arguments of the launching connector
        Map<String, Connector.Argument> defaultArguments = launchingConnector
                .defaultArguments();
        Connector.Argument mainArg = defaultArguments.get("main");
        Connector.Argument suspendArg = defaultArguments.get("suspend");

        // Set class of main method
        mainArg.setValue(className);
        suspendArg.setValue("true");
        try {
            vm = launchingConnector.launch(defaultArguments);
        } catch (Exception e) {
            // ignore
        }
    }

    public static void processDebuggeeVM() {
        process = vm.process();
    }

    public static void destroyDebuggeeVM() {
        process.destroy();
    }

    public static void registerEvent() {
        // Register ClassPrepareRequest
        eventRequestManager = vm.eventRequestManager();
        MethodEntryRequest entryReq = eventRequestManager.createMethodEntryRequest();

        entryReq.setSuspendPolicy(EventRequest.SUSPEND_EVENT_THREAD);
        entryReq.addClassFilter(className);
        entryReq.enable();

        MethodExitRequest exitReq = eventRequestManager.createMethodExitRequest();
        exitReq.addClassFilter(className);
        exitReq.setSuspendPolicy(EventRequest.SUSPEND_EVENT_THREAD);
        exitReq.enable();
    }

    private static void eventLoop() throws Exception {
        eventQueue = vm.eventQueue();
        while (true) {
            Thread.sleep(10000);
            if (vmExit == true) {
                System.out.println("vmexit");
                break;
            }
            eventSet = eventQueue.remove();
            EventIterator eventIterator = eventSet.eventIterator();
            while (eventIterator.hasNext()) {
                Event event = (Event) eventIterator.next();
                execute(event);
                if (!vmExit) {
                    eventSet.resume();
                }
            }
        }
    }

    private static void execute(Event event) throws Exception {
        if (event instanceof VMStartEvent) {
            System.out.println("VM started");
        } else if (event instanceof MethodEntryEvent) {
            Method method = ((MethodEntryEvent) event).method();
            System.out.printf("Enter -> Method: %s, Signature:%s\n",method.name(),method.signature());
            System.out.printf("\t ReturnType:%s\n", method.returnTypeName());
        } else if (event instanceof MethodExitEvent) {
            Method method = ((MethodExitEvent) event).method();
            System.out.printf("Exit -> method: %s\n",method.name());
        } else if (event instanceof VMDisconnectEvent) {
            vmExit = true;
        }
    }
}

(3)编译与运行
编译测试 HelloWorld :

javac HelloWorld.java

编译 JDI agent:

#注意 classpath 多个引用时,在linux环境使用“:”分割。
javac -classpath $JAVA_HOME/lib/tools.jar:. MethodTrace.java    

运行:

java -classpath $JAVA_HOME/lib/tools.jar:. MethodTrace HelloWorld

结果是这个样子的~

结果
4)demo下载地址
链接: https://pan.baidu.com/s/1bKVKexvWKCh6F0hG4nkRDQ 密码: etk3

3、核心api的分析

源码地址:
hotspot实现: hotspot的实现
JDK自带实现: openjdk 的实现

本文主要是对JDK自带的实现进行分析~

3.1 launchDebugee

launchDebugee

首先获取一个获取虚拟机管理器virtualmachineManager, 然后获取默认的连接器

public List<LaunchingConnector> launchingConnectors() {
        ArrayList var1 = new ArrayList(this.connectors.size());
        Iterator var2 = this.connectors.iterator();

        while(var2.hasNext()) {
            Connector var3 = (Connector)var2.next();
            if(var3 instanceof LaunchingConnector) {
                var1.add((LaunchingConnector)var3);
            }
        }

        return Collections.unmodifiableList(var1);
    }

然后为连接器设置参数 主要是设置main(mainArg.setValue 设置该main对应哪个类), 跟suspend (suspend=y/n 是否在调试客户端建立连接之后启动 VM 。如果是y,则方便调试vm启动过程中的一些步骤。本文设置为true.)
最后 launch ( 启动目标程序,连接调试器(Debuuger)与目标虚拟机(VirtualMachine)) 。主要是创建了一个socket 以及一个VM . 代码如下。

public VirtualMachine launch(Map<String, ? extends Argument> var1) throws IOException, IllegalConnectorArgumentsException, VMStartException {
        // 获取各种参数
            String var3 = this.argument("home", var1).value();
        String var4 = this.argument("options", var1).value();
        String var5 = this.argument("main", var1).value();
        boolean var6 = ((BooleanArgumentImpl)this.argument("suspend", var1)).booleanValue();
        String var7 = this.argument("quote", var1).value();
        String var8 = this.argument("vmexec", var1).value();
        String var9 = null;
        if(var7.length() > 1) {
            throw new IllegalConnectorArgumentsException("Invalid length", "quote");
        } else if(var4.indexOf("-Djava.compiler=") != -1 && var4.toLowerCase().indexOf("-djava.compiler=none") == -1) {
            throw new IllegalConnectorArgumentsException("Cannot debug with a JIT compiler", "options");
        } else {
            // 进入主题
            ListenKey var10;
            String var13;
            if(!this.usingSharedMemory) {
              // 本文分析的是SunCommandLineLauncher ,本默认走这个分支。  下面函数的作用是绑定了一个socket 到 SocketTransportService 
                var10 = this.transportService().startListening();
            } else {
                Random var11 = new Random();
                int var12 = 0;

                while(true) {
                    try {
                        var13 = "javadebug" + String.valueOf(var11.nextInt(100000));
                        var10 = this.transportService().startListening(var13);
                        break;
                    } catch (IOException var18) {
                        ++var12;
                        if(var12 > 5) {
                            throw var18;
                        }
                    }
                }
            }
                        // 获取了socket的地址。
            String var19 = var10.address();
                        // 创建了一个VM.
            VirtualMachine var2;
            try {
                if(var3.length() > 0) {
                    var9 = var3 + File.separator + "bin" + File.separator + var8;
                } else {
                    var9 = var8;
                }

                if(hasWhitespace(var9)) {
                    var9 = var7 + var9 + var7;
                }
                                // 组装参数: transport , address, suspend
                String var20 = "transport=" + this.transport().name() + ",address=" + var19 + ",suspend=" + (var6?'y':'n');
                if(hasWhitespace(var20)) {
                    var20 = var7 + var20 + var7;
                }
                                // 继续组装参数 组装之后的结构类似: "java ${jrePath} -Xdebug -Xrunjdwp:transport=dt_socket,address=local:40023,suspend=y"
                var13 = var9 + ' ' + var4 + ' ' + "-Xdebug " + "-Xrunjdwp:" + var20 + ' ' + var5;
                // 核心。 launcher. VM的创建  下面重点分析一下。
                var2 = this.launch(this.tokenizeCommand(var13, var7.charAt(0)), var19, var10, this.transportService());
            } finally {
                this.transportService().stopListening(var10);
            }

            return var2;
        }
    }

this.transportService().startListening(); 绑定了一个socket 到 SocketTransportService 。 分析如下:

ListenKey startListening(String var1, int var2) throws IOException {
        InetSocketAddress var3;
        if(var1 == null) {
            // 创建InetSocketAddress 里面包含了ip 跟 port , 此处ip为 0.0.0.0 (Returns the InetAddress representing anyLocalAddress)
            var3 = new InetSocketAddress(var2);
        } else {
            var3 = new InetSocketAddress(var1, var2);
        }
                // 创建socket 
        ServerSocket var4 = new ServerSocket();
            // socket 的bind
        var4.bind(var3);
        return new SocketTransportService.SocketListenKey(var4);
    }

this.launch(this.tokenizeCommand(var13, var7.charAt(0)), var19, var10, this.transportService()); 的实现

protected VirtualMachine launch(String[] var1, String var2, ListenKey var3, TransportService var4) throws IOException, VMStartException {
        AbstractLauncher.Helper var5 = new AbstractLauncher.Helper(var1, var2, var3, var4);
        var5.launchAndAccept(); // socket 执行 accept ,connect
        VirtualMachineManager var6 = Bootstrap.virtualMachineManager(); 
        return var6.createVirtualMachine(var5.connection(), var5.process());// 建立一个virtualMachineManager 
    }

createVirtualMachine 中最核心的就是 创建了一个 VirtualMachineImpl。

VirtualMachineImpl(VirtualMachineManager var1, Connection var2, Process var3, int var4) {
        super((VirtualMachine)null);
        this.vm = this;
        this.vmManager = (VirtualMachineManagerImpl)var1;
        this.process = var3;
        this.sequenceNumber = var4;
        this.threadGroupForJDI = new ThreadGroup(this.vmManager.mainGroupForJDI(), "JDI [" + this.hashCode() + "]");
            // 创建一个TargetVM, 这个过程新建了一个线程,并且设置为后台常驻。
        this.target = new TargetVM(this, var2);
        
        EventQueueImpl var5 = new EventQueueImpl(this, this.target);
            // 新增一个 event处理器 线程。 
        new InternalEventHandler(this, var5);
        this.eventQueue = new EventQueueImpl(this, this.target);
            // new一个事件请求管理器
        this.eventRequestManager = new EventRequestManagerImpl(this);
            // 起飞。。。
        this.target.start();

        IDSizes var6;
        try {
            var6 = IDSizes.process(this.vm);
        } catch (JDWPException var8) {
            throw var8.toJDIException();
        }
        // 下面的内容可能跟mirror中提到的各种镜像有关。。。
        this.sizeofFieldRef = var6.fieldIDSize; // 属性相关的镜像
        this.sizeofMethodRef = var6.methodIDSize;  // 方法相关的镜像
        this.sizeofObjectRef = var6.objectIDSize; // 对象实例 相关的镜像
        this.sizeofClassRef = var6.referenceTypeIDSize;  // 类相关的镜像
        this.sizeofFrameRef = var6.frameIDSize;   //   StackFrame 镜像
            
        this.internalEventRequestManager = new EventRequestManagerImpl(this);
        //  createClassPrepareRequest  相关的event
            ClassPrepareRequest var7 = this.internalEventRequestManager.createClassPrepareRequest();
        var7.setSuspendPolicy(0);
        var7.enable();
            //  createClassUnloadRequest  相关的event
        ClassUnloadRequest var9 = this.internalEventRequestManager.createClassUnloadRequest();
        var9.setSuspendPolicy(0);
        var9.enable();
        this.notifyInitCompletion();
    }

当连接完成后,调试器与目标虚拟机便可以进行双向通信了。调试器将用户的操作转化为调试命令,命令通过连接被发送到前端运行目标程序的虚拟机上;然后,目标虚拟机根据接受的命令做出相应的操作,将调试的结果发回给后端的调试器;最后,调试器可视化数据信息反馈给用户。

再补充个知识点: 如果采用jdwp agent, 则建立完连接之后,第一件事情就是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"就是JDWP协议里面规定的。在连接建立之后,发送数据包之前,debugger跟debuggee必须要有一个handshake的过程,handshake分为两步,

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

JDWP协议的细节将在另外一篇文章介绍。

3.2 registerEvent

注册了2个event.

 public void registerEvent() {
        // Register ClassPrepareRequest  这个方法继承了mirrorImpl, mirror机制也是我们后面分析的重点。
        eventRequestManager = vm.eventRequestManager();
        // 创建一个方法进入的event
            MethodEntryRequest entryReq = eventRequestManager.createMethodEntryRequest();
        entryReq.setSuspendPolicy(EventRequest.SUSPEND_EVENT_THREAD);
        entryReq.addClassFilter(className);
        entryReq.enable();

            // 创建一个方法退出的event
        MethodExitRequest exitReq = eventRequestManager.createMethodExitRequest();
        exitReq.addClassFilter(className);
        exitReq.setSuspendPolicy(EventRequest.SUSPEND_EVENT_THREAD);
        exitReq.enable();
    }

3.3 processDebuggeeVM

vm.process(). vm 跑起来。

3.4 eventLoop

事件循环,具体处理在execute.

private void eventLoop() throws Exception {
        eventQueue = vm.eventQueue();
        while (true) {
            Thread.sleep(10000);
            if (vmExit == true) {
                System.out.println("vmexit");
                break;
            }
            eventSet = eventQueue.remove();
            EventIterator eventIterator = eventSet.eventIterator();
            while (eventIterator.hasNext()) {
                Event event = (Event) eventIterator.next();
                execute(event);
                if (!vmExit) {
                    eventSet.resume();
                }
            }
        }
    }

execute: 目前只是简单地输出了各种event的日志。

private void execute(Event event) throws Exception {
        if (event instanceof VMStartEvent) {
            System.out.println("VM started");
        } else if (event instanceof MethodEntryEvent) {
            Method method = ((MethodEntryEvent) event).method();
            System.out.printf("Enter -> Method: %s, Signature:%s\n",method.name(),method.signature());
            System.out.printf("\t ReturnType:%s\n", method.returnTypeName());
        } else if (event instanceof MethodExitEvent) {
            Method method = ((MethodExitEvent) event).method();
            System.out.printf("Exit -> method: %s\n",method.name());
        } else if (event instanceof VMDisconnectEvent) {
            vmExit = true;
        }
    }

3.5 destroyDebuggeeVM

process.destroy();

参考文献

1、JPDA 架构研究20 - JDI的事件请求和处理模块
2、https://www.ibm.com/developerworks/cn/java/j-lo-jpda4/index.html?ca=drs-
3、https://yq.aliyun.com/articles/56?hmsr=toutiao.io&spm=5176.100240.searchblog.18&utm_medium=toutiao.io&utm_source=toutiao.io

上一篇下一篇

猜你喜欢

热点阅读