从零实现一个数据结构可视化调试器(一)

从零实现一个数据结构可视化调试器(一)

前言

这个项目是我 3 年前的毕业设计发展过来的------一个 Web 端的数据结构可视化调试器

简单来说,就是你可以在网页上写代码、设断点、单步调试,同时看到链表、二叉树这些数据结构的动态变化。效果大概是这样:

​​

说实话,看了下23年提交的第一个commit还挺怀念的。毕业后当了几年牛马,这个项目已经很久没更新了。趁着还没完全忘光,写篇文章记录一下实现思路。

本文是第一篇,主要聊如何实现在线调试功能。后面还会写数据结构可视化、还有容器隔离等。

项目已开源,欢迎 Star:github.com/fansqz/Show...

一、调试到底是怎么做的?

1.1 程序为什么能被"调试"?

要理解调试,首先要知道一个关键概念:断点 (Breakpoint)

当你在某一行设置断点后,程序执行到那里就会停下来。但程序怎么知道要停下来呢?

秘密在于操作系统提供的一种机制------中断指令 。以 x86 架构为例,调试器会在断点位置插入一条特殊指令 INT 3(机器码 0xCC)。当 CPU 执行到这条指令时,会触发一个软中断,操作系统接管程序控制权,然后通知调试器"嘿,程序停在这里了"。

简化流程:

sql 复制代码
原始代码:                设置断点后:
mov eax, 1              mov eax, 1
add eax, 2   <-- 断点    INT 3        <-- 被替换
mov ebx, eax            mov ebx, eax

1.2 调试器的核心能力

一个调试器本质上就是做这几件事:

  1. 控制程序执行:启动、暂停、继续、终止
  2. 设置断点:让程序在指定位置停下来
  3. 单步执行:一行一行地执行代码
  4. 查看状态:读取变量值、调用栈、内存内容

像 GDB、LLDB 这些调试器,就是专门干这个的工具。它们通过操作系统提供的调试接口来实现上述功能

GDB提供的常用命令:

命令 作用
break 10 在第 10 行设置断点
run 开始执行程序
continue 继续执行到下一个断点
step 单步执行(进入函数)
next 单步执行(跳过函数)
print x 打印变量 x 的值
backtrace 查看调用栈
quit 退出 GDB

二、我的第一版调试器

2.1 最直接的思路

既然 GDB 已经能调试程序了,最简单的做法就是:让后端启动 GDB,把用户的操作翻译成 GDB 命令

架构非常直白:

2.2 接口设计

基于上面的思路,我设计了这样一套 RESTful API:

ruby 复制代码
调试会话管理
POST   /debug/session              创建调试会话(上传代码,返回 sessionId)
DELETE /debug/session/:id          销毁调试会话
调试控制
POST   /debug/:id/breakpoints      设置断点(传入行号数组)
POST   /debug/:id/start            开始执行
POST   /debug/:id/continue         继续执行
POST   /debug/:id/step/in          单步进入
POST   /debug/:id/step/over        单步跳过
POST   /debug/:id/step/out         单步跳出
POST   /debug/:id/terminate        终止程序
状态查询
GET    /debug/:id/variables        获取当前变量
GET    /debug/:id/stacktrace       获取调用栈
事件订阅
GET    /debug/:id/events           SSE 连接,订阅调试事件

为什么需要 event?

你可能会问:查询变量、设置断点这些操作用普通的 HTTP 请求就够了,为什么还需要 event?

关键在于:有些事件需要服务端主动发生的,客户端无法预知

举个例子:

  1. 用户点击"继续执行"
  2. 程序跑起来了...
  3. 不知道什么时候,程序停在了下一个断点(可能是 0.1 秒后,也可能是 10 秒后)

2.2 通信方案:HTTP + SSE

在上面的接口设计中,我们可以看到是需要服务端主动发送event时间给客户端的。我选择的通信方式是 HTTP + SSE 的组合:

  • HTTP:用户的操作(设断点、单步执行等)通过普通 HTTP 请求发送
  • SSE:程序的状态变化(停在断点、输出内容等)通过 SSE 推送给前端

为什么用 SSE 而不是 WebSocket?

SSE WebSocket
方向 服务器 → 客户端(单向) 双向
复杂度 简单,就是 HTTP 长连接 需要握手、心跳
断线重连 浏览器自动处理 需要自己实现

对于调试场景,用户操作是"请求-响应"模式,状态推送是单向的,SSE 完全够用而且更简单。

2.3 前端实现

前端的核心任务:把"调试"这个抽象的过程,变成用户能看懂、能操作的界面

代码编辑器:Monaco Editor

用的是 Monaco Editor,就是 VS Code 同款编辑器。语法高亮、自动补全这些它都自带了,但调试相关的功能需要自己加。

断点功能

用户点击行号左边的空白区域,就能添加/删除断点。实现思路:

  1. 监听编辑器的鼠标点击事件
  2. 判断点击位置是否在行号区域
  3. 如果是,就在那一行添加一个红点装饰(Monaco 叫它 decoration)
  4. 同时把断点信息存起来,调试时发给后端

当前行高亮

程序停在某一行时,需要高亮显示。后端会告诉我们停在第几行,前端就给那一行加个黄色背景:

事件监听:前端怎么知道程序状态变了?

这就要用到前面说的 SSE 了。前端和后端之间有一条"管道",后端随时可以往里面塞消息。前端代码大概长这样(伪代码):

scss 复制代码
// 建立 SSE 连接
const source = new EventSource(`/debug/sse/${debugId}`)
​
// 监听消息
source.onmessage = (event) => {
  const data = JSON.parse(event.data)
​
  if (data.event === 'stopped') {
    highlightLine(data.line)      // 高亮当前行
    fetchVariables()              // 顺便拉取变量值
  }
​
  if (data.event === 'output') {
    appendToConsole(data.output)  // 显示程序输出
  }
​
  if (data.event === 'terminated') {
    clearHighlight()              // 清除高亮
    source.close()                // 关闭连接
  }
}

2.4 沙盒隔离

让用户在服务器上执行代码是非常危险的------如果有人写个死循环吃光 CPU,或者写个 rm -rf /,服务器就完蛋了。

第一版我用的是 Linux 的 cgroupnamespace、seccomp 来做隔离,这一套也是市面上很多沙盒的实现。具体我就只简单说下他们有什么作用:

cgroup(控制组) :限制资源使用

  • 限制 CPU、内存用量、超出限制就杀掉进程

namespace(命名空间) :隔离系统资源

  • PID namespace:让进程看不到其他进程
  • Mount namespace:让进程只能访问指定目录
  • Network namespace:禁止网络访问

seccomp(系统调用过滤) :限制能调用哪些系统调用

  • 只允许 read、write、exit 等基本操作
  • 禁止 execve、socket 等危险操作

坑:seccomp 需要在 fork 后设置,但调试时做不到

seccomp 的正确用法是:

scss 复制代码
*// C 语言实现*
pid_t pid = fork();
if (pid == 0) {
    *// 子进程*
    setup_seccomp();  *// 先设置系统调用过滤*
    execve(user_program);  *// 再执行用户程序*
}

问题来了:

  1. 我的后端是 Go 写的 :Go 的运行时机制导致它不能像 C 那样简单地 fork。Go 的 exec.Command 底层会处理 fork,但没法在 fork 后、exec 前插入 seccomp 设置
  2. 调试时进程是 GDB 启动的 :调试场景下,用户程序是由 GDB 通过 run 命令启动的,根本不经过我们的 fork 流程

这个问题我一直得不到解决,所以我第一版的调试流程并不是非常安全。

结语

到这里,第一版调试器就完成了。这一版能跑起来,但有两个遗留问题:

  • 安全性不够:seccomp 没法用,恶意代码还是有风险
  • 没有可视化:只能看变量值,看不到数据结构的图形化展示

下一篇,我会聊聊如何实现数据结构的可视化

如果觉得有帮助,欢迎点个 Star:github.com/fansqz/Show...

相关推荐
用户298698530141 分钟前
.NET 文档自动化:Spire.Doc 设置奇偶页页眉/页脚的最佳实践
后端·c#·.net
序安InToo32 分钟前
第6课|注释与代码风格
后端·操作系统·嵌入式
xyy12332 分钟前
C#: Newtonsoft.Json 到 System.Text.Json 迁移避坑指南
后端
洋洋技术笔记35 分钟前
Spring Boot Web MVC配置详解
spring boot·后端
JxWang0535 分钟前
VS Code 配置 Markdown 环境
后端
navms38 分钟前
搞懂线程池,先把 Worker 机制啃明白
后端
JxWang0538 分钟前
离线数仓的优化及重构
后端
Nyarlathotep011339 分钟前
gin01:初探gin的启动
后端·go
JxWang0540 分钟前
安卓手机配置通用多屏协同及自动化脚本
后端
JxWang0541 分钟前
Windows Terminal 配置 oh-my-posh
后端