v0.4: INT3 software breakpoints with by-name/by-id ops
- StopReason::Breakpoint(id), rewinds RIP on hit - bp.set(addr_or_symbol, name?), bp.remove(id_or_name), bp.list() - step/cont disarm-step-rearm when PC sits on an enabled bp - fix: PIE load_base = mapping_base - mapping_file_offset (was using the first executable mapping with its offset, which is wrong on toolchains that split read-only header pages)
This commit is contained in:
+195
-31
@@ -11,13 +11,16 @@ use nix::unistd::{execvp, fork, ForkResult, Pid};
|
||||
use std::ffi::CString;
|
||||
use std::io::IoSliceMut;
|
||||
|
||||
use crate::breakpoint::BreakpointSet;
|
||||
use crate::symbols::SymbolTable;
|
||||
|
||||
/// 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).
|
||||
/// Hit a SIGTRAP from single-stepping.
|
||||
Step,
|
||||
/// Hit a software breakpoint. Carries the breakpoint id.
|
||||
Breakpoint(u32),
|
||||
/// Caught some other signal — we don't yet inject it back, just report.
|
||||
Signal(Signal),
|
||||
/// Process exited normally with this code.
|
||||
@@ -31,6 +34,7 @@ impl StopReason {
|
||||
pub fn tag(&self) -> String {
|
||||
match self {
|
||||
StopReason::Step => "step".into(),
|
||||
StopReason::Breakpoint(id) => format!("breakpoint:{}", id),
|
||||
StopReason::Signal(s) => format!("signal:{}", s),
|
||||
StopReason::Exited(c) => format!("exited:{}", c),
|
||||
StopReason::Killed(s) => format!("killed:{}", s),
|
||||
@@ -38,7 +42,7 @@ impl StopReason {
|
||||
}
|
||||
|
||||
pub fn is_alive(&self) -> bool {
|
||||
matches!(self, StopReason::Step | StopReason::Signal(_))
|
||||
matches!(self, StopReason::Step | StopReason::Breakpoint(_) | StopReason::Signal(_))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,6 +64,8 @@ pub struct Target {
|
||||
/// ELF symbols of the main executable. None if parsing failed (e.g.
|
||||
/// stripped binary, unreadable /proc) — symbol lookups become no-ops.
|
||||
symbols: Option<SymbolTable>,
|
||||
/// Software (INT3) breakpoints.
|
||||
breakpoints: BreakpointSet,
|
||||
}
|
||||
|
||||
impl Target {
|
||||
@@ -114,6 +120,7 @@ impl Target {
|
||||
state: State::Stopped(StopReason::Step),
|
||||
prev_regs: None,
|
||||
symbols,
|
||||
breakpoints: BreakpointSet::new(),
|
||||
})
|
||||
}
|
||||
WaitStatus::Exited(_, code) => {
|
||||
@@ -125,29 +132,6 @@ impl Target {
|
||||
}
|
||||
}
|
||||
|
||||
/// 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());
|
||||
}
|
||||
// Snapshot regs before resuming so `prev` reflects the pre-step state.
|
||||
self.prev_regs = ptrace::getregs(self.pid).ok();
|
||||
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());
|
||||
}
|
||||
self.prev_regs = ptrace::getregs(self.pid).ok();
|
||||
ptrace::cont(self.pid, None).map_err(|e| format!("ptrace cont: {}", e))?;
|
||||
let reason = self.wait_for_stop()?;
|
||||
Ok(reason)
|
||||
}
|
||||
|
||||
pub fn prev_regs(&self) -> Option<libc::user_regs_struct> {
|
||||
self.prev_regs
|
||||
}
|
||||
@@ -156,7 +140,134 @@ impl Target {
|
||||
self.symbols.as_ref()
|
||||
}
|
||||
|
||||
fn wait_for_stop(&mut self) -> Result<StopReason, String> {
|
||||
pub fn breakpoints(&self) -> &BreakpointSet {
|
||||
&self.breakpoints
|
||||
}
|
||||
|
||||
// ----- breakpoint ops -------------------------------------------------
|
||||
|
||||
/// Install a software breakpoint at `addr`. Reads the original byte,
|
||||
/// writes 0xCC. Returns the breakpoint id.
|
||||
pub fn breakpoint_set(&mut self, addr: u64, name: Option<String>) -> Result<u32, String> {
|
||||
if !self.is_stopped() {
|
||||
return Err("target is not stopped".into());
|
||||
}
|
||||
let orig = read_byte(self.pid, addr)?;
|
||||
let id = self.breakpoints.add(addr, orig, name)?;
|
||||
if let Err(e) = write_byte(self.pid, addr, 0xCC) {
|
||||
// Roll back: the bp record was added but writing failed.
|
||||
let _ = self.breakpoints.remove(id);
|
||||
return Err(format!("install bp at 0x{:x}: {}", addr, e));
|
||||
}
|
||||
Ok(id)
|
||||
}
|
||||
|
||||
/// Remove a breakpoint by id, restoring the original byte if it was
|
||||
/// enabled. Returns Ok(()) on success, Err if the id doesn't exist.
|
||||
pub fn breakpoint_remove(&mut self, id: u32) -> Result<(), String> {
|
||||
if !self.is_stopped() {
|
||||
return Err("target is not stopped".into());
|
||||
}
|
||||
let bp = self.breakpoints.remove(id)
|
||||
.ok_or_else(|| format!("no breakpoint with id {}", id))?;
|
||||
if bp.enabled {
|
||||
write_byte(self.pid, bp.addr, bp.orig_byte)
|
||||
.map_err(|e| format!("remove bp 0x{:x}: {}", bp.addr, e))?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Look up a bp id by name; convenience for callers.
|
||||
pub fn breakpoint_id_by_name(&self, name: &str) -> Option<u32> {
|
||||
self.breakpoints.id_by_name(name)
|
||||
}
|
||||
|
||||
// ----- step / cont ----------------------------------------------------
|
||||
|
||||
/// PTRACE_SINGLESTEP + waitpid. Updates internal state. If the current PC
|
||||
/// has an enabled breakpoint, we temporarily disarm it, step over the
|
||||
/// original instruction, then re-arm.
|
||||
pub fn step(&mut self) -> Result<StopReason, String> {
|
||||
if !self.is_stopped() {
|
||||
return Err("target is not stopped".into());
|
||||
}
|
||||
self.prev_regs = ptrace::getregs(self.pid).ok();
|
||||
|
||||
let pc = self.prev_regs.map(|r| r.rip).unwrap_or(0);
|
||||
let bp_at_pc = self.breakpoints.find_by_addr(pc).map(|b| (b.id, b.orig_byte, b.enabled));
|
||||
|
||||
if let Some((_id, orig, enabled)) = bp_at_pc {
|
||||
if enabled {
|
||||
// Disarm, step, re-arm.
|
||||
write_byte(self.pid, pc, orig)
|
||||
.map_err(|e| format!("disarm bp at 0x{:x}: {}", pc, e))?;
|
||||
ptrace::step(self.pid, None)
|
||||
.map_err(|e| format!("ptrace step: {}", e))?;
|
||||
let reason = self.wait_for_stop_raw()?;
|
||||
// Re-arm even if the step exited or signalled (best-effort).
|
||||
if reason.is_alive() {
|
||||
let _ = write_byte(self.pid, pc, 0xCC);
|
||||
}
|
||||
self.classify_stop(reason);
|
||||
let r = match &self.state {
|
||||
State::Stopped(r) => r.clone(),
|
||||
State::Dead(r) => r.clone(),
|
||||
};
|
||||
return Ok(r);
|
||||
}
|
||||
}
|
||||
|
||||
ptrace::step(self.pid, None).map_err(|e| format!("ptrace step: {}", e))?;
|
||||
let reason = self.wait_for_stop_raw()?;
|
||||
self.classify_stop(reason);
|
||||
match &self.state {
|
||||
State::Stopped(r) => Ok(r.clone()),
|
||||
State::Dead(r) => Ok(r.clone()),
|
||||
}
|
||||
}
|
||||
|
||||
/// PTRACE_CONT + waitpid. If the current PC is a breakpoint, we have to
|
||||
/// step over it first (same disarm/step/re-arm dance), then continue.
|
||||
pub fn cont(&mut self) -> Result<StopReason, String> {
|
||||
if !self.is_stopped() {
|
||||
return Err("target is not stopped".into());
|
||||
}
|
||||
self.prev_regs = ptrace::getregs(self.pid).ok();
|
||||
|
||||
// Step over current PC if it has an active breakpoint.
|
||||
let pc = self.prev_regs.map(|r| r.rip).unwrap_or(0);
|
||||
if let Some(bp) = self.breakpoints.find_by_addr(pc).cloned() {
|
||||
if bp.enabled {
|
||||
write_byte(self.pid, pc, bp.orig_byte)
|
||||
.map_err(|e| format!("disarm bp at 0x{:x}: {}", pc, e))?;
|
||||
ptrace::step(self.pid, None)
|
||||
.map_err(|e| format!("ptrace step (over-bp): {}", e))?;
|
||||
let inner = self.wait_for_stop_raw()?;
|
||||
// If the step itself exited/signalled, classify and return.
|
||||
if !inner.is_alive() {
|
||||
self.classify_stop(inner);
|
||||
return match &self.state {
|
||||
State::Stopped(r) | State::Dead(r) => Ok(r.clone()),
|
||||
};
|
||||
}
|
||||
// Re-arm and fall through to PTRACE_CONT.
|
||||
let _ = write_byte(self.pid, pc, 0xCC);
|
||||
}
|
||||
}
|
||||
|
||||
ptrace::cont(self.pid, None).map_err(|e| format!("ptrace cont: {}", e))?;
|
||||
let reason = self.wait_for_stop_raw()?;
|
||||
// If we hit a breakpoint, rewind RIP to bp_addr (kernel left it at
|
||||
// bp_addr+1) so the user sees pc == bp_addr, which is what they expect.
|
||||
self.handle_possible_breakpoint(reason)?;
|
||||
match &self.state {
|
||||
State::Stopped(r) | State::Dead(r) => Ok(r.clone()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Raw waitpid: just decodes the status into a tentative StopReason::Step
|
||||
/// or Signal/Exited/Killed. Doesn't yet check for breakpoint matches.
|
||||
fn wait_for_stop_raw(&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,
|
||||
@@ -165,14 +276,42 @@ impl Target {
|
||||
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)
|
||||
}
|
||||
|
||||
/// After a wait, if the reason is a SIGTRAP, check whether PC-1 matches a
|
||||
/// known breakpoint; if so, rewind PC and mark the stop as
|
||||
/// Breakpoint(id). Update self.state.
|
||||
fn handle_possible_breakpoint(&mut self, mut reason: StopReason) -> Result<(), String> {
|
||||
if let StopReason::Step = reason {
|
||||
// After INT3 the kernel leaves RIP at addr+1.
|
||||
if let Ok(r) = ptrace::getregs(self.pid) {
|
||||
let candidate = r.rip.wrapping_sub(1);
|
||||
if let Some(bp) = self.breakpoints.find_by_addr(candidate).cloned() {
|
||||
// Rewind RIP to the breakpoint address.
|
||||
let mut new_regs = r;
|
||||
new_regs.rip = candidate;
|
||||
ptrace::setregs(self.pid, new_regs)
|
||||
.map_err(|e| format!("setregs (rewind bp): {}", e))?;
|
||||
if let Some(bpm) = self.breakpoints.get_mut(bp.id) {
|
||||
bpm.hits += 1;
|
||||
}
|
||||
reason = StopReason::Breakpoint(bp.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
self.classify_stop(reason);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn classify_stop(&mut self, reason: StopReason) {
|
||||
self.state = if reason.is_alive() {
|
||||
State::Stopped(reason)
|
||||
} else {
|
||||
State::Dead(reason)
|
||||
};
|
||||
}
|
||||
|
||||
pub fn regs(&self) -> Result<libc::user_regs_struct, String> {
|
||||
if !self.is_stopped() {
|
||||
return Err("target is not stopped".into());
|
||||
@@ -210,3 +349,28 @@ impl Drop for Target {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- ptrace byte-level peek/poke for breakpoint patching ------------------
|
||||
|
||||
/// Read a single byte at `addr` via PTRACE_PEEKDATA.
|
||||
fn read_byte(pid: Pid, addr: u64) -> Result<u8, String> {
|
||||
let word = ptrace::read(pid, addr as ptrace::AddressType)
|
||||
.map_err(|e| format!("peek 0x{:x}: {}", addr, e))?;
|
||||
let bytes = (word as u64).to_le_bytes();
|
||||
Ok(bytes[0])
|
||||
}
|
||||
|
||||
/// Write a single byte at `addr` via PEEK+POKE (read-modify-write of one word).
|
||||
/// Writes the 8-byte word starting at addr; the upper 7 bytes are unchanged
|
||||
/// because we read them first.
|
||||
fn write_byte(pid: Pid, addr: u64, value: u8) -> Result<(), String> {
|
||||
let word = ptrace::read(pid, addr as ptrace::AddressType)
|
||||
.map_err(|e| format!("peek 0x{:x} (for write): {}", addr, e))?;
|
||||
let mut bytes = (word as u64).to_le_bytes();
|
||||
bytes[0] = value;
|
||||
let new_word = u64::from_le_bytes(bytes) as i64;
|
||||
ptrace::write(pid, addr as ptrace::AddressType, new_word as libc::c_long)
|
||||
.map_err(|e| format!("poke 0x{:x}: {}", addr, e))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user