Rust in Everything
基于 probe-rs 0.32.0

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 用同一套代码。

💬 关联讨论

发起讨论
加载中...