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 范围内。