Rust 编译器开发者视角下的 LLDB 内部机制:插件管线、类型系统与 Rust 调试支持
【免费下载链接】rustEmpowering everyone to build reliable and efficient software.项目地址: https://gitcode.com/GitHub_Trending/ru/rust
导读
LLDB 是 Rust 目前支持的三类主要调试器(GDB、LLDB、CDB)之一,也是唯一同时原生处理 DWARF 与 PDB 两种调试信息格式的调试器,但对 Rust 的支持目前仍是"部分"级别。本文以 Rust 编译器开发指南(rustc-dev-guide)的 lldb-internals 文档为核心,系统讲解 LLDB 的语言支持插件管线(ASTParser -> TypeSystem -> ExpressionParser/Language)、Rust 当前如何借道TypeSystemClang工作、DWARF/PDB 双格式带来的复杂性,以及未来实现独立TypeSystemRust的关键路径。读完本文,你将理解 LLDB 调试 Rust 程序时从调试信息解析到变量显示的完整链路,并能在 rustc_codegen_llvm 调试信息模块 与 LLDB 可视化脚本 之间建立对应关系。
LLDB 语言支持的插件管线
LLDB 的调试信息处理依赖一组可扩展接口,这些接口的主体定义在lldb/src/Plugins目录下。它们的设计初衷是允许第三方编译器开发者添加在 LLDB 运行时加载的语言支持;不过截至本文写作时间(2025 年 11 月),公共 API 尚未最终定型,因此插件要么存在于 LLDB 主仓库中,要么存在于 LLDB 的独立 fork 中。
典型的语言支持以一条插件管线形式编写:
*ASTParser -> TypeSystem -> ExpressionParser/Language这条管线中各环节职责如下:
- ASTParser:读取反序列化的调试信息节点(如 DWARF DIE、CodeView/PDB 记录),将其翻译成类型系统可用的内部表示;
- TypeSystem:作为某门语言的类型"唯一权威",负责类型的创建、查找、生命周期管理与展示定制;
- ExpressionParser / Language:前者负责把用户输入的表达式(如
vec_v[0])解析执行,后者负责合成子项(synthetic children)与美化打印(pretty-printing)。
仓库中已有若干对该插件 API 的既有实现,可作为参照:
- Apple 为支持 Swift 维护的 LLDB fork(swiftlang/llvm-project);
- CodeLLDB 曾经为支持 Rust 维护的 fork(vadimcn/llvm-project),其
TypeSystem/Rust实现是理解最小 Rust 支持的重要参考资料; - 一个正在进行中的 Rust 支持重实现(Walnut356/llvm-project 的
lldbrust/19.x分支); - 一个 Rust 表达式解析器插件(tromey/lldb),该插件写于
TypeSystemAPI 创建之前,但由于表达式解析高度自由,其底层的词法、语法解析、函数调用等逻辑仍然极具参考价值。
Rust 支持与 TypeSystemClang:为什么是"部分支持"
如 debug info 概述 中所述,LLDB 对 Rust 是"部分"支持(Partial)。进一步澄清:Rust 当前使用的是为 C/C++ 构建的那套插件管线(虽然其中包含少量针对 Rust 枚举类型的辅助逻辑),它直接依赖clang编译器对类型的内部表示(clang::QualType、clang::Decl、clang::DeclContext)。
这带来一个直接后果:当 LLDB 的输出与我们的期望不符时,我们能做的改动非常有限。一些 workaround 可以帮忙,但归根结底,Rust 的需求相对于"保证 C/C++ 编译与调试正确"而言是次要的。因此:
- LLDB 方面对新增一个
TypeSystemRust持开放态度,但这是一个巨大的工程; - 本文对应章节的写作目的,既是记录当前如何与
TypeSystemClang交互,也是为将来实现TypeSystemRust提供轻量级指引。
需要特别强调的是,TypeSystem直接与目标语言的编译器交互是设计意图,但并非硬性要求——完全可以在插件实现内部自行创建所有必需的支撑类型。文档也给出了一个实用建议:LLDB 的文档(包括源码注释)相当稀疏,想通过阅读TypeSystemClang的实现来理解语言支持机制会比较困难,因为还要额外理解clang编译器内部。推荐直接看上面提到的两个TypeSystemRust实现,它们是"从零开始"编写的、不借用任何编译器类型表示的最小实现,更接近实现语言支持所需的最少代码量。
DWARF 与 PDB:双格式带来的复杂性
LLDB 的独特之处在于它同时能处理 DWARF 与 PDB 两种调试信息格式,这自然带来了额外的复杂度。
- DWARF:
*-gnu目标的主要调试信息格式,通常随二进制捆绑,也可通过 DebugFission 生成独立文件; - PDB/CodeView:
*-msvc目标的主要调试信息格式,文件独立于二进制(.pdb扩展名)。需要区分的是,这里讨论的是普通 PDB 文件(Portable PDB 主要用于 .NET 应用)。
PDB 支持本身又被拆分为两条实现路径:
dia:依赖随 Visual Studio 分发的msdia140.dll库;native:完全依据公开的 PDB 格式资料从零编写。
版本背景:
dia在 LLDB 21 之前是默认实现;自 LLDB 22 发布起,native成为新默认,并且有计划弃用并彻底移除dia系插件,因此后续讨论只围绕native解析。native可通过 LLDB 22 新增的plugin.symbol-file.pdb.reader设置,或环境变量LLDB_USE_NATIVE_PDB_READER=0/1进行切换。
Debug 节点解析:从原始调试信息到类型系统
处理调试信息的第一步,是把原始调试节点加工成可用的形态。这项工作主要发生在两个类中:
DWARFASTParser(位于 LLDB 的SymbolFile/DWARF插件目录);PdbAstBuilder(位于 LLDB 的SymbolFile/NativePDB插件目录)。
这两个类接收的是由SymbolFileDWARF与SymbolFileNativePDB分别生成的反序列化调试信息。值得注意的设计约束是:SymbolFile实现者在把调试信息交给解析器之前,几乎不做任何变换。对于 PDB 和 DWARF,调试信息都是通过 LLVM 的调试信息处理器读取的。
解析器会把节点翻译成更方便 LLDB 使用的格式。对clang而言,这些格式就是clang::QualType、clang::Decl和clang::DeclContext——也就是clang编译 C/C++ 时内部使用的类型。再次强调:使用编译器自身的类型表示不是要求,但插件系统在设计上确实把它作为一种可能性。
文档将这三个类型在语言无关的语境下统称为LangType、Decl、DeclContext(当不涉及TypeSystemClang的具体实现细节时):
LangType表示一个类型:包含类型名称、大小与对齐、分类(struct、primitive、pointer 等)、限定符(如const、volatile)、模板参数、函数参数与返回类型等信息;Decl表示任意一种声明:可以是类型、变量、结构体的静态字段、static/const 的初始化值等;DeclContext大致表示一个作用域:通常包含Decl与其它DeclContext,但这种关系并不那么简单直接。例如一个函数既是Decl(因为函数签名本身就是一种类型),又是DeclContext(因为函数体内包含变量声明、嵌套函数声明等)。
翻译过程可能相当冗长,但通常是直截了当的——大部分工作量取决于填充LangType、Decl、DeclContext具体需要哪些信息。
一旦某个节点被翻译完成,指向它的指针会被类型擦除(void*),并包装进CompilerType、CompilerDecl或CompilerDeclContext。这些包装器把自身与拥有它们的TypeSystem关联起来;对包装器的方法调用会委托给TypeSystem,由TypeSystem把void*重新转型回对应的LangType*/Decl*/DeclContext*并操作其内部。用 Rust 术语表达,这个关系大致如下:
struct CompilerType { inner_type: *mut c_void, type_system: Arc<dyn TypeSystem>, } impl CompilerType { pub fn get_byte_size(&self) -> usize { self.type_system.get_byte_size(self.lang_type) } } // ... impl TypeSystem for TypeSystemLang { pub fn get_byte_size(lang_type: *mut c_void) -> usize { let lang_type = lang_type as *mut LangType; // Operate on the internals of the LangType to // determine its size // ... } }这套"类型擦除 + 包装器委托"的设计是理解 LLDB 语言插件的关键:LLDB 其余部分只与CompilerType之类的类型擦除包装打交道,语言特定逻辑全部收敛在TypeSystem实现内部。从本仓库角度看,Rust 侧负责向 LLVM 输出这些调试节点的代码位于 compiler/rustc_codegen_llvm/src/debuginfo,其中类型信息相关工作的主体在 metadata.rs,枚举 DI 节点的生成在 metadata/enums(cpp_like.rs对应 C++ 风格表示、native.rs对应原生表示)。
TypeSystem 接口的三大职责
TypeSystem接口有三个主要目的:
- 充当一门语言类型的"唯一权威"。这使得该类型系统可以被加入 LLDB 的"类型系统池"。当可执行文件被加载时,LLDB 会确定目标语言,然后查询这个池,找出声称自己能处理该语言的
TypeSystem。此外,还可以通过TypeSystem取回底层的SymbolFile、按名称搜索类型,以及合成那些调试信息里可能不存在的"基础类型"(例如基本类型、T的数组、指向T的指针)。 - 管理
LangType、Decl、DeclContext对象的生命周期。 - 定制这些类型"默认"的呈现方式与可交互方式。
前两项职责比较直白,文档重点讨论第三项。
TypeSystem接口中的许多函数,对写过 visualizer 脚本的人而言会非常眼熟——这些函数支撑着同名(matching names)的SBType与SBValue函数。例如TypeSystem::GetFormat返回该类型在未被应用任何自定义 formatter 时的默认格式。
特别值得注意的是GetIndexOfChildWithName与GetNumChildren。TypeSystem版本的这两个函数作用在类型上,而不是像SBValue版本那样作用在值上。TypeSystem函数返回的值决定了结构体的哪些部分能够被 LLDB 其余部分交互——如果某个字段被遗漏,那么这个字段对 LLDB 来说实际上就不存在了。
此外,由于这些函数不处理对象,就没有底层内存可供检查或解释。这意味着它们与对应的SyntheticProvider函数目的并不相同:
- 无法确定一个
Vec有多少个元素,也无法知道这些元素位于哪个地址; - 也无法确定和类型(sum-type)判别式(discriminant)的值。
理想情况下,TypeSystem应该尽可能少地改动、按调试信息中的原样暴露类型,把"美化类型"的工作交给 LLDB 的 synthetics 与前端。如果某条信息确实无用,正确的做法是修改 Rust 编译器让它根本不输出这条调试信息,而不是在类型系统层硬凑——这与 rust-codegen 文档 中"调试节点假装成 C/C++、为绕过调试器限制而做 workaround"的总体策略是一致的。
表达式解析(Expression Parsing)
TypeSystem通常还会配套一个能处理表达式解析的对应物,这需要在TypeSystem接口中额外实现几个函数;表达式解析代码的主体应放在lldb/source/Plugins/ExpressionParser。
解析器本身没有太多特别之处:它需要实现一个能处理(可能是简化版)Rust 语法的简单解释器。这些解析器操作的是lldb::ValueObject——也就是支撑SBValue的那些对象。正因为表达式解析的形态相当自由,前面提到的那个早期 Rust 表达式解析器插件(写于TypeSystemAPI 诞生之前)虽然架构过时,但其词法、语法、函数调用等底层实现仍有很大的借鉴价值。
Language 插件:C++ 版的可视化脚本
Language插件是 Python visualizer 脚本的 C++ 等价物。它们出于相同的目的操作SBValue对象:创建合成子项(synthetic children)与美化打印(pretty-printing)。CPlusPlusLanguage中对 LibCxx 类型的实现是学习如何编写 visualizer 的绝佳资源——例如LibCxxVector(即 C++ 中对应Vec<T>的容器)。
与 Python 版本相比,Language插件可以访问 LLDB 的私有内部实现(包括底层的TypeSystem),因此以Language插件形式编写的 synthetic/summary provider 能提供比 Python 等价物更高质量的输出。
从工程角度看,调试节点解析、类型系统与表达式解析三者紧密耦合,而Language插件则封装得更好,可以为任何已有类型系统支持的语言"独立"编写。正因进入门槛更低,一个RustLanguage插件可能是迈向 LLDB 完整 Rust 支持的很好垫脚石——这也可以理解为:先用 C++ 插件把合成子项和美化打印做漂亮,再逐步深入类型系统层面。
仓库中与"Language 插件 / visualizer"对应的实际产物是 src/etc/lldb_providers.py(LLDB 的 Rust Python providers,包含StdVecSyntheticProvider、StdStringSyntheticProvider、StructSyntheticProvider、ClangEncodedEnumProvider、MSVCEnumSyntheticProvider等实现,以及get_template_args、resolve_msvc_template_arg等辅助函数)与 src/etc/lldb_lookup.py(providers 的注册/查找入口)。它们的注入方式与完整讲解见 lldb-visualizers 文档,包括type synthetic add/type summary add命令、__lldb_init_module钩子以及Vec<T>从 synthetic 到 summary 的完整示例。
Visualizers 与测试支撑
原文档的 Visualizers 章节目前标记为WIP(尚未完成)。结合仓库实际情况,这一块的工作状态可以从两个维度观察:
- 脚本侧:Rust 的 LLDB 可视化脚本主体已经存在,即 src/etc/lldb_providers.py。它同时需要考虑 DWARF 与 PDB 两种调试信息(例如
MSVCStrSyntheticProvider、MSVCEnumSyntheticProvider、MSVCTupleSyntheticProvider、MSVCStdSliceSyntheticProvider等专门针对 MSVC/PDB 的类),这与本文"DWARF vs PDB"一节所述的双格式复杂性直接对应; - 测试侧:
tests/debuginfo目录下的测试由compiletest执行,回答"我们输出的调试信息能否被调试器正确读取、visualizer 是否符合预期"等问题;其中repr伪指令及其输入数据定义、--bless机制等由 src/etc/lldb_batchmode 支撑,详见 testing 文档。
在向TypeSystemRust演进之前,理解上述每一层(插件管线 →TypeSystemClang约束 → 双格式解析 → 类型擦除包装 → 表达式解析 →Language插件)是如何串联的,是定位 Rust 调试问题"该改哪一层"的前提:能由 synthetics/前端解决的,就不要动调试信息;确实无用的调试信息,则应该在 Rust 编译器侧(rustc_codegen_llvm的 debuginfo 模块)修正输出。
【免费下载链接】rustEmpowering everyone to build reliable and efficient software.项目地址: https://gitcode.com/GitHub_Trending/ru/rust
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考