Repository: probe-rs/probe-rs
Pull request #2740 (2024-08-04 08:44:10)

Semihosting: print to console
If there is already a semihosting command cached, the previous code read PC, read around the PC, and then did nothing. This PR removes these reads, while making additional cleanups.

This PR also implements limited handling of SYS_OPEN, SYS_CLOSE and WRITE to allow the `semihosting` crate to print panic messages.


Patch:
diff --git a/changelog/added-semihosting-stdout.md b/changelog/added-semihosting-stdout.md
new file mode 100644
index 00000000..0515cd53
--- /dev/null
+++ b/changelog/added-semihosting-stdout.md
@@ -0,0 +1,1 @@
+Added support to probe-rs-tools to print messages using `semihosting`.
\ No newline at end of file
diff --git a/changelog/changed-semihosting.md b/changelog/changed-semihosting.md
new file mode 100644
index 00000000..1295bd5b
--- /dev/null
+++ b/changelog/changed-semihosting.md
@@ -0,0 +1,1 @@
+The `semihosting` module is now public, and the reexports have been removed from `probe_rs`.
\ No newline at end of file
diff --git a/changelog/fixed-halting.md b/changelog/fixed-halting.md
new file mode 100644
index 00000000..dc96acb9
--- /dev/null
+++ b/changelog/fixed-halting.md
@@ -0,0 +1,1 @@
+Fix excessive halting in `probe-rs attach` which may leave devices in a non-functional state
\ No newline at end of file
diff --git a/probe-rs-tools/src/bin/probe-rs/cmd/read.rs b/probe-rs-tools/src/bin/probe-rs/cmd/read.rs
index 725a589d..0fd6c097 100644
--- a/probe-rs-tools/src/bin/probe-rs/cmd/read.rs
+++ b/probe-rs-tools/src/bin/probe-rs/cmd/read.rs
@@ -34,6 +34,7 @@ pub struct Cmd {
 impl Cmd {
     pub fn run(self, lister: &Lister) -> anyhow::Result<()> {
         let (mut session, _probe_options) = self.probe_options.simple_attach(lister)?;
+
         let mut core = session.core(self.shared.core)?;
         let words = self.words as usize;
 
@@ -63,6 +64,9 @@ impl Cmd {
                 println!();
             }
         }
+        std::mem::drop(core);
+
+        session.resume_all_cores()?;
 
         Ok(())
     }
diff --git a/probe-rs-tools/src/bin/probe-rs/cmd/run/mod.rs b/probe-rs-tools/src/bin/probe-rs/cmd/run/mod.rs
index 81e3209e..6ad19052 100644
--- a/probe-rs-tools/src/bin/probe-rs/cmd/run/mod.rs
+++ b/probe-rs-tools/src/bin/probe-rs/cmd/run/mod.rs
@@ -1,10 +1,12 @@
 mod normal_run_mode;
 use normal_run_mode::*;
 mod test_run_mode;
+use probe_rs::semihosting::SemihostingCommand;
 use test_run_mode::*;
 
 use std::fs::{self, File};
 use std::io::{Read, Write};
+use std::num::NonZeroU32;
 use std::path::{Path, PathBuf};
 use std::sync::atomic::{AtomicBool, Ordering};
 use std::sync::Arc;
@@ -97,6 +99,9 @@ impl Cmd {
         let (mut session, probe_options) =
             self.shared_options.probe_options.simple_attach(lister)?;
 
+        // Resume cores now to prevent halting while processing elf
+        session.resume_all_cores()?;
+
         let rtt_scan_regions = match self.shared_options.rtt_scan_memory {
             true => session.target().rtt_scan_regions.clone(),
             false => ScanRegion::Ranges(vec![]),
@@ -349,11 +354,15 @@ impl RunLoop {
             // this is important so we do one last poll after halt, so we flush all messages
             // the core printed before halting, such as a panic message.
             let mut return_reason = None;
+            let mut was_halted = false;
             match core.status()? {
                 probe_rs::CoreStatus::Halted(reason) => match predicate(reason, core) {
                     Ok(Some(r)) => return_reason = Some(Ok(ReturnReason::Predicate(r))),
                     Err(e) => return_reason = Some(Err(e)),
-                    Ok(None) => core.run()?,
+                    Ok(None) => {
+                        was_halted = true;
+                        core.run()?
+                    }
                 },
                 probe_rs::CoreStatus::Running
                 | probe_rs::CoreStatus::Sleeping
@@ -387,9 +396,12 @@ impl RunLoop {
             // Poll RTT with a frequency of 10 Hz if we do not receive any new data.
             // Once we receive new data, we bump the frequency to 1kHz.
             //
+            // We also poll at 1kHz if the core was halted, to speed up reading strings
+            // from semihosting. The core is not expected to be halted for other reasons.
+            //
             // If the polling frequency is too high, the USB connection to the probe
             // can become unstable. Hence we only pull as little as necessary.
-            if had_rtt_data {
+            if had_rtt_data || was_halted {
                 thread::sleep(Duration::from_millis(1));
             } else {
                 thread::sleep(Duration::from_millis(100));
@@ -514,3 +526,103 @@ fn poll_rtt<S: Write + ?Sized>(
 
     Ok(out.had_data)
 }
+
+struct SemihostingPrinter {
+    stdout_open: bool,
+    stderr_open: bool,
+}
+
+impl SemihostingPrinter {
+    const STDOUT: u32 = 1;
+    const STDERR: u32 = 2;
+
+    pub fn new() -> Self {
+        Self {
+            stdout_open: false,
+            stderr_open: false,
+        }
+    }
+
+    pub fn handle(
+        &mut self,
+        command: SemihostingCommand,
+        core: &mut Core<'_>,
+    ) -> Result<(), Error> {
+        match command {
+            SemihostingCommand::Open(request) => {
+                let path = request.path(core)?;
+                if path == ":tt" {
+                    match request.mode().as_bytes()[0] {
+                        b'w' => {
+                            self.stdout_open = true;
+                            let fd = NonZeroU32::new(Self::STDOUT).unwrap();
+                            request.respond_with_handle(core, fd)?;
+                        }
+                        b'a' => {
+                            self.stderr_open = true;
+                            let fd = NonZeroU32::new(Self::STDERR).unwrap();
+                            request.respond_with_handle(core, fd)?;
+                        }
+                        other => {
+                            tracing::warn!(
+                                "Target wanted to open file {path} with mode {mode}, but probe-rs does not support this operation yet. Continuing...",
+                                path = path,
+                                mode = other
+                            );
+                        }
+                    };
+                } else {
+                    tracing::warn!(
+                        "Target wanted to open file {path}, but probe-rs does not support this operation yet. Continuing..."
+                    );
+                }
+            }
+            SemihostingCommand::Close(request) => {
+                let handle = request.file_handle(core)?;
+                if handle == Self::STDOUT {
+                    self.stdout_open = false;
+                    request.success(core)?;
+                } else if handle == Self::STDERR {
+                    self.stderr_open = false;
+                    request.success(core)?;
+                } else {
+                    tracing::warn!(
+                        "Target wanted to close file handle {handle}, but probe-rs does not support this operation yet. Continuing..."
+                    );
+                }
+            }
+            SemihostingCommand::Write(request) => match request.file_handle() {
+                handle if handle == Self::STDOUT => {
+                    if self.stdout_open {
+                        let bytes = request.read(core)?;
+                        let str = String::from_utf8_lossy(&bytes);
+                        std::io::stdout().write_all(str.as_bytes()).unwrap();
+                        request.write_status(core, 0)?;
+                    }
+                }
+                handle if handle == Self::STDERR => {
+                    if self.stderr_open {
+                        let bytes = request.read(core)?;
+                        let str = String::from_utf8_lossy(&bytes);
+                        std::io::stderr().write_all(str.as_bytes()).unwrap();
+                        request.write_status(core, 0)?;
+                    }
+                }
+                other => {
+                    tracing::warn!(
+                        "Target wanted to write to file handle {other}, but probe-rs does not support this operation yet. Continuing...",
+                    );
+                }
+            },
+            SemihostingCommand::WriteConsole(request) => {
+                std::io::stdout()
+                    .write_all(request.read(core)?.as_bytes())
+                    .unwrap();
+            }
+
+            _ => {}
+        };
+
+        Ok(())
+    }
+}
diff --git a/probe-rs-tools/src/bin/probe-rs/cmd/run/normal_run_mode.rs b/probe-rs-tools/src/bin/probe-rs/cmd/run/normal_run_mode.rs
index d67e548f..e8e279c7 100644
--- a/probe-rs-tools/src/bin/probe-rs/cmd/run/normal_run_mode.rs
+++ b/probe-rs-tools/src/bin/probe-rs/cmd/run/normal_run_mode.rs
@@ -1,6 +1,6 @@
-use crate::cmd::run::{OutputStream, RunLoop, RunMode};
+use crate::cmd::run::{OutputStream, RunLoop, RunMode, SemihostingPrinter};
 use anyhow::anyhow;
-use probe_rs::{BreakpointCause, Core, HaltReason, SemihostingCommand, Session};
+use probe_rs::{semihosting::SemihostingCommand, BreakpointCause, Core, HaltReason, Session};
 
 /// Options only used in normal run mode
 #[derive(Debug, clap::Parser, Clone)]
@@ -27,7 +27,8 @@ impl RunMode for NormalRunMode {
     fn run(&self, mut session: Session, mut run_loop: RunLoop) -> anyhow::Result<()> {
         let mut core = session.core(run_loop.core_id)?;
 
-        let halt_handler = |halt_reason: HaltReason, _core: &mut Core| {
+        let mut printer = SemihostingPrinter::new();
+        let halt_handler = |halt_reason: HaltReason, core: &mut Core| {
             let HaltReason::Breakpoint(BreakpointCause::Semihosting(cmd)) = halt_reason else {
                 anyhow::bail!("CPU halted unexpectedly.");
             };
@@ -50,6 +51,13 @@ impl RunMode for NormalRunMode {
                     tracing::warn!("Target wanted to run semihosting operation SYS_GET_CMDLINE, but probe-rs does not support this operation yet. Continuing...");
                     Ok(None) // Continue running
                 }
+                other @ (SemihostingCommand::Open(_)
+                | SemihostingCommand::Close(_)
+                | SemihostingCommand::WriteConsole(_)
+                | SemihostingCommand::Write(_)) => {
+                    printer.handle(other, core)?;
+                    Ok(None)
+                }
             }
         };
 
diff --git a/probe-rs/src/architecture/arm/core/cortex_m.rs b/probe-rs/src/architecture/arm/core/cortex_m.rs
index f81d73f3..886dce16 100644
--- a/probe-rs/src/architecture/arm/core/cortex_m.rs
+++ b/probe-rs/src/architecture/arm/core/cortex_m.rs
@@ -5,7 +5,8 @@ use crate::{
     core::RegisterId,
     memory_mapped_bitfield_register,
     semihosting::decode_semihosting_syscall,
-    CoreInterface, Error, MemoryMappedRegister, SemihostingCommand,
+    semihosting::SemihostingCommand,
+    CoreInterface, Error, MemoryMappedRegister,
 };
 use std::time::{Duration, Instant};
 
@@ -195,8 +196,6 @@ pub(crate) fn check_for_semihosting(
     cached_command: Option<SemihostingCommand>,
     core: &mut dyn CoreInterface,
 ) -> Result<Option<SemihostingCommand>, Error> {
-    let pc: u32 = core.read_core_reg(core.program_counter().id)?.try_into()?;
-
     // The Arm Semihosting Specification, specificies that the instruction
     // "BKPT 0xAB" (encoded as 0xBEAB) triggers a semihosting call.
     // <https://github.com/ARM-software/abi-aa/blob/main/semihosting/semihosting.rst#the-semihosting-interface>
@@ -205,6 +204,13 @@ pub(crate) fn check_for_semihosting(
         0xAB, 0xBE,
     ];
 
+    // We only want to decode the semihosting command once, since answering it might change some of the registers
+    if let Some(command) = cached_command {
+        return Ok(Some(command));
+    }
+
+    let pc: u32 = core.read_core_reg(core.program_counter().id)?.try_into()?;
+
     let mut actual_instruction = [0u8; 2];
     core.read_8(pc as u64, &mut actual_instruction)?;
     let actual_instruction = actual_instruction.as_slice();
@@ -215,22 +221,13 @@ pub(crate) fn check_for_semihosting(
         actual_instruction[0]
     );
 
-    if TRAP_INSTRUCTION == actual_instruction {
-        // BKPT 0xAB -> we are semihosting
-
-        Ok(Some(match cached_command {
-            None => {
-                // We only want to decode the semihosting command once, since answering it might change some of the registers
-                let r0: u32 = core.read_core_reg(RegisterId(0))?.try_into()?;
-                let r1: u32 = core.read_core_reg(RegisterId(1))?.try_into()?;
-                tracing::info!("Semihosting found pc={pc:#x} r0={r0:#x} r1={r1:#x}");
-                decode_semihosting_syscall(core, r0, r1)?
-            }
-            Some(cached_command) => cached_command,
-        }))
+    let command = if TRAP_INSTRUCTION == actual_instruction {
+        Some(decode_semihosting_syscall(core)?)
     } else {
-        Ok(None)
-    }
+        None
+    };
+
+    Ok(command)
 }
 
 fn wait_for_core_register_transfer(
diff --git a/probe-rs/src/architecture/arm/core/mod.rs b/probe-rs/src/architecture/arm/core/mod.rs
index 58b88253..6ff0bc56 100644
--- a/probe-rs/src/architecture/arm/core/mod.rs
+++ b/probe-rs/src/architecture/arm/core/mod.rs
@@ -4,7 +4,9 @@ use serde::{Deserialize, Serialize};
 
 use crate::{
     core::{BreakpointCause, RegisterValue},
-    memory_mapped_bitfield_register, CoreStatus, HaltReason, SemihostingCommand,
+    memory_mapped_bitfield_register,
+    semihosting::SemihostingCommand,
+    CoreStatus, HaltReason,
 };
 
 use super::memory::ArmMemoryInterface;
diff --git a/probe-rs/src/architecture/riscv/mod.rs b/probe-rs/src/architecture/riscv/mod.rs
index 0449c005..b6d1fdcd 100644
--- a/probe-rs/src/architecture/riscv/mod.rs
+++ b/probe-rs/src/architecture/riscv/mod.rs
@@ -9,8 +9,9 @@ use crate::{
     memory_mapped_bitfield_register,
     probe::DebugProbeError,
     semihosting::decode_semihosting_syscall,
+    semihosting::SemihostingCommand,
     CoreInterface, CoreRegister, CoreStatus, CoreType, Error, HaltReason, InstructionSet,
-    MemoryInterface, MemoryMappedRegister, SemihostingCommand,
+    MemoryInterface, MemoryMappedRegister,
 };
 use bitfield::bitfield;
 use communication_interface::{AbstractCommandErrorKind, RiscvCommunicationInterface, RiscvError};
@@ -75,8 +76,6 @@ impl<'state> Riscv32<'state> {
 
     /// Check if the current breakpoint is a semihosting call
     fn check_for_semihosting(&mut self) -> Result<Option<SemihostingCommand>, Error> {
-        let pc: u32 = self.read_core_reg(self.program_counter().id)?.try_into()?;
-
         // The Riscv Semihosting Specification, specificies the following sequence of instructions,
         // to trigger a semihosting call:
         // <https://github.com/riscv-software-src/riscv-semihosting/blob/main/riscv-semihosting-spec.adoc>
@@ -87,6 +86,13 @@ impl<'state> Riscv32<'state> {
             0x40705013, // srai x0, x0, 7 (NOP encoding the semihosting call number 7)
         ];
 
+        // We only want to decode the semihosting command once, since answering it might change some of the registers
+        if let Some(command) = self.state.semihosting_command {
+            return Ok(Some(command));
+        }
+
+        let pc: u32 = self.read_core_reg(self.program_counter().id)?.try_into()?;
+
         // Read the actual instructions, starting at the instruction before the ebreak (PC-4)
         let mut actual_instructions = [0u32; 3];
         self.read_32((pc - 4) as u64, &mut actual_instructions)?;
@@ -99,28 +105,14 @@ impl<'state> Riscv32<'state> {
             actual_instructions[2]
         );
 
-        if TRAP_INSTRUCTIONS == actual_instructions {
-            // Trap sequence found -> we're semihosting
-            match self.state.semihosting_command {
-                None => {
-                    // We only want to decode the semihosting command once, since answering it might change some of the registers
-                    let a0: u32 = self
-                        .read_core_reg(self.registers().get_argument_register(0).unwrap().id())?
-                        .try_into()?;
-                    let a1: u32 = self
-                        .read_core_reg(self.registers().get_argument_register(1).unwrap().id())?
-                        .try_into()?;
-
-                    tracing::info!("Semihosting found pc={pc:#x} a0={a0:#x} a1={a1:#x}");
-                    let cmd = decode_semihosting_syscall(self, a0, a1)?;
-                    self.state.semihosting_command = Some(cmd);
-                    Ok(Some(cmd))
-                }
-                Some(command) => Ok(Some(command)),
-            }
+        let command = if TRAP_INSTRUCTIONS == actual_instructions {
+            Some(decode_semihosting_syscall(self)?)
         } else {
-            Ok(None)
-        }
+            None
+        };
+        self.state.semihosting_command = command;
+
+        Ok(command)
     }
 
     fn determine_number_of_hardware_breakpoints(&mut self) -> Result<u32, RiscvError> {
diff --git a/probe-rs/src/architecture/xtensa/mod.rs b/probe-rs/src/architecture/xtensa/mod.rs
index c36dbbf9..2c23410a 100644
--- a/probe-rs/src/architecture/xtensa/mod.rs
+++ b/probe-rs/src/architecture/xtensa/mod.rs
@@ -6,7 +6,10 @@ use probe_rs_target::{Architecture, CoreType, InstructionSet};
 
 use crate::{
     architecture::xtensa::{
-        arch::{instruction::Instruction, Register, SpecialRegister},
+        arch::{
+            instruction::{Instruction, InstructionEncoding},
+            Register, SpecialRegister,
+        },
         communication_interface::{DebugCause, IBreakEn, XtensaCommunicationInterface},
         registers::{FP, PC, RA, SP, XTENSA_CORE_REGSISTERS},
         sequences::XtensaDebugSequence,
@@ -17,8 +20,8 @@ use crate::{
     },
     memory::CoreMemoryInterface,
     semihosting::decode_semihosting_syscall,
+    semihosting::SemihostingCommand,
     CoreInformation, CoreInterface, CoreRegister, CoreStatus, Error, HaltReason, MemoryInterface,
-    SemihostingCommand,
 };
 
 pub(crate) mod arch;
@@ -162,11 +165,10 @@ impl<'probe> Xtensa<'probe> {
 
             if pc_increment > 0 {
                 // Step through the breakpoint
-                let mut pc = self.read_core_reg(self.program_counter().id)?;
-
-                pc.increment_address(pc_increment)?;
-
-                self.write_core_reg(self.program_counter().into(), pc)?;
+                let pc = self.program_counter().id;
+                let mut pc_value = self.read_core_reg(pc)?;
+                pc_value.increment_address(pc_increment)?;
+                self.write_core_reg(pc, pc_value)?;
             }
         }
 
@@ -176,44 +178,37 @@ impl<'probe> Xtensa<'probe> {
     /// Check if the current breakpoint is a semihosting call
     // OpenOCD implementation: https://github.com/espressif/openocd-esp32/blob/93dd01511fd13d4a9fb322cd9b600c337becef9e/src/target/espressif/esp_xtensa_semihosting.c#L42-L103
     fn check_for_semihosting(&mut self) -> Result<Option<SemihostingCommand>, Error> {
-        let pc: u32 = self.read_core_reg(self.program_counter().id)?.try_into()?;
-
-        let mut actual_instructions = [0u8; 3];
-        self.read_8((pc) as u64, &mut actual_instructions)?;
-
-        tracing::debug!(
-            "Semihosting check pc={pc:#x} instructions={0:#08x} {1:#08x} {2:#08x}",
-            actual_instructions[0],
-            actual_instructions[1],
-            actual_instructions[2],
-        );
-
-        let expected_instruction = Instruction::Break(1, 14).to_bytes();
-
-        tracing::debug!(
-            "Expected instructions={0:#08x} {1:#08x} {2:#08x}",
-            expected_instruction[0],
-            expected_instruction[1],
-            expected_instruction[2]
-        );
-
-        if actual_instructions[..3] == expected_instruction.as_slice()[..3] {
-            match self.state.semihosting_command {
-                None => {
-                    // We only want to decode the semihosting command once, since answering it might change some of the registers
-                    let a2: u32 = self.read_core_reg(RegisterId::from(2))?.try_into()?;
-                    let a3: u32 = self.read_core_reg(RegisterId::from(3))?.try_into()?;
-
-                    tracing::info!("Semihosting found pc={pc:#x} a2={a2:#x} a3={a3:#x}");
-                    let cmd = decode_semihosting_syscall(self, a2, a3)?;
-                    self.state.semihosting_command = Some(cmd);
-                    Ok(Some(cmd))
-                }
-                Some(command) => Ok(Some(command)),
-            }
-        } else {
-            Ok(None)
+        const SEMI_BREAK: u32 = const {
+            let InstructionEncoding::Narrow(bytes) = Instruction::Break(1, 14).encode();
+            bytes
+        };
+
+        // We only want to decode the semihosting command once, since answering it might change some of the registers
+        if let Some(command) = self.state.semihosting_command {
+            return Ok(Some(command));
         }
+
+        let pc: u64 = self.read_core_reg(self.program_counter().id)?.try_into()?;
+
+        let mut actual_instruction = [0u8; 3];
+        self.read_8(pc, &mut actual_instruction)?;
+        let actual_instruction = u32::from_le_bytes([
+            actual_instruction[0],
+            actual_instruction[1],
+            actual_instruction[2],
+            0,
+        ]);
+
+        tracing::debug!("Semihosting check pc={pc:#x} instruction={actual_instruction:#010x}");
+
+        let command = if actual_instruction == SEMI_BREAK {
+            Some(decode_semihosting_syscall(self)?)
+        } else {
+            None
+        };
+        self.state.semihosting_command = command;
+
+        Ok(command)
     }
 
     fn on_halted(&mut self) -> Result<(), Error> {
@@ -256,19 +251,17 @@ impl<'probe> CoreInterface for Xtensa<'probe> {
     fn status(&mut self) -> Result<CoreStatus, Error> {
         let status = if self.core_halted()? {
             let debug_cause = self.interface.read_register::<DebugCause>()?;
-            let reason =
-                if debug_cause.halt_reason() == HaltReason::Breakpoint(BreakpointCause::Software) {
-                    // The chip initiated this halt, therefore we need to update pc_written state
-                    self.state.pc_written = false;
-                    // Check if the breakpoint is a semihosting call
-                    if let Some(cmd) = self.check_for_semihosting()? {
-                        HaltReason::Breakpoint(BreakpointCause::Semihosting(cmd))
-                    } else {
-                        debug_cause.halt_reason()
-                    }
-                } else {
-                    debug_cause.halt_reason()
-                };
+
+            let mut reason = debug_cause.halt_reason();
+            if reason == HaltReason::Breakpoint(BreakpointCause::Software) {
+                // The chip initiated this halt, therefore we need to update pc_written state
+                self.state.pc_written = false;
+                // Check if the breakpoint is a semihosting call
+                if let Some(cmd) = self.check_for_semihosting()? {
+                    reason = HaltReason::Breakpoint(BreakpointCause::Semihosting(cmd));
+                }
+            }
+
             CoreStatus::Halted(reason)
         } else {
             CoreStatus::Running
diff --git a/probe-rs/src/core/core_status.rs b/probe-rs/src/core/core_status.rs
index 5d252c1c..618f41e6 100644
--- a/probe-rs/src/core/core_status.rs
+++ b/probe-rs/src/core/core_status.rs
@@ -1,4 +1,4 @@
-use crate::SemihostingCommand;
+use crate::semihosting::SemihostingCommand;
 
 /// The status of the core.
 #[derive(Debug, PartialEq, Eq, Copy, Clone)]
diff --git a/probe-rs/src/lib.rs b/probe-rs/src/lib.rs
index 818faf3e..65b32685 100644
--- a/probe-rs/src/lib.rs
+++ b/probe-rs/src/lib.rs
@@ -85,7 +85,7 @@ pub mod integration;
 mod memory;
 pub mod probe;
 pub mod rtt;
-mod semihosting;
+pub mod semihosting;
 mod session;
 #[cfg(test)]
 mod test;
@@ -98,9 +98,6 @@ pub use crate::core::{
 };
 pub use crate::error::Error;
 pub use crate::memory::MemoryInterface;
-pub use crate::semihosting::{
-    ExitErrorDetails, GetCommandLineRequest, SemihostingCommand, UnknownCommandDetails,
-};
 pub use crate::session::{Permissions, Session};
 
 #[cfg(feature = "debug")]
diff --git a/probe-rs/src/probe/espusbjtag/protocol.rs b/probe-rs/src/probe/espusbjtag/protocol.rs
index 7dd476a6..a48a15af 100644
--- a/probe-rs/src/probe/espusbjtag/protocol.rs
+++ b/probe-rs/src/probe/espusbjtag/protocol.rs
@@ -353,12 +353,11 @@ impl ProtocolHandler {
 
     /// Adds a single command to the output buffer and writes it to the USB EP if the buffer reaches a limit of `OUT_BUFFER_SIZE`.
     fn add_raw_command(&mut self, command: Command) -> Result<(), DebugProbeError> {
-        self.output_buffer.push(command);
-
         // If we reach a maximal size of the output buffer, we flush.
         if self.output_buffer.len() == OUT_BUFFER_SIZE {
             self.send_buffer()?;
         }
+        self.output_buffer.push(command);
 
         Ok(())
     }
diff --git a/probe-rs/src/semihosting.rs b/probe-rs/src/semihosting.rs
index 91cde06c..81ed67dc 100644
--- a/probe-rs/src/semihosting.rs
+++ b/probe-rs/src/semihosting.rs
@@ -1,7 +1,13 @@
-use crate::{CoreInterface, Error, RegisterValue};
+//! ARM semihosting support.
+//!
+//! Specification: <https://github.com/ARM-software/abi-aa/blob/2024Q3/semihosting/semihosting.rst>
+
+use std::num::NonZeroU32;
+
+use crate::{CoreInterface, Error, MemoryInterface, RegisterValue};
 
 /// Indicates the operation the target would like the debugger to perform.
-#[derive(Debug, PartialEq, Eq, Copy, Clone)]
+#[derive(Debug, PartialEq, Eq, Clone, Copy)]
 pub enum SemihostingCommand {
     /// The target indicates that it completed successfully and no-longer wishes
     /// to run.
@@ -14,6 +20,18 @@ pub enum SemihostingCommand {
     /// The target indicates that it would like to read the command line arguments.
     GetCommandLine(GetCommandLineRequest),
 
+    /// The target requests to open a file on the host.
+    Open(OpenRequest),
+
+    /// The target requests to close a file on the host.
+    Close(CloseRequest),
+
+    /// The target indicated that it would like to write to the console.
+    WriteConsole(WriteConsoleRequest),
+
+    /// The target indicated that it would like to write to the console.
+    Write(WriteRequest),
+
     /// The target indicated that it would like to run a semihosting operation which we don't support yet.
     Unknown(UnknownCommandDetails),
 }
@@ -90,6 +108,92 @@ impl GetCommandLineRequest {
     }
 }
 
+/// A request to open a file on the host.
+///
+/// Note that this is not implemented by probe-rs yet.
+#[derive(Debug, PartialEq, Eq, Copy, Clone)]
+pub struct OpenRequest {
+    path: ZeroTerminatedString,
+    mode: &'static str,
+}
+
+impl OpenRequest {
+    /// Reads the path from the target.
+    pub fn path(&self, core: &mut dyn CoreInterface) -> Result<String, Error> {
+        self.path.read(core)
+    }
+
+    /// Reads the raw mode from the target.
+    pub fn mode(&self) -> &'static str {
+        self.mode
+    }
+
+    /// Responds with the opened file handle to the target.
+    pub fn respond_with_handle(
+        &self,
+        core: &mut dyn CoreInterface,
+        handle: NonZeroU32,
+    ) -> Result<(), Error> {
+        write_status(core, handle.get() as i32)
+    }
+}
+
+/// A request to open a file on the host.
+///
+/// Note that this is not implemented by probe-rs yet.
+#[derive(Debug, PartialEq, Eq, Copy, Clone)]
+pub struct CloseRequest {
+    pointer: u32,
+}
+
+impl CloseRequest {
+    /// Returns the handle of the file to close
+    pub fn file_handle(&self, core: &mut dyn CoreInterface) -> Result<u32, Error> {
+        core.read_word_32(self.pointer as u64)
+    }
+
+    /// Responds with success to the target.
+    pub fn success(&self, core: &mut dyn CoreInterface) -> Result<(), Error> {
+        write_status(core, 0)
+    }
+}
+
+/// A request to write to the console
+#[derive(Debug, PartialEq, Eq, Copy, Clone)]
+pub struct WriteConsoleRequest(ZeroTerminatedString);
+impl WriteConsoleRequest {
+    /// Reads the string from the target
+    pub fn read(&self, core: &mut crate::Core<'_>) -> Result<String, Error> {
+        self.0.read(core)
+    }
+}
+
+/// A request to write to the console
+#[derive(Debug, PartialEq, Eq, Copy, Clone)]
+pub struct WriteRequest {
+    handle: u32,
+    bytes: u32,
+    len: u32,
+}
+impl WriteRequest {
+    /// Returns the handle of the file to write to
+    pub fn file_handle(&self) -> u32 {
+        self.handle
+    }
+
+    /// Reads the buffer from the target
+    pub fn read(&self, core: &mut crate::Core<'_>) -> Result<Vec<u8>, Error> {
+        let mut buf = vec![0u8; self.len as usize];
+        core.read(self.bytes as u64, &mut buf)?;
+        Ok(buf)
+    }
+
+    /// Writes the status of the semihosting operation to the return register of the target
+    pub fn write_status(&self, core: &mut dyn CoreInterface, status: i32) -> Result<(), Error> {
+        write_status(core, status)
+    }
+}
+
 fn write_status(core: &mut dyn CoreInterface, value: i32) -> Result<(), crate::Error> {
     let reg = core.registers().get_argument_register(0).unwrap();
     core.write_core_reg(reg.into(), RegisterValue::U32(value as u32))?;
@@ -97,9 +201,9 @@ fn write_status(core: &mut dyn CoreInterface, value: i32) -> Result<(), crate::E
     Ok(())
 }
 
-// When using some semihosting commands, the target usually allocates a buffer for the host to read/write to.
-// The targets just gives us an address pointing to two u32 values, the address of the buffer and
-// the length of the buffer.
+/// When using some semihosting commands, the target usually allocates a buffer for the host to read/write to.
+/// The targets just gives us an address pointing to two u32 values, the address of the buffer and
+/// the length of the buffer.
 #[derive(Debug, PartialEq, Eq, Copy, Clone)]
 pub struct Buffer {
     buffer_location: u32, // The address where the buffer address and length are stored
@@ -145,13 +249,54 @@ impl Buffer {
     }
 }
 
+#[derive(Debug, PartialEq, Eq, Copy, Clone)]
+struct ZeroTerminatedString {
+    address: u32,
+    length: Option<u32>,
+}
+
+impl ZeroTerminatedString {
+    /// Reads the buffer contents from the target.
+    pub fn read(&self, core: &mut dyn CoreInterface) -> Result<String, Error> {
+        let mut bytes = Vec::new();
+
+        if let Some(len) = self.length {
+            bytes = vec![0; len as usize];
+            core.read(self.address as u64, &mut bytes)?;
+        } else {
+            let mut buf = [0; 128];
+            let mut from = self.address as u64;
+
+            loop {
+                core.read(from, &mut buf)?;
+                if let Some(end) = buf.iter().position(|&x| x == 0) {
+                    bytes.extend_from_slice(&buf[..end]);
+                    break;
+                }
+
+                bytes.extend_from_slice(&buf);
+                from += buf.len() as u64;
+            }
+        }
+
+        Ok(String::from_utf8_lossy(&bytes).to_string())
+    }
+}
+
 /// Decodes a semihosting syscall without running the requested action.
 /// Only supports SYS_EXIT, SYS_EXIT_EXTENDED and SYS_GET_CMDLINE at the moment
 pub fn decode_semihosting_syscall(
     core: &mut dyn CoreInterface,
-    operation: u32,
-    parameter: u32,
 ) -> Result<SemihostingCommand, Error> {
+    let operation: u32 = core
+        .read_core_reg(core.registers().get_argument_register(0).unwrap().id())?
+        .try_into()?;
+    let parameter: u32 = core
+        .read_core_reg(core.registers().get_argument_register(1).unwrap().id())?
+        .try_into()?;
+
+    tracing::debug!("Semihosting found r0={operation:#x} r1={parameter:#x}");
+
     // This is defined by the ARM Semihosting Specification:
     // <https://github.com/ARM-software/abi-aa/blob/main/semihosting/semihosting.rst#semihosting-operations>
 
@@ -159,6 +304,12 @@ pub fn decode_semihosting_syscall(
     const SYS_EXIT: u32 = 0x18;
     const SYS_EXIT_EXTENDED: u32 = 0x20;
     const SYS_EXIT_ADP_STOPPED_APPLICATIONEXIT: u32 = 0x20026;
+    const SYS_OPEN: u32 = 0x01;
+    const SYS_CLOSE: u32 = 0x02;
+    const SYS_WRITEC: u32 = 0x03;
+    const SYS_WRITE0: u32 = 0x04;
+    const SYS_WRITE: u32 = 0x05;
+
     Ok(match (operation, parameter) {
         (SYS_EXIT, SYS_EXIT_ADP_STOPPED_APPLICATIONEXIT) => SemihostingCommand::ExitSuccess,
         (SYS_EXIT, reason) => SemihostingCommand::ExitError(ExitErrorDetails {
@@ -199,6 +350,66 @@ pub fn decode_semihosting_syscall(
                 block_address,
             )?))
         }
+
+        (SYS_OPEN, pointer) => {
+            let [string, mode, str_len] = param3(core, pointer)?;
+
+            // signal to target: status = failure, in case the application does not answer this request
+            // -1 is the error value for SYS_OPEN
+            write_status(core, -1)?;
+            SemihostingCommand::Open(OpenRequest {
+                path: ZeroTerminatedString {
+                    address: string,
+                    length: Some(str_len),
+                },
+                mode: match mode {
+                    0 => "r",
+                    1 => "rb",
+                    2 => "r+",
+                    3 => "r+b",
+                    4 => "w",
+                    5 => "wb",
+                    6 => "w+",
+                    7 => "w+b",
+                    8 => "a",
+                    9 => "ab",
+                    10 => "a+",
+                    11 => "a+b",
+                    _ => "unknown",
+                },
+            })
+        }
+
+        (SYS_CLOSE, pointer) => {
+            // signal to target: status = failure, in case the application does not answer this request
+            // -1 is the error value for SYS_CLOSE
+            write_status(core, -1)?;
+            SemihostingCommand::Close(CloseRequest { pointer })
+        }
+
+        (SYS_WRITEC, pointer) => {
+            SemihostingCommand::WriteConsole(WriteConsoleRequest(ZeroTerminatedString {
+                address: pointer,
+                length: Some(1),
+            }))
+            // no response is given
+        }
+
+        (SYS_WRITE0, pointer) => {
+            SemihostingCommand::WriteConsole(WriteConsoleRequest(ZeroTerminatedString {
+                address: pointer,
+                length: None,
+            }))
+            // no response is given
+        }
+
+        (SYS_WRITE, pointer) => {
+            let [handle, bytes, len] = param3(core, pointer)?;
+            // signal to target: status = failure, in case the application does not answer this request
+            write_status(core, -1)?;
+            SemihostingCommand::Write(WriteRequest { handle, bytes, len })
+        }
+
         _ => {
             // signal to target: status = failure, in case the application does not answer this request
             // It is not guaranteed that a value of -1 will be treated as an error by the target, but it is a common value to indicate an error.
@@ -214,3 +425,9 @@ pub fn decode_semihosting_syscall(
         }
     })
 }
+
+fn param3(core: &mut dyn CoreInterface, pointer: u32) -> Result<[u32; 3], crate::Error> {
+    let mut buf = [0; 3];
+    core.read_32(pointer as u64, &mut buf)?;
+    Ok(buf)
+}
diff --git a/probe-rs/src/session.rs b/probe-rs/src/session.rs
index 7111448c..0c576165 100644
--- a/probe-rs/src/session.rs
+++ b/probe-rs/src/session.rs
@@ -778,6 +778,24 @@ impl Session {
             })
         })
     }
+
+    /// Resume all cores
+    pub fn resume_all_cores(&mut self) -> Result<(), Error> {
+        // Resume cores
+        for core_id in 0..self.cores.len() {
+            match self.core(core_id) {
+                Ok(mut core) => {
+                    if core.core_halted()? {
+                        core.run()?;
+                    }
+                }
+                Err(Error::CoreDisabled(i)) => tracing::debug!("Core {i} is disabled"),
+                Err(error) => return Err(error),
+            }
+        }
+
+        Ok(())
+    }
 }
 
 // This test ensures that [Session] is fully [Send] + [Sync].


Test patch:
diff --git a/probe-rs-tools/src/bin/probe-rs/cmd/run/test_run_mode.rs b/probe-rs-tools/src/bin/probe-rs/cmd/run/test_run_mode.rs
index 767c928e..e30c2cdc 100644
--- a/probe-rs-tools/src/bin/probe-rs/cmd/run/test_run_mode.rs
+++ b/probe-rs-tools/src/bin/probe-rs/cmd/run/test_run_mode.rs
@@ -1,7 +1,9 @@
-use crate::cmd::run::{print_stacktrace, OutputStream, ReturnReason, RunLoop, RunMode};
+use crate::cmd::run::{
+    print_stacktrace, OutputStream, ReturnReason, RunLoop, RunMode, SemihostingPrinter,
+};
 use anyhow::Result;
 use libtest_mimic::{Arguments, Failed, FormatSetting, Trial};
-use probe_rs::{BreakpointCause, Core, HaltReason, SemihostingCommand, Session};
+use probe_rs::{semihosting::SemihostingCommand, BreakpointCause, Core, HaltReason, Session};
 use serde::Deserialize;
 use std::sync::{Arc, Mutex};
 use std::time::Duration;
@@ -125,6 +127,7 @@ impl TestRunMode {
 
         let mut cmdline_requested = false;
 
+        let mut printer = SemihostingPrinter::new();
         // When the target first invokes SYS_GET_CMDLINE (0x15), we answer "list"
         // Then, we wait until the target invokes SEMIHOSTING_USER_LIST (0x100) with the json containing all tests
         let halt_handler = |halt_reason: HaltReason, core: &mut Core| {
@@ -156,6 +159,13 @@ impl TestRunMode {
 
                     Ok(Some(list))
                 }
+                other @ (SemihostingCommand::Open(_)
+                | SemihostingCommand::Close(_)
+                | SemihostingCommand::WriteConsole(_)
+                | SemihostingCommand::Write(_)) => {
+                    printer.handle(other, core)?;
+                    Ok(None)
+                }
                 other => anyhow::bail!(
                     "Unexpected semihosting command {:?} cmdline_requested: {:?}",
                     other,
@@ -198,6 +208,7 @@ impl TestRunMode {
         let timeout = timeout.unwrap_or(Duration::from_secs(60)); // TODO: make global timeout configurable: https://github.com/probe-rs/embedded-test/issues/3
         let mut cmdline_requested = false;
 
+        let mut printer = SemihostingPrinter::new();
         // When the target first invokes SYS_GET_CMDLINE (0x15), we answer "run <test_name>
         // Then we wait until the target invokes SYS_EXIT (0x18) or SYS_EXIT_EXTENDED(0x20) with the exit code
         let halt_handler = |halt_reason: HaltReason, core: &mut Core| {
@@ -222,7 +233,13 @@ impl TestRunMode {
                 SemihostingCommand::ExitError(_) if cmdline_requested => {
                     Ok(Some(TestOutcome::Panic))
                 }
-
+                other @ (SemihostingCommand::Open(_)
+                | SemihostingCommand::Close(_)
+                | SemihostingCommand::WriteConsole(_)
+                | SemihostingCommand::Write(_)) => {
+                    printer.handle(other, core)?;
+                    Ok(None)
+                }
                 other => {
                     // Invalid sequence of semihosting calls => Abort testing altogether
                     anyhow::bail!(