《leetcode-runner》【图解】如何手搓一个debug调试器——调试程序【JDI开发】【万字详解】

前文:
《leetcode-runner》如何手搓一个debug调试器——引言
《leetcode-runner》如何手搓一个debug调试器——架构
《leetcode-runner》如何手搓一个debug调试器——指令系统

本文主要聚焦于如何编写调试程序

背景

在leetcode算法背景下,用户只编写了一个Solution文件。在此基础上,项目该做出哪些额外操作,才能够启动并运行程序呢?

显然,我们需要程序入口。其次,还需要自己实现一套调试程序,控制调试进度

程序入口

方案选择

在Java中,程序入口是main方法。想要启动Solution类,我们存在两种解决方案

  1. 复制用户编写的Solution的所有内容,并动态添加main函数入口,将新得到的内容写入一个全新的文件。最后启动这个全新文件在这里插入图片描述

  2. 原封不动的拷贝用户编写的Solution的所有内容,同时创建一个全新的Main类,在Main类中调用Solution的方法在这里插入图片描述

考虑到规整性,leetcode-runner选择了第二种解决方案

如何将测试案例转换为适配Solution方法入参的代码

模板引入

让我们通过一个简单的case引入解决方案

solution模板

class Solution {
    public int lengthOfLongestSubstring(String s) {
        
    }
}

测试案例
“abcabcbb”

现在的需求是,根据solution模板测试案例,创建一个调用Solution的Main函数

假设我们没有经过程序自动计算生成,让我们手写,Main类长啥样呢?

import java.util.*;

public class Main {
	public static void main(String[] args) {
		Solution solution = new Solution(); // 创建实例
		String a = "abcabcbb"; // 测试案例转换为Java代码
		solution.lengthOfLongestSubstring(a); // 调用核心方法
	}
}

让我们分析一下这段代码哪些是固定的,哪些是需要动态生成的

首先整个Main的结构是死的,创建实例是死的,活的部分是测试案例转换代码方法调用

进一步改写,得到如下模板

import java.util.*;

public class Main {
    public static void main(String[] args) {
        Solution solution = new Solution();
        {{callCode}}
    }
}

{{callCode}},标志着调用代码生成的位置,需要程序动态生成创建

测试案例转换

现在有一个字符串——“abcabcbb”,我们需要将他转换为Java代码。但现在存在一个问题,我们需要将"abcabcbb"赋值给变量,那么变量名叫啥?变量类型是啥?

现在的测试案例非常简单,变量名给个a,类型一眼字符串

那我现在上上强度

请问,[1,2,3]应该转换成什么类型的变量呢?

a. 数组
b. TreeNode
c. ListNode

现在各位读者猜猜,选啥

3、2、1

答案是——都有可能!!!

以leetcode-1367为例

在这里插入图片描述

这样的数组类型既可以表示ListNode,也可以表示TreeNode。当然,int[]数组自然也是可以的

从上述案例可以发现,测试案例转变得到的变量类型并不是恒定的,它取决于上下文——Solution的方法入参

如果入参类型是ListNode,那么[1,2,3]就是ListNode。如果入参类型是TreeNode,那么[1,2,3]就是TreeNode。如果入参是int[],那么[1,2,3]就是int[]

因此,我们要明确将测试案例转换成何种类型的变量,就必须要知道入参类型是什么,而这就引出了核心代码分析功能

核心代码分析

所谓核心代码分析,就是对于leetcode提供的片段代码,识别出关键信息

在leetcode-runner项目中,核心信息有方法名所有的入参类型,并通过AnalysisResult封装分析结果

CodeAnalyzer是代码分析器,用于分析核心代码片段,返回分析的结果

至于如何分析核心代码,需要利用正则这项技术

这里我将提供leetcode-runner部分源码

/**
 * Java代码分析器
 *
 * @author feigebuge
 * @email 2508020102@qq.com
 */
public class JavaCodeAnalyzer extends AbstractCodeAnalyzer{

    public JavaCodeAnalyzer(Project project) {
        super(project);
    }
    /*
        (\w+) 捕获组, 匹配字母数字下划线
     */
    private static final String methodPattern = "public\\s+.*\\s+(\\w+)\\s*\\(([^)]*)\\)";
    private static final Pattern pattern  = Pattern.compile(methodPattern);

    public AnalysisResult analyze(String code) {
        LogUtils.simpleDebug(code);
        // 正则表达式匹配方法签名
        Matcher matcher = pattern.matcher(code);

        if (matcher.find()) {
            String methodName = matcher.group(1);  // 获取方法名
            String parameters = matcher.group(2);  // 获取参数列表

            // 解析参数类型
            List<String> parameterTypes = new ArrayList<>();
            String[] parametersArray = parameters.split("\\s*,\\s*");
            for (String param : parametersArray) {
                // 提取类型部分
                String[] parts = param.split("\\s+");
                if (parts.length > 1) {
                    parameterTypes.add(parts[0].trim()); // 只获取类型
                }
            }

            return new AnalysisResult(methodName, parameterTypes.toArray(new String[0]));
        }

        throw new DebugError("代码片段分析错误! 无法匹配任何有效信息\n code = " + code);
    }
}

在代码中,利用到正则的匹配组的功能,通过match.group方法,匹配得到不同的类型

举个例子

public String dfs(int a, int b)

这行代码将会被regex的group匹配到两组内容

group 1:dfs
group 2:int a, int b

group 1匹配得到方法名——dfs
group 2匹配得到括号内部的所有内容,接下来只需要按照逗号进行分割,取第一个符号就可以得到入参类型

通过分析结果,将测试案例转换为Java代码

先上UML,再解释
请添加图片描述

在leetcode-runner中,负责将测试案例转换为对应代码的类是TestcaseConvertor,但我目前写的毕竟是Java 调试器,自然而然的,负责这块的类就是JavaTestcaseConvertor,后文将称呼他为JTC

在JTC处理测试案例时,会将测试案例方法入参类型进行匹配,然后统一交给ConvertorFactory,根据不同的入参类型生成不同的VariableConvertor

VariableConvertor负责将测试案例转换成不同类型的代码。比如IntArrayConvertor,负责将输入转换为int[]变量;IntConvertor,负责将输入转为为int变量

流程汇总

现在,我们将上文介绍的类进行汇总,得到整体流程

在项目中,已经存在Main.template,具体内容如下

import java.util.*;

public class Main {
    public static void main(String[] args) {
        Solution solution = new Solution();
        {{callCode}}
    }
}

现在,我们需要做的是将测试案例转换成代码 + 创建实例调用代码,将代码填入{{callCode}}内部

整套流程如下

请添加图片描述

对应到leetcode-runner代码,就长这样
在这里插入图片描述

tip:

  • autoAnalyze()方法是对analyze(String)做出的封装,他会自动获取核心代码,并传递给analyze方法
  • autoConvert()方法是对convert(String testcase)方法做出的封装,autoConvert()会自动获取代码片段,并传递给convert(String testcase)方法

JDI层次

Mirror

JDI(Java Debug Interface),是一套为了debug调试获取目标JVM运行状态的接口

通过JDI定义的接口,我们可以驱动目标JVM调试目标代码,同时获取JVM执行的状态信息,以及目标代码的数据信息

在JDI开发中,最最核心的是VirtualMachie类,他封装了正在执行debug的JVM的所有信息。为了和执行调试程序的JVM做出区分,执行debug的JVM我们称为TargetVM

在这里插入图片描述

这里,需要进行一个区分,在JDI开发中,VirtualMachine是TargetVM的镜像——Mirror

在JDI开发过程中,所有的操作都会如实的作用到TargetVM,就像镜子那样,你一动,我就动。因此,在JDI的包下,所有类都是Mirror的子类,换句话说,Mirror是顶级父类

在这里插入图片描述

VirtualMachine

连接

总共有两种连接方式

  1. 连接正在运行的程序,并返回目标VM的镜像
  2. 启动一个应用程序并连接返回目标VM的镜像

这两者的区别是:方法1可以是远程连接,方法2必须是本地连接

对于方法一,连接需要地址端口信息,因此方法一在debug调试时,可以远程连接。JDI在电脑A,目标VM在电脑B

对于方法二,JDI提供的接口同时负责启动连接。因为启动的功能交由JDI,因此只能是本地连接

leetcode-runner提供了两种连接方式,笔者将提供部分连接代码

方法一

private void debugRemotely() {
    startVMService();
    connectVM();
}

private void startVMService() {
    this.port = DebugUtils.findAvailablePort();
    LogUtils.simpleDebug("get available port : " + this.port);

    String cdCmd = "cd " + env.getFilePath();
    String startCmd = String.format("%s -agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=%d -cp %s %s",
            env.getJava(), port, env.getFilePath(), "Main");

    String combinedCmd = "cmd /c " + cdCmd + " & " + startCmd;

    LogUtils.simpleDebug(combinedCmd);

    try {
        Process exec = Runtime.getRuntime().exec(combinedCmd);
        getRunInfo(exec);
    } catch(InterruptedException ignored) {
    } catch (Exception e) {
        throw new DebugError(e.toString(), e);
    }
}

/**
 * 连接VM, 开始debug
 */
private void connectVM() {
    // 创建连接
    VirtualMachineManager vmm = Bootstrap.virtualMachineManager();
    AttachingConnector connector = getConnector(vmm);

    // 配置调试连接信息
    Map<String, Connector.Argument> arguments = connector.defaultArguments();
    arguments.get("port").setValue(String.valueOf(port));

    // 连接到目标 JVM
    VirtualMachine vm = null;
    // 3次连接尝试
    int tryCount = 1;
    do {
        try {
            DebugUtils.simpleDebug("第 " + tryCount + " 次连接, 尝试中...", project);
            vm = connector.attach(arguments);
            DebugUtils.simpleDebug("连接成功", project);
            break;
        } catch (IOException | IllegalConnectorArgumentsException e) {
            DebugUtils.simpleDebug("连接失败: " + e, project);
            try {
                Thread.sleep(1000);
            } catch (InterruptedException ignored) {
            }
        }
        tryCount++;
    } while (tryCount <= 3);

    if (vm == null) {
        LogUtils.warn("vm 连接失败");
        throw new DebugError("vm 连接失败");
    }

    startProcessEvent(vm);
}

方法二

/**
 * 本地断点启动
 */
private void debugLocally() {
    // 获取 LaunchingConnector
    LaunchingConnector connector = Bootstrap.virtualMachineManager().defaultConnector();

    // 配置启动参数
    Map<String, Connector.Argument> arguments = connector.defaultArguments();
    arguments.get("main").setValue("Main"); // 替换为你的目标类全路径
    arguments.get("options").setValue("-classpath " + env.getFilePath() + " -Dencoding=utf-8"); // 指定类路径
    // fix: 编译jdk和运行jdk不一致问题
    arguments.get("home").setValue(env.getJAVA_HOME());
    arguments.get("vmexec").setValue("java.exe");

    // 启动目标 JVM
    VirtualMachine vm;
    try {
        vm = connector.launch(arguments);
    } catch (IOException | IllegalConnectorArgumentsException | VMStartException e) {
        throw new DebugError("vm启动失败!", e);
    }

    // 捕获目标虚拟机的输出
    captureStream(vm.process().getInputStream(), OutputHelper.STD_OUT);
    captureStream(vm.process().getErrorStream(), OutputHelper.STD_ERROR);

    // 获取当前类的调试信息
    startProcessEvent(vm);
}

EventQueue / EventSet

在介绍本小节内容前,请允许我提个问题

如果TargetVM处理产生各种结果,该如何通知VirtualMachine呢?

或者换个问法,VirtualMachine如何知道TargetVM干了啥,产生了生么结果呢?

这就得引出EventQueue

EventQueue,管理即将到来的TargetVM在debug过程中产生的事件。事件总是会被封装到EventSet当中。EventSet总是由debug程序创建生成,并可以通过EventQueue读取

VirtualMachine如果想要知道TargetVM产生了哪些事件,可以通过VirtualMachine.eventQueue()获得EventQueueEventQueue.remove()得到EventSet。最后通过遍历EventSet可以获得TargetVM即将处理的各种事件

笔者将简化leetcode-runner代码,提供一个基本的处理demo

EventQueue eventQueue = virtualMachine.eventQueue();
while (true) {
    EventSet set = eventQueue.remove();
    
    Iterator<Event> it = set.iterator();

    boolean resumeStoppedApp = false;

    while (it.hasNext()) {
        // 只要有一个事件不resume, 就必须resume
        resumeStoppedApp |= !handleEvent(it.next());
    }

    if (resumeStoppedApp) {
        set.resume();
    }
}

Event

发生在TargetVM,并且调试器非常感兴趣的事件。Event是所有事件的顶级父类,当事件发生,事件实例将会写入EventQueue,等待调试程序处理

Event有很多类型

  • BreakpointEvent
  • ClassPrepareEvent
  • StepEvent

Event中封装了非常多的重要信息

如果某个Event继承了LocatableEvent,那他就拥有了TargetVM执行的位置信息线程引用

这两个对象相当关键,其具体信息将在下一小节介绍

ThreadReference / Location

ThreadReference:目标JVM的线程引用,包含TargetVM当前执行线程的所有信息。我们都知道,代码会在某个线程中执行,在调用方法时,会执行入栈操作。栈中包含局部变量的引用,通过引用可以获取局部变量的实际值

Location:表示目标JVM当暂停线程当前执行到的位置信息

包括

  • sourceName:当前执行位置的源码名称
  • sourcePath:源码路径
  • lineNumber:执行代码行号

Value,Type

Value是TargetVM中,值的镜像。在JDI开发体系中,Value的整个继承图谱巨tm复杂且麻烦

下图是JDI的文档,大体上分,Java所有value镜像可以分为PrimitiveValueObjectReference。前者是基本类型的镜像,后者是对象类型的镜像

在这里插入图片描述

Type,类型的镜像
在这里插入图片描述

ValueType之间的关系,有点像实例,和Class之间的关系

对于对象类型的Value,如果想要获取对象的某个字段的值,需要通过ReferenceType获取Field信息,然后通过ObjectReference.getValue(Field)的方法获得Value

这里贴出一段处理ArrayDeque内部elements属性的代码

    private String handleArrayDeque(ObjectReference objRef, int depth) {
        ReferenceType referenceType = objRef.referenceType();
        Value elements = objRef.getValue(referenceType.fieldByName("elements"));
        ...
    }

EventRequestManager

事件请求管理器,通过管理器,可以向TargetVM发送各种事件请求

eg

  • createStepRequest 创建单步运行请求
  • createBreakpointRequest 创建断点请求
  • createClassPrepareRequest 类准备请求

md,写不动了,今天就这样吧,明天继续补充…

架构

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/954184.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

小米vela系统(基于开源nuttx内核)——openvela开源项目

前言 在 2024 年 12 月 27 日的小米「人车家全生态」合作伙伴大会上&#xff0c;小米宣布全面开源 Vela 操作系统。同时&#xff0c;OpenVela 项目正式上线 GitHub 和 Gitee&#xff0c;采用的是比较宽松的 Apache 2.0 协议&#xff0c;这意味着全球的开发者都可以参与到 Vela…

【 PID 算法 】PID 算法基础

一、简介 PID即&#xff1a;Proportional&#xff08;比例&#xff09;、Integral&#xff08;积分&#xff09;、Differential&#xff08;微分&#xff09;的缩写。也就是说&#xff0c;PID算法是结合这三种环节在一起的。粘一下百度百科中的东西吧。 顾名思义&#xff0c;…

使用 WPF 和 C# 绘制覆盖网格的 3D 表面

此示例展示了如何使用 C# 代码和 XAML 绘制覆盖有网格的 3D 表面。示例使用 WPF 和 C# 将纹理应用于三角形展示了如何将纹理应用于三角形。此示例只是使用该技术将包含大网格的位图应用于表面。 在类级别&#xff0c;程序使用以下代码来定义将点的 X 和 Z 坐标映射到 0.0 - 1.…

为深度学习创建PyTorch张量 - 最佳选项

为深度学习创建PyTorch张量 - 最佳选项 正如我们所看到的&#xff0c;PyTorch张量是torch.Tensor​ PyTorch类的实例。张量的抽象概念与PyTorch张量之间的区别在于&#xff0c;PyTorch张量为我们提供了一个可以在代码中操作的具体实现。 在上一篇文章中&#xff0c;我们看到了…

Linux下源码编译安装Nginx1.24及服务脚本实战

1、下载Nginx [rootlocalhost ~]# wget -c https://nginx.org/download/nginx-1.24.0.tar.gz2、解压 [rootlocalhost ~]# tar xf nginx-1.24.0.tar.gz -C /usr/local/src/3、安装依赖 [rootlocalhost ~]# yum install gcc gcc-c make pcre-devel openssl-devel -y4、 准备 N…

4、dockerfile实现lnmp和elk

dockerfile实现lnmp 使用dockerfile n&#xff1a;nginx&#xff0c;172.111.0.10 m&#xff1a;mysql&#xff0c;172.111.0.20 p&#xff1a;php&#xff0c;172.111.0.30 安装配置nginx 1、准备好nginx和wordpress安装包 2、配置dockerfile 3、配置nginx主配置文件ngin…

一文通透OpenVLA及其源码剖析——基于Prismatic VLM(SigLIP、DinoV2、Llama 2)及离散化动作预测

前言 当对机器人动作策略的预测越来越成熟稳定之后(比如ACT、比如扩散策略diffusion policy)&#xff0c;为了让机器人可以拥有更好的泛化能力&#xff0c;比较典型的途径之一便是基于预训练过的大语言模型中的广泛知识&#xff0c;然后加一个policy head(当然&#xff0c;一开…

《操作系统真象还原》第十三章——磁盘驱动程序

文件系统磁盘创建 创建磁盘 进入bochs安装目录&#xff0c;输入以下命令 ./bin/bximage 然后按照以下步骤创建硬盘 修改硬盘配置 vim boot.disk 添加以下代码行 ata0-slave: typedisk, path"hd80M.img", modeflat,cylinders162,heads16,spt63 完整配置如下 …

快速、可靠且高性价比的定制IP模式提升芯片设计公司竞争力

作者&#xff1a;Karthik Gopal&#xff0c;SmartDV Technologies亚洲区总经理 智权半导体科技&#xff08;厦门&#xff09;有限公司总经理 无论是在出货量巨大的消费电子市场&#xff0c;还是针对特定应用的细分芯片市场&#xff0c;差异化芯片设计带来的定制化需求也在芯片…

v-bind操作class

v-bind操作class 参考文献&#xff1a; Vue的快速上手 Vue指令上 Vue指令下 Vue指令的综合案例 指令的修饰符 文章目录 v-bind操作classv-bind对于样式控制的增强操作class案例(tab导航高亮)操作style操作style案例 结语 博客主页: He guolin-CSDN博客 关注我一起学习&#…

Kubernetes1.28 编译 kubeadm修改证书有效期到 100年.并更新k8s集群证书

文章目录 前言一、资源准备1. 下载对应源码2.安装编译工具3.安装并设置golang 二、修改证书有效期1.修改证书有效期2.修改 CA 证书有效期 三、编译kubeadm四、使用新kubeadm方式1.当部署新集群时,使用该kubeadm进行初始化2.替换现有集群kubeadm操作 前言 kubeadm 默认证书为一…

HarmonyOS NEXT应用开发边学边玩系列:从零实现一影视APP (三、影视搜索页功能实现)

在HarmonyOS NEXT开发环境中&#xff0c;我们可以使用nutpi/axios库来简化网络请求的操作。本文将展示如何使用HarmonyOS NEXT框架和nutpi/axios库&#xff0c;从零开始实现一个简单的影视APP&#xff0c;主要关注影视搜索页的功能实现。 为什么选择nutpi/axios&#xff1f; n…

高级运维:shell练习2

1、需求&#xff1a;判断192.168.1.0/24网络中&#xff0c;当前在线的ip有哪些&#xff0c;并编写脚本打印出来。 vim check.sh #!/bin/bash# 定义网络前缀 network_prefix"192.168.1"# 循环遍历1-254的IP for i in {1..254}; do# 构造完整的IP地址ip"$network_…

好用的php商城源码有哪些?

选择一个优秀的商城工具&#xff0c;能更好地帮助大家建立一个好用的商城系统。目前比较流行的都是开源PHP商城系统&#xff0c;那么现实中都有哪些好用的PHP商城源码值得推荐呢&#xff1f;下面就带大家一起来了解一下。 1.TigShop 【推荐指数】&#xff1a;★★★★★☆ 【推…

Docker Desktop 构建java8基础镜像jdk安装配置失效解决

Docker Desktop 构建java8基础镜像jdk安装配置失效解决 文章目录 1.问题2.解决方法3.总结 1.问题 之前的好几篇文章中分享了在Linux(centOs上)和windows10上使用docker和docker Desktop环境构建java8的最小jre基础镜像&#xff0c;前几天我使用Docker Desktop环境重新构建了一个…

Open FPV VTX开源之嵌入式OSD配置

Open FPV VTX开源之嵌入式OSD配置 1. 源由2. 安装3. 配置步骤一&#xff1a;备份/etc/telemetry.conf步骤二&#xff1a;修改/etc/telemetry.conf步骤三&#xff1a;配置时区步骤四&#xff1a;重启摄像头 4. 实测5. 参考资料 1. 源由 穿越机模拟图传延迟通常在10ms左右。 最…

数据平台浅理解

定义 数据平台架构是指用于收集、存储、处理和分析数据的一系列组件、技术和流程的整体架构设计。它就像是一个复杂的数据生态系统的蓝图&#xff0c;旨在高效地管理数据从产生源头到产生价值的整个生命周期。 主要层次 数据源层 这是数据的起点&#xff0c;包含各种类型的数据…

CSS3的aria-hidden学习

前言 aria-hidden 属性可用于隐藏非交互内容&#xff0c;使其在无障碍 API 中不可见。即当aria-hidden"true" 添加到一个元素会将该元素及其所有子元素从无障碍树中移除&#xff0c;这可以通过隐藏来改善辅助技术用户的体验&#xff1a; 纯装饰性内容&#xff0c;如…

【ArcGIS初学】产生随机点计算混淆矩阵

混淆矩阵&#xff1a;用于比较分类结果和地表真实信息 总体精度(overall accuracy) :指对角线上所有样本的像元数(正确分类的像元数)除以所有像元数。 生产者精度(producers accuracy) &#xff1a;某类中正确分类的像元数除以参考数据中该类的像元数(列方向)&#xff0c;又称…

认识机器学习中的结构风险最小化准则

上一篇文章我们学习了关于经验风险最小化准则&#xff0c;其核心思想是通过最小化训练数据上的损失函数来优化模型参数&#xff0c;从而提高模型在训练集上的表现。但是这也会导致一个问题&#xff0c;经验风险最小化原则很容易导致模型在训练集上错误率很低&#xff0c;但在未…