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:
@@ -0,0 +1,94 @@
|
|||||||
|
//! Software (INT3) breakpoints.
|
||||||
|
//!
|
||||||
|
//! Wiring lives in target.rs (since arming and trap-handling need ptrace
|
||||||
|
//! access). This module just owns the storage.
|
||||||
|
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct Breakpoint {
|
||||||
|
pub id: u32,
|
||||||
|
pub addr: u64,
|
||||||
|
/// The byte the INT3 (0xCC) replaced. Only valid when `enabled` is true.
|
||||||
|
pub orig_byte: u8,
|
||||||
|
pub enabled: bool,
|
||||||
|
pub hits: u64,
|
||||||
|
pub name: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct BreakpointSet {
|
||||||
|
/// id → bp. We keep tombstones (None) so ids are stable; we'd otherwise
|
||||||
|
/// reuse ids and confuse the user. For v0.x a Vec<Option<...>> is fine.
|
||||||
|
items: Vec<Option<Breakpoint>>,
|
||||||
|
/// Address → bp id, for O(1) lookup when SIGTRAP fires.
|
||||||
|
by_addr: HashMap<u64, u32>,
|
||||||
|
/// name → bp id, for `remove("name")`.
|
||||||
|
by_name: HashMap<String, u32>,
|
||||||
|
next_id: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BreakpointSet {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
BreakpointSet {
|
||||||
|
items: Vec::new(),
|
||||||
|
by_addr: HashMap::new(),
|
||||||
|
by_name: HashMap::new(),
|
||||||
|
next_id: 1,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Reserve a new id and create a Breakpoint record. Returns the id.
|
||||||
|
/// The caller is responsible for actually poking the INT3 byte.
|
||||||
|
pub fn add(&mut self, addr: u64, orig_byte: u8, name: Option<String>) -> Result<u32, String> {
|
||||||
|
if self.by_addr.contains_key(&addr) {
|
||||||
|
return Err(format!("breakpoint already set at 0x{:x}", addr));
|
||||||
|
}
|
||||||
|
if let Some(n) = &name {
|
||||||
|
if self.by_name.contains_key(n) {
|
||||||
|
return Err(format!("breakpoint name '{}' already in use", n));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let id = self.next_id;
|
||||||
|
self.next_id += 1;
|
||||||
|
let bp = Breakpoint { id, addr, orig_byte, enabled: true, hits: 0, name: name.clone() };
|
||||||
|
self.items.push(Some(bp));
|
||||||
|
self.by_addr.insert(addr, id);
|
||||||
|
if let Some(n) = name { self.by_name.insert(n, id); }
|
||||||
|
Ok(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get(&self, id: u32) -> Option<&Breakpoint> {
|
||||||
|
self.items.iter().find_map(|b| b.as_ref().filter(|b| b.id == id))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_mut(&mut self, id: u32) -> Option<&mut Breakpoint> {
|
||||||
|
self.items.iter_mut().find_map(|b| b.as_mut().filter(|b| b.id == id))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn find_by_addr(&self, addr: u64) -> Option<&Breakpoint> {
|
||||||
|
self.by_addr.get(&addr).and_then(|id| self.get(*id))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn find_by_name(&self, name: &str) -> Option<&Breakpoint> {
|
||||||
|
self.by_name.get(name).and_then(|id| self.get(*id))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Remove a breakpoint by id. Returns the removed record so the caller
|
||||||
|
/// can restore the original byte if needed.
|
||||||
|
pub fn remove(&mut self, id: u32) -> Option<Breakpoint> {
|
||||||
|
let pos = self.items.iter().position(|b| b.as_ref().map(|b| b.id == id).unwrap_or(false))?;
|
||||||
|
let bp = self.items[pos].take()?;
|
||||||
|
self.by_addr.remove(&bp.addr);
|
||||||
|
if let Some(n) = &bp.name { self.by_name.remove(n); }
|
||||||
|
Some(bp)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn id_by_name(&self, name: &str) -> Option<u32> {
|
||||||
|
self.by_name.get(name).copied()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Iterate over live breakpoints in id order.
|
||||||
|
pub fn iter(&self) -> impl Iterator<Item = &Breakpoint> {
|
||||||
|
self.items.iter().filter_map(|b| b.as_ref())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -365,6 +365,87 @@ fn install_globals<'js>(
|
|||||||
globals.set("symbols", f)?;
|
globals.set("symbols", f)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------- breakpoint / bp ---------------------------------------------
|
||||||
|
// breakpoint.set(target, name?) → id (target = address number/BigInt or symbol name)
|
||||||
|
// breakpoint.remove(id_or_name)
|
||||||
|
// breakpoint.list() → [{id, addr, name, hits, enabled}]
|
||||||
|
{
|
||||||
|
let bp_obj = Object::new(c.clone())?;
|
||||||
|
|
||||||
|
// set(target, name?)
|
||||||
|
{
|
||||||
|
let target = target.clone();
|
||||||
|
let f = Function::new(c.clone(), move |c: Ctx<'js>, location: Value<'js>, name: rquickjs::function::Opt<String>| -> rquickjs::Result<u32> {
|
||||||
|
let mut slot = target.borrow_mut();
|
||||||
|
let t = slot.as_mut().ok_or_else(|| throw(&c, "no target attached"))?;
|
||||||
|
// location: string → resolve via symbols; number/BigInt → coerce_addr.
|
||||||
|
let addr = if let Some(s) = location.clone().into_string() {
|
||||||
|
let s = s.to_string()?;
|
||||||
|
let sym = t.symbols().ok_or_else(|| throw(&c, "no symbol table"))?;
|
||||||
|
sym.addr_for_name(&s).ok_or_else(|| throw(&c, &format!("unknown symbol: {}", s)))?
|
||||||
|
} else {
|
||||||
|
coerce_addr(&c, &location)?
|
||||||
|
};
|
||||||
|
let name = name.0;
|
||||||
|
t.breakpoint_set(addr, name).map_err(|e| throw(&c, &e))
|
||||||
|
})?;
|
||||||
|
bp_obj.set("set", f)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// remove(id_or_name)
|
||||||
|
{
|
||||||
|
let target = target.clone();
|
||||||
|
let f = Function::new(c.clone(), move |c: Ctx<'js>, id_or_name: Value<'js>| -> rquickjs::Result<()> {
|
||||||
|
let mut slot = target.borrow_mut();
|
||||||
|
let t = slot.as_mut().ok_or_else(|| throw(&c, "no target attached"))?;
|
||||||
|
let id = if let Some(s) = id_or_name.clone().into_string() {
|
||||||
|
let s = s.to_string()?;
|
||||||
|
t.breakpoint_id_by_name(&s).ok_or_else(|| throw(&c, &format!("no breakpoint named '{}'", s)))?
|
||||||
|
} else if let Some(n) = id_or_name.as_number() {
|
||||||
|
if n < 0.0 || n.fract() != 0.0 {
|
||||||
|
return Err(throw(&c, "bp id must be a non-negative integer"));
|
||||||
|
}
|
||||||
|
n as u32
|
||||||
|
} else if id_or_name.is_int() {
|
||||||
|
let i: i32 = id_or_name.get()?;
|
||||||
|
if i < 0 { return Err(throw(&c, "bp id must be non-negative")); }
|
||||||
|
i as u32
|
||||||
|
} else {
|
||||||
|
return Err(throw(&c, "bp.remove expects id (number) or name (string)"));
|
||||||
|
};
|
||||||
|
t.breakpoint_remove(id).map_err(|e| throw(&c, &e))
|
||||||
|
})?;
|
||||||
|
bp_obj.set("remove", f)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// list() → array of breakpoint records
|
||||||
|
{
|
||||||
|
let target = target.clone();
|
||||||
|
let f = Function::new(c.clone(), move |c: Ctx<'js>| -> rquickjs::Result<Value<'js>> {
|
||||||
|
let slot = target.borrow();
|
||||||
|
let t = slot.as_ref().ok_or_else(|| throw(&c, "no target attached"))?;
|
||||||
|
let arr = rquickjs::Array::new(c.clone())?;
|
||||||
|
for (i, bp) in t.breakpoints().iter().enumerate() {
|
||||||
|
let rec = Object::new(c.clone())?;
|
||||||
|
rec.set("id", bp.id)?;
|
||||||
|
rec.set("addr", u64_to_bigint(&c, bp.addr)?)?;
|
||||||
|
rec.set("hits", u64_to_bigint(&c, bp.hits)?)?;
|
||||||
|
rec.set("enabled", bp.enabled)?;
|
||||||
|
match &bp.name {
|
||||||
|
Some(n) => rec.set("name", n.as_str())?,
|
||||||
|
None => rec.set("name", Value::new_null(c.clone()))?,
|
||||||
|
};
|
||||||
|
arr.set(i, rec)?;
|
||||||
|
}
|
||||||
|
Ok(arr.into_value())
|
||||||
|
})?;
|
||||||
|
bp_obj.set("list", f)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
globals.set("breakpoint", bp_obj.clone())?;
|
||||||
|
globals.set("bp", bp_obj)?;
|
||||||
|
}
|
||||||
|
|
||||||
// ---------- JS-side prelude ---------------------------------------------
|
// ---------- JS-side prelude ---------------------------------------------
|
||||||
// Loaded last, so it can reference any of the native globals above.
|
// Loaded last, so it can reference any of the native globals above.
|
||||||
// Lives in src/prelude.js, embedded at compile time.
|
// Lives in src/prelude.js, embedded at compile time.
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
|
|
||||||
mod target;
|
mod target;
|
||||||
mod symbols;
|
mod symbols;
|
||||||
|
mod breakpoint;
|
||||||
mod js;
|
mod js;
|
||||||
|
|
||||||
use std::io::{BufRead, Write};
|
use std::io::{BufRead, Write};
|
||||||
|
|||||||
+11
-14
@@ -150,32 +150,29 @@ fn read_load_base(pid: i32, exe_path: &PathBuf) -> Result<u64, String> {
|
|||||||
|
|
||||||
// /proc/<pid>/maps line format:
|
// /proc/<pid>/maps line format:
|
||||||
// ADDR_LO-ADDR_HI PERMS OFFSET DEV INODE PATH
|
// ADDR_LO-ADDR_HI PERMS OFFSET DEV INODE PATH
|
||||||
// We want the first line where PATH matches our exe AND PERMS contains 'x'.
|
//
|
||||||
|
// For PIE we want the load base = `mapping_base - mapping_file_offset` for
|
||||||
|
// any mapping of the file. The simplest pick is the *first* mapping
|
||||||
|
// (lowest address), which usually has file offset 0 and thus
|
||||||
|
// load_base == mapping_base. We compute it generically just in case the
|
||||||
|
// first mapping isn't at offset 0 (some toolchains do split read-only
|
||||||
|
// header pages out).
|
||||||
for line in maps.lines() {
|
for line in maps.lines() {
|
||||||
// Split into at most 6 whitespace-delimited fields; the last is the
|
|
||||||
// path which may itself contain spaces (unlikely in practice).
|
|
||||||
let mut it = line.splitn(6, ' ').filter(|s| !s.is_empty());
|
let mut it = line.splitn(6, ' ').filter(|s| !s.is_empty());
|
||||||
let range = match it.next() { Some(s) => s, None => continue };
|
let range = match it.next() { Some(s) => s, None => continue };
|
||||||
let perms = match it.next() { Some(s) => s, None => continue };
|
let _perms = match it.next() { Some(s) => s, None => continue };
|
||||||
let _offset = it.next();
|
let offset = it.next().unwrap_or("0");
|
||||||
let _dev = it.next();
|
let _dev = it.next();
|
||||||
let _inode = it.next();
|
let _inode = it.next();
|
||||||
let path = it.next().unwrap_or("").trim();
|
let path = it.next().unwrap_or("").trim();
|
||||||
if !perms.contains('x') { continue; }
|
|
||||||
if path != exe_str { continue; }
|
if path != exe_str { continue; }
|
||||||
let mut parts = range.split('-');
|
let mut parts = range.split('-');
|
||||||
let lo = parts.next().ok_or_else(|| "bad maps line".to_string())?;
|
let lo = parts.next().ok_or_else(|| "bad maps line".to_string())?;
|
||||||
let lo = u64::from_str_radix(lo, 16).map_err(|e| format!("bad maps addr: {}", e))?;
|
let lo = u64::from_str_radix(lo, 16).map_err(|e| format!("bad maps addr: {}", e))?;
|
||||||
|
let file_offset = u64::from_str_radix(offset, 16).unwrap_or(0);
|
||||||
|
|
||||||
// For ET_DYN this is the load base directly (file offset 0 maps to lo).
|
|
||||||
// For ET_EXEC, file vaddrs are absolute, and lo equals the first
|
|
||||||
// PT_LOAD's p_vaddr, so we want 0.
|
|
||||||
if is_pie(&elf) {
|
if is_pie(&elf) {
|
||||||
// We assume the first executable mapping starts at file offset 0
|
return Ok(lo.wrapping_sub(file_offset));
|
||||||
// for the .text segment — usually true. If we wanted to be careful
|
|
||||||
// we'd subtract the segment's file offset, but it's normally 0
|
|
||||||
// for the first executable PT_LOAD.
|
|
||||||
return Ok(lo);
|
|
||||||
} else {
|
} else {
|
||||||
return Ok(0);
|
return Ok(0);
|
||||||
}
|
}
|
||||||
|
|||||||
+195
-31
@@ -11,13 +11,16 @@ use nix::unistd::{execvp, fork, ForkResult, Pid};
|
|||||||
use std::ffi::CString;
|
use std::ffi::CString;
|
||||||
use std::io::IoSliceMut;
|
use std::io::IoSliceMut;
|
||||||
|
|
||||||
|
use crate::breakpoint::BreakpointSet;
|
||||||
use crate::symbols::SymbolTable;
|
use crate::symbols::SymbolTable;
|
||||||
|
|
||||||
/// Why the target stopped after a step/continue, or that it has exited.
|
/// Why the target stopped after a step/continue, or that it has exited.
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub enum StopReason {
|
pub enum StopReason {
|
||||||
/// Hit a SIGTRAP from single-stepping (or, in future, a breakpoint).
|
/// Hit a SIGTRAP from single-stepping.
|
||||||
Step,
|
Step,
|
||||||
|
/// Hit a software breakpoint. Carries the breakpoint id.
|
||||||
|
Breakpoint(u32),
|
||||||
/// Caught some other signal — we don't yet inject it back, just report.
|
/// Caught some other signal — we don't yet inject it back, just report.
|
||||||
Signal(Signal),
|
Signal(Signal),
|
||||||
/// Process exited normally with this code.
|
/// Process exited normally with this code.
|
||||||
@@ -31,6 +34,7 @@ impl StopReason {
|
|||||||
pub fn tag(&self) -> String {
|
pub fn tag(&self) -> String {
|
||||||
match self {
|
match self {
|
||||||
StopReason::Step => "step".into(),
|
StopReason::Step => "step".into(),
|
||||||
|
StopReason::Breakpoint(id) => format!("breakpoint:{}", id),
|
||||||
StopReason::Signal(s) => format!("signal:{}", s),
|
StopReason::Signal(s) => format!("signal:{}", s),
|
||||||
StopReason::Exited(c) => format!("exited:{}", c),
|
StopReason::Exited(c) => format!("exited:{}", c),
|
||||||
StopReason::Killed(s) => format!("killed:{}", s),
|
StopReason::Killed(s) => format!("killed:{}", s),
|
||||||
@@ -38,7 +42,7 @@ impl StopReason {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn is_alive(&self) -> bool {
|
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.
|
/// ELF symbols of the main executable. None if parsing failed (e.g.
|
||||||
/// stripped binary, unreadable /proc) — symbol lookups become no-ops.
|
/// stripped binary, unreadable /proc) — symbol lookups become no-ops.
|
||||||
symbols: Option<SymbolTable>,
|
symbols: Option<SymbolTable>,
|
||||||
|
/// Software (INT3) breakpoints.
|
||||||
|
breakpoints: BreakpointSet,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Target {
|
impl Target {
|
||||||
@@ -114,6 +120,7 @@ impl Target {
|
|||||||
state: State::Stopped(StopReason::Step),
|
state: State::Stopped(StopReason::Step),
|
||||||
prev_regs: None,
|
prev_regs: None,
|
||||||
symbols,
|
symbols,
|
||||||
|
breakpoints: BreakpointSet::new(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
WaitStatus::Exited(_, code) => {
|
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> {
|
pub fn prev_regs(&self) -> Option<libc::user_regs_struct> {
|
||||||
self.prev_regs
|
self.prev_regs
|
||||||
}
|
}
|
||||||
@@ -156,7 +140,134 @@ impl Target {
|
|||||||
self.symbols.as_ref()
|
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 status = waitpid(self.pid, None).map_err(|e| format!("waitpid: {}", e))?;
|
||||||
let reason = match status {
|
let reason = match status {
|
||||||
WaitStatus::Stopped(_, Signal::SIGTRAP) => StopReason::Step,
|
WaitStatus::Stopped(_, Signal::SIGTRAP) => StopReason::Step,
|
||||||
@@ -165,14 +276,42 @@ impl Target {
|
|||||||
WaitStatus::Signaled(_, sig, _) => StopReason::Killed(sig),
|
WaitStatus::Signaled(_, sig, _) => StopReason::Killed(sig),
|
||||||
other => return Err(format!("unexpected wait status: {:?}", other)),
|
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)
|
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> {
|
pub fn regs(&self) -> Result<libc::user_regs_struct, String> {
|
||||||
if !self.is_stopped() {
|
if !self.is_stopped() {
|
||||||
return Err("target is not stopped".into());
|
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