v0.1: spawn/step/cont, state.{pc,sp,registers,memory}, prelude.js
This commit is contained in:
+188
@@ -0,0 +1,188 @@
|
||||
//! Linux x86-64 ptrace wrapper for v0.1.
|
||||
//!
|
||||
//! Spawn-only, single-threaded debuggee. The `Target` owns the child PID
|
||||
//! and tracks whether the child is currently stopped or has exited.
|
||||
|
||||
use nix::sys::ptrace;
|
||||
use nix::sys::signal::Signal;
|
||||
use nix::sys::uio::{process_vm_readv, RemoteIoVec};
|
||||
use nix::sys::wait::{waitpid, WaitStatus};
|
||||
use nix::unistd::{execvp, fork, ForkResult, Pid};
|
||||
use std::ffi::CString;
|
||||
use std::io::IoSliceMut;
|
||||
|
||||
/// Why the target stopped after a step/continue, or that it has exited.
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum StopReason {
|
||||
/// Hit a SIGTRAP from single-stepping (or, in future, a breakpoint).
|
||||
Step,
|
||||
/// Caught some other signal — we don't yet inject it back, just report.
|
||||
Signal(Signal),
|
||||
/// Process exited normally with this code.
|
||||
Exited(i32),
|
||||
/// Process was killed by a signal.
|
||||
Killed(Signal),
|
||||
}
|
||||
|
||||
impl StopReason {
|
||||
/// Short tag for the REPL caller to print.
|
||||
pub fn tag(&self) -> String {
|
||||
match self {
|
||||
StopReason::Step => "step".into(),
|
||||
StopReason::Signal(s) => format!("signal:{}", s),
|
||||
StopReason::Exited(c) => format!("exited:{}", c),
|
||||
StopReason::Killed(s) => format!("killed:{}", s),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_alive(&self) -> bool {
|
||||
matches!(self, StopReason::Step | StopReason::Signal(_))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
#[allow(dead_code)]
|
||||
pub enum State {
|
||||
/// Stopped, ready to read regs/memory and step.
|
||||
Stopped(StopReason),
|
||||
/// Dead. Subsequent operations should fail cleanly.
|
||||
Dead(StopReason),
|
||||
}
|
||||
|
||||
pub struct Target {
|
||||
pid: Pid,
|
||||
state: State,
|
||||
}
|
||||
|
||||
impl Target {
|
||||
pub fn pid(&self) -> i32 {
|
||||
self.pid.as_raw()
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn state(&self) -> &State {
|
||||
&self.state
|
||||
}
|
||||
|
||||
/// True if the target is currently stopped and inspectable.
|
||||
pub fn is_stopped(&self) -> bool {
|
||||
matches!(self.state, State::Stopped(_))
|
||||
}
|
||||
|
||||
/// fork() + PTRACE_TRACEME + execvp(). Returns once the child has been
|
||||
/// caught at its initial post-exec SIGTRAP.
|
||||
pub fn spawn(path: &str, args: &[String]) -> Result<Target, String> {
|
||||
let c_path = CString::new(path).map_err(|e| format!("bad path: {}", e))?;
|
||||
let mut c_args: Vec<CString> = Vec::with_capacity(args.len() + 1);
|
||||
c_args.push(c_path.clone());
|
||||
for a in args {
|
||||
c_args.push(CString::new(a.as_str()).map_err(|e| format!("bad arg: {}", e))?);
|
||||
}
|
||||
|
||||
// SAFETY: fork() must only call async-signal-safe functions in the
|
||||
// child. We use ptrace::traceme and execvp, both fine.
|
||||
let fork_result = unsafe { fork() }.map_err(|e| format!("fork failed: {}", e))?;
|
||||
|
||||
match fork_result {
|
||||
ForkResult::Child => {
|
||||
// In the child. Anything that goes wrong here we report via
|
||||
// exit code; the parent will see the abnormal exit on waitpid.
|
||||
if ptrace::traceme().is_err() {
|
||||
unsafe { libc::_exit(127) };
|
||||
}
|
||||
// execvp replaces the process image; on success it does not return.
|
||||
let _ = execvp(&c_path, &c_args);
|
||||
unsafe { libc::_exit(127) };
|
||||
}
|
||||
ForkResult::Parent { child } => {
|
||||
// Wait for the initial post-exec SIGTRAP.
|
||||
let status = waitpid(child, None)
|
||||
.map_err(|e| format!("waitpid (initial) failed: {}", e))?;
|
||||
match status {
|
||||
WaitStatus::Stopped(_, Signal::SIGTRAP) => Ok(Target {
|
||||
pid: child,
|
||||
state: State::Stopped(StopReason::Step),
|
||||
}),
|
||||
WaitStatus::Exited(_, code) => {
|
||||
Err(format!("child exited immediately with code {} (exec failed?)", code))
|
||||
}
|
||||
other => Err(format!("unexpected initial wait status: {:?}", other)),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// PTRACE_SINGLESTEP + waitpid. Updates internal state.
|
||||
pub fn step(&mut self) -> Result<StopReason, String> {
|
||||
if !self.is_stopped() {
|
||||
return Err("target is not stopped".into());
|
||||
}
|
||||
ptrace::step(self.pid, None).map_err(|e| format!("ptrace step: {}", e))?;
|
||||
let reason = self.wait_for_stop()?;
|
||||
Ok(reason)
|
||||
}
|
||||
|
||||
/// PTRACE_CONT + waitpid.
|
||||
pub fn cont(&mut self) -> Result<StopReason, String> {
|
||||
if !self.is_stopped() {
|
||||
return Err("target is not stopped".into());
|
||||
}
|
||||
ptrace::cont(self.pid, None).map_err(|e| format!("ptrace cont: {}", e))?;
|
||||
let reason = self.wait_for_stop()?;
|
||||
Ok(reason)
|
||||
}
|
||||
|
||||
fn wait_for_stop(&mut self) -> Result<StopReason, String> {
|
||||
let status = waitpid(self.pid, None).map_err(|e| format!("waitpid: {}", e))?;
|
||||
let reason = match status {
|
||||
WaitStatus::Stopped(_, Signal::SIGTRAP) => StopReason::Step,
|
||||
WaitStatus::Stopped(_, sig) => StopReason::Signal(sig),
|
||||
WaitStatus::Exited(_, code) => StopReason::Exited(code),
|
||||
WaitStatus::Signaled(_, sig, _) => StopReason::Killed(sig),
|
||||
other => return Err(format!("unexpected wait status: {:?}", other)),
|
||||
};
|
||||
self.state = if reason.is_alive() {
|
||||
State::Stopped(reason.clone())
|
||||
} else {
|
||||
State::Dead(reason.clone())
|
||||
};
|
||||
Ok(reason)
|
||||
}
|
||||
|
||||
pub fn regs(&self) -> Result<libc::user_regs_struct, String> {
|
||||
if !self.is_stopped() {
|
||||
return Err("target is not stopped".into());
|
||||
}
|
||||
ptrace::getregs(self.pid).map_err(|e| format!("getregs: {}", e))
|
||||
}
|
||||
|
||||
/// Read `len` bytes from the target's memory at `addr` via process_vm_readv.
|
||||
/// Much faster than peek loops for >8 bytes, and works for any size.
|
||||
pub fn read_mem(&self, addr: u64, len: usize) -> Result<Vec<u8>, String> {
|
||||
if !self.is_stopped() {
|
||||
return Err("target is not stopped".into());
|
||||
}
|
||||
let mut buf = vec![0u8; len];
|
||||
let mut local = [IoSliceMut::new(&mut buf)];
|
||||
let remote = [RemoteIoVec { base: addr as usize, len }];
|
||||
let n = process_vm_readv(self.pid, &mut local, &remote)
|
||||
.map_err(|e| format!("read_mem(0x{:x}, {}): {}", addr, len, e))?;
|
||||
if n != len {
|
||||
return Err(format!(
|
||||
"short read at 0x{:x}: got {} of {}",
|
||||
addr, n, len
|
||||
));
|
||||
}
|
||||
Ok(buf)
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Target {
|
||||
fn drop(&mut self) {
|
||||
// Best effort: if the target is still alive, detach so we don't leave
|
||||
// it stopped. If it's already dead, nothing to do.
|
||||
if self.is_stopped() {
|
||||
let _ = ptrace::detach(self.pid, None);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user