use std::collections::HashMap;
use std::fs::File;
use std::io::{BufRead, BufReader};
use std::process::Command;
#[derive(Debug, Clone)]
pub struct MemoryRegion {
pub start_addr: u64,
pub end_addr: u64,
pub permissions: String,
pub offset: u64,
pub dev: String,
pub inode: u64,
pub pathname: Option<String>,
}
#[derive(Debug, Clone)]
pub struct SymbolInfo {
pub function: Option<String>,
pub file: Option<String>,
pub line: Option<u32>,
}
pub fn get_memory_maps(pid: u32) -> Vec<MemoryRegion> {
let path = format!("/proc/{pid}/maps");
let file = match File::open(&path) {
Ok(f) => f,
Err(e) => {
eprintln!("Failed to open maps file for PID {pid}: {e}");
return vec![];
}
};
let reader = BufReader::new(file);
let mut regions = Vec::new();
let mut skipped_lines = 0;
for (line_no, line_result) in reader.lines().enumerate() {
let line = match line_result {
Ok(l) => l,
Err(e) => {
eprintln!("Error reading line {line_no} from maps for PID {pid}: {e}");
skipped_lines += 1;
continue;
}
};
let parts: Vec<&str> = line.split_whitespace().collect();
if parts.len() < 5 {
skipped_lines += 1;
continue;
}
let addrs: Vec<&str> = parts[0].split('-').collect();
if addrs.len() != 2 {
skipped_lines += 1;
continue;
}
let start_addr = match u64::from_str_radix(addrs[0], 16) {
Ok(addr) => addr,
Err(_) => {
skipped_lines += 1;
continue;
}
};
let end_addr = match u64::from_str_radix(addrs[1], 16) {
Ok(addr) => addr,
Err(_) => {
skipped_lines += 1;
continue;
}
};
let permissions = parts[1].to_string();
let offset = match u64::from_str_radix(parts[2], 16) {
Ok(off) => off,
Err(_) => {
skipped_lines += 1;
continue;
}
};
let dev = parts[3].to_string();
let inode = match parts[4].parse::<u64>() {
Ok(i) => i,
Err(_) => {
skipped_lines += 1;
continue;
}
};
let pathname = if parts.len() >= 6 {
let path = parts[5..].join(" ");
if path.starts_with('[') && path.ends_with(']') {
Some(path)
}
else if path.is_empty() || path == "//anon" {
None
} else {
Some(path)
}
} else {
None
};
if pathname.is_some() || permissions.contains('x') {
regions.push(MemoryRegion {
start_addr,
end_addr,
permissions,
offset,
dev,
inode,
pathname,
});
}
}
if skipped_lines > 5 {
eprintln!("Warning: Skipped {skipped_lines} malformed lines in maps for PID {pid}");
}
regions
}
pub fn find_region_for_address(addr: u64, regions: &[MemoryRegion]) -> Option<&MemoryRegion> {
let exec_region = regions
.iter()
.find(|r| addr >= r.start_addr && addr < r.end_addr && r.permissions.contains('x'));
if exec_region.is_some() {
return exec_region;
}
let named_region = regions
.iter()
.find(|r| addr >= r.start_addr && addr < r.end_addr && r.pathname.is_some());
if named_region.is_some() {
return named_region;
}
regions
.iter()
.find(|r| addr >= r.start_addr && addr < r.end_addr)
}
pub fn get_symbol_info_with_addr2line(binary_path: &str, offset: u64) -> Option<SymbolInfo> {
if !std::path::Path::new(binary_path).exists() {
return None;
}
let addr_str = format!("0x{offset:x}");
for attempt in 1..=2 {
let mut cmd = Command::new("addr2line");
cmd.arg("-e")
.arg(binary_path)
.arg("-f") .arg("-C") .arg(&addr_str);
if attempt == 2 {
cmd.arg("-a"); }
let output = match cmd.output() {
Ok(out) => out,
Err(_) => continue, };
if !output.status.success() {
continue; }
let stdout = String::from_utf8_lossy(&output.stdout);
let mut lines = stdout.lines();
let function = lines
.next()
.map(|s| s.trim())
.filter(|s| !s.contains("??") && !s.is_empty())
.map(|s| s.to_string());
if function.is_none() && attempt < 2 {
continue;
}
let location = lines.next().unwrap_or("").trim();
let (file, line) = if let Some((f, l)) = location.rsplit_once(':') {
if f.contains("??") && attempt < 2 {
continue;
}
let line_num = l.parse::<u32>().ok();
(Some(f.to_string()).filter(|s| !s.contains("??")), line_num)
} else {
(None, None)
};
if function.is_some() {
return Some(SymbolInfo {
function,
file,
line,
});
}
}
if let Ok(output) = Command::new("objdump").arg("-t").arg(binary_path).output() {
if output.status.success() {
let stdout = String::from_utf8_lossy(&output.stdout);
let mut closest_symbol = None;
let mut closest_distance = u64::MAX;
for line in stdout.lines() {
let parts: Vec<&str> = line.split_whitespace().collect();
if parts.len() >= 6 {
if let Ok(addr) = u64::from_str_radix(parts[0], 16) {
if addr <= offset {
let distance = offset - addr;
if distance < closest_distance {
closest_distance = distance;
closest_symbol = Some(parts[5].to_string());
}
}
}
}
}
if closest_distance < 4096 {
return Some(SymbolInfo {
function: closest_symbol,
file: None,
line: None,
});
}
}
}
None
}
pub struct SymbolicationCache {
pub pid_maps: HashMap<u32, Vec<MemoryRegion>>,
pub symbol_cache: HashMap<(String, u64), SymbolInfo>,
pub last_refresh: HashMap<u32, std::time::Instant>,
pub max_age_secs: u64,
}
impl Default for SymbolicationCache {
fn default() -> Self {
Self::new()
}
}
impl SymbolicationCache {
pub fn new() -> Self {
Self {
pid_maps: HashMap::new(),
symbol_cache: HashMap::new(),
last_refresh: HashMap::new(),
max_age_secs: 30, }
}
pub fn get_memory_maps_for_pid(&mut self, pid: u32) -> &Vec<MemoryRegion> {
let should_refresh = match self.last_refresh.get(&pid) {
Some(instant) => {
instant.elapsed() > std::time::Duration::from_secs(self.max_age_secs)
|| self.pid_maps.get(&pid).is_none_or(|maps| maps.is_empty())
}
None => true,
};
if should_refresh {
let maps = get_memory_maps(pid);
self.pid_maps.insert(pid, maps);
self.last_refresh.insert(pid, std::time::Instant::now());
}
self.pid_maps.entry(pid).or_default()
}
pub fn get_symbol_info(&mut self, binary_path: &str, offset: u64) -> Option<SymbolInfo> {
let cache_key = (binary_path.to_string(), offset);
if let Some(symbol) = self.symbol_cache.get(&cache_key) {
return Some(symbol.clone());
}
if let Some(symbol) = get_symbol_info_with_addr2line(binary_path, offset) {
self.symbol_cache.insert(cache_key, symbol.clone());
return Some(symbol);
}
None
}
pub fn clear_pid(&mut self, pid: u32) {
self.pid_maps.remove(&pid);
self.last_refresh.remove(&pid);
}
pub fn stats(&self) -> (usize, usize, usize) {
(
self.pid_maps.len(),
self.symbol_cache.len(),
self.last_refresh.len(),
)
}
}