用 QEMU 的 GDB 服务端展开活固件
主线位置:调试主线的后半段,换一个后端。08 调试信息说 probe-rs-debug 只认
MemoryInterface,所以能接活芯片也能接 coredump;本章再接第三种:QEMU 里正在跑的固件,经 QEMU 自带的 GDB 服务端读。
sequenceDiagram
participant Q as QEMU:-gdb tcp:127.0.0.1:13600 -S
participant C as 自写 RSP 客户端
participant D as probe-rs-debug
C->>Q: c,300 ms 后发 0x03
Q-->>C: T02:停住了
C->>Q: p0 到 pf、p19、p2b、p2c
Q-->>C: 19 个寄存器
C->>D: CoreDump 只填寄存器,DebugRegisters::from_coredump
C->>D: unwind(&mut 客户端, ...)
loop 每读一次栈
D->>C: read_32(addr)
C->>Q: m addr,4
Q-->>C: 十六进制
end
D-->>C: 9 帧
为什么要自己写
probe-rs 源码里有 GDB 远程协议(RSP)的服务端(probe-rs gdb,基于 gdbstub,见 gdb server),没有客户端;QEMU 的 -gdb 也是服务端。两个服务端接不起来,所以 embassy 卷在 QEMU 上跑的固件,probe-rs 碰不到。要接,得自己写一个客户端,再找 probe-rs 公开 API 里能接受「外来数据」的入口。
先确定入口,试的结果:
| 想用的 | 能不能在库外用 | 结论 |
|---|---|---|
DebugInfo::unwind(&mut impl MemoryInterface, DebugRegisters, ..) | 能。MemoryInterface 是公开 trait | 读内存这一半自己实现 |
DebugRegisters | 是公开元组结构体,但要 &'static CoreRegister;from_coredump(&CoreDump) 能造 | 借 CoreDump:它的 7 个字段全是 pub |
Rtt::attach_at(core: &mut Core, ..) | 不能。Core::new 是 pub(crate) | RTT 只能手工解析 |
RttControlBlockHeader::try_from_header | 不能。类型本身不是 pub | 同上 |
rtt::find_rtt_control_block_in_raw_file(&[u8]) | 能 | 用它从 ELF 拿控制块地址 |
固件和 QEMU
固件就是cargo runner 与 defmt 三件套里那个:defmt-rtt 打日志,递归三层,到底以后原地自旋:
// 本站实验固件
#[inline(never)]
fn level(depth: u32) -> u32 {
if depth == 0 {
defmt::info!("reached the bottom, spinning");
loop {
SPINS.fetch_add(1, Ordering::Relaxed);
}
}
defmt::info!("level {=u32}", depth);
level(depth - 1) + depth
}
QEMU 11.1.3 用 embassy 卷同一个机器,GDB 服务端只绑本机,-S 让 CPU 停在复位处等客户端:
qemu-system-arm -M b-l475e-iot01a -display none -kernel fw.elf -gdb tcp:127.0.0.1:13600 -S
QEMU 的 target.xml 给出了寄存器编号:r0–r15 是 0–15,xpsr 是 25,msp、psp 是 43、44(M-profile 系统寄存器那个 feature)。probe-rs 的 Cortex-M RegisterId 是 R0–R15 为 0–15、XPSR 16、MSP 17、PSP 18,和 DCRSR 的 REGSEL 编号一致(Cortex-M 内核)。两张表对一下就是 19 行的映射。
客户端:三个函数
RSP 的包是 $内容#两位校验和,收到后回一个 +。客户端只需要发包、收包和一个「发 0x03 打断」:
// 本站实验:RSP 客户端的核心
fn send(&mut self, body: &str) -> Result<()> {
let sum = body.bytes().fold(0u8, |a, b| a.wrapping_add(b));
self.stream.write_all(format!("${body}#{sum:02x}").as_bytes())?;
Ok(())
}
fn read_mem(&mut self, addr: u64, out: &mut [u8]) -> Result<()> {
let reply = self.request(&format!("m{addr:x},{:x}", out.len()))?;
if reply.starts_with('E') {
bail!("RSP {reply} reading {addr:#x}");
}
out.copy_from_slice(&hex_bytes(&reply)?);
Ok(())
}
recv 跳过 +、读到 # 为止再读两位校验和、回 +,十几行。让固件跑一会儿再停:发 c,睡 300 ms,往 socket 写一个字节 0x03,QEMU 回停止包。
接到 probe-rs-debug
读内存:把客户端实现成 MemoryInterface。按宽度的块读都落到一个 m 包,小端拼回去;写方法返回「未实现」,展开用不到:
// 本站实验
macro_rules! read_words {
($name:ident, $t:ty) => {
fn $name(&mut self, address: u64, data: &mut [$t]) -> Result<(), probe_rs::Error> {
const N: usize = size_of::<$t>();
let mut raw = vec![0u8; data.len() * N];
self.read_mem(address, &mut raw).map_err(rsp_err)?;
for (d, c) in data.iter_mut().zip(raw.chunks_exact(N)) {
*d = <$t>::from_le_bytes(c.try_into().unwrap());
}
Ok(())
}
};
}
impl MemoryInterface for Rsp {
read_words!(read_32, u32);
// ... read_64、read_16、read_8,write_* 返回 Error::NotImplemented
}
寄存器:造一个没有内存段的 CoreDump,只填寄存器表,再交给 DebugRegisters::from_coredump:它按 core_type 找到 Cortex-M 的寄存器描述,给前 32 个配上 DWARF 编号,这些都是我们在库外做不了的事。
// 本站实验
let dump = CoreDump {
registers, // HashMap<RegisterId, RegisterValue>,19 个,来自 p 包
data: vec![],
instruction_set: InstructionSet::Thumb2,
supports_native_64bit_access: false,
core_type: CoreType::Armv7em,
fpu_support: false,
floating_point_register_count: None,
};
let frames = debug_info.unwind(
&mut rsp,
DebugRegisters::from_coredump(&dump),
exception_handler_for_core(CoreType::Armv7em).as_ref(),
Some(InstructionSet::Thumb2),
50,
)?;
结果
== qSupported -> PacketSize=20020;qXfer:features:read+;vContSupported+;multiprocess+
== ? -> T05core:01;
== c, 300 ms, ^C -> T02thread:01;
pc=0x08000522 sp=0x20017f80 lr=0x08000a57 xpsr=0x61000000
== unwind: 9 frames
#0 pc=0x08000522 inl cfa=0x20017f98 atomic_add<u32, u32> <rustup>/.../library/core/src/sync/atomic.rs:4029
#1 pc=0x08000522 inl cfa=0x20017f98 Atomic<u32>::fetch_add <rustup>/.../library/core/src/sync/atomic.rs:3220
#2 pc=0x08000522 cfa=0x20017f98 level <项目>/src/main.rs:18
#3 pc=0x08000500 cfa=0x20017fb0 level <项目>/src/main.rs:22
#4 pc=0x08000500 cfa=0x20017fc8 level <项目>/src/main.rs:22
#5 pc=0x08000500 cfa=0x20017fe0 level <项目>/src/main.rs:22
#6 pc=0x080004ae cfa=0x20017ff8 __cortex_m_rt_main <项目>/src/main.rs:28
#7 pc=0x08000536 cfa=0x20018000 __cortex_m_rt_main_trampoline <项目>/src/main.rs:25
#8 pc=0x0800043a cfa=- Reset
RSP during unwind: 20 packets, 20 m-packets, 80 bytes
逐帧对得上源码:停在第 18 行的 fetch_add 里(两层内联帧来自 core::sync::atomic);往上是三层递归 level,每层 CFA 差 0x18,PC 都落在第 22 行 level(depth - 1) 的调用处((LR - 2) & !1 的效果,见栈展开);再往上是 main 的第 28 行、cortex-m-rt 的跳板和 Reset。最外一帧的 CFA 0x20018000 是 RAM 的顶端(96 KiB 的 RAM 从 0x20000000 起),也就是初始栈指针。
整个展开读了 20 次、每次 4 字节,共 80 字节。换成真探针,这 20 次读各是一次 MEM-AP 访问,加上 DebugRegisters::from_core 读寄存器的开销(栈展开)。
把读回来的 R7 改成坏值再展开,能复现 ERROR: UNWIND 这个坑的两种形态:指到没映射的地址时最后一帧挂着 RSP E14;指到 RAM 里的错误位置时调用栈静默地少了 4 帧。
RTT:只能手工
Rtt::attach_at 要 &mut Core,而 Core 只能由 Session::core 造,Session 又只能经探针 attach 得到。能用的只有「从 ELF 找控制块地址」那一个函数,剩下照 RTT 控制块与通道的布局手工读:
== _SEGGER_RTT at 0x20000008: id="SEGGER RTT" max_up=1 max_down=0
up[0] name="defmt" buffer=0x20000048 size=1024 WrOff=29 RdOff=0 flags=1
first 29 bytes of the ring: [00, 03, 08, 70, 3a, 72, 6f, 62, 65, 2d, 72, 73, 80, 00, 04, 03, 7a, 00, 04, 02, 7a, 00, 04, 01, 7a, 00, 05, 7e, 00]
flags=1是defmt-rtt的初值NoBlockTrim:没有 probe-rs 连上过,没人把它改成BlockIfFull。RdOff=0:也没人读过。- 29 字节是 5 帧 defmt 日志,以
0x00分隔(rzCOBS 编码)。能直接认出来的:中间三帧04 03 7a、04 02 7a、04 01 7a,首字节 4 是.defmt里level {=u32}的索引,第二个字节是参数 3、2、1;最后一帧首字节 5 是「reached the bottom, spinning」;第一帧里夹着{=str}参数「probe-rs」的 ASCII。完整解码要用 defmt-decoder,本机缓存里没有,这里是按 ELF 里的索引手工对照的。
想让 probe-rs 的 Rtt、单步、断点这些需要 Core 的功能也作用在 QEMU 上,得换一层接:实现一个 DebugProbe,在它的 RawDapAccess 里把 MEM-AP 的 TAR / DRW 访问翻译成 m / M 包,把 DHCSR 的 C_HALT 翻译成 0x03、清 C_HALT 翻译成 c,把 DCRSR / DCRDR 翻译成 p / P,FP_COMPn 翻译成 Z1 / z1。那样 Probe::new(..).attach(..) 得到的就是一个真正的 Session。这比本章大一个量级,留给课程。
小结
- probe-rs 只有 GDB 服务端,QEMU 也只有服务端;要接,得自己写一个 RSP 客户端,核心是收发
$…#xx包和0x03打断。 - probe-rs-debug 的展开只要两样外来数据:
MemoryInterface(公开 trait,m包实现)和DebugRegisters(借CoreDump的公开字段和from_coredump造)。 - 对 QEMU 里自旋的固件展开出 9 帧,一直到
Reset,只读了 20 次、80 字节。 - RTT 卡在
Rtt::attach_at(&mut Core):Core和控制块头的类型都不能在库外构造,只能从 ELF 拿地址后手工解析;要完整接入,得把 RSP 包成一个DebugProbe。