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