v0.3: ELF symbol parsing, sym()/addr()/symbols() globals, addrstr/here prelude helpers
This commit is contained in:
Generated
+16
@@ -77,9 +77,16 @@ version = "0.1.0"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
"nix",
|
"nix",
|
||||||
|
"object",
|
||||||
"rquickjs",
|
"rquickjs",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "memchr"
|
||||||
|
version = "2.8.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6b947ae49db0d222b1dbc6b113ce7248a3fc3a6ca21b696717bfc000ba4484d8"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "nix"
|
name = "nix"
|
||||||
version = "0.31.3"
|
version = "0.31.3"
|
||||||
@@ -92,6 +99,15 @@ dependencies = [
|
|||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "object"
|
||||||
|
version = "0.36.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87"
|
||||||
|
dependencies = [
|
||||||
|
"memchr",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "proc-macro2"
|
name = "proc-macro2"
|
||||||
version = "1.0.106"
|
version = "1.0.106"
|
||||||
|
|||||||
@@ -7,3 +7,4 @@ edition = "2024"
|
|||||||
nix = { version = "0.31", features = ["ptrace", "process", "uio", "signal"] }
|
nix = { version = "0.31", features = ["ptrace", "process", "uio", "signal"] }
|
||||||
rquickjs = { version = "0.12", features = ["std"] }
|
rquickjs = { version = "0.12", features = ["std"] }
|
||||||
libc = "0.2"
|
libc = "0.2"
|
||||||
|
object = { version = "0.36", default-features = false, features = ["read", "std"] }
|
||||||
|
|||||||
@@ -318,6 +318,53 @@ fn install_globals<'js>(
|
|||||||
globals.set("prev", prev_obj)?;
|
globals.set("prev", prev_obj)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------- sym(addr) -> "name+0xN" | null ----------
|
||||||
|
{
|
||||||
|
let target = target.clone();
|
||||||
|
let f = Function::new(c.clone(), move |c: Ctx<'js>, addr: Value<'js>| -> rquickjs::Result<Value<'js>> {
|
||||||
|
let addr = coerce_addr(&c, &addr)?;
|
||||||
|
let slot = target.borrow();
|
||||||
|
let t = slot.as_ref().ok_or_else(|| throw(&c, "no target attached"))?;
|
||||||
|
let s = t.symbols().ok_or_else(|| throw(&c, "no symbol table"))?;
|
||||||
|
match s.name_for_addr(addr) {
|
||||||
|
Some(name) => Ok(rquickjs::String::from_str(c.clone(), &name)?.into_value()),
|
||||||
|
None => Ok(Value::new_null(c.clone())),
|
||||||
|
}
|
||||||
|
})?;
|
||||||
|
globals.set("sym", f)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- addr(name) -> BigInt | null ----------
|
||||||
|
{
|
||||||
|
let target = target.clone();
|
||||||
|
let f = Function::new(c.clone(), move |c: Ctx<'js>, name: String| -> rquickjs::Result<Value<'js>> {
|
||||||
|
let slot = target.borrow();
|
||||||
|
let t = slot.as_ref().ok_or_else(|| throw(&c, "no target attached"))?;
|
||||||
|
let s = t.symbols().ok_or_else(|| throw(&c, "no symbol table"))?;
|
||||||
|
match s.addr_for_name(&name) {
|
||||||
|
Some(a) => u64_to_bigint(&c, a),
|
||||||
|
None => Ok(Value::new_null(c.clone())),
|
||||||
|
}
|
||||||
|
})?;
|
||||||
|
globals.set("addr", f)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- symbols() -> { count, load_base, path } ----------
|
||||||
|
{
|
||||||
|
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 s = t.symbols().ok_or_else(|| throw(&c, "no symbol table"))?;
|
||||||
|
let obj = Object::new(c.clone())?;
|
||||||
|
obj.set("count", s.len() as u32)?;
|
||||||
|
obj.set("load_base", u64_to_bigint(&c, s.load_base)?)?;
|
||||||
|
obj.set("path", s.path.to_string_lossy().into_owned())?;
|
||||||
|
Ok(obj.into_value())
|
||||||
|
})?;
|
||||||
|
globals.set("symbols", f)?;
|
||||||
|
}
|
||||||
|
|
||||||
// ---------- 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.
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
//! - EOF on stdin exits cleanly.
|
//! - EOF on stdin exits cleanly.
|
||||||
|
|
||||||
mod target;
|
mod target;
|
||||||
|
mod symbols;
|
||||||
mod js;
|
mod js;
|
||||||
|
|
||||||
use std::io::{BufRead, Write};
|
use std::io::{BufRead, Write};
|
||||||
|
|||||||
@@ -85,3 +85,29 @@ globalThis.repr = function (v) {
|
|||||||
}
|
}
|
||||||
return String(v);
|
return String(v);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// addrstr(v) — render an address with symbol context.
|
||||||
|
// Returns "0x7fff... (main+0x4)" if the address is inside a known symbol,
|
||||||
|
// or just "0x7fff..." otherwise. Safe to call before a target is attached:
|
||||||
|
// falls back to bare hex if `sym` throws.
|
||||||
|
|
||||||
|
globalThis.addrstr = function (v) {
|
||||||
|
const h = hex(v);
|
||||||
|
try {
|
||||||
|
const s = sym(v);
|
||||||
|
return s ? `${h} (${s})` : h;
|
||||||
|
} catch (_) {
|
||||||
|
return h;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// here() — quick "where am I" one-liner. Prints pc with symbol context.
|
||||||
|
|
||||||
|
globalThis.here = function (label) {
|
||||||
|
const s = addrstr(state.pc);
|
||||||
|
if (label) print(label, s); else print(s);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+195
@@ -0,0 +1,195 @@
|
|||||||
|
//! ELF symbol table parsing and address lookups for the main executable.
|
||||||
|
//!
|
||||||
|
//! v0.3 scope:
|
||||||
|
//! - Parse the main binary's symbol table (.symtab + .dynsym).
|
||||||
|
//! - Handle PIE (ET_DYN) by reading the load base from /proc/<pid>/maps.
|
||||||
|
//! - Provide name → addr and addr → "name+0xoff" lookups.
|
||||||
|
//! - Shared libraries are NOT covered — those need ld.so awareness and live
|
||||||
|
//! in a later milestone.
|
||||||
|
|
||||||
|
use object::{Object, ObjectSymbol, ObjectSection, SymbolKind};
|
||||||
|
use std::collections::BTreeMap;
|
||||||
|
use std::fs;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
/// One named symbol with a load-base-adjusted virtual address and a size.
|
||||||
|
/// Size==0 is common (assembly globals etc.); we treat 0 as "unknown size,
|
||||||
|
/// match only by exact start address" when doing addr → name lookups.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct Symbol {
|
||||||
|
pub name: String,
|
||||||
|
pub addr: u64,
|
||||||
|
pub size: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct SymbolTable {
|
||||||
|
/// addr → symbol. Keys are post-relocation virtual addresses.
|
||||||
|
by_addr: BTreeMap<u64, Symbol>,
|
||||||
|
/// Lowercase name → addr. We keep the case-sensitive name in Symbol but
|
||||||
|
/// allow case-insensitive lookups since CLI debugger users won't always
|
||||||
|
/// remember exact capitalisation. Stores the *first* address seen for
|
||||||
|
/// a name (binaries can have duplicates after stripping).
|
||||||
|
by_name: BTreeMap<String, u64>,
|
||||||
|
/// Load base offset that was applied to file-relative addresses. 0 for
|
||||||
|
/// non-PIE binaries.
|
||||||
|
pub load_base: u64,
|
||||||
|
/// Path the symbols came from, for diagnostics.
|
||||||
|
pub path: PathBuf,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SymbolTable {
|
||||||
|
/// Load symbols from /proc/<pid>/exe, applying the load base from
|
||||||
|
/// /proc/<pid>/maps if the binary is PIE.
|
||||||
|
pub fn from_pid(pid: i32) -> Result<SymbolTable, String> {
|
||||||
|
let exe_path = read_exe_path(pid)?;
|
||||||
|
let load_base = read_load_base(pid, &exe_path)?;
|
||||||
|
let bytes = fs::read(&exe_path)
|
||||||
|
.map_err(|e| format!("read exe {}: {}", exe_path.display(), e))?;
|
||||||
|
let elf = object::File::parse(&*bytes)
|
||||||
|
.map_err(|e| format!("parse ELF: {}", e))?;
|
||||||
|
|
||||||
|
let mut by_addr: BTreeMap<u64, Symbol> = BTreeMap::new();
|
||||||
|
let mut by_name: BTreeMap<String, u64> = BTreeMap::new();
|
||||||
|
|
||||||
|
for sym in elf.symbols().chain(elf.dynamic_symbols()) {
|
||||||
|
if !matches!(sym.kind(), SymbolKind::Text | SymbolKind::Data | SymbolKind::Unknown) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let raw_name = match sym.name() {
|
||||||
|
Ok(n) if !n.is_empty() => n,
|
||||||
|
_ => continue,
|
||||||
|
};
|
||||||
|
// Filter out section-undefined symbols (imports).
|
||||||
|
if sym.section_index().is_none() && !sym.is_definition() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let addr = sym.address().wrapping_add(load_base);
|
||||||
|
if addr == 0 { continue; }
|
||||||
|
let s = Symbol {
|
||||||
|
name: raw_name.to_string(),
|
||||||
|
addr,
|
||||||
|
size: sym.size(),
|
||||||
|
};
|
||||||
|
// Prefer the entry already there if both exist at same address
|
||||||
|
// and the newer one has size 0 — keeps size info around.
|
||||||
|
match by_addr.get(&addr) {
|
||||||
|
Some(existing) if existing.size > 0 && s.size == 0 => { /* keep existing */ }
|
||||||
|
_ => { by_addr.insert(addr, s.clone()); }
|
||||||
|
}
|
||||||
|
by_name.entry(raw_name.to_string()).or_insert(addr);
|
||||||
|
// Also stash a lowercase alias if it isn't already a key
|
||||||
|
let lc = raw_name.to_lowercase();
|
||||||
|
if lc != raw_name {
|
||||||
|
by_name.entry(lc).or_insert(addr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// For diagnostic: also note executable section ranges (might help us
|
||||||
|
// later filter "is this addr even code"). Not used yet but cheap.
|
||||||
|
let _exec_sections: Vec<_> = elf.sections()
|
||||||
|
.filter(|s| s.kind() == object::SectionKind::Text)
|
||||||
|
.map(|s| (s.address().wrapping_add(load_base), s.size()))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
Ok(SymbolTable { by_addr, by_name, load_base, path: exe_path })
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Find the symbol containing or starting at `addr`.
|
||||||
|
/// Returns "name" if exact, "name+0xN" if interior, None if no match.
|
||||||
|
pub fn name_for_addr(&self, addr: u64) -> Option<String> {
|
||||||
|
// Look at the symbol whose addr ≤ ours, and check whether ours falls
|
||||||
|
// within [sym.addr, sym.addr + sym.size). For size==0 symbols, only
|
||||||
|
// match an exact equality.
|
||||||
|
let (&_, sym) = self.by_addr.range(..=addr).next_back()?;
|
||||||
|
let off = addr - sym.addr;
|
||||||
|
if sym.size == 0 {
|
||||||
|
if off == 0 { Some(sym.name.clone()) } else { None }
|
||||||
|
} else if off < sym.size {
|
||||||
|
if off == 0 { Some(sym.name.clone()) } else { Some(format!("{}+0x{:x}", sym.name, off)) }
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Resolve a name to a single address. Tries exact case first, then
|
||||||
|
/// lowercase. Returns None if not found or ambiguous.
|
||||||
|
pub fn addr_for_name(&self, name: &str) -> Option<u64> {
|
||||||
|
if let Some(&a) = self.by_name.get(name) { return Some(a); }
|
||||||
|
self.by_name.get(&name.to_lowercase()).copied()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn len(&self) -> usize {
|
||||||
|
self.by_addr.len()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_exe_path(pid: i32) -> Result<PathBuf, String> {
|
||||||
|
fs::read_link(format!("/proc/{}/exe", pid))
|
||||||
|
.map_err(|e| format!("read /proc/{}/exe: {}", pid, e))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// For a PIE (ET_DYN) binary, find the first executable mapping of the exe
|
||||||
|
/// file in /proc/<pid>/maps and use its address as the load base. For non-PIE
|
||||||
|
/// (ET_EXEC), the load base is 0.
|
||||||
|
fn read_load_base(pid: i32, exe_path: &PathBuf) -> Result<u64, String> {
|
||||||
|
// Peek at the ELF header e_type via the file we'll parse anyway.
|
||||||
|
let bytes = fs::read(exe_path)
|
||||||
|
.map_err(|e| format!("read exe {}: {}", exe_path.display(), e))?;
|
||||||
|
let elf = object::File::parse(&*bytes)
|
||||||
|
.map_err(|e| format!("parse ELF: {}", e))?;
|
||||||
|
// object doesn't expose e_type directly on the high-level File, but
|
||||||
|
// it does expose `relative_address_base` which is what we want: for
|
||||||
|
// ET_EXEC this is the absolute base of the file's address space,
|
||||||
|
// for ET_DYN it's 0 (so symbol addresses are file-offsets-as-vaddrs).
|
||||||
|
// We compute load base = first executable mapping start - (file's PT_LOAD
|
||||||
|
// p_vaddr for that segment). Easier: parse /proc/maps and look for the
|
||||||
|
// first executable mapping whose pathname matches exe_path.
|
||||||
|
let exe_str = exe_path.to_string_lossy().into_owned();
|
||||||
|
let maps = fs::read_to_string(format!("/proc/{}/maps", pid))
|
||||||
|
.map_err(|e| format!("read /proc/{}/maps: {}", pid, e))?;
|
||||||
|
|
||||||
|
// /proc/<pid>/maps line format:
|
||||||
|
// ADDR_LO-ADDR_HI PERMS OFFSET DEV INODE PATH
|
||||||
|
// We want the first line where PATH matches our exe AND PERMS contains 'x'.
|
||||||
|
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 range = match it.next() { Some(s) => s, None => continue };
|
||||||
|
let perms = match it.next() { Some(s) => s, None => continue };
|
||||||
|
let _offset = it.next();
|
||||||
|
let _dev = it.next();
|
||||||
|
let _inode = it.next();
|
||||||
|
let path = it.next().unwrap_or("").trim();
|
||||||
|
if !perms.contains('x') { continue; }
|
||||||
|
if path != exe_str { continue; }
|
||||||
|
let mut parts = range.split('-');
|
||||||
|
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))?;
|
||||||
|
|
||||||
|
// 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) {
|
||||||
|
// We assume the first executable mapping starts at file offset 0
|
||||||
|
// 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 {
|
||||||
|
return Ok(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Fallback: no executable mapping found. Probably never happens.
|
||||||
|
Ok(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_pie(elf: &object::File) -> bool {
|
||||||
|
// ET_DYN is what gcc produces by default for executables ("-pie") and
|
||||||
|
// also for shared libraries. For an executable file, ET_DYN ≡ PIE.
|
||||||
|
match elf {
|
||||||
|
object::File::Elf64(e) => e.elf_header().e_type.get(object::Endianness::Little) == object::elf::ET_DYN,
|
||||||
|
object::File::Elf32(e) => e.elf_header().e_type.get(object::Endianness::Little) == object::elf::ET_DYN,
|
||||||
|
_ => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
+15
-2
@@ -11,6 +11,8 @@ 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::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 {
|
||||||
@@ -55,6 +57,9 @@ pub struct Target {
|
|||||||
/// Snapshot of registers from the previous stop. None until at least one
|
/// Snapshot of registers from the previous stop. None until at least one
|
||||||
/// step/cont has happened after spawn.
|
/// step/cont has happened after spawn.
|
||||||
prev_regs: Option<libc::user_regs_struct>,
|
prev_regs: Option<libc::user_regs_struct>,
|
||||||
|
/// ELF symbols of the main executable. None if parsing failed (e.g.
|
||||||
|
/// stripped binary, unreadable /proc) — symbol lookups become no-ops.
|
||||||
|
symbols: Option<SymbolTable>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Target {
|
impl Target {
|
||||||
@@ -102,11 +107,15 @@ impl Target {
|
|||||||
let status = waitpid(child, None)
|
let status = waitpid(child, None)
|
||||||
.map_err(|e| format!("waitpid (initial) failed: {}", e))?;
|
.map_err(|e| format!("waitpid (initial) failed: {}", e))?;
|
||||||
match status {
|
match status {
|
||||||
WaitStatus::Stopped(_, Signal::SIGTRAP) => Ok(Target {
|
WaitStatus::Stopped(_, Signal::SIGTRAP) => {
|
||||||
|
let symbols = SymbolTable::from_pid(child.as_raw()).ok();
|
||||||
|
Ok(Target {
|
||||||
pid: child,
|
pid: child,
|
||||||
state: State::Stopped(StopReason::Step),
|
state: State::Stopped(StopReason::Step),
|
||||||
prev_regs: None,
|
prev_regs: None,
|
||||||
}),
|
symbols,
|
||||||
|
})
|
||||||
|
}
|
||||||
WaitStatus::Exited(_, code) => {
|
WaitStatus::Exited(_, code) => {
|
||||||
Err(format!("child exited immediately with code {} (exec failed?)", code))
|
Err(format!("child exited immediately with code {} (exec failed?)", code))
|
||||||
}
|
}
|
||||||
@@ -143,6 +152,10 @@ impl Target {
|
|||||||
self.prev_regs
|
self.prev_regs
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn symbols(&self) -> Option<&SymbolTable> {
|
||||||
|
self.symbols.as_ref()
|
||||||
|
}
|
||||||
|
|
||||||
fn wait_for_stop(&mut self) -> Result<StopReason, String> {
|
fn wait_for_stop(&mut 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 {
|
||||||
|
|||||||
Reference in New Issue
Block a user