内核用户态协同调试设计与实现#
基本介绍#
调试操作系统与调试普通程序的关键区别在于:普通程序只有一份 ELF 符号表,调试器加载后即可完成断点、单步、调用栈查看等操作;而操作系统运行时涉及内核态和用户态,两者使用不同的符号来源:
| 状态 | 符号来源 | 典型代码 |
|---|---|---|
| 内核态 | 内核 ELF | kernel/src/... |
| 用户态 | 用户程序 ELF | user_apps/... |
当 CPU 在内核态运行时,用户程序源码断点不能直接命中;当 CPU 在用户态运行时,内核源码断点也不能简单按普通程序方式处理。OSGDB 的作用就是在内核态和用户态切换时,自动切换符号表和断点组。
调试后端#
整体链路:
VS Code 前端 ←— DAP —→ 调试插件 ←— GDB/MI —→ GDB ←→ QEMU GDB stub / OpenOCD(JTAG + 真实硬件)- DAP(Debug Adapter Protocol):插件与 VS Code 调试界面交互的标准协议
- GDB/MI:插件通过 GDB 的机器接口控制 GDB
- 调试目标:QEMU 的 GDB stub(
-s参数开启,默认监听 1234 端口),或通过 OpenOCD 连接的 JTAG 真实硬件 - 远程 attach 模式:支持不带
qemuPath/qemuArgs的 attach 配置——调试器只负责连接已在运行的 GDB stub,QEMU 需要预先手动启动
内核/用户态协同调试#
1. 状态机#
内核/用户态切换是一个复杂的异步流程:特权级变化、符号表切换、断点组恢复等操作需要按严格顺序执行。为此引入了状态机架构(OSStateMachine),将整个切换流程建模为 4 种状态和 5 种事件的状态转移图;每次状态转移触发对应的动作序列(DebuggerActions,共 8 种动作)。状态机的具体定义见源码。
状态机重构显著提升了代码的可读性、可维护性和可扩展性——新增调试场景时只需扩展状态与动作,而不必修改分散在各处的流程代码。
2. 断点组管理#
断点组是 OSGDB 的核心概念:将用户设置的 GDB 断点按所在的地址空间分组,任意时刻只有当前地址空间对应的断点组生效。
当调试器检测到特权级切换时,自动完成:
- 保存当前地址空间的断点
- 卸载当前符号表,加载目标地址空间的符号表
- 恢复目标地址空间的断点
- 继续执行
整个过程对使用者透明:你只会看到程序停在了下一个断点上,无论它在内核还是在用户程序中。
断点组支持动态创建,因此可以同时调试多个用户态进程。
3. 边界断点#
边界断点用于标记内核态和用户态的切换位置(例如 enter_user、handle_syscall)。设计上经历了一次重要重构:
- 第一版:边界断点是独立模块,实现逻辑与内核符号表耦合,普适性差
- 重构后:边界断点逻辑上独立,但数据结构上作为断点组的属性;支持通过
customRequest动态设置/取消;提供右键菜单操作(行号右键设为边界断点或取消)
针对不同调试目标的适配:
- xv6:
usys.S中有多个ecall指令(多个用户态出口),因此边界断点数据结构支持一个断点组拥有多个边界断点 - 真实硬件(星光 2):DCSR 寄存器不支持修改
stepie字段,边界断点调整为设置在ecall前一条指令(而非 interrupt handler 入口)
在 osgdb 中,边界断点还经历了以下改造:
- 函数名边界断点:支持在
border_breakpoints中通过function字段按函数名设置边界断点——相比按行号设置,函数名方式不随代码行号变动而失效;同时支持按函数名删除,并管理函数名边界断点在断点组切换时的生命周期 - 标记(marker)机制:支持在源码注释中放置标记,扫描识别后自动设置边界/钩子断点
- 切换时序修复:当 PC 已切换到内核后再处理边界断点逻辑;user_to_kernel 边界的检测改用 PC 地址检查,防止误判
4. 钩子断点#
钩子断点是可在触发时执行自定义 JavaScript 函数的断点。核心用途:设在内核的进程调度相关代码处(如 exec),触发时自动获取即将被调度的用户进程名称,函数返回值用于设置 nextBreakpointGroup,使调试器知道接下来加载哪个用户程序的符号表和断点。
钩子断点在 osgdb 中的补充:
- 动态读取 Rust
String变量:钩子函数需要在运行时读取被调试对象的变量(如sys_execve的path参数)。难点在于 RustString的内部结构随 Rust 版本变化——旧版 Rust(rCore 使用)中字符串数据指针的字段路径为String.vec.buf.ptr.pointer.pointer,新版 Rust(StarryOS 使用)则多了inner一层(String.vec.buf.inner.ptr.pointer.pointer),硬编码字段路径只能支持一个 Rust 版本。解决方案是利用 GDB 顺序处理命令的保证:新增captureConsoleOutput方法,用命令 token 圈定interpreter-exec console的 console 输出范围,让 GDB 自己完成结构解析,再正则提取指针与长度、读取内存——自动兼容所有 Rust 版本,且仅需两条命令。备用的 fallback 字段路径方案(新旧版本分别尝试)保留在代码中。该方案在 StarryOS 调试环境中验证通过(成功捕获/usr/bin/env)。 - 钩子切换创建的断点组会自动注入 user_to_kernel 边界断点,保证后续特权级切换正常工作
VS Code 前端交互#
- 断点同步:利用 VS Code 的
debug.addBreakpointsAPI,使调试器自动设置的断点也能在界面中正确显示 - 侧边栏 TreeView:寄存器查看(RISC-V)、内存读写、断点表展示
OSGDB 本身没有 VS Code 可视化面板;Async Inspector 面板属于异步调试功能(async-debug),见异步调试设计与实现。
配置机制#
所有调试行为均通过 launch.json 配置,其中两个映射函数是断点组机制的关键:
filePathToBreakpointGroupNames:文件路径 → 断点组(源码文件属于哪个断点组)breakpointGroupNameToDebugFilePaths:断点组 → 调试文件(断点组对应加载哪个符号文件)
配合 ${workspaceFolder} 等 VS Code 变量替换,同一份配置可在不同机器上使用。