.vscode 平台实现
本文档按文件说明当前调试方案在 .vscode 目录中的实现方式。
重点不是介绍“如何点击调试”,而是解释这几个文件各自负责什么、为什么要这样拆,以及它们如何共同完成一次完整的本地调试。
当前实现主要由三个文件组成:
.vscode/launch.json.vscode/tasks.json.vscode/session.py
设计分层
这三个文件的职责边界是刻意分开的:
launch.json负责“调试器视角”的配置tasks.json负责“任务编排视角”的配置session.py负责“会话管理视角”的实现
这样分层的原因是:
- VS Code 的调试配置更适合表达“附加到哪里、用什么方式附加、起始断点在哪里”
- VS Code 的任务系统更适合表达“构建和启动的顺序关系”
- QEMU debug 会话的等待、输出接管、状态管理、退出清理更适合落在脚本里实现
如果把这些逻辑全部塞进单一层里,调试入口会更难维护,也更难处理 Linux / Windows 的差异。
launch.json
launch.json 是 VS Code 调试入口的最上层描述。当前每个系统都提供 Main 和 Boot 两类配置。
核心字段
当前预置配置统一采用:
"type": "lldb",
"request": "custom"
这意味着当前调试器使用的是 CodeLLDB,并且通过自定义命令流完成目标创建和远程附加。
initCommands:信号处理与步进控制
每个配置都包含 initCommands,在调试器附加之前执行。所有配置共享的基础设置是:
"initCommands": [
"process handle SIGINT -p false -s false -n false"
]
这条命令告诉 LLDB 不捕获、不停止、不通知 SIGINT 信号。原因是:在 QEMU + GDB stub 场景下,SIGINT 需要透传到被调试目标(例如让内核正确响应 Ctrl+C 中断),而不是被 LLDB 拦截后暂停目标进程。
Axvisor 配置额外包含一条步进过滤规则:
"settings set target.process.thread.step-avoid-regexp ^(core::|alloc::|bitflags::|page_table_generic::)"
这条规则让 LLDB 在单步执行(step over / step into)时自动跳过匹配的 crate 路径。Axvisor 作为 hypervisor,其执行路径会频繁穿过 core::(Rust 核心库)、alloc::(全局分配器)、bitflags::(位标志宏展开)以及页表操作 crate。如果不做步进过滤,开发者按一次 F10 可能会陷入数十个无关帧才回到业务代码。ArceOS 和 StarryOS 当前未启用此规则——它们的调用深度和 crate 依赖模式使得默认步进行为已经可用。
sourceLanguages 字段
所有配置均声明:
"sourceLanguages": ["rust"]
此字段帮助 CodeLLDB 优先使用 Rust 源码级别符号进行断点解析和堆栈展示。如果省略此项,LLDB 在某些混合二进制场景下可能退化为纯地址/反汇编视图,降低调试效率。
调试前后任务
launch.json 不直接负责构建或启动 QEMU,而是通过任务名把这件事交给 tasks.json:
"preLaunchTask": "TGOS: Prepare Axvisor QEMU debug",
"postDebugTask": "TGOS: Stop Axvisor QEMU debug"
这里体现了第一层职责划分:
launch.json只声明“调试前必须准备好什么”- 真正的准备流程在
tasks.json
调试目标与附加方式
每个配置都会显式指定调试目标路径,例如:
"targetCreateCommands": [
"target create ${workspaceFolder}/target/aarch64-unknown-none-softfloat/debug/axvisor",
"target modules load --file ${workspaceFolder}/target/aarch64-unknown-none-softfloat/debug/axvisor --slide 0"
]
随后通过:
"processCreateCommands": [
"gdb-remote 127.0.0.1:1234"
]
附加到 QEMU 暴露出来的 GDB stub。
这里说明 launch.json 只假设两件事已经成立:
- 对应的 debug 二进制已经构建出来
127.0.0.1:1234已经可连接
而这两件事都不是 launch.json 自己保证的,而是由 tasks.json 和 session.py 提前完成。
Main 与 Boot 的区别
launch.json 中 Main / Boot 的差异主要体现在 postRunCommands 上。
例如:
Main更偏向应用或主路径断点Boot更偏向平台入口、runtime 初始化、早期引导断点
也就是说,launch.json 的价值不只是“能附加”,还负责把不同问题类型映射到不同的断点入口。
各系统具体断点位置
| 配置 | 断点策略 | 典型命中位置 |
|---|---|---|
| ArceOS Main | 单个软件断点 + continue | apps/arceos/helloworld/src/main.rs:8 |
| ArceOS Boot | 多个符号/行号断点(不自动 continue) | ax_plat::call_main、axruntime/src/lib.rs:141、main.rs:8 |
| Axvisor Main | 单个软件断点 + continue | os/axvisor/src/main.rs:42 |
| Axvisor Boot | 多个行号断点(不自动 continue) | platforms/axplat-dyn/src/boot.rs:8、axvisor/src/main.rs:42 |
| StarryOS Main | 单个硬件断点 + continue | os/StarryOS/starryos/src/main.rs:12 |
| StarryOS Boot | 混合符号/行号断点(不自动 continue) | ax_plat::call_main、axruntime/src/lib.rs:141、starry_kernel::entry::init、starryos/src/main.rs:12 |
StarryOS 硬件断点
StarryOS Main 配置使用 --hardware true:
"breakpoint set --hardware true --file ... --line 12"
这是因为 StarryOS 在早期引导阶段可能运行在内存权限受限的页面布局上,软件断点(通过写入 0xCC / 0xE7FFFFFF trap 指令实现)不一定能成功写入目标代码页。硬件断点使用 CPU 的调试寄存器(DR0-DR3 on x86, HWBP on AArch64),不需要修改代码内存,因此在任何内存布局下都能可靠命中。ArceOS 和 Axvisor 当前未启用硬件断点——它们的引导阶段内存布局允许软件断点正常工作。
tasks.json
tasks.json 负责把一次完整调试拆成可维护的任务链,而不是依赖单个巨大命令。