Add gdb_qemu utility (#1331)

This commit is contained in:
WorksButNotTested 2023-06-30 19:36:46 +01:00 committed by GitHub
parent c6062889d5
commit 97b3d3c7c7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 786 additions and 0 deletions

View File

@ -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"

2
utils/gdb_qemu/.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
/target
gdb_qemu.log

View File

@ -0,0 +1,6 @@
[workspace]
resolver = "2"
members = [
"gdb_qemu",
"demo",
]

View File

@ -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"]

69
utils/gdb_qemu/README.md Normal file
View File

@ -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 <PORT> <PROGRAM> [-- <ARGS>...]
Arguments:
<PROGRAM>
Name of the qemu-user binary to launch
[ARGS]...
Arguments passed to the target
Options:
-p, --port <PORT>
Port
-t, --timeout <TIMEOUT>
Timeout Ms
[default: 2000]
-l, --log-file <LOG_FILE>
Log file (Requires --log-level)
[default: gdb_qemu.log]
-L, --log-level <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
```

View File

@ -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"] }

View File

@ -0,0 +1,13 @@
use {std::error::Error, vergen::EmitBuilder};
fn main() -> Result<(), Box<dyn Error>> {
EmitBuilder::builder()
.all_build()
.all_cargo()
.all_git()
.all_rustc()
.all_sysinfo()
.emit()?;
Ok(())
}

View File

@ -0,0 +1,35 @@
use clap::{builder::Str, Parser};
#[derive(Default)]
pub struct Version;
impl From<Version> 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::<String>();
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;

View File

@ -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);
}

View File

@ -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 }

View File

@ -0,0 +1,13 @@
use {std::error::Error, vergen::EmitBuilder};
fn main() -> Result<(), Box<dyn Error>> {
EmitBuilder::builder()
.all_build()
.all_cargo()
.all_git()
.all_rustc()
.all_sysinfo()
.emit()?;
Ok(())
}

View File

@ -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<Level> 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,
}
}
}

View File

@ -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<String>;
}
impl ChildArgs for Args {
fn argv(&self) -> Vec<String> {
iter::once(&self.program)
.chain(self.args.iter())
.cloned()
.collect::<Vec<String>>()
}
}
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<String>,
}

View File

@ -0,0 +1,26 @@
use clap::builder::Str;
#[derive(Default)]
pub struct Version;
impl From<Version> 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::<String>();
format!("\n{version:}").into()
}
}

View File

@ -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<String>,
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::<Result<Vec<CString>>>()?;
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,
}
}
}

View File

@ -0,0 +1,5 @@
use libc::__errno_location;
pub fn errno() -> i32 {
unsafe { *__errno_location() }
}

View File

@ -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(())
}
}

View File

@ -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(())
}
}

View File

@ -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(())
}

View File

@ -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<TcpStream> {
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(())
}
}