rustc 调试信息生成剖析:从 Rust MIR 到 LLVM DIBuilder 的 rust-codegen 阶段
【免费下载链接】rustEmpowering everyone to build reliable and efficient software.项目地址: https://gitcode.com/GitHub_Trending/ru/rust
rustc 编译器生成调试信息(debug info)的第一个阶段,是检查程序的中级表示(MIR),把类型、源码位置等信息转交给 LLVM,再由 LLVM 负责产出 DWARF 或 PDB 等最终调试符号。本指南以 rustc 开发指南(rustc-dev-guide)的Rust codegen章节为骨架,结合 rustc 仓库中rustc_codegen_llvm与rustc_codegen_ssa的真实实现,讲解这一阶段的工作方式:类型信息如何生成、为何"假装"成 C/C++、DWARF 与 PDB 两条路径有何差异,以及这些设计对调试体验的深层影响。读完本文,你将能够理解 rustc 调试信息的整体架构、熟悉 MSVC 命名转换规则,并能循着源码路径深入探索调试器兼容性的种种"奇技淫巧"。
第一阶段:Rust 代码生成(Rust codegen)在调试信息流程中的位置
调试信息的生成不是一个单一模块完成的,而是分成多个阶段、横跨多个 crate:
- Rust codegen(本指南主题):rustc 检查程序的 MIR,将类型与源码信息翻译成 LLVM 可理解的描述,主要工作位于
rustc_codegen_llvm/src/debuginfo目录,少量类型名处理在rustc_codegen_ssa/src/debuginfo中。 - LLVM codegen(下一阶段):当 Rust 调用 LLVM 的
DIBuilder函数后,LLVM 会把信息翻译成与最终格式无关的 "debug record"。值得注意的是,debug record 中的标签始终以 DWARF 标签存储;如果目标是 PDB 调试信息,LLVM 在代码生成期间会通过一个将 DWARF 标签翻译为 CodeView 对应物的模块来处理。
Rust 与 LLVM 之间的通信桥梁是DIBuilderAPI,一个存在于rustc_llvmcrate 中、对 LLVM 内部实现的薄封装。rustc_llvm负责将 Rust 侧的调用转发到 LLVM 的 C++ 实现(参见 llvm-wrapper),从而隔离 Rust 侧与 LLVM 版本相关的元数据格式差异。
在 rustc 源码中,调试信息模块的整体设计记录于 debuginfo 模块文档:模块的公开 API 是一组"以正确参数向 LLVM IR 插入正确元数据"的函数,内部通过缓存复用已创建的元数据节点,所有私有状态存放在CodegenUnitDebugContext(由CodegenCx持有)与FunctionDebugContext(由FunctionCx持有)中。
递归类型的处理:stub 机制
doc.md还揭示了类型描述的核心难题:递归类型。对于形如struct List { value: i32, tail: Option<Box<List>> }的类型,朴素的深度优先遍历会陷入List → Option<Box<List>> → Box<List> → List的无限循环。rustc 的解法是"stub":当算法遇到可能递归的类型(任意 struct 或 enum)时,在描述其成员之前先创建类型描述节点并插入缓存——此时它只是一个空壳,但已经可以被引用;后续若再遇到递归引用,直接命中缓存而不再重新描述。这一行为被封装在type_map::build_type_with_children()函数中。
类型信息:目标不是"精确还原",而是"便于调试器重建"
类型信息通常包含类型名、大小(size)、对齐(alignment),以及字段(fields)、泛型参数(generic parameters)、存储修饰符(storage modifiers)等。大部分工作发生在 rustc_codegen_llvm/src/debuginfo/metadata 中,核心入口是 metadata.rs。
理解这一层工作的关键前提是:调试信息的目标并不是"类型在 Rust 中长什么样就精确还原成什么样",而是"用让调试器在调试时能够最准确地重建数据的方式来表示它们"。这个区别至关重要——这一层上做的很多改动,都是为了在别无他法时绕开调试器的限制。因此你会看到大量"非惯用"的调试信息,它们并不反映 Rust 源码的原貌,而是调试器兼容性的产物。
Quirks:Rust 生成的 DI 节点"假装"是 C/C++
Rust 生成的调试信息节点(DI nodes)在 CDB(Windows 控制台调试器)和 LLDB 面前都"假装"自己是 C/C++,这会导致一些反直觉、非惯用的调试信息。下面逐一剖析。
指针与引用(Pointers and references)
宽指针/宽引用/Box被视为一个含 2 个字段的结构体:data_ptr与length。
所有非宽指针、引用和Box指针都以指针节点(pointer nodes)输出,并且不区分mut与非mut。社区曾多次尝试修正这一点,但始终没有直截了当的解决方案——直接使用各调试格式原生的referenceDI 节点存在陷阱:C++ 引用与 Rust 引用之间存在无法调和的语义差异。正如 cppreference 所述:
引用不是对象;它们不必然占用存储,尽管编译器可能分配存储以实现所需语义(例如,引用类型的非静态数据成员通常会增大类的尺寸,以容纳一个内存地址)。 因为引用不是对象,不存在引用的数组、指向引用的指针,也不存在引用的引用。
当前的提议方案是直接对指针节点做 typedef。
至于用const限定符来区分非mut,同样有隐患:LLDB 内部在单步执行时会缓存变量的子值(如结构体字段、数组元素),并有一套启发式规则判断哪些值可以安全缓存,而const正是该启发式的一部分。目前尚未研究这种做法会如何与 Rust 的内部可变性(interior mutability)构造相互作用。
DWARF vs PDB:按目标格式差异化生成
大部分类型信息是直白的,但一个突出问题是被调试目标(target)的调试信息格式:每种格式语义和限制不同,因此在某些情况下需要略微不同的调试信息。这一分支由对cpp_like_debuginfo的调用控制,其实现为:
/// Check if we should generate C++ like names and debug information. pub fn cpp_like_debuginfo(tcx: TyCtxt<'_>) -> bool { tcx.sess.target.is_like_msvc }即:当目标平台是 MSVC 风格(is_like_msvc)时返回true,调试信息生成将走"类 C++"路径;否则(典型如 ELF 平台)走原生(native)路径。该开关在 type_names.rs 中遍布各处,控制着诸如参数分隔符、尖括号闭合、函数指针格式等细节;在 枚举 DI 节点构建 中也据此分派到cpp_like与native两个子模块。
值得一提的实现细节:在cpp_like_debuginfo为真时,push_arg_separator输出,(不带空格),因为 Natvis 不喜欢类型名各部分之间有空格,这会导致在 natvis 中书写类型名(例如HashMap可视化器里的强制类型转换)时出问题;而push_close_angle_bracket会在输出以>结尾时先补一个空格,因为 MSVC 调试器即使在解析模板时也总把>>当作右移运算符。
命名:MSVC 表达式解析器的妥协
Rust 尽力最准确地传达类型名,但调试器与调试信息格式并不总是尊重这一点。由于 MSVC 表达式解析器的限制,生成 PDB 调试信息时会做如下名称转换(见 type_names.rs 中ty::Tuple、ty::RawPtr、ty::Ref、ty::Array等分支的实现):
| Rust 名称 | MSVC 名称 |
|---|---|
&str/&mut str | ref$<str$>/ref_mut$<str$> |
&[T]/&mut [T] | ref$<slice$<T> >/ref_mut$<slice$<T> >1 |
[T; N] | array$<T, N> |
RustEnum | enum2$<RustEnum> |
(T1, T2) | tuple$<T1, T2> |
*const T | ptr_const$<T> |
*mut T | ptr_mut$<T> |
usize | size_t2 |
isize | ptrdiff_t2 |
uN | unsigned __intN2 |
iN | __intN2 |
f32 | float2 |
f64 | double2 |
f128 | fp1282 |
对于 C 风格枚举(无字段枚举),rustc 会为 MSVC 生成enum2$<>包装名;闭包与协程(coroutine)类型在类 C++ 模式下也会被包裹进人工的enum2$<>类型中(见 msvc_enum_fallback),从而让 Natvis 可视化规则能够统一识别并正确渲染活动变体。
泛型:只输出类型参数,不输出值参数
Rust 会输出泛型类型信息(ArrayVec<T, N: usize>中的T),但不会输出泛型值信息(其中的N)。
原因在于:CodeView 没有用于泛型/C++ 模板的 leaf 节点,因此生成 PDB 调试信息时所有泛型信息都会丢失。有一些变通方法可以让调试器通过类型名取回泛型实参,但充其量是脆弱的方案。rustc 社区正在努力联系 Microsoft 以纠正这一缺陷,或者使用某个未使用的 CodeView 节点类型作为合适的等价物。
类型别名:当前不输出
rustc 在多种情况下会输出 typedef 节点以应对调试器的限制,但目前不会为源码中的类型别名输出节点。
枚举(Enums)
枚举的 DI 节点生成于 rustc_codegen_llvm/src/debuginfo/metadata/enums 目录,其中mod.rs负责分派:若枚举是 C 风格(变体无字段)则走build_c_style_enum_di_node(生成DW_TAG_enumeration_type);否则依据cpp_like_debuginfo分别调用 cpp_like.rs(PDB 路径)或 native.rs(DWARF 路径)。两者都支持协程(coroutine)的 DI 节点构建,因为协程状态机本质上也是一种带判别值的多变体类型。
DWARF:专用节点 DW_TAG_variant
DWARF 有专用于"可判别联合"(discriminated union)的节点:DW_TAG_variant。它是一个容器,引用可能包含也可能不包含判别值的DW_TAG_variant_part节点。层级结构如下:
DW_TAG_structure_type (top-level type for the coroutine) DW_TAG_variant_part (variant part) DW_AT_discr (reference to discriminant DW_TAG_member) DW_TAG_member (discriminant member) DW_TAG_variant (variant 1) DW_TAG_variant (variant 2) DW_TAG_variant (variant 3) DW_TAG_structure_type (type of variant 1) DW_TAG_structure_type (type of variant 2) DW_TAG_structure_type (type of variant 3)这与 native.rs 的文档注释 完全一致:顶层是DW_TAG_structure_type,内含单个DW_TAG_variant_part;variant-part 里有一个描述判别值的 member,以及每个变体对应的DW_TAG_variant;变体的具体类型则作为嵌套的 struct 类型挂在顶层之下。每个变体 struct 的字段布局(字段名、类型、对齐、DW_AT_data_member_location)由 build_enum_variant_struct_type_di_node 构建。
PDB:生成 C 风格的判别联合
PDB 没有对应的专用节点,因此 rustc 生成 C 语言的"可判别联合"等价物(可在 cpp_like.rs 的文档注释 中找到更完整的版本,此处为简洁示意):
union enum2$<RUST_ENUM_NAME> { enum VariantNames { First, Second }; struct Variant0 { struct First { // fields }; static const enum2$<RUST_ENUM_NAME>::VariantNames NAME; static const unsigned long DISCR_EXACT; enum2$<RUST_ENUM_NAME>::Variant0::First value; }; struct Variant1 { struct Second { // fields }; static enum2$<RUST_ENUM_NAME>::VariantNames NAME; static unsigned long DISCR_EXACT; enum2$<RUST_ENUM_NAME>::Variant1::Second value; }; enum2$<RUST_ENUM_NAME>::Variant0 variant0; enum2$<RUST_ENUM_NAME>::Variant1 variant1; unsigned long tag; }这种编码的要点(均可从 cpp_like.rs 源码确认):
- 顶层是一个 union,每个变体对应一个
variantN字段,外加显式的tag字段;union 中还嵌套了一个VariantNames枚举,其枚举值对应变体索引而非判别值,用于高效编码变体名。 - 每个变体包装结构体(如
Variant0)内含NAME(变体名)、DISCR_EXACT(精确判别值)与value(变体数据)。 - Niche 布局枚举(借助有效值区间编码判别值的枚举,如
Option<&T>)有一个特殊变体通常称为"untagged variant":其字段兼任 tag,当该字段值落在预定义范围内时该变体生效,因此其结构体携带DISCR_BEGIN/DISCR_END(闭区间)而非DISCR_EXACT;这些区间可能环绕,所以可能出现DISCR_END < DISCR_BEGIN。 - 单变体枚举实际上没有 tag 字段,此时会生成一个恒为 0 的静态 tag 字段,以保持统一的表示与 NatVis 规则。
- 128 位 tag:NatVis、Visual Studio 与 WinDbg(当前类 C++ 调试信息的主要目标)不支持 128 位整数,因此涉及的值全部拆分为两个 64 位字段:
tag128_lo/tag128_hi取代tag,DISCR128_EXACT_LO/DISCR128_EXACT_HI取代DISCR_EXACT,以此类推。split_128函数负责高/低 64 位拆分,字段偏移量还按目标端序(大端/小端)调整。
重要提示:由于 LLDB 的限制,生成的DISCR_*值始终是u64,即使枚举并非#[repr(u64)]。这在 LLDB 中基本不是问题,因为无论类型如何,DISCR_*值和tag都会被读入uint64_t值进行比较。该逻辑在 build_assoc_const 中有注释说明,并会将成员类型包装进const限定符,以便 LLDB 能够检查成员的值。
对应的调试器解码逻辑(查找活动变体)也记录在 cpp_like.rs 的文档注释 中:读取tag(或拼接tag128_lo/tag128_hi),遍历variant*字段,依据DISCR_EXACT相等或DISCR_BEGIN/DISCR_END区间命中(含环绕区间判断)确定活动变体。
源码信息(Source information)
原文档在"源码信息"一节标记为TODO,尚未展开。可以推断,这一部分未来将描述调试信息中与源码位置相关的生成逻辑。实际上,这部分工作已经分布在 rustc 代码中,例如 mod.rs 中的lookup_debug_loc(把BytePos映射为文件/行/列,MSVC 目标会省略列号以模仿 clang 行为)、dbg_loc(DWARF 下将第 0 行视为"无法归属到任何源码行"的魔法值)、函数序言(prologue)期间禁用源码位置发射、llvm.dbg.declare指令需绑定到变量声明位置等,读者可自行对照 doc.md 的 Source Locations and Line Information 一节 深入。
如何继续深入:仓库中的相关路径
若你想继续在 rustc 仓库中追踪调试信息生成的细节,以下路径是很好的起点:
- Rust codegen 调试信息总入口:
CodegenUnitDebugContext、dbg_scope_fn、create_dbg_var、dbg_var_addr/dbg_var_value(含DW_OP_LLVM_fragment等位置表达式生成)。 - 类型元数据生成 与 类型映射与 stub 机制。
- 枚举 DI 节点:
mod.rs(分派)、native.rs(DWARF)、cpp_like.rs(PDB/类 C++)。 - 类型名计算与 MSVC 命名转换:
compute_debuginfo_type_name、push_close_angle_bracket、cpp_like_debuginfo。 - 调试信息模块设计文档:递归类型 stub、源码位置与行信息、函数序言的处理。
- 调试器交互文档 及其下的
gdb-*、lldb-*、debugger-visualizers.md、natvis-visualizers.md等章节,讲述各调试器如何消费这些调试信息。 - LLVM codegen 阶段:debug record 与 DWARF 标签到 CodeView 的转换。
MSVC 的表达式解析器会把
↩>>当作右移运算符,因此必须在连续出现的>之间用空格隔开(写作> >)。虽然这些类型名作为调试信息节点的一部分生成(随后被包裹进一个带 Rust 名称的 typedef 节点),但当 LLVM-IR 节点被转换为 CodeView 节点后,类型名信息就丢失了——因为 CodeView 对基本类型有专门的简写节点,而这些简写节点没有 "name" 字段。
↩ ↩ ↩ ↩ ↩ ↩ ↩
【免费下载链接】rustEmpowering everyone to build reliable and efficient software.项目地址: https://gitcode.com/GitHub_Trending/ru/rust
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考