Session、Core 与 MemoryInterface
主线位置:访问主线的最上面一层,三条主线在这里汇合。命令主线里 RPC 服务端存的是一个
Session;访问主线从Core::read_word_32开始;调试主线里 probe-rs-debug 只认MemoryInterface。本页讲这三个类型各管什么、谁持有状态,以及 coredump 为什么能冒充一颗活的芯片。
flowchart TB sess["Session:target、interfaces、cores 状态"] -->|"core(i),每次新建"| core["Core:id、name、target、Box dyn CoreInterface"] core --> ci["CoreInterface:halt、run、step、寄存器、断点"] ci --> mi["MemoryInterface:read_32、write_8 等"] core -.->|"CoreMemoryInterface blanket impl"| mi dump["CoreDump:寄存器表加若干段内存"] --> mi mi --> dbg["probe-rs-debug:栈展开、变量"]
Session:唯一应该长期持有的东西
Session 只有四个字段:
// probe-rs/src/session.rs
pub struct Session {
target: Target,
interfaces: ArchitectureInterface,
cores: Vec<CombinedCoreState>,
configured_trace_sink: Option<TraceSink>,
}
target:从注册表里查出来的目标描述(目标描述:YAML 到注册表)。interfaces:调试接口。ArchitectureInterface有三种:纯 ARM(一个ArmDebugInterface)、ARM 加经 MEM-AP 访问的 RISC-V 核(RP2350,见 RISC-V 调试模块)、JTAG 扫描链上的若干 RISC-V / Xtensa TAP。源码在这里留了一句 TODO:「this is somewhat messy because I omitted separating the Probe out of the ARM interface」。cores:每个核一份CombinedCoreState,里面是架构无关的部分(核在哪个 AP / TAP 上、用哪个调试序列)和架构相关的部分(SpecificCoreState::Armv7m(CortexMState)之类)。Cortex-M 内核讲过的「上次看到的核状态、断点是否已全局打开、pending_step」就存在这里。
Session::new 怎么一步步建出这些字段,见调试序列与 vendor。Session 实现了 Send,文档建议多个使用者(比如 gdb 服务器和 RTT)共享时包一层 Arc<FairMutex<Session>>、轮流使用。
Session 被丢弃时,Drop 先清掉所有硬件断点,再对每个核调 debug_core_stop(Cortex-M 的默认序列写 DHCSR = 0xA05F0000 清掉 C_DEBUGEN,再把 DEMCR 清零,关掉 vector catch;FakeProbe 的日志最后两行就是这两写)。所以命令行退出后芯片不会留着断点,也不会停在下一次复位上。
Core:每次新建的视图
Session::core 每调一次就构造一个新的 Core:
// probe-rs/src/core.rs
pub struct Core<'probe> {
id: usize,
name: &'probe str,
target: &'probe Target,
inner: Box<dyn CoreInterface + 'probe>,
}
inner 是 Armv7m、Riscv32、Xtensa 这些具体类型,它们借着 Session 里的接口和核状态。文档反复强调不要长期持有 Core:「It is strongly advised to never store the Core handle for any significant duration!」理由是 Core 可变借用了整个 Session,拿着它别人就什么都做不了;而状态本来就存在 Session 里,丢掉 Core 什么也不损失。
代价在访问主线上:ARM 目标每次 core(i) 都要重建 MEM-AP 的状态,模拟目标上是 8 次 DP / AP 访问(MEM-AP 与 ROM table)。命令行的轮询循环每 100 ms 调一次 core(i),这 8 次访问也跟着每轮发生;后面 RTT 一章的测量里能看到它在一次轮询里占多大比例。
core(i) 遇到核被禁用(比如 RP2040 的第二个核还在复位里)时返回 Error::CoreDisabled,下一次再试;halted_access 停住所有能停的核、执行闭包、再把原本在跑的核放行,烧录前后会用到。
CoreInterface:架构要实现的方法
CoreInterface 是三种架构的公共面,它本身要求先实现 MemoryInterface:
| 类别 | 方法 |
|---|---|
| 运行控制 | status、halt、run、step、reset、reset_and_halt、wait_for_core_halted、core_halted |
| 寄存器 | read_core_reg、write_core_reg;registers、program_counter、stack_pointer、frame_pointer、return_address 给出寄存器表 |
| 断点 | available_breakpoint_units、hw_breakpoints、enable_breakpoints、set_hw_breakpoint、clear_hw_breakpoint、hw_breakpoints_enabled |
| 属性 | architecture、core_type、instruction_set、endianness、fpu_support、floating_point_register_count、is_64_bit |
| 生命周期 | reset_catch_set / clear、debug_core_stop、enable_vector_catch / disable_vector_catch、spill_registers |
有默认实现的只有 endianness、vector catch 两个、is_64_bit 和 spill_registers(Xtensa 用它把窗口寄存器写回栈上)。Core 自己再实现一遍 CoreInterface,每个方法转给 inner,个别方法在转发前后加一点逻辑:比如 Core::set_hw_breakpoint(addr) 负责找一个空闲比较器,再调 inner 的 set_hw_breakpoint(unit, addr)。
MemoryInterface 与 blanket impl
MemoryInterface<ERR = Error> 要求实现者提供各宽度的块读写(read_8/16/32/64、write_8/16/32/64)、supports_8bit_transfers、supports_native_64bit_access 和 flush;read_word_32 这类单字访问、按对齐自动拆分的 read / write、批量的 execute_memory_operations 都有默认实现。FakeProbe 一章的实验看到过一个后果:MockCore 声称支持 64 位访问,read 就退到逐字节读。
Core 并没有手写这几十个方法。它只实现一个很小的 trait:
// probe-rs/src/memory/mod.rs
pub trait CoreMemoryInterface {
type ErrorType: std::error::Error + From<InvalidDataLengthError> + From<MemoryNotAlignedError>;
/// Returns a reference to the underlying memory interface.
fn memory(&self) -> &dyn MemoryInterface<Self::ErrorType>;
/// Returns a mutable reference to the underlying memory interface.
fn memory_mut(&mut self) -> &mut dyn MemoryInterface<Self::ErrorType>;
}
impl<T> MemoryInterface<Error> for T
where
T: CoreMemoryInterface,
Error: From<<T as CoreMemoryInterface>::ErrorType>,
{
// ... 每个方法转给 memory_mut(),再把错误转成 probe_rs::Error
(memory/mod.rs:396)Core 的 memory_mut 返回 inner,Armv7m 的 memory_mut 返回它持有的 ADIMemoryInterface,于是 core.read_word_32(addr) 两次转发就落到访问主线的 TAR / DRW 上。错误类型也在这里统一:ARM 层的 ArmError 经 From 变成 probe_rs::Error。
CoreDump:没有芯片的 MemoryInterface
CoreDump 是一个普通的可序列化结构:寄存器表、若干段 (地址范围, 字节)、指令集、核类型、FPU 信息。dump_core 从一个 Core 读出所有寄存器和调用方指定的内存范围;store 用 MessagePack 写盘;load 既能读 MessagePack,也能读 32 / 64 位 ELF 格式的 core 文件(只认 RISC-V 和 Xtensa,ESP-IDF 产生的就是这种)。
它也实现了 MemoryInterface(dump.rs:409):读的时候找一段完整包含所请求范围的数据,找不到就报错;所有写方法、flush 和 supports_8bit_transfers 都是 todo!()。probe-rs-debug 的栈展开和变量解析只要求 &mut dyn MemoryInterface,所以同一套代码既能对着活芯片跑,也能对着 coredump 跑,08 调试信息的实验全靠这一点。
实验:在本卷的模拟 SWD 目标(STM32F103C8)上停核、转储 1 KiB RAM,存盘、读回:
== dump_core: 20 registers, ranges [20000000..20000400], core_type Armv7m; DP/AP transfers 399
stored 1338 bytes (MessagePack)
loaded: read_word_32(0x20000000) = 0xcafef00d
loaded: read_32(0x200003f8, 2) = [11111111, 22222222]
loaded: read_word_32(0x20000400) = Err("The coredump does not include the memory for address 0x20000400 of size 0x4")
loaded: read_32(0x200003fc, 2 words, crosses the end) = Err("The coredump does not include the memory for address 0x200003fc of size 0x8")
thread 'main' panicked at <cargo registry>/probe-rs-0.32.0/src/core/dump.rs:463:9:
not yet implemented
loaded: write_word_32 -> panicked (todo!)
- 转储 20 个寄存器加 256 个字,在模拟目标上是 399 次 DP / AP 访问:每个寄存器 3 次以上(DCRSR、等
S_REGRDY、DCRDR),内存走块读。 - 范围检查是「整个请求必须落在同一段里」:跨段末尾的 8 字节读整体失败,哪怕前 4 个字节在段内。栈展开读到 coredump 没存的栈时,报的就是这句「does not include the memory」,栈展开一章会看到它在哪一帧出现。
- 写一律 panic。调试层如果试图改变量(DAP 的 setVariable),对着 coredump 会直接崩,而不是返回错误;DAP 服务器在 coredump 模式下怎么处理这件事未核实。
小结
Session持有目标描述、调试接口和每个核的状态,是唯一该长期持有的句柄;丢弃时清断点、让每个核退出调试。Core是session.core(i)每次新建的短命视图,ARM 上每次新建都要重建 MEM-AP 状态(模拟目标上 8 次访问)。CoreInterface是三种架构的公共面;Core靠CoreMemoryInterface的 blanket impl 免费得到整个MemoryInterface,并在这里统一错误类型。CoreDump也实现MemoryInterface:读按「整段包含」查找,越界报错;写和flush是todo!()。调试层因此能对活目标和 coredump 用同一套代码。