mod disasm;
pub mod dwarf;
mod ldd;
mod registry;
mod rendezvous;
pub mod tracee;
pub mod tracer;
pub use registry::RegionInfo;
pub use rendezvous::RendezvousError;
use super::r#async::TokioVersion;
use super::r#async::extract_tokio_version_naive;
use crate::debugger::address::{GlobalAddress, RelocatedAddress};
use crate::debugger::breakpoint::{Breakpoint, BrkptType};
use crate::debugger::debugee::disasm::Disassembler;
use crate::debugger::debugee::dwarf::DebugInformation;
use crate::debugger::debugee::dwarf::unit::PlaceDescriptorOwned;
use crate::debugger::debugee::dwarf::unwind;
use crate::debugger::debugee::dwarf::unwind::Backtrace;
use crate::debugger::debugee::registry::DwarfRegistry;
use crate::debugger::debugee::rendezvous::Rendezvous;
use crate::debugger::debugee::tracee::{Tracee, TraceeCtl};
use crate::debugger::debugee::tracer::{StopReason, TraceContext, Tracer};
use crate::debugger::error::Error;
use crate::debugger::error::Error::{
FunctionNotFound, FunctionRangeNotFound, MappingOffsetNotFound, TraceeNotFound,
};
use crate::debugger::process::{Child, Installed};
use crate::debugger::register::DwarfRegisterMap;
use crate::debugger::unwind::FrameSpan;
use crate::debugger::{ExplorationContext, PlaceDescriptor};
use crate::{muted_error, print_warns, weak_error};
use log::{info, warn};
use nix::NixPath;
use nix::unistd::Pid;
use object::{Object, ObjectSection};
use rayon::prelude::*;
use std::collections::HashMap;
use std::fs;
use std::path::{Path, PathBuf};
use std::sync::Arc;
#[derive(Debug, Default, Clone)]
pub struct FrameInfo {
pub num: u32,
pub frame: FrameSpan,
pub base_addr: RelocatedAddress,
pub cfa: RelocatedAddress,
pub return_addr: Option<RelocatedAddress>,
}
#[derive(Debug, Clone)]
pub struct ThreadSnapshot {
pub thread: Tracee,
pub bt: Option<Backtrace>,
pub place: Option<PlaceDescriptorOwned>,
pub focus_frame: Option<usize>,
pub in_focus: bool,
}
#[derive(Clone, Copy, Debug)]
pub struct Location {
pub pc: RelocatedAddress,
pub global_pc: GlobalAddress,
pub pid: Pid,
}
impl Location {
pub fn new(pc: RelocatedAddress, global_pc: GlobalAddress, pid: Pid) -> Self {
Self { pc, global_pc, pid }
}
}
#[derive(PartialEq, Clone, Copy)]
pub enum ExecutionStatus {
Unload,
InProgress,
Exited,
}
pub struct FunctionAssembly {
pub name: Option<String>,
pub addr_in_focus: GlobalAddress,
pub instructions: Vec<disasm::Instruction>,
}
pub struct FunctionRange<'a> {
pub name: Option<String>,
pub stop_place: PlaceDescriptor<'a>,
pub file: &'a Path,
pub start_line: u64,
pub end_line: u64,
}
pub struct Debugee {
execution_status: ExecutionStatus,
tracer: Tracer,
path: PathBuf,
object_sections: HashMap<String, u64>,
rendezvous: Option<Rendezvous>,
dwarf_registry: DwarfRegistry,
disassembly: Disassembler,
libthread_db: Arc<thread_db::Lib>,
tokio_version: Option<TokioVersion>,
}
impl Debugee {
pub fn new_non_running(
path: &Path,
process: &Child<Installed>,
object: &object::File,
) -> Result<Self, Error> {
let dwarf_builder = dwarf::DebugInformationBuilder;
let dwarf = dwarf_builder.build(path, object)?;
let mut registry = DwarfRegistry::new(process.pid(), path.to_path_buf(), dwarf);
let deps = muted_error!(
ldd::find_dependencies(path),
"unsuccessful attempt to use ldd"
);
parse_dependencies_into_registry(&mut registry, deps.unwrap_or_default().into_iter(), true);
let tokio_ver: Option<TokioVersion> = object
.section_by_name(".rodata")
.and_then(|sect| sect.data().ok())
.and_then(extract_tokio_version_naive);
Ok(Self {
execution_status: ExecutionStatus::Unload,
path: path.into(),
object_sections: object
.sections()
.filter_map(|section| Some((section.name().ok()?.to_string(), section.address())))
.collect(),
rendezvous: None,
tracer: Tracer::new(process.pid()),
dwarf_registry: registry,
disassembly: Disassembler::new()?,
libthread_db: Arc::new(thread_db::Lib::try_load()?),
tokio_version: tokio_ver,
})
}
pub fn new_from_external_process(
path: &Path,
process: &Child<Installed>,
object: &object::File,
) -> Result<Self, Error> {
let dwarf_builder = dwarf::DebugInformationBuilder;
let dwarf = dwarf_builder.build(path, object)?;
let mut registry = DwarfRegistry::new(process.pid(), path.to_path_buf(), dwarf);
registry.update_mappings(false)?;
let main_dwarf = registry
.find_main_program_dwarf()
.ok_or(Error::NoDebugInformation("executable object"))?;
let main_dwarf_offset = registry
.find_mapping_offset_for_file(main_dwarf)
.ok_or(MappingOffsetNotFound("unknown segment"))?;
let object_sections = object
.sections()
.filter_map(|section| Some((section.name().ok()?.to_string(), section.address())))
.collect();
let tokio_ver: Option<TokioVersion> = object
.section_by_name(".rodata")
.and_then(|sect| sect.data().ok())
.and_then(extract_tokio_version_naive);
let mut debugee = Self {
execution_status: ExecutionStatus::InProgress,
path: path.into(),
rendezvous: Some(Rendezvous::new(
process.pid(),
main_dwarf_offset,
&object_sections,
)?),
object_sections,
tracer: Tracer::new_external(
process.pid(),
&process
.external_info()
.expect("process is not external")
.threads,
),
dwarf_registry: registry,
disassembly: Disassembler::new()?,
libthread_db: Arc::new(thread_db::Lib::try_load()?),
tokio_version: tokio_ver,
};
debugee.attach_libthread_db();
debugee.update_debug_info_registry(true)?;
Ok(debugee)
}
pub fn extend(&self, proc: Pid) -> Self {
Self {
execution_status: ExecutionStatus::Unload,
path: self.path.clone(),
object_sections: self.object_sections.clone(),
rendezvous: None,
tracer: Tracer::new(proc),
dwarf_registry: self.dwarf_registry.extend(proc),
disassembly: Disassembler::new().expect("infallible"),
libthread_db: self.libthread_db.clone(),
tokio_version: self.tokio_version,
}
}
pub fn tokio_version(&self) -> Option<TokioVersion> {
self.tokio_version
}
pub fn execution_status(&self) -> ExecutionStatus {
self.execution_status
}
pub fn is_in_progress(&self) -> bool {
self.execution_status == ExecutionStatus::InProgress
}
pub fn is_exited(&self) -> bool {
self.execution_status == ExecutionStatus::Exited
}
pub fn rendezvous(&self) -> &Rendezvous {
self.rendezvous.as_ref().expect("rendezvous must exists")
}
pub fn tracer_mut(&mut self) -> &mut Tracer {
&mut self.tracer
}
fn attach_libthread_db(&mut self) {
let tracee_ctl = &mut self.tracer.tracee_ctl;
match tracee_ctl.attach_thread_db(self.libthread_db.clone()) {
Ok(_) => {
info!(target: "loading", "libthread_db enabled")
}
Err(e) => {
warn!(
target: "loading", "libthread_db load fail with \"{e}\", some thread debug functions are omitted"
);
}
}
}
pub fn trace_until_stop(&mut self, tcx: TraceContext) -> Result<StopReason, Error> {
let event = self.tracer.resume(tcx)?;
match event {
StopReason::DebugeeExit(_) => {
self.execution_status = ExecutionStatus::Exited;
}
StopReason::DebugeeStart => {
self.execution_status = ExecutionStatus::InProgress;
print_warns!(self.dwarf_registry.update_mappings(true)?);
}
StopReason::Breakpoint(tid, addr) => {
let mb_brkpt = tcx.breakpoints.iter().find(|bp| bp.addr == addr);
let mb_type = mb_brkpt.map(|brkpt| brkpt.r#type());
match mb_type {
Some(BrkptType::EntryPoint) => {
let main_dwarf = self.program_debug_info()?;
self.rendezvous = Some(Rendezvous::new(
tid,
self.mapping_offset_for_file(main_dwarf)?,
&self.object_sections,
)?);
self.attach_libthread_db();
self.update_debug_info_registry(true)?;
}
Some(BrkptType::LinkerMapFn) => {
self.update_debug_info_registry(false)?;
}
_ => {}
}
}
StopReason::NoSuchProcess(_) => {
self.execution_status = ExecutionStatus::Exited;
}
_ => {}
}
Ok(event)
}
pub fn pause(&mut self, tcx: TraceContext) -> Result<(), Error> {
self.tracer.pause(tcx)
}
fn update_debug_info_registry(&mut self, quite: bool) -> Result<(), Error> {
let lmaps = self.rendezvous().link_maps()?;
let current_deps = lmaps
.into_iter()
.map(|lm| PathBuf::from(&lm.name))
.collect();
let reload_plan = self.dwarf_registry.reload_plan(current_deps);
for lib_to_del in reload_plan.to_del {
self.dwarf_registry.remove(lib_to_del);
}
parse_dependencies_into_registry(
&mut self.dwarf_registry,
reload_plan.to_add.into_iter(),
quite,
);
print_warns!(self.dwarf_registry.update_mappings(false)?);
Ok(())
}
#[inline(always)]
pub fn tracee_ctl(&self) -> &TraceeCtl {
&self.tracer.tracee_ctl
}
pub fn frame_info(&self, ecx: &ExplorationContext) -> Result<FrameInfo, Error> {
let dwarf = self.debug_info(ecx.location().pc)?;
let (func, _) = dwarf
.find_function_by_pc(ecx.location().global_pc)?
.ok_or(FunctionNotFound(ecx.location().global_pc))?;
let base_addr = func.frame_base_addr(ecx, self)?;
let cfa = dwarf.get_cfa(self, ecx)?;
let backtrace = self.unwind(ecx.pid_on_focus())?;
let (bt_frame_num, frame) = backtrace
.iter()
.enumerate()
.find(|(_, frame)| frame.ip == ecx.location().pc)
.expect("frame must exists");
let return_addr = backtrace.get(bt_frame_num + 1).map(|f| f.ip);
Ok(FrameInfo {
frame: frame.clone(),
num: bt_frame_num as u32,
cfa,
base_addr,
return_addr,
})
}
pub fn thread_state(&self, ecx: &ExplorationContext) -> Result<Vec<ThreadSnapshot>, Error> {
let threads = self.tracee_ctl().snapshot();
Ok(threads
.into_iter()
.filter_map(|tracee| {
let _tracee_ecx;
let tracee_ecx = if tracee.pid == ecx.pid_on_focus() {
ecx
} else {
let location = weak_error!(tracee.location(self))?;
_tracee_ecx = ExplorationContext::new(location, 0);
&_tracee_ecx
};
let mb_bt = weak_error!(self.unwind(tracee_ecx.pid_on_focus()));
let frame_num = mb_bt.as_ref().and_then(|bt| {
bt.iter()
.enumerate()
.find_map(|(i, frame)| (frame.ip == ecx.location().pc).then_some(i))
});
let place = mb_bt.as_ref().and_then(|bt| {
bt.first().and_then(|first_frame| {
let debug_info = self.debug_info(first_frame.ip).ok()?;
debug_info
.find_place_from_pc(first_frame.ip.into_global(self).ok()?)
.ok()?
})
});
Some(ThreadSnapshot {
in_focus: tracee.pid == ecx.pid_on_focus(),
thread: tracee,
bt: mb_bt,
place: place.map(|p| p.to_owned()),
focus_frame: frame_num,
})
})
.collect())
}
pub fn get_tracee_ensure(&self, pid: Pid) -> &Tracee {
self.tracee_ctl().tracee_ensure(pid)
}
pub fn get_tracee_by_num(&self, num: u32) -> Result<Tracee, Error> {
let mut snapshot = self.tracee_ctl().snapshot();
let tracee = snapshot.drain(..).find(|tracee| tracee.number == num);
tracee.ok_or(TraceeNotFound(num))
}
#[inline(always)]
pub fn debug_info(&self, addr: RelocatedAddress) -> Result<&DebugInformation, Error> {
self.dwarf_registry
.find_by_addr(addr)
.ok_or(Error::NoDebugInformation("current location"))
}
#[inline(always)]
pub fn debug_info_from_file(&self, path: &Path) -> Result<&DebugInformation, Error> {
self.dwarf_registry
.find_by_file(path)
.ok_or(Error::NoDebugInformation("file"))
}
#[inline(always)]
pub fn program_debug_info(&self) -> Result<&DebugInformation, Error> {
self.dwarf_registry
.find_main_program_dwarf()
.ok_or(Error::NoDebugInformation("executable object"))
}
#[inline(always)]
pub fn debug_info_all(&self) -> Vec<&DebugInformation> {
self.dwarf_registry.all_dwarf()
}
pub fn mapping_offset_for_pc(&self, addr: RelocatedAddress) -> Result<usize, Error> {
self.dwarf_registry
.find_mapping_offset(addr)
.ok_or(MappingOffsetNotFound("address out of bounds"))
}
pub fn mapping_offset_for_file(&self, dwarf: &DebugInformation) -> Result<usize, Error> {
self.dwarf_registry
.find_mapping_offset_for_file(dwarf)
.ok_or(MappingOffsetNotFound("unknown segment"))
}
pub fn unwind(&self, pid: Pid) -> Result<Backtrace, Error> {
unwind::unwind(self, pid)
}
#[allow(unused)]
pub fn restore_registers_at_frame(
&self,
pid: Pid,
registers: &mut DwarfRegisterMap,
frame_num: u32,
) -> Result<(), Error> {
unwind::restore_registers_at_frame(self, pid, registers, frame_num)
}
#[allow(unused)]
pub fn return_addr(&self, pid: Pid) -> Result<Option<RelocatedAddress>, Error> {
unwind::return_addr(self, pid)
}
pub fn dump_mapped_regions(&self) -> Vec<RegionInfo> {
self.dwarf_registry.dump()
}
pub fn disasm(
&self,
ecx: &ExplorationContext,
breakpoints: &[&Breakpoint],
) -> Result<FunctionAssembly, Error> {
let debug_information = self.debug_info(ecx.location().pc)?;
let (function, info) = debug_information
.find_function_by_pc(ecx.location().global_pc)?
.ok_or(FunctionNotFound(ecx.location().global_pc))?;
let name = info.full_name();
let instructions =
self.disassembly
.disasm_function(self, debug_information, function, breakpoints)?;
Ok(FunctionAssembly {
name,
addr_in_focus: ecx.location().global_pc,
instructions,
})
}
pub fn function_range(&self, ecx: &ExplorationContext) -> Result<FunctionRange<'_>, Error> {
let debug_information = self.debug_info(ecx.location().pc)?;
let (function, info) = debug_information
.find_function_by_pc(ecx.location().global_pc)?
.ok_or(FunctionNotFound(ecx.location().global_pc))?;
let unit = function.unit();
let stop_place = debug_information
.find_place_from_pc(ecx.location().global_pc)?
.ok_or(Error::PlaceNotFound(ecx.location().global_pc))?;
let (file, start_line) = info.decl_file_line.ok_or(FunctionRangeNotFound)?;
let file = unit.files()[file as usize].as_path();
let fn_places: Vec<_> = function
.ranges()
.iter()
.flat_map(|range| function.unit().find_lines_for_range(range))
.collect();
let mut end_line = fn_places
.iter()
.map(|place| place.line_number)
.max()
.unwrap_or(start_line);
if start_line > end_line {
warn!(target: "debugger", "irrational function range ({start_line};{end_line})");
end_line = start_line;
}
Ok(FunctionRange {
name: info.full_name(),
stop_place,
file,
start_line,
end_line,
})
}
}
fn parse_dependency(dep_file: impl Into<PathBuf>) -> Result<Option<DebugInformation>, Error> {
let dep_file = dep_file.into();
if dep_file.is_empty() || dep_file.to_string_lossy().contains("vdso") {
return Ok(None);
}
let file = fs::File::open(&dep_file)?;
let mmap = unsafe { memmap2::Mmap::map(&file)? };
let object = object::File::parse(&*mmap)?;
let dwarf_builder = dwarf::DebugInformationBuilder;
let dwarf = dwarf_builder.build(dep_file.as_path(), &object)?;
Ok(Some(dwarf))
}
fn parse_dependencies_into_registry(
registry: &mut DwarfRegistry,
deps: impl Iterator<Item = impl Into<PathBuf>>,
quiet: bool,
) {
let dwarfs: Vec<_> = deps
.map(|dep| dep.into())
.collect::<Vec<_>>()
.into_par_iter()
.filter_map(|dep| {
let parse_result = parse_dependency(&dep);
match parse_result {
Ok(mb_dep) => mb_dep.map(|dwarf| {
if !quiet {
info!(target: "dwarf-loader", "load shared library {dep:?}");
}
(dep, dwarf)
}),
Err(e) => {
warn!(target: "debugger", "broken dependency {:?}: {:#}", dep, e);
None
}
}
})
.collect();
dwarfs.into_iter().for_each(|(dep_name, dwarf)| {
if let Err(e) = registry.add(&dep_name, dwarf) {
warn!(target: "debugger", "broken dependency {:?}: {:#}", dep_name, e);
}
});
}