You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
reacted with thumbs up emoji reacted with thumbs down emoji reacted with laugh emoji reacted with hooray emoji reacted with confused emoji reacted with heart emoji reacted with rocket emoji reacted with eyes emoji
Uh oh!
There was an error while loading. Please reload this page.
异步 OS 调试 — 整体设计(第一版)
背景
我们这个调试器目前有两个独立工作的功能:
异步函数追踪(launch 模式)。在调试单个 Rust 异步程序时,通过 GDB Python 脚本在 poll 函数入口和 call 指令处自动安装断点,维护一个"协程影子栈",追踪协程之间的调用关系。用户可以在 VS Code 的 Async Inspector 面板里看到一棵异步调用树。
OS 特权级调试(attach 模式)。通过 QEMU + GDB remote protocol 连接到一个运行中的操作系统,自动检测 kernel↔user 的切换,在边界处切换符号表和断点组,让用户的断点在正确的地址空间里生效。这个功能是从 code-debug 移植过来的。
问题是:这两个功能目前完全独立,无法在一个调试会话里同时使用。如果我想调试一个异步 OS(比如用 Rust async 写的内核组件,或者内核提供了 async syscall),我就没法在 OS 调试模式下看到异步调用树。
下面提到的python端即为异步追踪的部分,TypeScript端即为OS调试的部分。
为什么现在不兼容
主要为两个层面上的不兼容:
第一层:事件分发层。
由于OS调试和异步程序调试是在不同的模式下设计的,前者是attach模式,后者为launch模式,当前两个调试模式的事件分发逻辑不同,所以OS调试和异步调试没办法一起进行。
第二层:追踪断点的存活问题。
当特权级切换时,异步追踪中基于地址的 CallSiteBP 存储的地址不会被清除,但是这些地址的内容会发生变化。
比如在内核里有一个 call 0x80200500 指令,CallSiteBP 装在 *0x80200100。切到用户空间后,地址 0x80200100 还是那个数字,但里面装的是用户程序的代码了。这个断点如果还在,会在一个错误的指令上触发。
要让异步追踪在 OS 调试下工作,需要解决两个问题——确保OS调试和异步调试在统一模式下能够一起工作,以及异步调试追踪断点能跟着OS调试断点组一起切换。
核心设计思路
思路一:影子栈全程连续,不需要在边界插入节点
我之前考虑过在 kernel↔user 切换时往影子栈里插入一个
type: "boundary"的标记节点,用来在树里表示"这里穿越了特权级边界"。后来想了一下,没必要。因为:
所以最终决定:影子栈不区分特权级,CID 全局递增,一棵树贯穿 kernel 和 user。
思路二:Python端追踪状态按断点组保存和恢复
基于不兼容的第二层,我们使用保存/恢复方案:切走时把当前组的追踪状态存下来(包括装了哪些 PollEntryBP、扫过了哪些函数的 call sites、白名单内容等),切回来时恢复。Python 端每次只维护当前活跃组的追踪断点。
思路三:切换时机由 TypeScript 端控制,Python 不自行感知
Python 可以监听
gdb.events.new_objfile来检测符号表的变化,然后推断当前处于哪个特权级。但这种方式不靠谱——加载符号表的动作不一定只来自我们的切换逻辑,而且从符号表反推特权级本身就是猜。更干净的做法是:TypeScript 端是切换的发起者,它在
updateCurrentBreakpointGroup的前后,显式调用 Python 命令来保存和恢复追踪状态。Python 只需要提供保存和恢复的接口,不需要知道"group"是什么概念。改动
Python 端
新增三个 GDB 命令:
ardb-save-trace-state <label>把当前追踪相关的状态序列化存到内存里(按 label 索引)。保存的内容包括:
_WHITELIST_EXACT、_WHITELIST_PREFIX、_WHITELIST_ADDR_MAP、_WHITELIST_ADDR_READY_ACTIVE_ROOTS(哪些 poll 函数已装有 PollEntryBP)、_CALLSITE_INSTALLED_FOR_FN(哪些函数已扫描过 call 指令)_ASYNC_SYMBOL_SET这些状态保存后,当前的追踪断点本身不删——由 TypeScript 端在切换时统一清理。
以下状态不保存,因为它们需要跨特权级连续:
_TLS_STACK(影子栈)— kernel 里 push 的 CID 在切到 user 后还在栈上_CO_BY_KEY/_CO_META/_CO_POLL_SEQ— 全局 CID 注册表_CO_NEXT_ID— CID 自增计数器_PopOnReturnBP实例 — 它们绑定到具体栈帧,只要帧还在就活着ardb-restore-trace-state <label>从 label 恢复追踪状态:
_ACTIVE_ROOTS和_CALLSITE_INSTALLED_FOR_FN恢复后不重新扫描 call sites——之前已经扫描过的函数不需要再扫。如果新地址空间下有之前没见过的 poll 调用关系,会在 PollEntryBP 命中时自然被发现和追踪。
ardb-reset-trace-state <label>清除保存的状态,释放内存。在调试会话结束或用户手动 reset 时调用。
TypeScript 端(gdbDebugSession.ts)
第一个改动:确保 stopped 事件能到达 Async Inspector。
目前的
sendUserStoppedEvent()已经会发送StoppedEvent(在handleBreakpointHit里或直接发送pause类型)。OS 状态机的各个 path(check_stop_in_kernel、check_if_user_to_kernel_border_yet等)最终也会调到sendUserStoppedEvent。我需要确认每条路径都不会漏掉,特别是 hook 匹配成功自动 continue 的路径(hook 不应该触发 snapshot,因为那是透明切换)。第二个改动:在断点组切换前后加入追踪状态切换。
在
doAction方法中,涉及断点组切换的 action 有两个:high_level_switch_breakpoint_group_to_low_level— user→kernel 切换low_level_switch_breakpoint_group_to_high_level— kernel→user 切换这两个 action 内部调用了
updateCurrentBreakpointGroup。我需要在调用前加上ardb-save-trace-state <当前组名>,在updateCurrentBreakpointGroup的 Promise 链(重新插入断点之后)加上ardb-restore-trace-state <目标组名>。UI 层(Async Inspector 面板)
目前的 inspector 面板只有一个全局的白名单和 trace root 设置。改动:
All reactions