尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

rustc 调试信息生成剖析:从 Rust MIR 到 LLVM DIBuilder 的 rust-codegen 阶段

rustc 调试信息生成剖析:从 Rust MIR 到 LLVM DIBuilder 的 rust-codegen 阶段 rustc 调试信息生成剖析从 Rust MIR 到 LLVM DIBuilder 的 rust-codegen 阶段【免费下载链接】rustEmpowering everyone to build reliable and efficient software.项目地址: https://gitcode.com/GitHub_Trending/ru/rustrustc 编译器生成调试信息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在调试信息流程中的位置调试信息的生成不是一个单一模块完成的而是分成多个阶段、横跨多个 crateRust 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: OptionBoxList }的类型朴素的深度优先遍历会陷入List → OptionBoxList → BoxList → 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 源码的原貌而是调试器兼容性的产物。QuirksRust 生成的 DI 节点假装是 C/CRust 生成的调试信息节点DI nodes在 CDBWindows 控制台调试器和 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 strref$str$/ref_mut$str$[T]/mut [T]ref$slice$T /ref_mut$slice$T 1[T; N]array$T, NRustEnumenum2$RustEnum(T1, T2)tuple$T1, T2*const Tptr_const$T*mut Tptr_mut$Tusizesize_t2isizeptrdiff_t2uNunsigned __intN2iN__intN2f32float2f64double2f128fp1282对于 C 风格枚举无字段枚举rustc 会为 MSVC 生成enum2$包装名闭包与协程coroutine类型在类 C 模式下也会被包裹进人工的enum2$类型中见 msvc_enum_fallback从而让 Natvis 可视化规则能够统一识别并正确渲染活动变体。泛型只输出类型参数不输出值参数Rust 会输出泛型类型信息ArrayVecT, 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.rsPDB 路径或 native.rsDWARF 路径。两者都支持协程coroutine的 DI 节点构建因为协程状态机本质上也是一种带判别值的多变体类型。DWARF专用节点 DW_TAG_variantDWARF 有专用于可判别联合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_partvariant-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 布局枚举借助有效值区间编码判别值的枚举如OptionT有一个特殊变体通常称为untagged variant其字段兼任 tag当该字段值落在预定义范围内时该变体生效因此其结构体携带DISCR_BEGIN/DISCR_END闭区间而非DISCR_EXACT这些区间可能环绕所以可能出现DISCR_END DISCR_BEGIN。单变体枚举实际上没有 tag 字段此时会生成一个恒为 0 的静态 tag 字段以保持统一的表示与 NatVis 规则。128 位 tagNatVis、Visual Studio 与 WinDbg当前类 C 调试信息的主要目标不支持 128 位整数因此涉及的值全部拆分为两个 64 位字段tag128_lo/tag128_hi取代tagDISCR128_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_locDWARF 下将第 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.rsDWARF、cpp_like.rsPDB/类 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),仅供参考
返回列表