ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

rustc 调试支持全解析:DWARF、PDB 与 GDB/LLDB/WinDbg 的集成原理

rustc 调试支持全解析:DWARF、PDB 与 GDB/LLDB/WinDbg 的集成原理 rustc 调试支持全解析DWARF、PDB 与 GDB/LLDB/WinDbg 的集成原理【免费下载链接】rustEmpowering everyone to build reliable and efficient software.项目地址: https://gitcode.com/GitHub_Trending/ru/rust本文系统梳理 Rust 编译器rustc中调试支持的整体架构涵盖 DWARF 与 CodeView/PDB 两大调试信息格式、GDB / LLDB / WinDbg 三大调试器的 Rust 语言支持机制、rustc 为弥补 DWARF 语义缺口所做的扩展trait 对象 vtable、tagless 判别联合体等以及源码文件校验和checksum在调试信息中的嵌入方式。读完本文你将理解调试器如何读懂Rust 程序掌握调试信息相关 rustc 配置如-Z source-file-checksum、strip、#[debugger_visualizer]的底层原理与使用前提并了解一条调试信息新特性从 LLVM 到调试器的完整改动链路。本文内容以 rustc 开发指南中的 debugging-support-in-rustc.md 为骨架并结合当前仓库的 rustc 源码compiler/下各 crate与标准库调试可视化文件src/etc/natvis进行印证。若你想调试的是 rustc 编译器本身而非 Rust 程序请参考 compiler-debugging.md。预备知识调试器、DWARF 与 CodeView/PDB在进入 rustc 的具体实现之前先厘清三个基础概念调试器是什么、DWARF 如何组织调试信息、Windows 生态下的 CodeView/PDB 又是什么。这三者是理解后文一切机制的基石。调试器Debugger调试器debugger or debugging tool是一类用于测试和调试其他程序即目标程序的计算机程序。为一种语言从零编写调试器工作量巨大尤其是当调试器需要支持多种平台时。Rust 选择的路线是扩展现有调试器。GDB 和 LLDB 都支持通过扩展机制来支持新语言rustc 正是沿着这条路径让 GDB、LLDB、WinDbg/CDB 能够理解 Rust 的调试信息与表达式语法。DWARFDWARF 是许多编译器和调试器用来支持源码级调试的调试文件格式它面向 C、C、Fortran 等一系列过程式语言设计并具备扩展到其他语言的扩展能力。DWARF 与架构无关适用于任何处理器或操作系统在 Unix、Linux 以及其他操作系统和独立环境中被广泛使用。DWARF 读取器DWARF reader是消费 DWARF 格式并生成调试器可用输出的程序该程序可能就位于编译器内部。DWARF 使用一种名为Debugging Information EntryDIE的数据结构以标签tag形式存储信息用于标注函数、变量等实体例如DW_TAG_variable变量DW_TAG_pointer_type指针类型DW_TAG_subprogram子程序函数DWARF 允许发明自定义的标签tag和属性attribute这正是 rustc 扩展 DWARF 的法律依据后文rustc 的 DWARF 扩展一节会详细展开。CodeView/PDBPDBProgram Database是微软创建的包含调试信息的文件格式WinDbg/CDB 等调试器可以消费 PDB 来展示调试信息。一个 PDB 包含多个流stream描述特定二进制的类型、符号以及编译该二进制所用的源文件等信息。CodeView 是定义 PDB 流中符号记录symbol records与类型记录type records结构格式的另一套规范。rustc 支持的调试器rustc 通过为各主流调试器注入 Rust 语言支持来实现调试不同调试器的实现语言、解析能力和扩展方式各不相同。下面逐一分析。GDBBison 编写的 Rust 表达式解析器要在调试器中展示调试输出首先需要一个表达式解析器。GDB 的 Rust 表达式解析器使用BisonGNU 的 LALR 解析器生成器编写只能解析 Rust 表达式的子集且是从零编写、与 rustc 自身的解析器没有任何关系。GDB 具备 Rust 风格的取值与类型输出能力它可以按近似 Rust 语法的方式打印值和类型在 GDB 中使用ptype命令打印类型时输出同样接近 Rust 源码风格。GDB 手册的 Rust 章节对这些能力有完整说明。解析器扩展为 DWARF 读取器提供特殊支持GDB 的表达式解析器内置了一些针对 Rust 的扩展用于实现 Rust 本身无法直接表达的功能这些扩展依赖 GDB 中 DWARF 读取器的特殊代码。rustc 写入 DWARF 的信息GDB 读取后需要额外理解典型场景有两个枚举Enum支持Rust 编译器把枚举信息写入 DWARFGDB 读取 DWARF 来判断 tag 字段在哪里、是否存在 tag 字段、或者在非零优化niche optimization下 tag 槽位是否与其他字段共享等。这对应 compiler/rustc_codegen_llvm/src/debuginfo 中枚举调试信息的具体编码逻辑。trait 对象剖析Dissect trait objects这是 DWARF 的一种扩展用法——trait 对象在 DWARF 中的描述会同时指向对应 vtable 的一个 stub 描述而该 stub 又指向该 trait 对象实际对应的具体类型。这意味着你可以对 trait 对象执行print *objectGDB 能据此正确找到 trait 对象中负载payload的真实类型。说明GDB 的 Rust 扩展与限制在 GDB 手册中已有记载但手册遗漏了一点——GDB 的便利变量convenience variables和寄存器遵循 GDB 的$约定且 Rust 解析器实现了 GDB 的扩展用于数组切片等场景。LLDBC 递归下降解析器LLDB 的 Rust 表达式解析器使用C编写属于递归下降解析器Recursive Descent parser其实现的 Rust 语言子集比 GDB 略少同样具备 Rust 风格的值与类型输出。开发者经验笔记LLDB 有插件架构但该架构并不适用于语言支持语言支持需要改动 LLDB 核心而非插件。在 Linux 上 GDB 通常表现更好。WinDbg/CDB基于 PDB 与 Natvis 可视化微软的 Windows 调试工具Windows Debugging Tools中的 Windows 调试器WinDbg与控制台调试器CDB都支持调试 Rust 程序。它们从二进制对应的PDB中解析调试信息如果存在构造可视化结果并在调试器中展示。Natvis标准库类型的自定义可视化WinDbg 和 CDB 都支持通过Natvis框架为任意类型定义和查看自定义可视化。rustc 定义了一组 Natvis 文件为标准库std、core、alloc中的一部分类型提供自定义可视化这些文件位于仓库的 src/etc/natvis 目录下共四个libstd.natvisstd库类型可视化libcore.natviscore库类型可视化liballoc.natvisalloc库类型可视化intrinsic.natvis内建类型可视化这些 Natvis 文件会被嵌入到*-pc-windows-msvc目标三元组生成的 PDB 中从而在调试时自动启用这些自定义可视化。默认行为可以被覆盖设置 rustc 的strip标志为debuginfo或symbols即可去除 PDB 中的 Natvis 可视化。通过#[debugger_visualizer]嵌入自定义 Natvis对于标准库之外的 crateRust 提供#[debugger_visualizer]属性来嵌入 Natvis以及 GDB pretty printer文件。其底层实现位于 compiler/rustc_passes/src/debugger_visualizer.rs编译器通过 AST 遍历器DebuggerVisualizerCollector扫描 crate 与模块上的#[debugger_visualizer]属性compiler/rustc_passes/src/debugger_visualizer.rs#L15-L48。解析属性中的可视化文件路径后通过sess.source_map().load_binary_file以二进制方式加载文件内容封装为DebuggerVisualizerFile包含源内容、可视化类型、文件名。查询debugger_visualizers按 AST 顺序收集结果保证确定性compiler/rustc_passes/src/debugger_visualizer.rs#L69-L79。GDB pretty printer 的嵌入细节在 LLVM 后端中GDB pretty printer 被写入二进制的.debug_gdb_scripts段具体实现见 compiler/rustc_codegen_llvm/src/debuginfo/gdb.rs段内容以\x01gdb_load_rust_pretty_printers.py\0开头先加载标准库 pretty printer。随后按 crate 顺序追加通过#[debugger_visualizer]声明的 pretty printer每个 printer 以字节\x04开头告知 GDB 该 printer 以内联方式定义而非独立文件以\0结尾告知 GDB 该 printer 已完整定义可继续搜索后续 printer。该段只对叶子 crateexecutable、dylib、cdylib、staticlib、sdylib生成rlib 和 proc-macro crate 不生成以避免不同 rlib 携带不同 visualizer 在链接期产生 ODR 冲突见 compiler/rustc_codegen_llvm/src/debuginfo/gdb.rs#L84-L116 中needs_gdb_debug_scripts_section的逻辑。Natvis 的嵌入路径Natvis 文件同样经由#[debugger_visualizer]收集在代码生成基类中通过collect_debugger_visualizers_transitive传递收集compiler/rustc_codegen_ssa/src/base.rs 中DebuggerVisualizerType::Natvis分支最终以debugger_visualizer元数据形式进入 rlib 元数据并随链接嵌入 PDB。DWARF 与 rustc复用标准格式并扩展DWARF 是编译器生成、调试器读取的调试信息的标准方式也是 macOS 和 Linux 上唯一的调试格式。它是多语言、可扩展的格式对 Rust 而言基本够用。因此 rustc 的当前实现复用 DWARF 的概念即使 DWARF 中某些概念与 Rust 语义并不完全对齐——因为两者之间通常总能找到某种映射关系。不过rustc 还发射了一些不在 DWARF 标准中的 DWARF 扩展这些扩展由调试器理解。主要有两个扩展一vtable 的DW_AT_containing_typeRust 编译器为虚表virtual table发射 DWARF这个 vtable 对象带有DW_AT_containing_type属性指向真实类型。这让调试器能够剖析 trait 对象指针、正确找到其负载类型。下面是来自 GDB 仓库测试用例的一个典型 DIE 示例11a9: Abbrev Number: 3 (DW_TAG_structure_type) 1aa DW_AT_containing_type: 0x1b4 1ae DW_AT_name : (indirect string, offset: 0x23d): vtable 1b2 DW_AT_byte_size : 0 1b3 DW_AT_alignment : 8可以看到这是一个大小为 0 的DW_TAG_structure_type名为vtable其DW_AT_containing_type指向0x1b4处的具体类型——正是 trait 对象背后真实类型的描述。扩展二tagless 判别联合体tagless discriminated unionrustc 可以发射无 tag 字段的判别联合体Rust 中通过 niche 优化压缩判别的枚举这一项已在 DWARF 标准组织有对应的特性请求issue 180517.2。调试器需要借助枚举的判别信息布局来正确解读这种枚举。DWARF 当前限制Traits在 DWARF 中表示 trait 需要比普通改动更大的变更详见下文DWARF 与 Traits。无法区分结构体与元组DWARF 没有提供区分 Struct 与 Tuple 的手段。rustc 以__0这样的名字发射元组字段调试器通过查找一系列此类名字来克服限制。例如调试器内部会通过x.__0访问字段而 GDB/LLDB 的 Rust 解析器会将其转换为用户友好的x.0语法。DWARF 依赖调试器了解平台 ABI 的某些信息而 Rust 并非总是如此。尚缺的能力与开发笔记这一节来自相关技术演讲介绍调试支持开发中尚未解决或受外部条件制约的问题。macOS 上 LLDB 调试服务器的代码签名macOS 的系统完整性保护System Integrity ProtectionSIP又称 rootless是 OS X El Capitan 引入的安全特性由内核强制一系列机制核心是保护系统所有的文件和目录免受无特定entitlement进程的修改即使以 root 或 sudo 执行也不行。SIP 会阻止进程使用ptrace系统调用进程若想使用ptrace必须经过代码签名且签名证书必须在你的机器上被信任。rustc 调试支持面临的实际问题是可能需要注册 Apple 开发者账号并获取密钥来完成签名。演讲中提到Mozilla 无法代为签名是因为已达其允许签名的密钥数量上限且不确定能否获取更多密钥另一种可行方案是由一个 Rust 法律实体通过 Apple 获取密钥。这本质上不是技术问题。若拥有这样的密钥还可以顺带签名 GDB 并随 Rust 一起分发。DWARF 与 Traitstrait 方法无法直接调用Rust 的 trait完全不会发射到 DWARF 中。由此带来的直接影响是调用 trait 方法x.method()在调试器中无法直接工作——因为该方法由 trait 实现而非类型实现而 DWARF 中没有该信息调试器便无法找到 trait 方法。DWARF 本身有一种接口类型interface type可能为 Java 而加入概念。演讲中的设想是用这种接口类型来表示 trait——DWARF 只处理具体名字而非引用类型因此某个类型的一个 trait 实现就是其中一个接口DW_TAG_interface而该类型会描述它实现的所有接口。这需要一项 DWARF 扩展对应 rustc 的 issue #33014。调试信息改动的典型流程LLVM 路径LLVM 拥有 Debug InfoDI构建器这是 rustc 调用调试信息产出的主要入口。由于 LLVM 先于 DWARF 发射调试信息rustc 并非直接发射 DWARF而是构建一种元数据metadata并移交给 LLVM。在 rustc 与 LLVM 的交接中rustc 调用若干 LLVM DI builder 方法来构建类型的表示。一次调试信息特性改动的完整步骤先改 LLVM。例如 LLVM 目前根本不发射接口类型所以必须在 LLVM 中先实现它并取得 LLVM 维护者的认可。修改 DWARF 扩展。更新调试器更新 DWARF 读取器与表达式求值器。更新 Rust 编译器改为发射这份新信息。该链路在仓库中的对应物是 compiler/rustc_codegen_llvm/src/debuginfo/metadata.rsrustc 的SourceFileHashAlgorithm在这里映射为 LLVM 的ChecksumKindMD5、SHA1、SHA256 分别对应 LLVM 的 MD5/SHA1/SHA256BLAKE3 目前映射为 None因为 LLVM 尚不支持。过程宏Procedural macro的断点步进一个深刻的问题是如何调试过程宏宏展开后你为该展开发射的位置location到底是什么几种候选宏调用处的位置invocation宏定义处的位置definition宏内容的位置content相关 RFC 的核心思路是让宏自己决定如何处理通过某种属性让宏告诉编译器 line marker 应该落在哪里。这直接影响你在哪里设置断点、步进step时发生什么。源码文件校验和checksum嵌入调试信息DWARF 与 CodeViewPDB都支持嵌入参与编译的每个源文件的密码学哈希。该哈希的用途调试器据此验证源文件与可执行文件是否匹配若不匹配则向用户告警哈希还可用于证明某源文件自编译以来未被修改。由于 MD5 与 SHA1 均已被证实存在漏洞此场景下推荐使用 SHA256。rustc 中的实现rustc 把每个源文件的哈希存储在SourceMap中对应SourceFile里外部 crate 输入文件的哈希存储在rlib元数据中。默认哈希算法在目标规范target specification中设置允许每个目标指定其可用的最佳哈希——因为并非所有目标都支持所有哈希算法。当前 rustc 支持的哈希算法枚举见 compiler/rustc_span/src/lib.rs#L1712-L1719算法枚举值LLVM 支持MD5Md5是ChecksumKind::MD5SHA1Sha1是ChecksumKind::SHA1SHA256Sha256LLVM 11 支持ChecksumKind::SHA256BLAKE3Blake3否当前映射为None目标的默认算法选择逻辑在 compiler/rustc_session/src/config.rs 的src_hash_algorithm方法中例如支持 SHA256 的目标默认使用 SHA256否则回退到 MD5对应 compiler/rustc_session/src/config.rs#L1578-L1586。目标哈希算法可以被命令行选项覆盖使用-Z source-file-checksum算法指定对应 rustc 会话选项中的src_hash_algorithm见 compiler/rustc_session/src/options.rs#L2880。另外还有一个相关选项-Z checksum-hash-algorithmchecksum_hash_algorithm见 compiler/rustc_session/src/options.rs#L2420用于 cargo 判断 crate 是否新鲜fresh而非嵌入调试信息。rustc 发射这些校验和时会依据每个源文件的src_hash.kind与 LLVM 的DIFile节点对接在 cranelift 后端中同样有对应逻辑见 compiler/rustc_codegen_cranelift/src/debuginfo/line_info.rs 中has_md5等分支仅在调试信息级别允许时才发射。各编译器/格式的现状DWARF 5支持嵌入 MD5 哈希以校验源文件版本DWARF 5 规范第 6.2.4.1 节的DW_LNCT_MD5操作码。LLVMLLVM IR 的DIFile节点支持 MD5 与 SHA1 源文件校验和LLVM 11 增加 SHA256。MSVCMSVC 编译器通过/ZH编译选项支持在 PDB 中嵌入 MD5、SHA1 或 SHA256 哈希。Clang总是嵌入 MD5 校验和虽然未出现在文档中。未来工作调试支持仍有若干演进方向名称修饰Name mangling变更计划中的工作包括在libibertygcc 源码树中新增 demangler以及在 LLVM 或 LLDB 中新增 demanglerdemangler 源码位置待确认。复用 Rust 编译器求值表达式这是一个重要设想调试器大多不实现类型推断因此你在调试器中输入表达式时必须比源码中更显式——无法直接把源码表达式复制粘贴到调试器中并期望得到相同结果。复用编译器可以改善这一点。这在技术上是可行的但工程量大必须搭建通往调试器的桥bridge因为只有调试器能访问内存。GDBgcc和 LLDBclang都已具备此类能力——LLDB 使用 Clang 将代码 JIT 编译GDB 可对 GCC 做同样的事。另一个需要权衡的点两个调试器的表达式求值实现都同时是 Rust 的超集和子集——它们只实现表达式语言但额外加入了一些扩展如 GDB 的便利变量。因此走这条路不仅需要搭桥还可能需要增加某种模式让编译器理解这些扩展语法。小结rustc 的调试支持遵循扩展成熟调试器的总体路线在 GDB 中通过 Bison 解析器与 DWARF 读取器扩展支持 Rust 表达式与 trait 对象剖析在 LLDB 中通过 C 递归下降解析器实现略少的语言子集在 Windows 上则借助 PDB 与 Natvis 框架提供开箱即用的标准库可视化。rustc 一方面尽量复用 DWARF 标准概念另一方面通过 vtableDW_AT_containing_type、tagless 判别联合体等扩展弥合 Rust 语义与 DWARF 的差距。若你需要在项目中嵌入自定义调试器可视化直接使用#[debugger_visualizer]属性即可其背后由 compiler/rustc_passes/src/debugger_visualizer.rs 收集、由 LLVM 后端写入.debug_gdb_scripts段或 PDB而源码文件校验和的算法选择与覆盖规则则可在 compiler/rustc_session/src/config.rs 与 compiler/rustc_session/src/options.rs 中继续深入研读。【免费下载链接】rustEmpowering everyone to build reliable and efficient software.项目地址: https://gitcode.com/GitHub_Trending/ru/rust创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表