- 学校院系:北京工商大学计算机系;
- 队员:曾小红,张弈帆,董嘉誉;
- 指导老师:吴竞邦;
- 队伍ID:T202510011995491;
- 队伍名:Async_Avengers;
- 赛题:proj158 支持Rust语言的源代码级内核调试工具;
本项目基于赛题 Proj-158:支持 Rust 语言的源代码级内核调试工具,在已有工作对 Rust 中同步函数调试方法的基础上,本项目面向 Rust 异步函数的调试,解决传统调试工具在 Rust 异步函数调试中断点后异步函数调用栈不正确,以及已有调试工具对不同运行时的 Rust 异步函数调试的通用性问题,设计并实现了一套面向 Rust 语言异步函数的基于 GDB 非侵入式代码插桩的动态调试工具。本项目主要采用基于编译产物静态分析得到异步函数状态机依赖关系并基于 GDB 动态“非侵入式”插桩目标函数,实现断点调试中区分异步函数执行流的函数调用栈打印,以及异步函数调用数据的跟踪和火焰图展示两个核心功能。
具体解决了以下五个关键问题:
- Rust 异步函数的运行状态不透明:Rust 采用的 DWARF 调试信息格式本身是为同步 函数打造的,不能很好的适配 Rust 异步函数信息记录,导致异步函数内信息含义缺 失,运行状态不透明。可以通过实现相应的解析逻辑在 DWARF 信息中的特殊字段挖 掘出调试器所需要的信息,例如运行状态、局部变量等。
- 传统调试器无法区分异步程序的逻辑执行流:传统调试器依靠物理调用栈回溯函数的 调用逻辑,Rust 异步函数的调用是非线性的、非阻塞的,物理调用栈会丢失异步函数 的逻辑调用关系,导致传统调试器无法区分异步程序的逻辑执行流。通过静态分析编 译产物获取异步函数依赖关系,区分程序执行流,对相关函数进行跟踪记录,动态构 建逻辑调用关系。
- Rust 异步运行时缺少统一暴露接口:Rust 没有官方运行时,社区提供的各大运行时都 有自己的任务调度器,缺乏统一的状态暴露接口。可以通过静态解析编译产物,脱离 对运行时暴露上下文信息的依赖,即可解决这一问题。
- 现有异步调试工具与运行时的强耦合导致通用性低:现有的 Rust 异步调试方案大多 与特定的平台或运行时深度绑定,通用性低。解决方法同上。
- 缺乏灵活、低开销的按需追踪能力:全面的动态插桩会带来不可忽视的性能开销,缺 乏灵活性。我们基于 GDB 解析获取程序中完整的 poll 函数名单,采用“白名单式”追 踪,用户可自行设定跟踪目标。
根据问题分析,项目抽离出了以下四个目标:
- 目标 1:实现异步代码的静态依赖关系解析;
- 目标 2:实现“白名单”式按需追踪;
- 目标 3:实现基于 GDB 非侵入式动态插桩与模块化数据采集框架;
- 目标 4:实现多维度的异步行为可视化分析; 通过实现以上目标,能够解决当前 Rust 异步程序动态跟踪工具的普遍性问题。
- Async Debugger Usage Guide
- 旧版文档(
docs/guide.md、docs/async_backtrace.md)已不准确。
我们的参赛项目决赛文档在这里,请老师下载pdf格式文档阅读项目详细说明。
我们团队的工作日志在这里
Note:由于美观、便捷等原因,我们的日志维护在原仓库的discussion板块,而非文件,不方便搬运。所以此处我们的工作日志外链到了github中。
| 目标 | 完成情况 | 说明 |
|---|---|---|
| 1 | 完成 | ✓ objdump 输出信息解析 ✓ 用 GDB 代替 objdump ✓ 构建异步函数依赖关系树 |
| 2 | 完成 | ✓ 解析获取源代码中的完整 poll函数 |
| 3 | 完成 | ✓ 实现 GDB Python 脚本插件加载机制 ✓ 实现自动在函数进入和返回处打断点的插件 ✓ 实现在断点触发后自动收集异步函数运行状态的插件 ✓ 实现从异步函数名对应到 poll 函数的插件 ✓实现异步程序跟踪框架(方便的异步运行时插桩,方便的异步运行时状态获取功能) ✓ 实现tracer(函数参数、全局/本地变量,栈回溯获取等) ✓ 内核态适配(拟embassy) ✓ 用户态适配(tokio) |
| 4 | 完成 | ✓ 利用 GDB 插桩功能生成和绘制火焰图相关的事件 ✓ Chrome Trace Event 格式 json 输出 |
├── docs/ # 存放项目相关说明文档,包括参赛文档、实现思路、使用方法等
├── dwarf_analyzer/ # 初版DWARF 调试信息解析模块
├── gdb_profiler/ # 性能分析工具
├── results/ # 存放分析工具生成的中间文件和最终结果,如 .json, .dot, .svg 文件
├── src/ # 项目核心源代码目录
│ ├── core/ # 存放与具体调试目标无关的核心、通用逻辑
│ │ ├── dwarf/ # DWARF 调试信息解析模块
│ │ ├── runtime_plugins/ # 存放具体的运行时插件,用于组合和配置 Tracers
│ │ ├── tracers/ # 存放具体的追踪探针,每个 Tracer 负责一种特定的数据采集任务
│ │ ├── __init__.py # GDB 动态插桩框架的核心实现,包含断点、插件加载等逻辑
│ │ ├── config.py # 项目配置文件,如指定当前要加载的运行时插件
│ │ ├── find_poll_fn.py # 实现 find-poll-fn GDB 命令,用于查找所有 poll 函数
│ │ └── init_dwarf_analysis.py # 实现 init-dwarf-analysis GDB 命令,用于初始化 DWARF 解析树
│ ├── tokio/ # 存放与 Tokio 运行时相关的特定分析逻辑或插件
│ ├── main.py # GDB 脚本的总入口,负责导入所有模块和命令
│ └── requirements.txt # 项目的 Python 依赖库列表
├── tests/ # 存放用于测试本工具功能的 Rust 测试项目
└── README.md # 项目的顶层说明文档
- CAT: Context Aware Tracing for Rust Asynchronous Programs 论文的工作(利用符号表进行异步函数跟踪)
这篇论文所作的工作是: 论文的作者发现被拆分出的常规异步函数的函数名是有规律的(这个规律简单来说就是至少包含 future, poll, closure 三个关键字中的一个), 因此他们对函数名符合这个规律的函数都进行插桩, 记录这些函数的调用和返回时间,进而画出异步任务的火焰图.
有一些和异步任务无关的函数恰巧也符合上述规律, 作者们采用了一个算法将这种函数过滤掉.
这个算法在论文里没有说明, 论文附带的代码中有。
- 利用dwarf调试信息进行异步函数跟踪 链接: https://cubele.github.io/probe-docs/async-probe/
该工作是分析了编译后生成的dwarf调试信息, 发现一个异步函数在dwarf调试信息里被表示为一个结构体, 且结构体内明确写出了这个异步函数对应的future会调用哪些future. 但是用户自己实现的 future 并不会被记录在这些结构体里, 因此作者建议先静态分析 dwarf 调试信息, 得到 future 依赖树. 该树的叶子节点很可能包含用户自定义的 future, 因此对叶子 future 的 poll 函数和 closure 函数进行动态插桩, 从而获得足够多的异步函数执行信息(因为对用户debug有用的阻塞发生在叶子future里).
但是作者没有用代码完整实现这个思路, 原因是zCore内没有 jprobe 插桩工具:
如果真的要做到完整的动态插桩,加入编译器支持以后使用jprobe应该是最可行的做法。具体来说我们要知道async函数生成的Future里面的poll函数的地址以及具体的参数类型(状态机对应的struct结构),然后在对应的地址插入jprobe直接访问这个struct,从而每次poll以后struct更新的状态都可以直接看到。
现在不能实现的原因是:
-
目前这个struct的结构只有在编译后才可以看到,可能要用别的手段实现jprobe的handler
-
async函数生成的poll函数的地址在debuginfo里面不太好找,需要编译器支持
-
rust想要实现jprobe可能会比较复杂。 但是我们想到可以用 gdb 代替 jprobe 插桩工具.
- lilos async rust debugger 链接:https://cliffle.com/blog/async-decl-coords/
在 dwarf 调试信息中, 异步函数被表示成形如 {async_fn_env#0} 的形式. 该博客解释了如何将它对应回源代码文件名和行号.