调试信息
03 ARM 调试架构讲到核停下来、寄存器读得出来为止。到这一步,调试器手里只有一串数:PC 是 0x080008dc,SP 是 0x200bff80,LR 是 0x0800087b。把它们变成「停在 hf_inner 的第 81 行,它是从 HardFault 里调用的,HardFault 打断了 svc_inner……」,靠的是固件 ELF 里的 DWARF 调试信息,这是 probe-rs-debug 这个 crate 的工作。它是调试主线的后半段。
三章:
- DebugInfo 与 DWARF:
DebugInfo从 ELF 里读哪些节(只有.debug_frame,不读.eh_frame);每个编译单元一个UnitInfo,按地址找函数 DIE 和内联函数;地址怎么查到源码行;变量的类型和位置表达式怎么求值。 - 栈展开:CFI 与异常帧:
unwind的三段循环,CFA 和寄存器规则,Thumb 下 PC 为什么取(LR - 2) & !1;遇到EXC_RETURN时ExceptionInterface怎么从栈上取回被打断的寄存器,Armv6/7-M 与 Armv8-M 各算多大的异常帧;三个 coredump 夹具的展开结果,以及 coredump 缺内存时展开停在哪一帧。 - 变量与单步:
VariableCache这棵树和延迟展开的规则,Rust 类型的特殊处理;SteppingMode怎么用行表和一个借来的硬件断点实现 step over / into / out。
flowchart TB regs["寄存器:PC、SP、LR、xPSR(03 部分)"] --> di["DebugInfo:DWARF 各节、UnitInfo"] di --> unwind["unwind:CFI 一帧帧往回推"] unwind --> exc["ExceptionInterface:跨过异常帧"] exc --> unwind unwind --> frames["StackFrame 列表"] frames --> vars["VariableCache:按需展开变量"] di --> step["SteppingMode:行表加硬件断点"] frames --> dap["09 DAP server:VS Code 的 stackTrace、variables"] vars --> dap step --> dap
只认 MemoryInterface
probe-rs-debug 不依赖任何具体的探针或目标。它的入口函数都只要一个 &mut impl MemoryInterface(读内存)和一组寄存器值。活的目标上,这两样来自 Core;事后分析时,来自一个 CoreDump。CoreDump 实现了 MemoryInterface(会话与目标描述),所以同一套展开代码既能接一块正在调试的板子,也能接一个文件。仓库的测试就是这么做的:tests/debug-unwind-tests/ 下放了一批从真实芯片上抓的 coredump 和对应的 ELF,展开结果用 insta 存成快照,改了展开逻辑就会看到快照的差异。
本卷没有开发板,这一部分的实验全部建立在这些夹具上:自己的实验程序依赖 probe-rs-debug =0.32.0,把夹具文件读进来,调公开的 DebugInfo::unwind、VariableCache::recurse_deferred_variables、DebugInfo::get_source_location。单步需要能动的核,用本卷 03 部分用过的软件模拟目标代替(它不执行指令,只模拟调试寄存器),结论都注明「模拟目标」。真实板子上的展开对照待实测。
主要文件
probe-rs-debug/src/ 共 11 912 行(含文件内测试),依赖 gimli 0.34、addr2line 0.27、object 0.39:
| 文件 | 行数 | 管什么 | 章 |
|---|---|---|---|
debug_info.rs | 2 190 | DebugInfo、unwind、CFA 与寄存器规则、源码行 | 1、2 |
unit_info.rs、function_die.rs | 2 466 / 308 | 编译单元、函数 DIE、类型与位置表达式 | 1、3 |
exception_handling.rs、exception_handling/ | 2 001 | ExceptionInterface 与 Armv6-M / v7-M / v8-M、RISC-V、Xtensa 的实现 | 2 |
variable.rs、variable_cache.rs、language/ | 976 / 753 / 1 418 | 变量、缓存树、Rust 与 C 的特殊处理 | 3 |
debug_step.rs、source_instructions.rs | 460 / 604 | 单步、合法的断点位置 | 3 |
registers.rs、stack_frame.rs、lib.rs | 313 / 96 / 327 | DebugRegisters、StackFrame、错误类型与 ObjectRef | 2 |
本部分的三个夹具:
| 夹具 | 核 | 场景 | 来源 |
|---|---|---|---|
nRF52833_xxAA_hardfault_from_usagefault | Armv7-M(Cortex-M4) | 执行 udf 触发 UsageFault,升级成 HardFault | 一个叫 probe-rs-debugger-test 的测试固件(仓库里只有它的一个 ELF,没有源码) |
RP2040_full_unwind | Armv6-M(Cortex-M0+) | 递归函数 test_deep_stack 停在第 6 层 | 同上 |
stm32u585_nested_exceptions | Armv8-M(Cortex-M33) | 同一个 MSP 上两层嵌套异常:SVCall 里再触发 HardFault | tests/debug-unwind-tests/stm32u585-src/ |
夹具的 DWARF 里记着生成它的机器上的路径;本部分贴出的输出一律改写成 <项目>/src/...、<cargo registry>/...、<rustup>/...。