opentitanlib/debug/
openocd.rs

1// Copyright lowRISC contributors (OpenTitan project).
2// Licensed under the Apache License, Version 2.0, see LICENSE for details.
3// SPDX-License-Identifier: Apache-2.0
4
5use std::fmt::LowerHex;
6use std::io::{BufRead, BufReader, Write};
7use std::mem::size_of;
8use std::net::TcpStream;
9use std::os::unix::process::CommandExt;
10use std::path::Path;
11use std::process::{Child, Command, Stdio};
12use std::sync::LazyLock;
13use std::time::{Duration, Instant};
14
15use anyhow::{Context, Result, bail, ensure};
16use regex::Regex;
17use serde::{Deserialize, Serialize};
18use thiserror::Error;
19
20use crate::dif::lc_ctrl::LcCtrlReg;
21use crate::impl_serializable_error;
22use crate::io::jtag::{Jtag, JtagChain, JtagError, JtagParams, JtagTap, RiscvReg};
23use crate::util::parse_int::ParseInt;
24use crate::util::printer;
25
26/// Represents an OpenOCD server that we can interact with.
27pub struct OpenOcd {
28    /// OpenOCD child process.
29    server_process: Child,
30    /// Receiving side of the stream to the telnet interface of OpenOCD.
31    reader: BufReader<TcpStream>,
32    /// Sending side of the stream to the telnet interface of OpenOCD.
33    writer: TcpStream,
34}
35
36impl Drop for OpenOcd {
37    fn drop(&mut self) {
38        let _ = self.server_process.kill();
39    }
40}
41
42impl OpenOcd {
43    /// How long to wait for OpenOCD to get ready to accept a TCL connection.
44    const OPENOCD_TCL_READY_TMO: Duration = Duration::from_secs(30);
45
46    /// Wait until we see a particular message on the output.
47    fn wait_until_regex_match<'a>(
48        stderr: &mut impl BufRead,
49        regex: &Regex,
50        timeout: Duration,
51        log_stdio: bool,
52        s: &'a mut String,
53    ) -> Result<regex::Captures<'a>> {
54        let start = Instant::now();
55        loop {
56            // NOTE the read could block indefinitely, a proper solution would involved spawning
57            // a thread or using async.
58            let n = stderr.read_line(s)?;
59            if n == 0 {
60                bail!("OpenOCD stopped before being ready?");
61            }
62            if log_stdio {
63                log::info!(target: concat!(module_path!(), "::stderr"), "{}", s);
64            }
65            if regex.is_match(s) {
66                // This is not a `if let Some(capture) = regex.captures(s) {}` to to Rust
67                // borrow checker limitations. Can be modified if Polonius lands.
68                return Ok(regex.captures(s).unwrap());
69            }
70            s.clear();
71
72            if start.elapsed() >= timeout {
73                bail!("OpenOCD did not become ready to accept a TCL connection");
74            }
75        }
76    }
77
78    /// Spawn an OpenOCD server with given path.
79    pub fn spawn(path: &Path, log_stdio: bool) -> Result<Self> {
80        let mut cmd = Command::new(path);
81
82        // Let OpenOCD choose which port to bind to, in order to never unnecesarily run into
83        // issues due to a particular port already being in use.
84        // We don't use the telnet and GDB ports so disable them.
85        // The configuration will happen through the TCL interface, so use `noinit` to prevent
86        // OpenOCD from transition to execution mode.
87        cmd.arg("-c")
88            .arg("tcl_port 0; telnet_port disabled; gdb_port disabled; noinit;");
89
90        log::info!("Spawning OpenOCD: {cmd:?}");
91
92        cmd.stdin(Stdio::null())
93            .stdout(Stdio::piped())
94            .stderr(Stdio::piped());
95
96        // SAFETY: prctl is a syscall which is atomic and thus async-signal-safe.
97        unsafe {
98            cmd.pre_exec(|| {
99                // Since we use OpenOCD as a library, make sure it's killed when
100                // the parent process dies. This setting is preserved across execve.
101                rustix::process::set_parent_process_death_signal(Some(
102                    rustix::process::Signal::HUP,
103                ))?;
104                Ok(())
105            });
106        }
107
108        let mut child = cmd
109            .spawn()
110            .with_context(|| format!("failed to spawn openocd: {cmd:?}",))?;
111        let stdout = child.stdout.take().unwrap();
112        let mut stderr = BufReader::new(child.stderr.take().unwrap());
113        // Wait until we see 'Info : Listening on port XXX for tcl connections' before knowing
114        // which port to connect to.
115        if log_stdio {
116            log::info!("Waiting for OpenOCD to be ready to accept a TCL connection...");
117        }
118        static READY_REGEX: LazyLock<Regex> = LazyLock::new(|| {
119            Regex::new("Info : Listening on port ([0-9]+) for tcl connections").unwrap()
120        });
121        let mut buf = String::new();
122        let regex_captures = Self::wait_until_regex_match(
123            &mut stderr,
124            &READY_REGEX,
125            Self::OPENOCD_TCL_READY_TMO,
126            log_stdio,
127            &mut buf,
128        )
129        .context("OpenOCD was not ready in time to accept a connection")?;
130        let openocd_port: u16 = regex_captures.get(1).unwrap().as_str().parse()?;
131        // Print stdout and stderr with log
132        if log_stdio {
133            std::thread::spawn(move || {
134                printer::accumulate(
135                    stdout,
136                    concat!(module_path!(), "::stdout"),
137                    Default::default(),
138                )
139            });
140            std::thread::spawn(move || {
141                printer::accumulate(
142                    stderr,
143                    concat!(module_path!(), "::stderr"),
144                    Default::default(),
145                )
146            });
147        }
148
149        let kill_guard = scopeguard::guard(child, |mut child| {
150            let _ = child.kill();
151        });
152
153        log::info!("Connecting to OpenOCD tcl interface...");
154
155        let stream = TcpStream::connect(("localhost", openocd_port))
156            .context("failed to connect to OpenOCD socket")?;
157
158        let mut connection = Self {
159            server_process: scopeguard::ScopeGuard::into_inner(kill_guard),
160            reader: BufReader::new(stream.try_clone()?),
161            writer: stream,
162        };
163
164        // Test the connection by asking for OpenOCD's version.
165        let version = connection.execute("version")?;
166        log::info!("OpenOCD version: {version}");
167
168        Ok(connection)
169    }
170
171    /// Send a string to OpenOCD Tcl interface.
172    fn send(&mut self, cmd: &str) -> Result<()> {
173        // The protocol is to send the command followed by a `0x1a` byte,
174        // see https://openocd.org/doc/html/Tcl-Scripting-API.html#Tcl-RPC-server
175
176        // Sanity check to ensure that the command string is not malformed.
177        if cmd.contains('\x1A') {
178            bail!("TCL command string should be contained inside the text to send");
179        }
180
181        self.writer
182            .write_all(cmd.as_bytes())
183            .context("failed to send a command to OpenOCD server")?;
184        self.writer
185            .write_all(&[0x1a])
186            .context("failed to send the command terminator to OpenOCD server")?;
187        self.writer.flush().context("failed to flush stream")?;
188        Ok(())
189    }
190
191    fn recv(&mut self) -> Result<String> {
192        let mut buf = Vec::new();
193        self.reader.read_until(0x1A, &mut buf)?;
194        if !buf.ends_with(b"\x1A") {
195            bail!(OpenOcdError::PrematureExit);
196        }
197        buf.pop();
198        String::from_utf8(buf).context("failed to parse OpenOCD response as UTF-8")
199    }
200
201    pub fn shutdown(mut self) -> Result<()> {
202        self.execute("shutdown")?;
203        // Wait for it to exit.
204        self.server_process
205            .wait()
206            .context("failed to wait for OpenOCD server to exit")?;
207        Ok(())
208    }
209
210    /// Send a TCL command to OpenOCD and wait for its response.
211    pub fn execute(&mut self, cmd: &str) -> Result<String> {
212        self.send(cmd)?;
213        self.recv()
214    }
215
216    /// Load instruction register of a given tap.
217    pub fn irscan(&mut self, tap: &str, ir: u32) -> Result<()> {
218        let cmd = format!("irscan {} {:#x}", tap, ir);
219        let result = self.execute(&cmd)?;
220        ensure!(result.is_empty(), "unexpected response: '{result}'");
221        Ok(())
222    }
223
224    /// Load data register of a given tap and return the scan.
225    pub fn drscan<T: ParseInt + LowerHex>(
226        &mut self,
227        tap: &str,
228        numbits: u32,
229        data: T,
230    ) -> Result<T> {
231        let cmd = format!("drscan {} {} {:#x}", tap, numbits, data);
232        let result = self.execute(&cmd)?;
233        Ok(T::from_str_radix(&result, 16).map_err(|x| x.into())?)
234    }
235}
236
237/// An JTAG interface driver over OpenOCD.
238pub struct OpenOcdJtagChain {
239    /// OpenOCD server instance.
240    openocd: OpenOcd,
241}
242
243/// Errors related to the OpenOCD server.
244#[derive(Error, Debug, Deserialize, Serialize)]
245pub enum OpenOcdError {
246    #[error("OpenOCD initialization failed: {0}")]
247    InitializeFailure(String),
248    #[error("OpenOCD server exists prematurely")]
249    PrematureExit,
250    #[error("Generic error {0}")]
251    Generic(String),
252}
253impl_serializable_error!(OpenOcdError);
254
255impl OpenOcdJtagChain {
256    /// Start OpenOCD with given JTAG options but do not connect any TAP.
257    pub fn new(adapter_command: &str, opts: &JtagParams) -> Result<OpenOcdJtagChain> {
258        let mut openocd = OpenOcd::spawn(&opts.openocd, opts.log_stdio)?;
259
260        openocd.execute(adapter_command)?;
261        openocd.execute(&format!("adapter speed {}", opts.adapter_speed_khz))?;
262        openocd.execute("transport select jtag")?;
263        openocd.execute("scan_chain")?;
264
265        Ok(OpenOcdJtagChain { openocd })
266    }
267}
268
269impl JtagChain for OpenOcdJtagChain {
270    fn connect(mut self: Box<Self>, tap: JtagTap) -> Result<Box<dyn Jtag>> {
271        // Pass through the config for the chosen TAP.
272        let target = match tap {
273            JtagTap::RiscvTap => include_str!(env!("openocd_riscv_target_cfg")),
274            JtagTap::LcTap => include_str!(env!("openocd_lc_target_cfg")),
275        };
276        self.openocd.execute(target)?;
277
278        // Capture outputs during initialization to see if error has occurred during the process.
279        let resp = self.openocd.execute("capture init")?;
280        if resp.contains("JTAG scan chain interrogation failed") {
281            bail!(OpenOcdError::InitializeFailure(resp));
282        }
283
284        Ok(Box::new(OpenOcdJtagTap {
285            openocd: self.openocd,
286            jtag_tap: tap,
287        }))
288    }
289
290    fn into_raw(self: Box<Self>) -> Result<OpenOcd> {
291        Ok(self.openocd)
292    }
293}
294
295/// An JTAG interface driver over OpenOCD.
296pub struct OpenOcdJtagTap {
297    /// OpenOCD server instance.
298    openocd: OpenOcd,
299    /// JTAG TAP OpenOCD is connected to.
300    jtag_tap: JtagTap,
301}
302
303impl OpenOcdJtagTap {
304    /// Send a TCL command to OpenOCD and wait for its response.
305    fn send_tcl_cmd(&mut self, cmd: &str) -> Result<String> {
306        self.openocd.execute(cmd)
307    }
308
309    fn read_memory_impl<T: ParseInt>(&mut self, addr: u32, buf: &mut [T]) -> Result<usize> {
310        // Ibex does not have a MMU so always tell OpenOCD that we are using physical addresses
311        // otherwise it will try to translate the address through the (non-existent) MMU
312        let cmd = format!(
313            "read_memory 0x{addr:x} {width} {count} phys",
314            width = 8 * size_of::<T>(),
315            count = buf.len()
316        );
317        let response = self.send_tcl_cmd(cmd.as_str())?;
318        response.trim().split(' ').try_fold(0, |idx, val| {
319            if idx < buf.len() {
320                buf[idx] = T::from_str(val).context(format!(
321                    "expected response to be an hexadecimal byte, got '{response}'"
322                ))?;
323                Ok(idx + 1)
324            } else {
325                bail!("OpenOCD returned too much data on read".to_string())
326            }
327        })
328    }
329
330    fn write_memory_impl<T: ToString>(&mut self, addr: u32, bigbuf: &[T]) -> Result<()> {
331        const CHUNK_SIZE: usize = 1024;
332        for (idx, buf) in bigbuf.chunks(CHUNK_SIZE).enumerate() {
333            // Convert data to space-separated strings.
334            let data: Vec<_> = buf.iter().map(ToString::to_string).collect();
335            let data_str = &data[..].join(" ");
336            // See [read_memory] about physical addresses
337            let cmd = format!(
338                "write_memory 0x{chunk_addr:x} {width} {{ {data_str} }} phys",
339                chunk_addr = addr + (idx * CHUNK_SIZE * size_of::<T>()) as u32,
340                width = 8 * size_of::<T>()
341            );
342            let response = self.send_tcl_cmd(cmd.as_str())?;
343            if !response.is_empty() {
344                bail!("unexpected response: '{response}'");
345            }
346        }
347
348        Ok(())
349    }
350
351    /// Read a register: this function does not attempt to translate the
352    /// name or number of the register. If force is set, bypass OpenOCD's
353    /// register cache.
354    fn read_register<T: ParseInt>(&mut self, reg_name: &str, force: bool) -> Result<T> {
355        let cmd = format!(
356            "get_reg {} {{ {} }}",
357            if force { "-force" } else { "" },
358            reg_name,
359        );
360        let response = self.send_tcl_cmd(cmd.as_str())?;
361        // the expected output format is 'reg_name 0xabcdef', e.g 'pc 0x10009858'
362        let (out_reg_name, value) = response.trim().split_once(' ').with_context(|| {
363            format!("expected response of the form 'reg value', got '{response}'")
364        })?;
365        ensure!(
366            out_reg_name == reg_name,
367            "OpenOCD returned the value for register '{out_reg_name}' instead of '{reg_name}"
368        );
369        T::from_str(value).context(format!(
370            "expected value to be an hexadecimal string, got '{value}'"
371        ))
372    }
373
374    fn write_register<T: ToString>(&mut self, reg_name: &str, value: T) -> Result<()> {
375        let cmd = format!("set_reg {{ {reg_name} {} }}", T::to_string(&value));
376        let response = self.send_tcl_cmd(cmd.as_str())?;
377        if !response.is_empty() {
378            bail!("unexpected response: '{response}'");
379        }
380
381        Ok(())
382    }
383}
384
385impl Jtag for OpenOcdJtagTap {
386    fn into_raw(self: Box<Self>) -> Result<OpenOcd> {
387        Ok(self.openocd)
388    }
389
390    fn as_raw(&mut self) -> Result<&mut OpenOcd> {
391        Ok(&mut self.openocd)
392    }
393
394    fn disconnect(self: Box<Self>) -> Result<()> {
395        self.openocd.shutdown()
396    }
397
398    fn tap(&self) -> JtagTap {
399        self.jtag_tap
400    }
401
402    fn read_lc_ctrl_reg(&mut self, reg: &LcCtrlReg) -> Result<u32> {
403        ensure!(
404            matches!(self.jtag_tap, JtagTap::LcTap),
405            JtagError::Tap(self.jtag_tap)
406        );
407        let reg_offset = reg.word_offset();
408        let cmd = format!("riscv dmi_read 0x{reg_offset:x}");
409        let response = self.send_tcl_cmd(cmd.as_str())?;
410
411        let value = u32::from_str(response.trim()).context(format!(
412            "expected response to be hexadecimal word, got '{response}'"
413        ))?;
414
415        Ok(value)
416    }
417
418    fn write_lc_ctrl_reg(&mut self, reg: &LcCtrlReg, value: u32) -> Result<()> {
419        ensure!(
420            matches!(self.jtag_tap, JtagTap::LcTap),
421            JtagError::Tap(self.jtag_tap)
422        );
423        let reg_offset = reg.word_offset();
424        let cmd = format!("riscv dmi_write 0x{reg_offset:x} 0x{value:x}");
425        let response = self.send_tcl_cmd(cmd.as_str())?;
426
427        if !response.is_empty() {
428            bail!("unexpected response: '{response}'");
429        }
430
431        Ok(())
432    }
433
434    fn read_memory(&mut self, addr: u32, buf: &mut [u8]) -> Result<usize> {
435        ensure!(
436            matches!(self.jtag_tap, JtagTap::RiscvTap),
437            JtagError::Tap(self.jtag_tap)
438        );
439        self.read_memory_impl(addr, buf)
440    }
441
442    fn read_memory32(&mut self, addr: u32, buf: &mut [u32]) -> Result<usize> {
443        ensure!(
444            matches!(self.jtag_tap, JtagTap::RiscvTap),
445            JtagError::Tap(self.jtag_tap)
446        );
447        self.read_memory_impl(addr, buf)
448    }
449
450    fn write_memory(&mut self, addr: u32, buf: &[u8]) -> Result<()> {
451        ensure!(
452            matches!(self.jtag_tap, JtagTap::RiscvTap),
453            JtagError::Tap(self.jtag_tap)
454        );
455        self.write_memory_impl(addr, buf)
456    }
457
458    fn write_memory32(&mut self, addr: u32, buf: &[u32]) -> Result<()> {
459        ensure!(
460            matches!(self.jtag_tap, JtagTap::RiscvTap),
461            JtagError::Tap(self.jtag_tap)
462        );
463        self.write_memory_impl(addr, buf)
464    }
465
466    fn halt(&mut self) -> Result<()> {
467        ensure!(
468            matches!(self.jtag_tap, JtagTap::RiscvTap),
469            JtagError::Tap(self.jtag_tap)
470        );
471        let response = self.send_tcl_cmd("halt")?;
472        if !response.is_empty() {
473            bail!("unexpected response: '{response}'");
474        }
475
476        Ok(())
477    }
478
479    fn wait_halt(&mut self, timeout: Duration) -> Result<()> {
480        ensure!(
481            matches!(self.jtag_tap, JtagTap::RiscvTap),
482            JtagError::Tap(self.jtag_tap)
483        );
484        let cmd = format!("wait_halt {}", timeout.as_millis());
485        let response = self.send_tcl_cmd(cmd.as_str())?;
486        if !response.is_empty() {
487            bail!("unexpected response: '{response}'");
488        }
489        Ok(())
490    }
491
492    fn resume(&mut self) -> Result<()> {
493        ensure!(
494            matches!(self.jtag_tap, JtagTap::RiscvTap),
495            JtagError::Tap(self.jtag_tap)
496        );
497        let response = self.send_tcl_cmd("resume")?;
498        if !response.is_empty() {
499            bail!("unexpected response: '{response}'");
500        }
501
502        Ok(())
503    }
504
505    fn resume_at(&mut self, addr: u32) -> Result<()> {
506        ensure!(
507            matches!(self.jtag_tap, JtagTap::RiscvTap),
508            JtagError::Tap(self.jtag_tap)
509        );
510        let cmd = format!("resume 0x{:x}", addr);
511        let response = self.send_tcl_cmd(&cmd)?;
512        if !response.is_empty() {
513            bail!("unexpected response: '{response}'");
514        }
515
516        Ok(())
517    }
518
519    fn reset(&mut self, run: bool) -> Result<()> {
520        ensure!(
521            matches!(self.jtag_tap, JtagTap::RiscvTap),
522            JtagError::Tap(self.jtag_tap)
523        );
524        let cmd = format!("reset {}", if run { "run" } else { "halt" });
525        let response = self.send_tcl_cmd(&cmd)?;
526        if !response.is_empty() {
527            bail!("unexpected response: '{response}'");
528        }
529
530        Ok(())
531    }
532
533    fn step(&mut self) -> Result<()> {
534        ensure!(
535            matches!(self.jtag_tap, JtagTap::RiscvTap),
536            JtagError::Tap(self.jtag_tap)
537        );
538        let response = self.send_tcl_cmd("step")?;
539        if !response.is_empty() {
540            bail!("unexpected response: '{response}'");
541        }
542
543        Ok(())
544    }
545
546    fn step_at(&mut self, addr: u32) -> Result<()> {
547        ensure!(
548            matches!(self.jtag_tap, JtagTap::RiscvTap),
549            JtagError::Tap(self.jtag_tap)
550        );
551        let cmd = format!("step 0x{:x}", addr);
552        let response = self.send_tcl_cmd(&cmd)?;
553        if !response.is_empty() {
554            bail!("unexpected response: '{response}'");
555        }
556
557        Ok(())
558    }
559
560    fn read_riscv_reg(&mut self, reg: &RiscvReg) -> Result<u32> {
561        ensure!(
562            matches!(self.jtag_tap, JtagTap::RiscvTap),
563            JtagError::Tap(self.jtag_tap)
564        );
565        self.read_register::<u32>(reg.name(), true)
566    }
567
568    fn write_riscv_reg(&mut self, reg: &RiscvReg, val: u32) -> Result<()> {
569        ensure!(
570            matches!(self.jtag_tap, JtagTap::RiscvTap),
571            JtagError::Tap(self.jtag_tap)
572        );
573        self.write_register(reg.name(), val)
574    }
575
576    fn set_breakpoint(&mut self, address: u32, hw: bool) -> Result<()> {
577        let cmd = format!("bp {:#x} 2{}", address, if hw { " hw" } else { "" });
578        let response = self.send_tcl_cmd(&cmd)?;
579        if !response.starts_with("breakpoint set at ") {
580            bail!("unexpected response: '{response}'");
581        }
582        Ok(())
583    }
584
585    fn remove_breakpoint(&mut self, addr: u32) -> Result<()> {
586        let cmd = format!("rbp {:#x}", addr);
587        let response = self.send_tcl_cmd(&cmd)?;
588        if !response.is_empty() {
589            bail!("unexpected response: '{response}'");
590        }
591        Ok(())
592    }
593
594    fn remove_all_breakpoints(&mut self) -> Result<()> {
595        let response = self.send_tcl_cmd("rbp all")?;
596        if !response.is_empty() {
597            bail!("unexpected response: '{response}'");
598        }
599        Ok(())
600    }
601}