rustc_codegen_llvm debuginfo 模块Rust 编译器 DWARF 调试符号生成的原理与实现剖析【免费下载链接】rustEmpowering everyone to build reliable and efficient software.项目地址: https://gitcode.com/GitHub_Trending/ru/rust在构建带-g选项的 Rust 可执行文件时rustc_codegen_llvm中的 debuginfo 模块负责把类型布局、函数签名、变量作用域与源码位置等信息转化为 LLVM 元数据再由 LLVM 生成最终的可被 GDB/LLDB 读取的 DWARF或 Windows 下的 CodeView调试信息。本文基于仓库内 debuginfo 模块设计文档 逐节展开结合 模块入口、类型元数据缓存 与 会话配置 中的实际实现讲清该模块的设计原则、递归类型的 stub 打断机制以及源码位置prologue 处理的完整链路帮助读者理解从rustc -g到调试器可定位源码行号这一整条路径的底层机制。模块定位文档如何被加载模块做什么仓库中该模块的文档文件 doc.md 并不是独立存放的说明文件而是被直接编织进了 rustdoc 文档系统。mod.rs 的第一行即为#![doc include_str!(doc.md)]这意味着对debuginfo模块做文档查询时看到的内容就是这份设计文档。文档开篇阐明了模块的核心职责该模块用于生成调试符号。我们使用 LLVM 的 source level debugging 特性来生成调试信息。基本原则是只要 LLVM IR 中具备正确的元数据metadataLLVM 代码生成器就能为给定代码创建 DWARF 调试符号。元数据的组织方式非常接近 DWARF 的debugging information entriesDIE表示数据类型布局、函数签名、块布局、变量位置与作用域等信息。本模块的目的就是生成正确的元数据并插入 LLVM IR。可以将其概括为三层职责类型/符号描述为每种参与调试的类型struct、enum、vtable 等生成对应的DIType节点函数与变量描述为函数subprogram、参数、局部变量、全局变量生成DIScope/DIVariable节点源码位置绑定为 IR 指令打上线号信息使机器码可以映射回源码。文档还指出一个重要工程决策由于元数据树的确切格式在不同 LLVM 版本之间可能变化模块尽量使用 LLVM 的DIBuilder来创建元数据而不是手工拼装!DICompositeType(...)这类字符串化元数据。当前仓库的实现也印证了这一点——di_builder.rs 中封装了DIBuilderBox而 metadata.rs 中的各类LLVMDIBuilder*调用都通过DIB(cx)辅助函数取得 builder。公开 API、缓存与私有状态的组织方式文档对该模块的对外形态给出了明确描述模块的公开 API 是一组函数用正确的参数调用它们时就会把正确的元数据插入 LLVM IR。因此模块是被外部客户端驱动的。文档中给出的示意性调用是debuginfo::create_local_var_metadata(bx: block, local: ast::local)。从当前源码结构看这套被外部驱动的 API 已经演进为 trait 方法的形式mod.rs 中实现了DebugInfoBuilderMethodstcx for Builder其中的dbg_scope_fn等方法对应文档所说的为函数创建 scope 节点这类公开操作例如dbg_scope_fn内部依次完成通过file_metadata获取文件节点、create_subroutine_type构造子例程类型、计算DISubprogram的 flagsSPFlagDefinition、SPFlagLocalToUnit、SPFlagOptimized、SPFlagMainSubprogram最后调用LLVMRustDIBuilderCreateFunction生成函数 DIE。对方法还会额外先创建LLVMRustDIBuilderCreateMethod的声明节点源码注释解释了原因LLVM LTO 在类型 DIE 的子 DIE 是完整 subprogram 定义时无法统一类型定义因此方法在类型 DIE 中只放DW_AT_declaration完整定义放在编译单元CU层级并用DW_AT_specification指回声明。文档中强调的另一原则是缓存复用模块内部通过缓存尽量复用已创建的元数据调用方只需调用对应函数文档示例为file_metadata(cx, file)函数会自行探测是否已存在该文件路径对应的节点。当前实现中的对应函数是 metadata.rs 中的file_metadata而缓存本体挂在编译单元级上下文上——CodegenUnitDebugContext结构体中的created_files字段pub(crate) struct CodegenUnitDebugContextll, tcx { builder: DIBuilderBoxll, created_files: RefCellUnordMapOption(StableSourceFileId, SourceFileHash), ll DIFile, type_map: metadata::TypeMapll, tcx, adt_stack: RefCellVec(DefId, GenericArgsReftcx), namespace_map: RefCellDefIdMapll DIScope, recursion_marker_type: OnceCellll DIType, }见 mod.rs。这与文档所述模块使用的全部私有状态存放在CodegenUnitDebugContext由CodegenCx持有或FunctionDebugContext由FunctionCx持有之中完全一致。类型维度的缓存则由TypeMap承担下文递归类型一节详述。此外CodegenUnitDebugContext::new在初始化时会根据目标平台的DebuginfoKind设置 LLVM 模块级标志见 mod.rs目标为Dwarf或DwarfDsymmacOS 的 dSYM时写入Dwarf Version模块标志合并行为为Max多 CGU 交叉 LTO 合并时取最高版本与 Clang 行为一致源码注释指出 macOS 与 Android 存在对高版本 DWARF 的兼容性问题可用--llvm-opts -dwarf-version,N覆盖目标为Pdb时写入CodeView标志指示 LLVM 生成 CodeView 调试信息所有情况都写入Debug Info Version标志LLVMRustDebugMetadataVersion()防止位码读取器丢弃调试信息。文档最后还给出了该模块源码文件的组织结构——三个概念性区域1模块公开接口2模块内部元数据创建函数3小工具函数。这一划分在目录中依然清晰可见mod.rs承担接口区DebugInfoBuilderMethods实现metadata/子模块含type_map.rs、enums/承担元数据创建utils.rs、dwarf_const.rs、namespace.rs、gdb.rs则是工具与辅助区。递归类型问题stub 机制如何打断类型引用环文档中最具技术含量的一节是Recursive Types。struct 和 enum 这类类型可能是递归的某个类型 X 的定义经由其他类型传递性地指回 X 自身这在类型引用图中引入了环。文档用一个单链表示例说明问题struct List { value: i32, tail: OptionBoxList, }对这种类型做朴素的需求驱动深度优先遍历DFS描述时会产生如下无限调用栈describe(t List) describe(t i32) describe(t OptionBoxList) describe(t BoxList) describe(t List) // at the beginning again... ...文档给出的解决方案是stub桩节点当算法遇到可能递归的类型任何 struct 或 enum时立即创建一个类型描述节点并先于描述其成员之前插入缓存。这个节点此刻只是一个 stub尚未描述和挂接成员但已经可以让算法引用该类型之后若成员描述中遇到递归引用就会命中缓存而不再重新描述该类型。文档指出这一行为封装在type_map::build_type_with_children()函数中。当前仓库的 type_map.rs 中确实可以找到这套机制的完整实现且比文档描述更为精细1类型标识与缓存。缓存的键不是类型名而是UniqueTypeId枚举区分普通类型、枚举变体 partDWARF 中DW_TAG_variant_part、单个变体对应的合成 struct 类型、CPP-like 模式下的变体 wrapper 以及 vtable 合成类型pub(super) enum UniqueTypeIdtcx { Ty(Tytcx, private::HiddenZst), VariantPart(Tytcx, private::HiddenZst), VariantStructType(Tytcx, VariantIdx, private::HiddenZst), VariantStructTypeCppLikeWrapper(Tytcx, VariantIdx, private::HiddenZst), VTableTy(Tytcx, OptionExistentialTraitReftcx, private::HiddenZst), }UniqueTypeId通过稳定哈希StableHash生成十六进制字符串作为 LLVMDIBuilderCreate*Type系列接口的UniqueId参数见 type_map.rs。TypeMap本体则是FxHashMapUniqueTypeId, ll DITypeinsert时若键已存在会直接bug!报错保证一个类型标识至多对应一个 DIE的不变式。2stub 的创建。stub()函数type_map.rs针对Stub::Struct/Stub::VTableTy调用LLVMDIBuilderCreateStructType针对Stub::Union调用LLVMDIBuilderCreateUnionType关键在于元素数组为空no_elements——即节点先占位字段后补。3先插缓存、再描述成员。build_type_with_children的核心流程是断言该 stub 尚未入缓存 → 将 stub 插入TypeMap→ 执行members闭包描述成员成员描述中若递归指回本类型type_di_node查缓存即命中 stub→ 用LLVMRustDICompositeTypeReplaceArrays把成员数组与泛型参数数组回填到 stub 上。其源码注释与文档描述一一对应This function enables creating debuginfo nodes that can recursively refer to themselves. It will first insert the given stub into the type map and only then execute themembersandgenericsclosures passed in. ... If the type of a field transitively refers back to the type currently being built, the stub will already be found in the type map, which effectively breaks the recursion cycle.4对膨胀式递归的额外防护。文档未展开、但当前实现中存在的一个细节值得注意对于像enum RecursiveT { Recurse(*const RecursiveWrapT), Item(T) }这样参数严格包含祖先参数的膨胀型递归类型展开理论上可以无限进行。build_type_with_children 借助CodegenUnitDebugContext中的adt_stack一个(DefId, GenericArgsRef)栈检测这种情况当前类型与栈上某祖先def_id相同且某参数严格包含arg ! ancestor_arg arg.contains(ancestor_arg)祖先的对应参数、且中间各层类型都使用该祖先参数时判定为is_expanding_recursive直接返回 stub 而不再展开成员源码标注了FIXME: indicate that this is an expanding recursive type in stub metadata?。对普通的BoxBoxRecursive这类正常递归则不触发该短路注释中专门解释了原因。栈的压入/弹出由 RAII 守卫AdtStackPopGuard保证。从源码结构看这套 stub 栈检测的组合正是文档所述stub 打断环策略在长期演进后的完整形态基础环靠 stub 缓存打断膨胀环靠adt_stack检测后以未展开 stub 兜底。源码位置与行号信息prologue 的特别处理文档第二节Source Locations and Line Information指出除类型描述外调试信息还必须能把机器码位置映射回源码位置这一功能同样由本模块处理并列出三个控制函数set_source_location()clear_source_location()start_emitting_source_locations()其语义是一个有状态 API刻意模仿 LLVM 的行为调用set_source_location()后之后创建的所有 IR 指令都会关联到该源码位置直到再次设置或清除清除之后创建的指令不关联任何源码位置。文档特别提醒不要依赖调用点处存在某个特定状态因为先前调用可能已经改变了它。当前实现中该状态读取封装在Builder::get_dbg_locmod.rs底层即 LLVM 的LLVMGetCurrentDebugLocation2指令创建时的位置注入由CodegenBuilderMethods各路径调用该模块的设置接口完成——与文档描述的状态机模型一致。文档随后花较大篇幅讨论一个容易踩坑的话题——函数 prologue。函数机器码开头通常有若干指令用于把参数装入 alloca、检查栈空间是否足够这段prologue在源码中是不可见的。LLVM 会在行表line table中第一个非 prologue 指令处放置特殊的 PROLOGUE END 标记而 LLVM 判断 prologue 结束位置的方法是找到函数体中第一条关联了源码位置的指令。因此生成 prologue 指令时必须保证在真正的函数体开始之前不发出源码位置信息——这正是文档中第三个函数start_emitting_source_locations()的作用新 codegen 的函数默认禁用源码位置发出只有在即将开始 codegen 该函数顶层基本块之前调用该函数后才激活。文档还给出了一个例外llvm.dbg.declare指令必须关联到被声明变量的源码位置。对函数参数而言这些dbg.declare通常出现在 prologue 中段但 LLVM 的 prologue 检测会忽略它们因此create_argument_metadata()等函数负责在源码位置发出仍被禁用时为llvm.dbg.declare正确关联位置调用方无需做任何特殊处理。理解这一节的实际意义在于它解释了为什么调试器停在函数第一行时栈回溯与行号表能正确区分编译器生成的序言与用户代码——PROLOGUE END 标记的位置完全由模块何时开始发射源位置这一策略决定而该策略被刻意推迟到函数体真正开始之处。与编译选项的衔接从-g到 DebugInfo 枚举上述元数据生成并非无条件进行它与rustc的调试信息级别直接挂钩。从 rustc_session/src/config.rs 可以看到-g被定义为等价于-C debuginfo2select_debuginfo中取-g次数与-C debuginfo数值的最大值max_g max_c时得到DebugInfo::FullDebugInfo枚举区分None/Limited/Full等档位-g0对应DebugInfo::None此时不生成调试元数据debuginfo 模块基本空转若使用-C remark而未开启任何debuginfo会话配置会给出需要-C debuginfon才能显示源码位置的早期告警印证了行号信息与调试信息级别的绑定关系。在模块内部这个档位直接决定元数据的厚度mod.rs 中get_function_signature与get_template_parameters均在cx.sess().opts.debuginfo ! DebugInfo::Full时返回空签名/空模板参数——即较低档位下仍生成函数 DIE变量与行号信息可用但不生成完整的参数类型与泛型模板信息。这为为什么-g1比-g2在调试器里看到的类型细节更少提供了源码级解释。验证路径如何在仓库中观察其行为该模块产出的元数据最终体现为二进制中的 DWARF/CodeView 段仓库提供了相应的测试面供进一步观察tests/debuginfo/针对 GDB 调试体验natvis 文件、-gdwarf-*变体的集成测试tests/run-make/ 中与 dwarf/split-debuginfo 相关的 run-make 用例覆盖模块标志如Dwarf Version、-C split-debuginfo等路径模块自身的行为约定如TypeMap::insert的重复键bug!、build_type_with_children的先插后建顺序可从 type_map.rs 直接阅读核对。需要注意的适用前提本文涉及的实现细节均以当前仓库状态为准文档中出现的create_local_var_metadata、start_emitting_source_locations等函数名为设计文档中的示意性/历史命名当前代码中对应的实际入口是DebugInfoBuilderMethodstrait 方法由 MIR 构建与 codegen 路径调用与Builder::get_dbg_loc的读侧接口二者语义与文档描述的状态机模型一致。小结debuginfo 模块的设计可以浓缩为三点以 LLVM 元数据为中间表示借助DIBuilder而非手写元数据字符串以平滑跨 LLVM 版本演进以缓存为核心CodegenUnitDebugContext持有文件、类型、命名空间三级缓存调用方通过查缓存或创建的统一入口取值以 stub 机制解决类型引用环先占位入缓存后回填成员另以adt_stack检测膨胀式递归并以默认禁用、函数体开始时启用的源码位置发射策略正确切出 prologue。理解这套机制后再去看-g级别差异、Dwarf Version模块标志或调试器中看到的类型/行号行为都能追溯到本模块中对应的具体实现。【免费下载链接】rustEmpowering everyone to build reliable and efficient software.项目地址: https://gitcode.com/GitHub_Trending/ru/rust创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考