From 97b3d3c7c7c943c832d2061baaab26cce54e81ae Mon Sep 17 00:00:00 2001 From: WorksButNotTested <62701594+WorksButNotTested@users.noreply.github.com> Date: Fri, 30 Jun 2023 19:36:46 +0100 Subject: [PATCH] Add gdb_qemu utility (#1331) --- utils/gdb_qemu/.cargo/config.toml | 6 + utils/gdb_qemu/.gitignore | 2 + utils/gdb_qemu/Cargo.toml | 6 + utils/gdb_qemu/Makefile.toml | 82 +++++++++ utils/gdb_qemu/README.md | 69 +++++++ utils/gdb_qemu/demo/Cargo.toml | 11 ++ utils/gdb_qemu/demo/build.rs | 13 ++ utils/gdb_qemu/demo/src/args.rs | 35 ++++ utils/gdb_qemu/demo/src/main.rs | 30 +++ utils/gdb_qemu/gdb_qemu/Cargo.toml | 16 ++ utils/gdb_qemu/gdb_qemu/build.rs | 13 ++ utils/gdb_qemu/gdb_qemu/src/args/level.rs | 24 +++ utils/gdb_qemu/gdb_qemu/src/args/mod.rs | 83 +++++++++ utils/gdb_qemu/gdb_qemu/src/args/version.rs | 26 +++ utils/gdb_qemu/gdb_qemu/src/child.rs | 55 ++++++ utils/gdb_qemu/gdb_qemu/src/errno.rs | 5 + utils/gdb_qemu/gdb_qemu/src/exit.rs | 57 ++++++ utils/gdb_qemu/gdb_qemu/src/logger.rs | 21 +++ utils/gdb_qemu/gdb_qemu/src/main.rs | 38 ++++ utils/gdb_qemu/gdb_qemu/src/parent.rs | 194 ++++++++++++++++++++ 20 files changed, 786 insertions(+) create mode 100644 utils/gdb_qemu/.cargo/config.toml create mode 100644 utils/gdb_qemu/.gitignore create mode 100644 utils/gdb_qemu/Cargo.toml create mode 100644 utils/gdb_qemu/Makefile.toml create mode 100644 utils/gdb_qemu/README.md create mode 100644 utils/gdb_qemu/demo/Cargo.toml create mode 100644 utils/gdb_qemu/demo/build.rs create mode 100644 utils/gdb_qemu/demo/src/args.rs create mode 100644 utils/gdb_qemu/demo/src/main.rs create mode 100644 utils/gdb_qemu/gdb_qemu/Cargo.toml create mode 100644 utils/gdb_qemu/gdb_qemu/build.rs create mode 100644 utils/gdb_qemu/gdb_qemu/src/args/level.rs create mode 100644 utils/gdb_qemu/gdb_qemu/src/args/mod.rs create mode 100644 utils/gdb_qemu/gdb_qemu/src/args/version.rs create mode 100644 utils/gdb_qemu/gdb_qemu/src/child.rs create mode 100644 utils/gdb_qemu/gdb_qemu/src/errno.rs create mode 100644 utils/gdb_qemu/gdb_qemu/src/exit.rs create mode 100644 utils/gdb_qemu/gdb_qemu/src/logger.rs create mode 100644 utils/gdb_qemu/gdb_qemu/src/main.rs create mode 100644 utils/gdb_qemu/gdb_qemu/src/parent.rs diff --git a/utils/gdb_qemu/.cargo/config.toml b/utils/gdb_qemu/.cargo/config.toml new file mode 100644 index 0000000000..e39712c3f6 --- /dev/null +++ b/utils/gdb_qemu/.cargo/config.toml @@ -0,0 +1,6 @@ +[build] +target = "x86_64-unknown-linux-gnu" + +[target.powerpc-unknown-linux-gnu] +linker = "powerpc-linux-gnu-gcc" +runner = "qemu-ppc -L /usr/powerpc-linux-gnu" diff --git a/utils/gdb_qemu/.gitignore b/utils/gdb_qemu/.gitignore new file mode 100644 index 0000000000..7455e40c6c --- /dev/null +++ b/utils/gdb_qemu/.gitignore @@ -0,0 +1,2 @@ +/target +gdb_qemu.log diff --git a/utils/gdb_qemu/Cargo.toml b/utils/gdb_qemu/Cargo.toml new file mode 100644 index 0000000000..4f626a8850 --- /dev/null +++ b/utils/gdb_qemu/Cargo.toml @@ -0,0 +1,6 @@ +[workspace] +resolver = "2" +members = [ + "gdb_qemu", + "demo", +] diff --git a/utils/gdb_qemu/Makefile.toml b/utils/gdb_qemu/Makefile.toml new file mode 100644 index 0000000000..dbdb4bda72 --- /dev/null +++ b/utils/gdb_qemu/Makefile.toml @@ -0,0 +1,82 @@ +[config] +default_to_workspace = false + +[env] +DEMO_TARGET="powerpc-unknown-linux-gnu" +HOST_TARGET="x86_64-unknown-linux-gnu" +PROFILE="dev" +DEMO_DIR="${CARGO_MAKE_CRATE_TARGET_DIRECTORY}/${DEMO_TARGET}/debug" +TARGET_DIR="${CARGO_MAKE_CRATE_TARGET_DIRECTORY}/${HOST_TARGET}/debug" + +[env.release] +PROFILE="release" +DEMO_DIR="${CARGO_MAKE_CRATE_TARGET_DIRECTORY}/${DEMO_TARGET}/release" +TARGET_DIR="${CARGO_MAKE_CRATE_TARGET_DIRECTORY}/${HOST_TARGET}/release" + +[tasks.clean] +command = "cargo" +args = ["clean"] + +[tasks.format] +install_crate = "rustfmt" +command = "cargo" +args = ["fmt", "--", "--emit=files"] + +[tasks.demo] +dependencies = ["format", "clippy"] +command = "cargo" +args = [ + "build", + "-p", "demo", + "--profile", "${PROFILE}", + "--target", "powerpc-unknown-linux-gnu", +] + +[tasks.run_demo] +dependencies = ["demo"] +command = "cargo" +args = [ + "run", + "-p", "demo", + "--target", "powerpc-unknown-linux-gnu", +] + +[tasks.build] +dependencies = ["format", "clippy"] +command = "cargo" +args = [ + "build", + "-p", "gdb_qemu", + "--profile", "${PROFILE}", +] + +[tasks.run] +command = "cargo" +dependencies = [ "demo" ] +args = [ + "run", + "-p", "gdb_qemu", + "--profile", "${PROFILE}", + "--", + "-p", "1234", + "-L", "trace", + "--", + "qemu-ppc", + "-L", "/usr/powerpc-linux-gnu", + "-g", "1234", + "${DEMO_DIR}/demo" +] + +[tasks.gdb] +command = "gdb-multiarch" +dependencies = ["demo", "build"] +args = [ + "-ex", "set architecture powerpc:MPC8XX", + "-ex", "set pagination off", + "-ex", "set confirm off", + "-ex", "file ${DEMO_DIR}/demo", + "-ex", "target remote | ${TARGET_DIR}/gdb_qemu -p 1234 -L trace qemu-ppc -- -L /usr/powerpc-linux-gnu -g 1234 ${DEMO_DIR}/demo" +] + +[tasks.all] +dependencies = ["demo", "build"] diff --git a/utils/gdb_qemu/README.md b/utils/gdb_qemu/README.md new file mode 100644 index 0000000000..d391f3fe4d --- /dev/null +++ b/utils/gdb_qemu/README.md @@ -0,0 +1,69 @@ +# GDB-QEMU +`gdb-qemu` is a launcher for running `qemu-user` within `gdb`. + +# Test +``` +rustup target add powerpc-unknown-linux-gnu +$ cargo make gdb +``` + +# Example +``` +gdb-multiarch \ + -ex "set architecture powerpc:MPC8XX" \ + -ex "set pagination off" \ + -ex "set confirm off" \ + -ex "file demo" \ + -ex "target remote | gdb-qemu -p 1234 qemu-ppc -- -L /usr/powerpc-linux-gnu -g 1234 demo +``` + +# About +`qemu-gdb` does the following: +* Creates two pipes for the target program to send its `stdout`, `stderr`. +* Forks a child process and sets the `stdout` and `stderr` using `dup2`. +* Exec's the target program (passing the provided arguments). +* Connects to the specified TCP debug port on the target program. +* Forwards data from `gdb-qemu`'s `stdin` and `stdout` to the TCP port. +* Forwards data from the target program's `stdout` and `stderr` to `gdb-qemu`s `stderr`. +* Optionally logs to the specified log file. +* Optionally logs trace information of the data transferred by the message pumps. + +# Usage +``` +Tool launching qemu-user for debugging + +Usage: gdb-qemu [OPTIONS] --port [-- ...] + +Arguments: + + Name of the qemu-user binary to launch + + [ARGS]... + Arguments passed to the target + +Options: + -p, --port + Port + + -t, --timeout + Timeout Ms + + [default: 2000] + + -l, --log-file + Log file (Requires --log-level) + + [default: gdb_qemu.log] + + -L, --log-level + Log level + + [default: off] + [possible values: off, error, warn, info, debug, trace] + + -h, --help + Print help (see a summary with '-h') + + -V, --version + Print version +``` diff --git a/utils/gdb_qemu/demo/Cargo.toml b/utils/gdb_qemu/demo/Cargo.toml new file mode 100644 index 0000000000..0eab37d4b8 --- /dev/null +++ b/utils/gdb_qemu/demo/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "demo" +version = "0.1.0" +edition = "2021" + +[build-dependencies] +vergen = { version = "8.1.1", features = ["build", "cargo", "git", "gitcl", "rustc", "si"] } + +[dependencies] +anyhow = { version = "1.0.71", default-features = false } +clap = { version = "4.2.0", default-features = false, features = ["derive", "string", "std", "help"] } diff --git a/utils/gdb_qemu/demo/build.rs b/utils/gdb_qemu/demo/build.rs new file mode 100644 index 0000000000..b8c56e8d82 --- /dev/null +++ b/utils/gdb_qemu/demo/build.rs @@ -0,0 +1,13 @@ +use {std::error::Error, vergen::EmitBuilder}; + +fn main() -> Result<(), Box> { + EmitBuilder::builder() + .all_build() + .all_cargo() + .all_git() + .all_rustc() + .all_sysinfo() + .emit()?; + + Ok(()) +} diff --git a/utils/gdb_qemu/demo/src/args.rs b/utils/gdb_qemu/demo/src/args.rs new file mode 100644 index 0000000000..4895944b88 --- /dev/null +++ b/utils/gdb_qemu/demo/src/args.rs @@ -0,0 +1,35 @@ +use clap::{builder::Str, Parser}; + +#[derive(Default)] +pub struct Version; + +impl From for Str { + fn from(_: Version) -> Str { + let version = [ + ("Build Timestamp:", env!("VERGEN_BUILD_TIMESTAMP")), + ("Describe:", env!("VERGEN_GIT_DESCRIBE")), + ("Commit SHA:", env!("VERGEN_GIT_SHA")), + ("Commit Date:", env!("VERGEN_RUSTC_COMMIT_DATE")), + ("Commit Branch:", env!("VERGEN_GIT_BRANCH")), + ("Rustc Version:", env!("VERGEN_RUSTC_SEMVER")), + ("Rustc Channel:", env!("VERGEN_RUSTC_CHANNEL")), + ("Rustc Host Triple:", env!("VERGEN_RUSTC_HOST_TRIPLE")), + ("Rustc Commit SHA:", env!("VERGEN_RUSTC_COMMIT_HASH")), + ("Cargo Target Triple", env!("VERGEN_CARGO_TARGET_TRIPLE")), + ] + .iter() + .map(|(k, v)| format!("{k:25}: {v}\n")) + .collect::(); + + format!("\n{version:}").into() + } +} + +#[derive(Parser, Debug)] +#[clap(author, version, about, long_about = None)] +#[command( + version = Version::default(), + about = "gdb-qemu", + long_about = "Tool launching qemu-user for debugging" +)] +pub struct Args; diff --git a/utils/gdb_qemu/demo/src/main.rs b/utils/gdb_qemu/demo/src/main.rs new file mode 100644 index 0000000000..1ea296a3cb --- /dev/null +++ b/utils/gdb_qemu/demo/src/main.rs @@ -0,0 +1,30 @@ +mod args; + +use { + crate::args::Args, + clap::Parser, + std::{thread::sleep, time::Duration}, +}; + +#[no_mangle] +extern "C" fn run_test(num: usize) { + println!("OUT - test: {num:}"); + if num & 1 == 0 { + eprintln!("ERR - test: {num:}"); + } +} + +#[no_mangle] +extern "C" fn test(num: usize) { + for i in 0..num { + run_test(i); + sleep(Duration::from_millis(250)); + } +} + +fn main() { + println!("Hello demo!"); + let args = Args::parse(); + println!("Args: {args:#?}"); + test(10); +} diff --git a/utils/gdb_qemu/gdb_qemu/Cargo.toml b/utils/gdb_qemu/gdb_qemu/Cargo.toml new file mode 100644 index 0000000000..2f25e24b9e --- /dev/null +++ b/utils/gdb_qemu/gdb_qemu/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "gdb_qemu" +version = "0.1.0" +edition = "2021" + +[build-dependencies] +vergen = { version = "8.1.1", features = ["build", "cargo", "git", "gitcl", "rustc", "si"] } + +[dependencies] +anyhow = { version = "1.0.71", default-features = false } +clap = { version = "4.2.0", default-features = false, features = ["derive", "string", "std", "help", "derive", "error-context", "usage"] } +libc = {version = "0.2.146", default-features = false } +log = { version = "0.4.19", default-features = false } +nix = { version = "0.26.2", default-features = false, features = ["signal", "fs"] } +readonly = { version = "0.2.8", default-features = false } +simplelog = { version = "0.12.1", default-features = false } diff --git a/utils/gdb_qemu/gdb_qemu/build.rs b/utils/gdb_qemu/gdb_qemu/build.rs new file mode 100644 index 0000000000..b8c56e8d82 --- /dev/null +++ b/utils/gdb_qemu/gdb_qemu/build.rs @@ -0,0 +1,13 @@ +use {std::error::Error, vergen::EmitBuilder}; + +fn main() -> Result<(), Box> { + EmitBuilder::builder() + .all_build() + .all_cargo() + .all_git() + .all_rustc() + .all_sysinfo() + .emit()?; + + Ok(()) +} diff --git a/utils/gdb_qemu/gdb_qemu/src/args/level.rs b/utils/gdb_qemu/gdb_qemu/src/args/level.rs new file mode 100644 index 0000000000..e516f47ceb --- /dev/null +++ b/utils/gdb_qemu/gdb_qemu/src/args/level.rs @@ -0,0 +1,24 @@ +use {clap::ValueEnum, simplelog::LevelFilter}; + +#[derive(ValueEnum, Debug, Clone, Copy)] +pub enum Level { + Off, + Error, + Warn, + Info, + Debug, + Trace, +} + +impl From for LevelFilter { + fn from(level: Level) -> LevelFilter { + match level { + Level::Off => LevelFilter::Off, + Level::Error => LevelFilter::Error, + Level::Warn => LevelFilter::Warn, + Level::Info => LevelFilter::Info, + Level::Debug => LevelFilter::Debug, + Level::Trace => LevelFilter::Trace, + } + } +} diff --git a/utils/gdb_qemu/gdb_qemu/src/args/mod.rs b/utils/gdb_qemu/gdb_qemu/src/args/mod.rs new file mode 100644 index 0000000000..dc7ce27b66 --- /dev/null +++ b/utils/gdb_qemu/gdb_qemu/src/args/mod.rs @@ -0,0 +1,83 @@ +pub mod level; +mod version; + +use { + crate::args::{level::Level, version::Version}, + clap::Parser, + std::iter, +}; + +pub trait ParentArgs { + fn port(&self) -> u16; + fn timeout(&self) -> u64; +} + +impl ParentArgs for Args { + fn port(&self) -> u16 { + self.port + } + fn timeout(&self) -> u64 { + self.timeout + } +} + +pub trait ChildArgs { + fn argv(&self) -> Vec; +} + +impl ChildArgs for Args { + fn argv(&self) -> Vec { + iter::once(&self.program) + .chain(self.args.iter()) + .cloned() + .collect::>() + } +} + +pub trait LogArgs { + fn log_file(&self) -> String; + fn log_level(&self) -> Level; +} + +impl LogArgs for Args { + fn log_file(&self) -> String { + self.log_file.clone() + } + fn log_level(&self) -> Level { + self.log_level + } +} + +#[derive(Parser, Debug)] +#[clap(author, version, about, long_about = None)] +#[command( + version = Version::default(), + about = "gdb-qemu", + long_about = "Tool launching qemu-user for debugging" +)] +#[readonly::make] +pub struct Args { + #[arg(short, long, help = "Port", value_parser = clap::value_parser!(u16).range(1..))] + port: u16, + + #[arg(short, long, help = "Timeout Ms", default_value_t = 2000)] + timeout: u64, + + #[arg( + short, + long, + help = "Log file (Requires --log-level)", + default_value = "gdb_qemu.log", + requires = "log_level" + )] + log_file: String, + + #[arg(short='L', long, help = "Log level", value_enum, default_value_t = Level::Off)] + log_level: Level, + + #[arg(help = "Name of the qemu-user binary to launch")] + program: String, + + #[arg(last = true, value_parser, value_delimiter = ' ', num_args = 1.., help = "Arguments passed to the target")] + args: Vec, +} diff --git a/utils/gdb_qemu/gdb_qemu/src/args/version.rs b/utils/gdb_qemu/gdb_qemu/src/args/version.rs new file mode 100644 index 0000000000..b664e960b3 --- /dev/null +++ b/utils/gdb_qemu/gdb_qemu/src/args/version.rs @@ -0,0 +1,26 @@ +use clap::builder::Str; + +#[derive(Default)] +pub struct Version; + +impl From for Str { + fn from(_: Version) -> Str { + let version = [ + ("Build Timestamp:", env!("VERGEN_BUILD_TIMESTAMP")), + ("Describe:", env!("VERGEN_GIT_DESCRIBE")), + ("Commit SHA:", env!("VERGEN_GIT_SHA")), + ("Commit Date:", env!("VERGEN_RUSTC_COMMIT_DATE")), + ("Commit Branch:", env!("VERGEN_GIT_BRANCH")), + ("Rustc Version:", env!("VERGEN_RUSTC_SEMVER")), + ("Rustc Channel:", env!("VERGEN_RUSTC_CHANNEL")), + ("Rustc Host Triple:", env!("VERGEN_RUSTC_HOST_TRIPLE")), + ("Rustc Commit SHA:", env!("VERGEN_RUSTC_COMMIT_HASH")), + ("Cargo Target Triple", env!("VERGEN_CARGO_TARGET_TRIPLE")), + ] + .iter() + .map(|(k, v)| format!("{k:25}: {v}\n")) + .collect::(); + + format!("\n{version:}").into() + } +} diff --git a/utils/gdb_qemu/gdb_qemu/src/child.rs b/utils/gdb_qemu/gdb_qemu/src/child.rs new file mode 100644 index 0000000000..9ceacd6a46 --- /dev/null +++ b/utils/gdb_qemu/gdb_qemu/src/child.rs @@ -0,0 +1,55 @@ +use { + crate::{args::ChildArgs, exit::Exit}, + anyhow::{anyhow, Result}, + nix::unistd::{dup2, execvp}, + std::ffi::CString, + std::os::fd::{AsRawFd, RawFd}, +}; + +pub struct Child { + argv: Vec, + fd1: RawFd, + fd2: RawFd, +} + +impl Child { + fn launch(&self) -> Result<()> { + let cargs = self + .argv + .iter() + .map(|x| CString::new(x.clone()).map_err(|e| anyhow!("Failed to read argument: {e:}"))) + .collect::>>()?; + + info!("cargs: {cargs:#?}"); + + execvp(&cargs[0], &cargs).map_err(|e| anyhow!("Failed to exceve: {e:}"))?; + Ok(()) + } + + fn redirect(&self) -> Result<()> { + let stdout = std::io::stdout(); + let stderr = std::io::stderr(); + dup2(self.fd1, stdout.as_raw_fd()) + .map_err(|e| anyhow!("Failed to redirect stdout: {e:}"))?; + + dup2(self.fd2, stderr.as_raw_fd()) + .map_err(|e| anyhow!("Failed to redirect stderr: {e:}"))?; + Ok(()) + } + + pub fn run(&self) -> Result<()> { + Exit::die_on_parent_exit()?; + self.redirect()?; + self.launch()?; + + Ok(()) + } + + pub fn new(args: &impl ChildArgs, fd1: RawFd, fd2: RawFd) -> Child { + Child { + argv: args.argv().to_vec(), + fd1, + fd2, + } + } +} diff --git a/utils/gdb_qemu/gdb_qemu/src/errno.rs b/utils/gdb_qemu/gdb_qemu/src/errno.rs new file mode 100644 index 0000000000..9cc0dae35e --- /dev/null +++ b/utils/gdb_qemu/gdb_qemu/src/errno.rs @@ -0,0 +1,5 @@ +use libc::__errno_location; + +pub fn errno() -> i32 { + unsafe { *__errno_location() } +} diff --git a/utils/gdb_qemu/gdb_qemu/src/exit.rs b/utils/gdb_qemu/gdb_qemu/src/exit.rs new file mode 100644 index 0000000000..c961039f2f --- /dev/null +++ b/utils/gdb_qemu/gdb_qemu/src/exit.rs @@ -0,0 +1,57 @@ +use { + crate::errno::errno, + anyhow::{anyhow, Result}, + libc::{_exit, prctl, PR_SET_PDEATHSIG}, + nix::sys::signal::SIGKILL, + nix::{ + sys::{ + signal::{sigaction, SaFlags, SigAction, SigHandler, SigSet, SIGCHLD}, + wait::{waitpid, WaitStatus::Exited}, + }, + unistd::Pid, + }, +}; + +pub struct Exit; + +impl Exit { + pub fn die_on_parent_exit() -> Result<()> { + if unsafe { prctl(PR_SET_PDEATHSIG, SIGKILL) } != 0 { + Err(anyhow!("Failed to prctl(PR_SET_PDEATHSIG): {}", errno()))?; + } + Ok(()) + } + + pub fn die_on_child_exit() -> Result<()> { + let sig_action = SigAction::new( + SigHandler::Handler(Self::handle_sigchld), + SaFlags::empty(), + SigSet::empty(), + ); + + unsafe { sigaction(SIGCHLD, &sig_action) } + .map_err(|e| anyhow!("Failed to sigaction: {e:}"))?; + Ok(()) + } + + extern "C" fn handle_sigchld(sig: libc::c_int) { + info!("handle_sigchld: {sig:}"); + let status = waitpid(Pid::from_raw(-1), None).expect("Failed to wait for child"); + match status { + Exited(pid, exit) => { + info!("Exited: {pid:}"); + unsafe { _exit(exit) }; + } + _ => { + panic!("Invalid exit status: {status:#?}"); + } + } + } + + pub fn wait_for_child() -> Result<()> { + let status = + waitpid(Pid::from_raw(-1), None).map_err(|e| anyhow!("Failed to waitpid: {e:}"))?; + info!("STATUS: {status:#?}"); + Ok(()) + } +} diff --git a/utils/gdb_qemu/gdb_qemu/src/logger.rs b/utils/gdb_qemu/gdb_qemu/src/logger.rs new file mode 100644 index 0000000000..6e1ae877dd --- /dev/null +++ b/utils/gdb_qemu/gdb_qemu/src/logger.rs @@ -0,0 +1,21 @@ +use { + crate::args::LogArgs, + anyhow::{anyhow, Result}, + simplelog::{Config, LevelFilter, WriteLogger}, + std::fs::File, +}; + +pub struct Logger; + +impl Logger { + pub fn init(args: &impl LogArgs) -> Result<()> { + let filter: LevelFilter = args.log_level().into(); + if filter != LevelFilter::Off { + let logfile = File::create(args.log_file()) + .map_err(|e| anyhow!("Failed to open log file: {e:}"))?; + WriteLogger::init(filter, Config::default(), logfile) + .map_err(|e| anyhow!("Failed to initalize logger: {e:}"))?; + } + Ok(()) + } +} diff --git a/utils/gdb_qemu/gdb_qemu/src/main.rs b/utils/gdb_qemu/gdb_qemu/src/main.rs new file mode 100644 index 0000000000..c5234044a2 --- /dev/null +++ b/utils/gdb_qemu/gdb_qemu/src/main.rs @@ -0,0 +1,38 @@ +mod args; +mod child; +mod errno; +mod exit; +mod logger; +mod parent; + +#[macro_use] +extern crate log; +extern crate simplelog; + +use { + crate::{args::Args, child::Child, exit::Exit, logger::Logger, parent::Parent}, + anyhow::{anyhow, Result}, + clap::Parser, + nix::unistd::{fork, pipe, ForkResult}, +}; + +fn main() -> Result<()> { + let args = Args::parse(); + Logger::init(&args)?; + + info!("Started gdb-qemu..."); + + info!("Args: {args:#?}"); + + Exit::die_on_child_exit()?; + + let (a1, b1) = pipe().map_err(|e| anyhow!("Failed to create pipe #1: {e:}"))?; + let (a2, b2) = pipe().map_err(|e| anyhow!("Failed to create pipe #2: {e:}"))?; + + match unsafe { fork() } { + Ok(ForkResult::Parent { child: _, .. }) => Parent::new(&args, a1, a2).run()?, + Ok(ForkResult::Child) => Child::new(&args, b1, b2).run()?, + Err(e) => Err(anyhow!("main: fork failed: {e:}"))?, + }; + Ok(()) +} diff --git a/utils/gdb_qemu/gdb_qemu/src/parent.rs b/utils/gdb_qemu/gdb_qemu/src/parent.rs new file mode 100644 index 0000000000..8faadefa00 --- /dev/null +++ b/utils/gdb_qemu/gdb_qemu/src/parent.rs @@ -0,0 +1,194 @@ +use { + crate::{args::ParentArgs, exit::Exit}, + anyhow::{anyhow, Result}, + nix::unistd::read, + std::{ + fmt, + io::{Read, Write}, + net::{SocketAddr, TcpStream}, + os::fd::RawFd, + str::from_utf8, + thread::spawn, + time::{Duration, SystemTime}, + }, +}; + +enum Direction { + GdbToTarget, + TargetToGdb, +} + +impl fmt::Display for Direction { + fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { + match self { + Direction::GdbToTarget => write!(fmt, "GDB --> TGT"), + Direction::TargetToGdb => write!(fmt, "GDB <-- TGT"), + } + } +} + +enum Channel { + Stdout, + StdErr, +} + +impl fmt::Display for Channel { + fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { + match self { + Channel::Stdout => write!(fmt, "[STDOUT]"), + Channel::StdErr => write!(fmt, "[STDERR]"), + } + } +} + +pub struct Parent { + port: u16, + timeout: u64, + fd1: RawFd, + fd2: RawFd, +} + +impl Parent { + const BUFFER_SIZE: usize = 16 << 10; + + pub fn new(args: &impl ParentArgs, fd1: RawFd, fd2: RawFd) -> Parent { + Parent { + port: args.port(), + timeout: args.timeout(), + fd1, + fd2, + } + } + + fn log_packets(direction: &Direction, buffer: &[u8]) -> Result<()> { + for pkt in from_utf8(buffer) + .map_err(|e| anyhow!("Failed to read buffer: {e:}"))? + .split('$') + .filter(|x| !x.is_empty()) + .filter(|x| x != &"+") + { + trace!("{direction:} - ${pkt:}"); + } + Ok(()) + } + + fn log_io(channel: &Channel, buffer: &[u8]) -> Result<()> { + for line in from_utf8(buffer) + .map_err(|e| anyhow!("Failed to read buffer: {e:}"))? + .lines() + .filter(|x| !x.is_empty()) + { + trace!("{channel:} - {line:}"); + } + Ok(()) + } + + fn pump(input: &mut impl Read, output: &mut impl Write, direction: Direction) -> Result<()> { + let mut buffer = [0u8; Parent::BUFFER_SIZE]; + loop { + let n = input + .read(&mut buffer) + .map_err(|e| anyhow!("Failed to read input: {e:}"))?; + if n == 0 { + break; + } + + Parent::log_packets(&direction, &buffer[..n])?; + + output + .write_all(&buffer[..n]) + .map_err(|e| anyhow!("Failed to write output: {e:}"))?; + output + .flush() + .map_err(|e| anyhow!("Failed to flush output: {e:}"))?; + } + Ok(()) + } + + fn pumpfd(input: RawFd, output: &mut impl Write, channel: Channel) -> Result<()> { + let mut buffer = [0u8; Parent::BUFFER_SIZE]; + loop { + let n = read(input, &mut buffer).map_err(|e| anyhow!("Failed to read input: {e:}"))?; + if n == 0 { + break; + } + + Parent::log_io(&channel, &buffer[..n])?; + + output + .write_all(&buffer[..n]) + .map_err(|e| anyhow!("Failed to write output: {e:}"))?; + output + .flush() + .map_err(|e| anyhow!("Failed to flush output: {e:}"))?; + } + Ok(()) + } + + fn connect(&self) -> Result { + let addr = SocketAddr::from(([127, 0, 0, 1], self.port)); + let timeout = Duration::from_millis(self.timeout); + + let now = SystemTime::now(); + + loop { + let result = TcpStream::connect(addr); + if let Ok(stream) = result { + return Ok(stream); + } + + let elapsed = now + .elapsed() + .map_err(|e| anyhow!("Failed to measure elapsed time: {e:}"))?; + + if elapsed > timeout { + return result.map_err(|e| anyhow!("Failed to connect: {e:}")); + } + } + } + + pub fn run(&self) -> Result<()> { + let stream = self.connect()?; + info!("Connected to client: {stream:#?}"); + + let mut read_stream = stream + .try_clone() + .map_err(|e| anyhow!("Failed to clone read_stream: {e:}"))?; + let mut stdout = std::io::stdout(); + let reader = spawn(move || { + Self::pump(&mut read_stream, &mut stdout, Direction::TargetToGdb).unwrap() + }); + + let mut stdin = std::io::stdin(); + let mut write_stream = stream + .try_clone() + .map_err(|e| anyhow!("Failed to clone write_stream: {e:}"))?; + let writer = spawn(move || { + Self::pump(&mut stdin, &mut write_stream, Direction::GdbToTarget).unwrap() + }); + + let mut stderr1 = std::io::stderr(); + let fd1 = self.fd1; + let stdout_pump = spawn(move || Self::pumpfd(fd1, &mut stderr1, Channel::Stdout).unwrap()); + + let mut stderr2 = std::io::stderr(); + let fd2 = self.fd2; + let stderr_pump = spawn(move || Self::pumpfd(fd2, &mut stderr2, Channel::StdErr).unwrap()); + + reader + .join() + .map_err(|e| anyhow!("Failed to join reader: {e:#?}"))?; + writer + .join() + .map_err(|e| anyhow!("Failed to join writer: {e:#?}"))?; + stdout_pump + .join() + .map_err(|e| anyhow!("Failed to join stdout_pump: {e:#?}"))?; + stderr_pump + .join() + .map_err(|e| anyhow!("Failed to join stderr_pump: {e:#?}"))?; + + Exit::wait_for_child()?; + Ok(()) + } +}