Rust in Everything
基于 probe-rs-debug 0.32.0

ERROR: UNWIND: Failed to read value for register …

主线位置:调试主线后半段的「CFI 栈展开」。展开每一步都要按 CFI 规则从栈上读回调用者的寄存器;读不到时的处理在栈展开:CFI 与异常帧里讲过。本页把它当成用户会看到的报错:在哪里看到、为什么、哪种情况更危险。

flowchart TB
  regs["停下时的 SP、R7、LR、PC"] --> cfa["CFA = R7 或 SP 加偏移"]
  cfa --> rd{"读 CFA 附近的栈"}
  rd -->|"读失败"| err["最后一帧名字后面挂 ERROR: UNWIND,停止"]
  rd -->|"读到了"| ok{"地址是对的栈吗"}
  ok -->|是| next["下一帧"]
  ok -->|"不是,但在 RAM 里"| wrong["读出垃圾:帧被跳过或乱接,没有任何报错"]

现象

它出现在三个地方:probe-rs run 在 HardFault 后打印的调用栈、VS Code 的调用栈窗口、对 coredump 的离线分析。形式都一样:展开不失败,只是最后一帧的函数名后面接着一段错误,后面再没有帧。仓库自带的 nRF52833 coredump 夹具(栈展开):

#5  pc=0x00000184     cfa=0x20003f60 trigger_hardfault_from_usagefault : ERROR: UNWIND: Failed to read value for register R14/LR from address 0x0000000020003f5c (4 bytes): The coredump does not include the memory for address 0x20003f5c of size 0x4

活目标上也一样。下面用 13 动手里那个「QEMU 加 GDB 远程协议」的 MemoryInterface,对 QEMU 里正在跑的固件展开;正常是 9 帧,到 Reset 为止。把读回来的 R7(这个固件里各函数的 CFA 规则都以 R7 为基准)改成一个没有映射的地址 0x90000000:

== unwind: 3 frames
#0  pc=0x08000522 inl cfa=0x90000008 atomic_add<u32, u32>  <rustup>/.../library/core/src/sync/atomic.rs:4029
#1  pc=0x08000522 inl cfa=0x90000008 Atomic<u32>::fetch_add  <rustup>/.../library/core/src/sync/atomic.rs:3220
#2  pc=0x08000522     cfa=0x90000008 level : ERROR: UNWIND: Failed to read value for register R4 from address 0x000000008ffffff8 (4 bytes): RSP E14 reading 0x8ffffff8  <项目>/src/main.rs:18

冒号后面那一截是 MemoryInterface 返回的原始错误:coredump 是「does not include the memory」,GDB 远程协议是 E14,真探针上会是 MEM-AP 的总线错误之类(待实测)。

从哪来

unwind_register_using_rule 处理 Offset(N) 规则时,从 CFA + N 读 4 字节,失败就返回:

// probe-rs-debug/src/debug_info.rs
                    return Err(Error::Other(format!(
                        "UNWIND: Failed to read value for register {} from address {} ({} bytes): {}",
                        debug_register,
                        RegisterValue::from(previous_frame_register_address),
                        4,
                        error
                    )));

unwind_impl 接住这个错误,不往上抛,而是 format!("{} : ERROR: {error}", first_frame.function_name) 改写最后一帧的名字,然后结束循环(debug_info.rs:734)。读异常帧那 8 个字失败时则是多插一帧,名字是「UNWIND: Error while checking for exception context. The stack trace will not include the calling frames.」(debug_info.rs:775)。两种都让 unwind 返回 Ok:能展开多少给多少,这对调试器是对的选择,但调用方如果只看 Result,会以为一切正常。

更危险的情况:读得到,但读错了

把 R7 改成一个在 RAM 里、只是不对的地址 0x20017ff0(真实值是 0x20017f90):

== unwind: 5 frames
#0  pc=0x08000522 inl cfa=0x20017ff8 atomic_add<u32, u32>  <rustup>/.../library/core/src/sync/atomic.rs:4029
#1  pc=0x08000522 inl cfa=0x20017ff8 Atomic<u32>::fetch_add  <rustup>/.../library/core/src/sync/atomic.rs:3220
#2  pc=0x08000522     cfa=0x20017ff8 level  <项目>/src/main.rs:18
#3  pc=0x08000536     cfa=0x20018000 __cortex_m_rt_main_trampoline  <项目>/src/main.rs:25
#4  pc=0x0800043a     cfa=-          Reset

没有任何错误,调用栈「完整」地展开到了 Reset,可三层 level 和 main 都不见了:CFA 落到了栈顶附近,从那里读回的返回地址恰好属于 __cortex_m_rt_main_trampoline,于是直接接了上去。CFI 展开只是按规则读内存,它没有办法知道读到的值是不是一个真的返回地址。

所以看到一个「短得奇怪」的调用栈,要和看到 ERROR: UNWIND 一样警惕。

什么时候会这样

场合原因
coredump只存了部分内存。DAP 调试控制台的 dump 命令不带参数时只转储当前作用域用到的内存(栈展开),栈上更深的帧不在里面
活目标,栈溢出SP 越过了 RAM 的下界,或者踩坏了栈上保存的 R7 / LR;HardFault 之后的展开在第一个坏帧停下
活目标,内存踩坏缓冲区溢出覆盖了栈上的返回地址或保存的 R7:可能报错,也可能像上一节那样静默地接错
没有 CFI不是这个报错,而是只靠 LR 推一层就停(栈展开)

怎么办

  • 做 coredump 时显式给范围:dump <起始地址> <字节数> [路径],把整个栈(从 RAM 顶端往下足够大的一段)都存进去;仓库夹具里能展开到 Reset 的那一个,就存了栈顶的 8 KiB。
  • 活目标上先看初始寄存器:probe-rs run 打印的 HardFault 调用栈、VS Code 的 Registers 作用域里,SP 和 R7 是否落在芯片的 RAM 范围内(probe-rs chip info <芯片> 列出 RAM 区)。不在范围内,后面的帧都不可信。
  • 帧数比预期少、而且中间少了你确定调用过的函数时,不要信它;在可疑的调用点下断点,从正常状态展开一次对照。

小结

  • 「ERROR: UNWIND: Failed to read value for register …」是按 CFI 规则读栈失败,错误接在最后一帧的名字后面,unwind 仍返回 Ok。
  • 冒号后面是底层 MemoryInterface 的原始错误:coredump 缺内存、远程协议的 E 应答、探针的总线错误。
  • 帧指针或栈坏了但仍指向 RAM 时没有报错,调用栈静默地少帧或乱接,比报错更难发现。
  • coredump 存完整的栈;活目标先检查 SP、R7 是否在 RAM 范围内。

💬 关联讨论

发起讨论
加载中...