FakeProbe:没有硬件时能测什么
主线位置:访问主线的「替身」。
FakeProbe在最底下替掉了真探针,让 attach、halt、step、烧录计划这些上层代码不插板子也能跑;本页讲它截在哪一层、模拟了什么、哪里会 panic。本卷大量实验建立在它和自己写的模拟目标上。
flowchart TB sess["Session 与 Armv7m"] --> fai["FakeArmInterface"] fai -->|"memory_interface"| mc["MockCore:只认 DHCSR"] mc --> elf["带 ELF 时:按 PT_LOAD 加载地址返回内容"] mc --> zero["其他地址:读 0、写只打印"] fai -->|"read_raw_ap_register"| ops["expect_operation 的脚本,没有就 panic"] fai -->|"其他 DapAccess 方法"| todo["todo!"]
在哪一层截住
probe/fake_probe.rs 771 行,通过 probe_rs::integration::{FakeProbe, Operation} 导出。它实现了 DebugProbe,try_get_arm_debug_interface 返回自己的 FakeArmInterface,而不是 ArmCommunicationInterface。关键在 memory_interface:
// probe-rs/src/probe/fake_probe.rs
fn memory_interface(
&mut self,
access_port_address: &FullyQualifiedApAddress,
) -> Result<Box<dyn ArmMemoryInterface + '_>, ArmError> {
match self.probe.memory_ap {
MockedAp::MemoryAp(ref mut _memory_ap) => {
let memory = ADIMemoryInterface::new(self, access_port_address)?;
Ok(Box::new(memory) as _)
}
MockedAp::Core(ref mut core) => Ok(Box::new(core) as _),
}
}
用 with_mocked_core 建的 FakeProbe,内存接口直接就是 MockCore:Armv7m 读写 0xE000_EDF0 时,调用落进 MockCore::read_32 / write_32,中间没有 DP、AP、SELECT、TAR。所以 FakeProbe 能测「内核层发了哪些内存访问」,测不了 03 ARM 调试架构讲的 DP / AP 层。后者本卷用自己实现 RawDapAccess 的模拟目标来测(见 DebugProbe 与驱动注册表)。
三个构造函数:
| 构造函数 | 内存一侧是什么 | 能走多远 |
|---|---|---|
FakeProbe::new() | MockMemoryAp:经 ADIMemoryInterface,数据来自一块填了花纹的 32 KiB 内存 | 建 MEM-AP 时要写 CSW,FakeArmInterface::write_raw_ap_register 是 todo!,走不远 |
with_mocked_core() | MockCore | attach、halt、step、run、读寄存器;probe-rs gdb --dry-run 用的就是它 |
with_mocked_core_and_binary(elf)(要 test feature) | MockCore 加一份 ELF | 同上,外加按 ELF 的加载地址读出内容 |
MockCore 模拟了什么
MockCore 只有一个寄存器是「活的」,就是 DHCSR:
// probe-rs/src/probe/fake_probe.rs,read_32 里
Dhcsr::ADDRESS_OFFSET => {
let mut dhcsr: u32 = self.dhcsr.into();
if self.is_halted {
dhcsr |= 1 << 17;
}
// Always set S_REGRDY, and say that a register value can
// be read.
dhcsr |= 1 << 16;
写 DHCSR 时检查高 16 位的钥匙 0xA05F,C_HALT 置 1 就进入 halted;C_HALT 为 0 而 C_DEBUGEN、C_STEP 为 1 时(单步)也进入 halted(write_32)。S_REGRDY 永远是 1,所以通过 DCRSR / DCRDR 读寄存器永远「立刻就绪」。
其他地址:
- 读:有 ELF 时,按加载地址查表返回 ELF 里的字节;没有就是 0。
- 写:只打印一行
Write 0x... = 0x...,不保存。所以写了 R0 再读回来还是 0,PC 永远是 0,FP_CTRL 读出来的比较器个数是 0。 read_16、read_64、write_8、write_16、write_64都是todo!。- 日志有一处误导:
read_8和非 DHCSR 的read_32不管读到什么,都先打印Read 0x... = 0,再去 ELF 里取值。下面的实验能看到这一点。
实验一:按加载地址读 ELF
core_with_binary 遍历 ELF 的 program header,把每个非空的 PT_LOAD 段按 p_paddr(加载地址,LMA)登记下来。用仓库自带的 nRF52833 coredump 夹具里的 ELF 看一下它的段(自己写的小程序用 object crate 读的):
segment vaddr=0x00000000 file_off=0x10000 filesz=0x100
segment vaddr=0x00000100 file_off=0x10100 filesz=0x5fe8
segment vaddr=0x000060e8 file_off=0x160e8 filesz=0x212c
segment vaddr=0x20000000 file_off=0x20000 filesz=0x70
segment vaddr=0x20000070 file_off=0x30070 filesz=0x0
section .data addr=0x20000000 size=0x70
.data 运行在 RAM 的 0x2000_0000,初值存在 flash 里 .rodata 后面的 0x8218(烧录计划里的第二块,见从镜像到烧录计划)。nRF52 的调试序列 attach 时要读 CTRL-AP(实验三),先用 expect_operation 应付它,然后读几个地址:
Read 0x00000000 = 0
== read_word_32(0x00000000) = 0x20004000
Read 0x00000004 = 0
== read_word_32(0x00000004) = 0x00000101
Read 0x000000fc = 0
== read_word_32(0x000000fc) = 0x00000000
Read 0x20000000 = 0
== read_word_32(0x20000000) = 0x00000000
Read 0x00008218 = 0
== read_word_32(0x00008218) = 0xffffffff
Read 0x00008280 = 0
== read_word_32(0x00008280) = 0x00006764
Read 0x00008284 = 0
== read_word_32(0x00008284) = 0x00000000
读出三件事:
- 向量表的前两项(初始 SP
0x20004000、复位向量0x101)读对了,而每行前面的Read ... = 0是上面说的误导性打印。 - RAM 里的变量读不到:
0x2000_0000是.data的运行地址,读出 0;它的初值要到加载地址0x8218才读得到。这对测 RTT 很要紧:控制块在 RAM 里,FakeProbe上永远找不到它(RTT 控制块与通道)。 - 每个段的最后 4 个字节读不到:
0xfc是第一个段(向量表,filesz=0x100)的最后一个字,文件里是0x00000671,读出 0;0x8284是.data初值的最后一个字,文件里是0x55,也读出 0。原因在LoadableSegment::contains:
// probe-rs/src/probe/fake_probe.rs
fn contains(&self, physical_address: u64, len: u64) -> bool {
physical_address >= self.physical_address
&& physical_address < (self.physical_address + self.size - len)
}
右边界是 <,应该是 <=:长度为 len 的访问从 start + size - len 开始正好落在段内,却被判为不在。这是测试辅助代码的边界问题,不影响真实探针,但用 FakeProbe 做快照测试时,段末尾的数据会悄悄变成 0。
实验二:halt、step 和断点
with_mocked_core 上 attach STM32F103C8,再调几个内核操作(只看结果行):
== status: Running
== halt: CoreInformation { pc: 0 }
== step: CoreInformation { pc: 0 }
== breakpoint units: 0
== set_hw_breakpoint: Err(Other("No available hardware breakpoints"))
== run: Ok(())
== status after run: Running
halt / step / run 的寄存器序列都是对的(下一节逐行对照),但因为写不保存,PC 读出来永远是 0,FP_CTRL 读出 0 个比较器,于是设硬件断点必然失败。
实验三:脚本化 AP 读
除了 MockCore,FakeArmInterface 只实现了 DapAccess 的一个方法:read_raw_ap_register,它从一个操作队列里取预先登记的应答(fake_probe.rs:405),队列空了就 panic。用本机的 probe-rs 对 nRF52 做 dry-run,正好撞上:
$ probe-rs gdb --dry-run --chip nRF52840_xxAA --gdb-connection-string 127.0.0.1:13582
thread 'main' (2794366) panicked at probe-rs/src/probe/fake_probe.rs:423:21:
No more operations expected, but got read_raw_ap_register ap=FullyQualifiedApAddress { dp: Default, ap: V1(1) }, address:12
exit=101
AP 1、地址 12(0x0C)是 nRF52 的 CTRL-AP 的 APPROTECTSTATUS:nRF52 的调试序列在 debug_device_unlock 里先读它,看芯片有没有被读保护锁住(调试序列与 vendor)。命令行没法往 FakeProbe 里塞脚本,库可以:
// 本站实验,依赖 probe-rs =0.32.0(features = ["test"])
let fake = FakeProbe::with_mocked_core();
fake.expect_operation(Operation::ReadRawApRegister {
ap: FullyQualifiedApAddress::v1_with_default_dp(1),
address: 0x0C,
result: 1, // 非 0:没有被锁
});
let mut session = fake.into_probe().attach("nRF52840_xxAA", Permissions::default())?;
== attached nRF52840_xxAA, cores=[(0, Armv7m)]
== RAM word = 0x0
不登记这一条,同一段代码在库里也是那个 panic(<cargo registry>/probe-rs-0.32.0/src/probe/fake_probe.rs:423:21)。所以「gdb --dry-run 选 nRF52 就 panic」是 FakeProbe 的边界,不是用户的错(常见坑里讲 dry-run 时提到它)。
实验四:gdb --dry-run + lldb
probe-rs gdb --dry-run 起的 gdb 服务器背后就是 MockCore,它把每次访问打印出来。用 macOS 自带的 lldb 连上去(gdb-remote 127.0.0.1:13581),依次执行几条命令,对照服务端的打印:
| lldb 命令 | 服务端打印(节选) | 含义 |
|---|---|---|
| 连接 | Write DHCSR = 0xa05f0003,读 DFSR、写 0x1f 清掉,Write 0xe000edf4 = 0x0000000f、读 0xe000edf8,再对寄存器 0 做一遍,然后从 0x0 起读 512 个字节 | halt;读停下的原因;DCRSR 选 15 号寄存器(PC),从 DCRDR 读值,再读 R0;lldb 按 512 字节一块读停下位置附近的内存来反汇编 |
memory read ... 0x08000000 | 从 0x08000000 起读 512 个字节,每字节一行 | 同样按块读;逐字节是因为 MockCore 声称支持 64 位访问,MemoryInterface::read 于是走 read_8(memory/mod.rs:154) |
register write r0 0x1234 | Write 0xe000edf8 = 0x00001234,Write 0xe000edf4 = 0x00010000 | 先把值写进 DCRDR,再写 DCRSR:REGWnR(bit 16)置 1、寄存器号 0 |
breakpoint set 后 process continue | Write 0xe0002000 = 0x00000003,读 0xe0002000 得 0,Target threw a fatal error: No available hardware breakpoints | 继续运行前才真正下断点:先写 FP_CTRL 的 KEY 和 ENABLE,再读比较器个数;0 个,失败,lldb 报 Resume timed out |
退出时服务端还打印了一行 Write 0xe0042004 = 0x00000000:STM32 的调试序列在断开时把 attach 时保存的 DBGMCU_CR 写回去。连接时 lldb 还读了 0xffff0110 起和 0xffff1010 起各 4 个字节,这是 lldb 自己发起的,用途未核实。这些寄存器的含义在 Cortex-M 内核里逐个讲。
25 处边界
fake_probe.rs 里一共 25 处 todo!() / unimplemented!():
| 位置 | 个数 | 哪些操作会撞上 |
|---|---|---|
MockCore 的 MemoryInterface:16 / 64 位读写、8 位写、supports_8bit_transfers | 6 | 任何按字节写、按半字读写的操作 |
MockCore 的 ArmMemoryInterface:base_address、fully_qualified_address、get_arm_debug_interface、generic_status | 4 | 遍历 ROM table、复位后重连调试接口 |
MockCore 的 SwdSequence | 2 | 在内存接口上发 SWJ 序列 |
FakeProbe 的 RawDapAccess:jtag_sequence、swj_sequence、swj_pins | 3 | 任何需要线复位、引脚控制的序列 |
FakeArmInterface 的 DapAccess:DP 读写、AP 写、AP 块读写 | 5 | 直接访问 DP、写 AP(比如 nRF 的擦除解锁) |
FakeArmInterface 的 SwoAccess | 3 | SWO |
target_reset_assert | 1 | --connect-under-reset |
非 ELF 文件交给 with_mocked_core_and_binary | 1 | 传 BIN / HEX |
--connect-under-reset 一行可以直接验证:
$ probe-rs gdb --dry-run --connect-under-reset --chip STM32F103C8 --gdb-connection-string 127.0.0.1:13583
thread 'main' (2822253) panicked at probe-rs/src/probe/fake_probe.rs:539:9:
not implemented
另外,FakeArmInterface::access_ports 不管什么 DP 都只返回 AP 1,复位相关的方法(target_reset)返回「不支持」。这些边界加起来决定了本卷的分工:上层的流程用 FakeProbe 和 gdb --dry-run 看;DP / AP 和线上的细节用自己写的模拟目标看;时序、真实寄存器值和 RAM 内容等待实测。
小结
FakeProbe在内存接口这一层截住访问:with_mocked_core的MockCore只模拟 DHCSR(halt / step 置S_HALT,S_REGRDY恒为 1),其他地址读 0、写不保存;DP / AP 层它看不到。- 带 ELF 时按
PT_LOAD的加载地址返回内容,所以 RAM 里的变量读不到;contains的右边界差一个,每个段最后 4 个字节也读成 0。 expect_operation只能脚本化read_raw_ap_register;不登记时,nRF52 的 attach 读 CTRL-AP 就 panic。gdb --dry-run加 lldb 能把每条调试命令对照到 DHCSR、DCRSR / DCRDR、FP_CTRL 的读写;断点必然失败,因为比较器个数读出来是 0。- 25 处
todo!/unimplemented!覆盖 16 / 64 位访问、DP 访问、AP 写、SWJ 序列、SWO 和 connect-under-reset。