use crate::{
core::{
mapping::ModuleMapping, CallerFrameRecovery, DebugInfoSource, ModuleAddress, Result,
SectionType, SourceLocation,
},
loader::ExplicitDebugFile,
objfile::LoadedObjfile,
semantics::{CompactUnwindRow, CompactUnwindTable, PcContext, VisibleVariable},
};
use ghostscope_debuginfod::DebuginfodClient;
use object::{Object, ObjectSection};
use std::collections::{HashMap, VecDeque};
use std::path::{Path, PathBuf};
use std::sync::{Arc, RwLock};
mod module_resolution;
mod plan_global;
mod plan_pc;
mod source_resolution;
mod type_lookup;
pub use module_resolution::ModuleDefaultPolicy;
pub use source_resolution::{SourceLineAddressSearch, SourceLineQuerySearch};
pub use type_lookup::TypeLookupAmbiguity;
#[cfg(test)]
use crate::{
core::{AddressExpr, Availability, Provenance, VariableLocation},
semantics::VariableReadPlan,
};
#[derive(Debug, Clone)]
pub enum ModuleLoadingEvent {
Discovered {
module_path: String,
current: usize,
total: usize,
},
LoadingStarted {
module_path: String,
current: usize,
total: usize,
},
LoadingCompleted {
module_path: String,
stats: ModuleLoadingStats,
current: usize,
total: usize,
},
LoadingFailed {
module_path: String,
error: String,
current: usize,
total: usize,
},
}
#[derive(Debug, Clone)]
pub struct ModuleLoadingStats {
pub functions: usize,
pub variables: usize,
pub types: usize,
pub debug_info_source: DebugInfoSource,
pub load_time_ms: u64,
pub parse_time_ms: u64,
pub index_time_ms: u64,
pub module_total_time_ms: u64,
}
#[derive(Debug, Clone)]
pub struct AddressQueryResult {
pub module_path: PathBuf,
pub address: u64,
pub source_file: Option<String>,
pub source_line: Option<u32>,
pub source_column: Option<u32>,
pub function_name: Option<String>,
pub is_inline: Option<bool>,
pub variables: Vec<VisibleVariable>,
pub parameters: Vec<VisibleVariable>,
}
#[derive(Debug, Clone)]
pub struct LoadedModuleRuntimeInfo {
pub module_path: PathBuf,
pub loaded_address: Option<u64>,
pub load_bias: Option<u64>,
pub size: u64,
}
#[derive(Debug, Clone)]
pub struct FunctionQueryResult {
pub function_name: String,
pub addresses: Vec<AddressQueryResult>,
}
#[derive(Debug)]
pub struct DwarfAnalyzer {
pid: u32,
modules: HashMap<PathBuf, LoadedObjfile>,
pc_context_cache: RwLock<PcContextCache>,
}
const PC_CONTEXT_CACHE_MAX_ENTRIES: usize = 8192;
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
struct PcContextCacheKey {
module_path: PathBuf,
address: u64,
}
#[derive(Debug)]
struct PcContextCache {
entries: HashMap<PathBuf, HashMap<u64, PcContext>>,
insertion_order: VecDeque<PcContextCacheKey>,
len: usize,
max_entries: usize,
}
impl Default for PcContextCache {
fn default() -> Self {
Self {
entries: HashMap::new(),
insertion_order: VecDeque::new(),
len: 0,
max_entries: PC_CONTEXT_CACHE_MAX_ENTRIES,
}
}
}
impl PcContextCache {
fn get(&self, module_path: &Path, address: u64) -> Option<PcContext> {
self.entries
.get(module_path)
.and_then(|entries| entries.get(&address))
.cloned()
}
fn insert(&mut self, module_path: PathBuf, address: u64, context: PcContext) {
if self.max_entries == 0 {
return;
}
let key = PcContextCacheKey {
module_path,
address,
};
let module_entries = self.entries.entry(key.module_path.clone()).or_default();
if module_entries.insert(address, context).is_none() {
self.insertion_order.push_back(key.clone());
self.len += 1;
}
while self.len > self.max_entries {
let Some(expired) = self.insertion_order.pop_front() else {
break;
};
if let Some(module_entries) = self.entries.get_mut(&expired.module_path) {
if module_entries.remove(&expired.address).is_some() {
self.len -= 1;
}
if module_entries.is_empty() {
self.entries.remove(&expired.module_path);
}
}
}
}
}
impl DwarfAnalyzer {
fn build_address_query_result(
&self,
module_address: &ModuleAddress,
) -> Result<AddressQueryResult> {
self.build_address_query_result_with_source_hint(module_address, None)
}
fn build_address_query_result_with_source_hint(
&self,
module_address: &ModuleAddress,
source_hint: Option<(&str, u32)>,
) -> Result<AddressQueryResult> {
let mut variables = Vec::new();
let mut parameters = Vec::new();
for variable in self.visible_variables_at_address(module_address)? {
if variable.is_parameter {
parameters.push(variable);
} else {
variables.push(variable);
}
}
let source_location = if let Some((file_path, line_number)) = source_hint {
self.modules
.get(&module_address.module_path)
.and_then(|module_data| {
module_data.lookup_source_location_for_source_line(
module_address.address,
file_path,
line_number,
)
})
} else {
self.lookup_source_location(module_address)
};
let function_name = self.find_function_name_by_module_address(module_address);
let is_inline = self.is_inline_at(module_address);
Ok(AddressQueryResult {
module_path: module_address.module_path.clone(),
address: module_address.address,
source_file: source_location.as_ref().map(|sl| sl.file_path.clone()),
source_line: source_location.as_ref().map(|sl| sl.line_number),
source_column: source_location.as_ref().and_then(|sl| sl.column),
function_name,
is_inline,
variables,
parameters,
})
}
fn query_module_addresses(
&self,
module_addresses: Vec<ModuleAddress>,
) -> Result<Vec<AddressQueryResult>> {
module_addresses
.iter()
.map(|module_address| self.build_address_query_result(module_address))
.collect()
}
fn query_module_addresses_for_source_line(
&self,
module_addresses: Vec<ModuleAddress>,
file_path: &str,
line_number: u32,
) -> Result<Vec<AddressQueryResult>> {
module_addresses
.iter()
.map(|module_address| {
self.build_address_query_result_with_source_hint(
module_address,
Some((file_path, line_number)),
)
})
.collect()
}
fn query_module_addresses_best_effort(
&self,
module_addresses: Vec<ModuleAddress>,
query_label: &str,
) -> Result<Vec<AddressQueryResult>> {
let mut results = Vec::new();
let mut first_error: Option<(ModuleAddress, String)> = None;
for module_address in &module_addresses {
match self.build_address_query_result(module_address) {
Ok(result) => results.push(result),
Err(error) => {
let error_string = error.to_string();
tracing::warn!(
"Skipping failed address query for {} at {}:0x{:x}: {}",
query_label,
module_address.module_display(),
module_address.address,
error_string
);
if first_error.is_none() {
first_error = Some((module_address.clone(), error_string));
}
}
}
}
if results.is_empty() {
if let Some((module_address, error)) = first_error {
return Err(anyhow::anyhow!(
"Failed to analyze any address for {} (first failure at {}:0x{:x}: {})",
query_label,
module_address.module_display(),
module_address.address,
error
));
}
}
Ok(results)
}
fn query_module_addresses_for_source_line_best_effort(
&self,
module_addresses: Vec<ModuleAddress>,
file_path: &str,
line_number: u32,
query_label: &str,
) -> Result<Vec<AddressQueryResult>> {
let mut results = Vec::new();
let mut first_error: Option<(ModuleAddress, String)> = None;
for module_address in &module_addresses {
match self.build_address_query_result_with_source_hint(
module_address,
Some((file_path, line_number)),
) {
Ok(result) => results.push(result),
Err(error) => {
let error_string = error.to_string();
tracing::warn!(
"Skipping failed address query for {} at {}:0x{:x}: {}",
query_label,
module_address.module_display(),
module_address.address,
error_string
);
if first_error.is_none() {
first_error = Some((module_address.clone(), error_string));
}
}
}
}
if results.is_empty() {
if let Some((module_address, error)) = first_error {
return Err(anyhow::anyhow!(
"Failed to analyze any address for {} (first failure at {}:0x{:x}: {})",
query_label,
module_address.module_display(),
module_address.address,
error
));
}
}
Ok(results)
}
fn find_function_name_by_module_address(
&self,
module_address: &ModuleAddress,
) -> Option<String> {
self.loaded_module_path_for(&module_address.module_path)
.and_then(|module_path| self.modules.get(module_path))
.and_then(|module_data| {
module_data.find_function_name_by_address(module_address.address)
})
}
fn sorted_module_paths(&self) -> Vec<&PathBuf> {
let mut paths: Vec<&PathBuf> = self.modules.keys().collect();
paths.sort();
paths
}
pub(crate) fn loaded_module_path_for<P: AsRef<Path>>(
&self,
module_path: P,
) -> Option<&PathBuf> {
let module_path = module_path.as_ref();
if let Some((path, _)) = self.modules.get_key_value(module_path) {
return Some(path);
}
self.sorted_module_paths()
.into_iter()
.find(|path| Self::module_paths_equivalent(path.as_path(), module_path))
}
pub fn module_id_for_path<P: AsRef<Path>>(&self, module_path: P) -> Option<crate::ModuleId> {
let module_path = self.loaded_module_path_for(module_path)?;
self.sorted_module_paths()
.into_iter()
.position(|path| path.as_path() == module_path.as_path())
.map(|index| crate::ModuleId(index as u32))
}
pub fn module_path_for_id(&self, module: crate::ModuleId) -> Option<&Path> {
self.sorted_module_paths()
.get(module.0 as usize)
.map(|path| path.as_path())
}
pub async fn from_pid(pid: u32) -> Result<Self> {
Self::from_pid_parallel(pid).await
}
pub fn is_inline_at(&self, module_address: &ModuleAddress) -> Option<bool> {
if let Some(module_data) = self
.loaded_module_path_for(&module_address.module_path)
.and_then(|module_path| self.modules.get(module_path))
{
module_data.is_inline_at(module_address.address)
} else {
None
}
}
pub async fn from_pid_parallel(pid: u32) -> Result<Self> {
Self::from_pid_parallel_with_config(pid, &[], false, |_event| {}).await
}
pub async fn from_pid_parallel_with_progress<F>(pid: u32, progress_callback: F) -> Result<Self>
where
F: Fn(ModuleLoadingEvent) + Send + Sync + 'static,
{
Self::from_pid_parallel_with_config(pid, &[], false, progress_callback).await
}
pub async fn from_pid_parallel_with_config<F>(
pid: u32,
debug_search_paths: &[String],
allow_loose_debug_match: bool,
progress_callback: F,
) -> Result<Self>
where
F: Fn(ModuleLoadingEvent) + Send + Sync + 'static,
{
Self::from_pid_parallel_with_config_and_debuginfod(
pid,
debug_search_paths,
allow_loose_debug_match,
None,
progress_callback,
)
.await
}
pub async fn from_pid_parallel_with_config_and_debuginfod<F>(
pid: u32,
debug_search_paths: &[String],
allow_loose_debug_match: bool,
debuginfod_client: Option<Arc<DebuginfodClient>>,
progress_callback: F,
) -> Result<Self>
where
F: Fn(ModuleLoadingEvent) + Send + Sync + 'static,
{
tracing::info!("Creating DWARF analyzer for PID {} (parallel)", pid);
let module_runtime_info = Self::discover_pid_runtime_modules(pid)?;
Self::from_pid_runtime_modules_with_config_and_debuginfod(
pid,
module_runtime_info,
debug_search_paths,
allow_loose_debug_match,
debuginfod_client,
progress_callback,
)
.await
}
pub fn discover_pid_runtime_modules(pid: u32) -> Result<Vec<LoadedModuleRuntimeInfo>> {
let mut coord = ghostscope_process::ProcessManager::new();
coord.ensure_prefill_pid(pid)?;
Ok(coord
.cached_offsets_with_paths_for_pid(pid)
.map(Self::runtime_modules_from_pid_offsets)
.unwrap_or_default())
}
pub fn runtime_modules_from_pid_offsets(
entries: &[ghostscope_process::PidOffsetsEntry],
) -> Vec<LoadedModuleRuntimeInfo> {
let mut seen = std::collections::HashSet::new();
entries
.iter()
.filter(|entry| seen.insert(entry.module_path.clone()))
.map(|entry| LoadedModuleRuntimeInfo {
module_path: PathBuf::from(&entry.module_path),
loaded_address: Some(entry.base),
load_bias: Some(entry.offsets.text),
size: entry.size,
})
.collect()
}
fn runtime_modules_to_module_mappings(
runtime_modules: Vec<LoadedModuleRuntimeInfo>,
) -> Vec<ModuleMapping> {
runtime_modules
.into_iter()
.map(|module| {
let mut mapping = ModuleMapping::from_path(module.module_path);
mapping.loaded_address = module.loaded_address;
mapping.load_bias = module.load_bias;
mapping.size = module.size;
mapping
})
.collect()
}
pub async fn refresh_pid_runtime_modules_with_config_and_debuginfod<F>(
&mut self,
runtime_modules: Vec<LoadedModuleRuntimeInfo>,
debug_search_paths: &[String],
allow_loose_debug_match: bool,
debuginfod_client: Option<Arc<DebuginfodClient>>,
progress_callback: F,
) -> Result<usize>
where
F: Fn(ModuleLoadingEvent) + Send + Sync + 'static,
{
let mut new_runtime_modules = Vec::new();
let mut updated_existing = 0usize;
for runtime_module in runtime_modules {
let existing = self.modules.iter_mut().find(|(path, _)| {
Self::module_paths_equivalent(path.as_path(), &runtime_module.module_path)
});
if let Some((_path, loaded)) = existing {
let mapping = loaded.module_mapping();
if mapping.loaded_address != runtime_module.loaded_address
|| mapping.load_bias != runtime_module.load_bias
|| mapping.size != runtime_module.size
{
loaded.update_runtime_mapping(
runtime_module.loaded_address,
runtime_module.load_bias,
runtime_module.size,
);
updated_existing += 1;
}
} else {
new_runtime_modules.push(runtime_module);
}
}
if updated_existing > 0 {
self.clear_pc_context_cache();
tracing::debug!(
"Updated runtime mapping metadata for {} loaded module(s)",
updated_existing
);
}
if new_runtime_modules.is_empty() {
return Ok(0);
}
tracing::info!(
"Refreshing DWARF analyzer for PID {} with {} newly mapped module(s)",
self.pid,
new_runtime_modules.len()
);
let module_mappings = Self::runtime_modules_to_module_mappings(new_runtime_modules);
for (index, mapping) in module_mappings.iter().enumerate() {
progress_callback(ModuleLoadingEvent::Discovered {
module_path: mapping.path.to_string_lossy().to_string(),
current: index + 1,
total: module_mappings.len(),
});
}
let mut loader = crate::loader::ModuleLoader::new(module_mappings).parallel();
if !debug_search_paths.is_empty() {
loader = loader.with_debug_search_paths(debug_search_paths.to_vec());
}
loader = loader.with_loose_debug_match(allow_loose_debug_match);
loader = loader.with_debuginfod_client(debuginfod_client);
let modules = loader
.with_progress_callback(progress_callback)
.load()
.await?;
let loaded_count = modules.len();
for module in modules {
let module_path = module.module_path().clone();
self.modules.insert(module_path, module);
}
if loaded_count > 0 {
self.clear_pc_context_cache();
tracing::info!(
"DWARF analyzer for PID {} loaded {} new module(s)",
self.pid,
loaded_count
);
}
Ok(loaded_count)
}
pub async fn from_pid_runtime_modules_with_config_and_debuginfod<F>(
pid: u32,
runtime_modules: Vec<LoadedModuleRuntimeInfo>,
debug_search_paths: &[String],
allow_loose_debug_match: bool,
debuginfod_client: Option<Arc<DebuginfodClient>>,
progress_callback: F,
) -> Result<Self>
where
F: Fn(ModuleLoadingEvent) + Send + Sync + 'static,
{
Self::from_pid_runtime_modules_with_config_debuginfod_and_explicit_debug_file(
pid,
runtime_modules,
debug_search_paths,
allow_loose_debug_match,
debuginfod_client,
None,
progress_callback,
)
.await
}
pub async fn from_pid_runtime_modules_with_config_debuginfod_and_explicit_debug_file<F>(
pid: u32,
runtime_modules: Vec<LoadedModuleRuntimeInfo>,
debug_search_paths: &[String],
allow_loose_debug_match: bool,
debuginfod_client: Option<Arc<DebuginfodClient>>,
explicit_debug_file: Option<ExplicitDebugFile>,
progress_callback: F,
) -> Result<Self>
where
F: Fn(ModuleLoadingEvent) + Send + Sync + 'static,
{
tracing::info!(
"Creating DWARF analyzer for PID {} from {} runtime module mappings",
pid,
runtime_modules.len()
);
let module_mappings = Self::runtime_modules_to_module_mappings(runtime_modules);
tracing::info!(
"Discovered {} modules for PID {}",
module_mappings.len(),
pid
);
for (index, mapping) in module_mappings.iter().enumerate() {
progress_callback(ModuleLoadingEvent::Discovered {
module_path: mapping.path.to_string_lossy().to_string(),
current: index + 1,
total: module_mappings.len(),
});
}
let mut loader = crate::loader::ModuleLoader::new(module_mappings).parallel();
if !debug_search_paths.is_empty() {
loader = loader.with_debug_search_paths(debug_search_paths.to_vec());
}
loader = loader.with_loose_debug_match(allow_loose_debug_match);
loader = loader.with_explicit_debug_file(explicit_debug_file);
loader = loader.with_debuginfod_client(debuginfod_client);
let modules = loader
.with_progress_callback(progress_callback)
.load()
.await?;
tracing::info!(
"Created DWARF analyzer for PID {} with {} modules (parallel)",
pid,
modules.len()
);
Ok(Self::from_modules(pid, modules))
}
pub async fn from_exec_path<P: AsRef<std::path::Path>>(exec_path: P) -> Result<Self> {
Self::from_exec_path_with_config(exec_path, &[], false).await
}
pub async fn from_exec_path_with_config<P: AsRef<std::path::Path>>(
exec_path: P,
debug_search_paths: &[String],
allow_loose_debug_match: bool,
) -> Result<Self> {
Self::from_exec_path_with_config_and_debuginfod(
exec_path,
debug_search_paths,
allow_loose_debug_match,
None,
)
.await
}
pub async fn from_exec_path_with_config_and_debuginfod<P: AsRef<std::path::Path>>(
exec_path: P,
debug_search_paths: &[String],
allow_loose_debug_match: bool,
debuginfod_client: Option<Arc<DebuginfodClient>>,
) -> Result<Self> {
Self::from_exec_path_with_config_and_debuginfod_and_progress(
exec_path,
debug_search_paths,
allow_loose_debug_match,
debuginfod_client,
|_event| {},
)
.await
}
pub async fn from_exec_path_with_config_and_progress<P, F>(
exec_path: P,
debug_search_paths: &[String],
allow_loose_debug_match: bool,
progress_callback: F,
) -> Result<Self>
where
P: AsRef<std::path::Path>,
F: Fn(ModuleLoadingEvent) + Send + Sync + 'static,
{
Self::from_exec_path_with_config_and_debuginfod_and_progress(
exec_path,
debug_search_paths,
allow_loose_debug_match,
None,
progress_callback,
)
.await
}
pub async fn from_exec_path_with_config_and_debuginfod_and_progress<P, F>(
exec_path: P,
debug_search_paths: &[String],
allow_loose_debug_match: bool,
debuginfod_client: Option<Arc<DebuginfodClient>>,
progress_callback: F,
) -> Result<Self>
where
P: AsRef<std::path::Path>,
F: Fn(ModuleLoadingEvent) + Send + Sync + 'static,
{
Self::from_exec_path_with_config_debuginfod_explicit_debug_file_and_progress(
exec_path,
debug_search_paths,
allow_loose_debug_match,
debuginfod_client,
None,
progress_callback,
)
.await
}
pub async fn from_exec_path_with_config_debuginfod_explicit_debug_file_and_progress<P, F>(
exec_path: P,
debug_search_paths: &[String],
allow_loose_debug_match: bool,
debuginfod_client: Option<Arc<DebuginfodClient>>,
explicit_debug_file: Option<PathBuf>,
progress_callback: F,
) -> Result<Self>
where
P: AsRef<std::path::Path>,
F: Fn(ModuleLoadingEvent) + Send + Sync + 'static,
{
let exec_path = exec_path.as_ref().to_path_buf();
tracing::info!(
"Creating DWARF analyzer for executable: {}",
exec_path.display()
);
let mut analyzer = Self {
pid: 0, modules: HashMap::new(),
pc_context_cache: RwLock::new(PcContextCache::default()),
};
let module_mapping = ModuleMapping {
path: exec_path.clone(),
loaded_address: None, load_bias: None,
size: 0, };
let module_path = exec_path.to_string_lossy().to_string();
progress_callback(ModuleLoadingEvent::Discovered {
module_path: module_path.clone(),
current: 1,
total: 1,
});
progress_callback(ModuleLoadingEvent::LoadingStarted {
module_path: module_path.clone(),
current: 1,
total: 1,
});
let start_time = std::time::Instant::now();
match LoadedObjfile::load_parallel(
module_mapping,
debug_search_paths,
allow_loose_debug_match,
explicit_debug_file,
debuginfod_client,
)
.await
{
Ok(module_data) => {
let (functions, variables, types) = module_data.get_lightweight_index().get_stats();
let (parse_time_ms, index_time_ms, module_total_time_ms) =
module_data.get_load_timing_ms();
progress_callback(ModuleLoadingEvent::LoadingCompleted {
module_path,
stats: ModuleLoadingStats {
functions,
variables,
types,
debug_info_source: module_data.get_debug_info_source().clone(),
load_time_ms: start_time.elapsed().as_millis() as u64,
parse_time_ms,
index_time_ms,
module_total_time_ms,
},
current: 1,
total: 1,
});
analyzer.modules.insert(exec_path.clone(), module_data);
tracing::info!(
"Created DWARF analyzer for executable {} with 1 module",
exec_path.display()
);
}
Err(e) => {
progress_callback(ModuleLoadingEvent::LoadingFailed {
module_path,
error: e.to_string(),
current: 1,
total: 1,
});
return Err(crate::DwarfError::ModuleLoadError(format!(
"Failed to load executable {}: {}",
exec_path.display(),
e
))
.into());
}
}
Ok(analyzer)
}
pub(crate) fn from_modules(pid: u32, modules: Vec<LoadedObjfile>) -> Self {
let mut analyzer = Self {
pid,
modules: HashMap::new(),
pc_context_cache: RwLock::new(PcContextCache::default()),
};
for module in modules {
let module_path = module.module_path().clone();
analyzer.modules.insert(module_path, module);
}
tracing::info!(
"Created DWARF analyzer for PID {} with {} pre-loaded modules",
pid,
analyzer.modules.len()
);
analyzer
}
fn clear_pc_context_cache(&self) {
if let Ok(mut cache) = self.pc_context_cache.write() {
*cache = PcContextCache::default();
}
}
pub fn lookup_function_addresses(&self, name: &str) -> Vec<ModuleAddress> {
let mut results = Vec::new();
for (module_path, module_data) in &self.modules {
let addresses = module_data.lookup_function_addresses_any(name);
for address in addresses {
tracing::debug!(
"Function '{}' found in module {} at address: 0x{:x}",
name,
module_path.display(),
address
);
results.push(ModuleAddress::new(module_path.clone(), address));
}
}
results.sort_by(|a, b| {
let pa = a.module_path.to_string_lossy();
let pb = b.module_path.to_string_lossy();
match pa.cmp(&pb) {
std::cmp::Ordering::Equal => a.address.cmp(&b.address),
other => other,
}
});
results
}
pub fn query_function(&self, name: &str) -> Result<FunctionQueryResult> {
let module_addresses = self.lookup_function_addresses(name);
let addresses = self.query_module_addresses(module_addresses)?;
Ok(FunctionQueryResult {
function_name: name.to_string(),
addresses,
})
}
pub fn query_function_best_effort(&self, name: &str) -> Result<FunctionQueryResult> {
let module_addresses = self.lookup_function_addresses(name);
let addresses = self
.query_module_addresses_best_effort(module_addresses, &format!("function '{name}'"))?;
Ok(FunctionQueryResult {
function_name: name.to_string(),
addresses,
})
}
pub fn vaddr_to_file_offset<P: AsRef<std::path::Path>>(
&self,
module_path: P,
vaddr: u64,
) -> Option<u64> {
let path_buf = module_path.as_ref().to_path_buf();
if let Some(module_data) = self.modules.get(&path_buf) {
module_data.vaddr_to_file_offset(vaddr)
} else {
None
}
}
pub fn recover_caller_frame(
&self,
module_address: &ModuleAddress,
registers: &[u16],
) -> Result<Option<CallerFrameRecovery>> {
if let Some(module_data) = self
.loaded_module_path_for(&module_address.module_path)
.and_then(|module_path| self.modules.get(module_path))
{
module_data.recover_caller_frame(module_address.address, registers)
} else {
Ok(None)
}
}
pub fn recover_caller_frame_for_context(
&self,
ctx: &PcContext,
registers: &[u16],
) -> Result<Option<CallerFrameRecovery>> {
let module_address = self.module_address_for_context(ctx)?;
self.recover_caller_frame(&module_address, registers)
}
pub fn compact_unwind_table_for_context(
&self,
ctx: &PcContext,
) -> Result<Option<Arc<CompactUnwindTable>>> {
let module_path = self
.module_path_for_id(ctx.module)
.ok_or_else(|| anyhow::anyhow!("Semantic module id {:?} is not loaded", ctx.module))?;
self.modules
.get(module_path)
.ok_or_else(|| anyhow::anyhow!("Module {} not loaded", module_path.display()))?
.compact_unwind_table(ctx.module)
}
pub fn compact_unwind_row_for_context(
&self,
ctx: &PcContext,
) -> Result<Option<CompactUnwindRow>> {
let module_path = self
.module_path_for_id(ctx.module)
.ok_or_else(|| anyhow::anyhow!("Semantic module id {:?} is not loaded", ctx.module))?;
self.modules
.get(module_path)
.ok_or_else(|| anyhow::anyhow!("Module {} not loaded", module_path.display()))?
.compact_unwind_row(ctx.module, ctx.normalized_pc)
}
pub fn compact_unwind_table_for_module(
&self,
module: crate::ModuleId,
) -> Result<Option<Arc<CompactUnwindTable>>> {
let module_path = self
.module_path_for_id(module)
.ok_or_else(|| anyhow::anyhow!("Semantic module id {:?} is not loaded", module))?;
self.modules
.get(module_path)
.ok_or_else(|| anyhow::anyhow!("Module {} not loaded", module_path.display()))?
.compact_unwind_table(module)
}
pub fn get_loaded_modules(&self) -> Vec<&PathBuf> {
self.modules.keys().collect()
}
pub fn loaded_module_runtime_info(&self) -> Vec<LoadedModuleRuntimeInfo> {
let mut modules: Vec<_> = self
.modules
.values()
.map(|module| {
let mapping = module.module_mapping();
LoadedModuleRuntimeInfo {
module_path: mapping.path.clone(),
loaded_address: mapping.loaded_address,
load_bias: mapping.load_bias,
size: mapping.size,
}
})
.collect();
modules.sort_by(|left, right| left.module_path.cmp(&right.module_path));
modules
}
pub fn module_entry_address<P: AsRef<Path>>(&self, module_path: P) -> Option<u64> {
self.modules
.get(module_path.as_ref())
.and_then(|module| module.entry_address())
}
pub fn classify_section_for_address<P: AsRef<Path>>(
&self,
module_path: P,
vaddr: u64,
) -> Option<SectionType> {
let path = module_path.as_ref();
if let Some(module_data) = self.modules.get(path) {
module_data.classify_section_for_vaddr(vaddr)
} else {
None
}
}
pub fn lookup_function_address_by_name(&self, function_name: &str) -> Option<ModuleAddress> {
let module_addresses = self.lookup_function_addresses(function_name);
if let Some(first_module_address) = module_addresses.first() {
tracing::info!(
"Found function '{}' in module '{}' at address 0x{:x}",
function_name,
first_module_address.module_display(),
first_module_address.address
);
Some(first_module_address.clone())
} else {
tracing::warn!("Function '{}' not found in any module", function_name);
None
}
}
pub fn lookup_source_location(&self, module_address: &ModuleAddress) -> Option<SourceLocation> {
if let Some(module_data) = self
.loaded_module_path_for(&module_address.module_path)
.and_then(|module_path| self.modules.get(module_path))
{
module_data.lookup_source_location(module_address.address)
} else {
tracing::warn!("Module {} not found", module_address.module_display());
None
}
}
pub fn lookup_addresses_by_source_line(
&self,
file_path: &str,
line_number: u32,
) -> Vec<ModuleAddress> {
let mut results = Vec::new();
for (module_path, module_data) in &self.modules {
let addresses = module_data.lookup_addresses_by_source_line(file_path, line_number);
for address in addresses {
results.push(ModuleAddress::new(module_path.clone(), address));
}
}
if !results.is_empty() {
tracing::info!(
"Found {} addresses for {}:{} across {} modules",
results.len(),
file_path,
line_number,
self.modules.len()
);
}
results.sort_by(|a, b| {
let pa = a.module_path.to_string_lossy();
let pb = b.module_path.to_string_lossy();
match pa.cmp(&pb) {
std::cmp::Ordering::Equal => a.address.cmp(&b.address),
other => other,
}
});
results
}
pub fn query_source_line(
&self,
file_path: &str,
line_number: u32,
) -> Result<Vec<AddressQueryResult>> {
let module_addresses = self.lookup_addresses_by_source_line(file_path, line_number);
self.query_module_addresses_for_source_line(module_addresses, file_path, line_number)
}
pub fn query_source_line_best_effort(
&self,
file_path: &str,
line_number: u32,
) -> Result<Vec<AddressQueryResult>> {
let module_addresses = self.lookup_addresses_by_source_line(file_path, line_number);
self.query_module_addresses_for_source_line_best_effort(
module_addresses,
file_path,
line_number,
&format!("source line '{file_path}:{line_number}'"),
)
}
pub fn query_address<P: AsRef<Path>>(
&self,
module_path: P,
address: u64,
) -> Result<AddressQueryResult> {
let module_address = ModuleAddress::new(module_path.as_ref().to_path_buf(), address);
self.build_address_query_result(&module_address)
}
pub fn get_all_function_names(&self) -> Vec<String> {
let mut all_names = std::collections::HashSet::new();
for module_data in self.modules.values() {
for name in module_data.get_function_names() {
all_names.insert(name.clone());
}
}
all_names.into_iter().collect()
}
pub fn get_stats(&self) -> AnalyzerStats {
let mut total_functions = 0;
let mut total_variables = 0;
let mut total_line_headers = 0;
for module_data in self.modules.values() {
total_functions += module_data.get_function_names().len();
total_variables += module_data.get_variable_names().len();
total_line_headers += module_data.get_line_header_count();
}
AnalyzerStats {
pid: self.pid,
module_count: self.modules.len(),
total_functions,
total_variables,
total_line_headers,
}
}
pub fn get_module_stats(&self) -> ModuleStats {
let mut total_symbols = 0;
let mut executable_modules = 0;
let mut library_modules = 0;
let mut modules_with_debug_info = 0;
for (module_path, module_data) in &self.modules {
let function_names = module_data.get_function_names();
total_symbols += function_names.len();
if !matches!(
module_data.get_debug_info_source(),
DebugInfoSource::Missing
) {
modules_with_debug_info += 1;
}
if self.is_main_executable_module(module_path) {
executable_modules += 1;
} else {
library_modules += 1;
}
}
ModuleStats {
total_modules: self.modules.len(),
executable_modules,
library_modules,
total_symbols,
modules_with_debug_info,
}
}
pub fn get_main_executable(&self) -> Option<MainExecutableInfo> {
for module_path in self.modules.keys() {
if self.is_main_executable_module(module_path) {
return Some(MainExecutableInfo {
path: module_path.to_string_lossy().to_string(),
});
}
}
None
}
fn is_main_executable_module(&self, module_path: &Path) -> bool {
let filename = module_path
.file_name()
.and_then(|name| name.to_str())
.unwrap_or("");
!filename.contains(".so") &&
!module_path.to_string_lossy().starts_with("/lib") &&
!module_path.to_string_lossy().starts_with("/usr/lib")
}
pub fn list_functions(&self) -> Vec<String> {
let mut all_functions = Vec::new();
for module_data in self.modules.values() {
let function_names = module_data.get_function_names();
for name in function_names {
all_functions.push(name.clone());
}
}
all_functions.sort();
all_functions.dedup();
tracing::debug!(
"Listed {} unique functions across {} modules",
all_functions.len(),
self.modules.len()
);
all_functions
}
pub fn lookup_functions_by_pattern(&self, pattern: &str) -> Vec<String> {
let all_functions = self.list_functions();
all_functions
.into_iter()
.filter(|name| name.contains(pattern))
.collect()
}
pub fn lookup_all_function_names(&self) -> Vec<String> {
self.list_functions()
}
pub fn get_pid(&self) -> u32 {
self.pid
}
pub fn get_shared_library_info(&self) -> Vec<SharedLibraryInfo> {
self.modules
.iter()
.filter(|(path, _)| self.is_shared_library(path))
.map(|(path, module_data)| {
let mapping = module_data.module_mapping();
let debug_file_path = module_data
.get_debug_file_path()
.map(|p| p.to_string_lossy().to_string());
SharedLibraryInfo {
from_address: mapping.loaded_address.unwrap_or(0),
to_address: mapping.loaded_address.map_or(0, |addr| addr + mapping.size),
symbols_read: !module_data.get_function_names().is_empty(),
debug_info_available: module_data.has_dwarf_info(),
library_path: path.to_string_lossy().to_string(),
size: mapping.size,
debug_file_path,
}
})
.collect()
}
pub fn get_executable_file_info(&self) -> Option<ExecutableFileInfo> {
let executable = self
.modules
.iter()
.find(|(path, _)| !self.is_shared_library(path))?;
let (exe_path, module_data) = executable;
let file_path = exe_path.to_string_lossy().to_string();
let file_bytes = std::fs::read(exe_path).ok()?;
let obj = object::File::parse(&file_bytes[..]).ok()?;
let file_type = match obj.format() {
object::BinaryFormat::Elf => {
if obj.is_64() {
"ELF 64-bit executable"
} else {
"ELF 32-bit executable"
}
}
_ => "Unknown format",
}
.to_string();
let has_symbols = !module_data.get_function_names().is_empty()
|| obj.symbols().count() > 0
|| obj.dynamic_symbols().count() > 0;
let has_debug_info = module_data.has_dwarf_info();
let debug_file_path = module_data.get_debug_file_path();
let load_bias = if self.pid != 0 {
module_data.module_mapping().loaded_address.unwrap_or(0)
} else {
0
};
let entry_point = Some(obj.entry() + load_bias);
let text_section = obj.section_by_name(".text").map(|section| {
let addr = section.address() + load_bias;
let size = section.size();
SectionInfo {
start_address: addr,
end_address: addr + size,
size,
}
});
let data_section = obj.section_by_name(".data").map(|section| {
let addr = section.address() + load_bias;
let size = section.size();
SectionInfo {
start_address: addr,
end_address: addr + size,
size,
}
});
let mode_description = if self.pid != 0 {
format!("Attached to process {} (PID mode)", self.pid)
} else {
"Static analysis mode (target file specified with -t)".to_string()
};
Some(ExecutableFileInfo {
file_path,
file_type,
entry_point,
has_symbols,
has_debug_info,
debug_file_path: debug_file_path.map(|p| p.to_string_lossy().to_string()),
text_section,
data_section,
mode_description,
})
}
fn is_shared_library(&self, module_path: &Path) -> bool {
let filename = module_path
.file_name()
.and_then(|name| name.to_str())
.unwrap_or("");
filename.contains(".so")
|| module_path.to_string_lossy().starts_with("/lib")
|| module_path.to_string_lossy().starts_with("/usr/lib")
}
pub fn get_grouped_file_info_by_module(&self) -> Result<Vec<(String, Vec<SimpleFileInfo>)>> {
let mut grouped = Vec::new();
for (module_path, module_data) in &self.modules {
let files = module_data.get_all_files();
if !files.is_empty() {
let simple_files: Vec<SimpleFileInfo> = files
.into_iter()
.map(|source_file| SimpleFileInfo {
full_path: source_file.full_path,
basename: source_file.filename,
directory: source_file.directory_path,
})
.collect();
grouped.push((module_path.to_string_lossy().to_string(), simple_files));
}
}
Ok(grouped)
}
}
#[derive(Debug, Clone)]
pub struct ModuleStats {
pub total_modules: usize,
pub executable_modules: usize,
pub library_modules: usize,
pub total_symbols: usize,
pub modules_with_debug_info: usize,
}
#[derive(Debug, Clone)]
pub struct MainExecutableInfo {
pub path: String,
}
#[derive(Debug, Clone)]
pub struct AnalyzerStats {
pub pid: u32,
pub module_count: usize,
pub total_functions: usize,
pub total_variables: usize,
pub total_line_headers: usize,
}
#[derive(Debug, Clone)]
pub struct SharedLibraryInfo {
pub from_address: u64, pub to_address: u64, pub symbols_read: bool, pub debug_info_available: bool, pub library_path: String, pub size: u64, pub debug_file_path: Option<String>, }
#[derive(Debug, Clone)]
pub struct ExecutableFileInfo {
pub file_path: String,
pub file_type: String,
pub entry_point: Option<u64>,
pub has_symbols: bool,
pub has_debug_info: bool,
pub debug_file_path: Option<String>,
pub text_section: Option<SectionInfo>,
pub data_section: Option<SectionInfo>,
pub mode_description: String,
}
#[derive(Debug, Clone)]
pub struct SectionInfo {
pub start_address: u64,
pub end_address: u64,
pub size: u64,
}
#[derive(Debug, Clone)]
pub struct SimpleFileInfo {
pub full_path: String,
pub basename: String,
pub directory: String,
}
#[cfg(test)]
mod tests {
use super::*;
fn global_plan(name: &str, address: u64) -> VariableReadPlan {
VariableReadPlan {
name: name.to_string(),
type_name: "int".to_string(),
access_path: crate::VariableAccessPath::default(),
module_path: None,
dwarf_type: Some(crate::TypeInfo::BaseType {
name: "int".to_string(),
size: 4,
encoding: gimli::constants::DW_ATE_signed.0 as u16,
}),
declaration: None,
type_id: None,
location: VariableLocation::Address(AddressExpr::constant(address)),
availability: Availability::Available,
scope_depth: 0,
is_parameter: false,
is_artificial: false,
pc_range: None,
inline_context: None,
provenance: Provenance::Synthesized {
detail: "test".to_string(),
},
}
}
fn visible_var(name: &str, scope_depth: usize) -> VisibleVariable {
VisibleVariable {
name: name.to_string(),
type_name: "int".to_string(),
dwarf_type: Some(crate::TypeInfo::BaseType {
name: "int".to_string(),
size: 4,
encoding: gimli::constants::DW_ATE_signed.0 as u16,
}),
declaration: None,
type_id: None,
location: VariableLocation::RegisterValue { dwarf_reg: 0 },
availability: Availability::Available,
scope_depth,
is_parameter: false,
is_artificial: false,
}
}
fn diagnostic(
name: &str,
scope_depth: usize,
detail: &str,
) -> crate::semantics::VariableQueryDiagnostic {
crate::semantics::VariableQueryDiagnostic {
pc: 0x1234,
name: Some(name.to_string()),
scope_depth,
availability: Availability::Unsupported(crate::UnsupportedReason::ExpressionShape {
detail: detail.to_string(),
}),
detail: detail.to_string(),
}
}
#[test]
fn variable_selection_rejects_inner_diagnostic_over_outer_match() {
let err = DwarfAnalyzer::select_visible_variable_by_name(
0x1234,
"state",
vec![visible_var("state", 1)],
&[diagnostic("state", 2, "DW_OP_bad is unsupported")],
)
.expect_err("inner unavailable variable should block outer fallback");
assert!(err.to_string().contains("Unavailable variable 'state'"));
assert!(err.to_string().contains("DW_OP_bad is unsupported"));
}
#[test]
fn variable_selection_keeps_inner_match_over_outer_diagnostic() {
let selected = DwarfAnalyzer::select_visible_variable_by_name(
0x1234,
"state",
vec![visible_var("state", 2)],
&[diagnostic("state", 1, "outer variable is unavailable")],
)
.expect("outer diagnostic should not block inner match")
.expect("inner match should be returned");
assert_eq!(selected.name, "state");
assert_eq!(selected.scope_depth, 2);
}
#[test]
fn global_plan_selection_rejects_ambiguous_matches() {
let err = DwarfAnalyzer::select_unambiguous_global_plan(
"state",
vec![
(PathBuf::from("/tmp/a"), global_plan("state", 0x1000)),
(PathBuf::from("/tmp/b"), global_plan("state", 0x2000)),
],
)
.expect_err("multiple global candidates should be ambiguous");
assert!(err.to_string().contains("Ambiguous global 'state'"));
assert!(err.to_string().contains("2 matches"));
}
#[test]
fn global_plan_selection_accepts_single_match() {
let selected = DwarfAnalyzer::select_unambiguous_global_plan(
"state",
vec![(PathBuf::from("/tmp/a"), global_plan("state", 0x1000))],
)
.expect("single global candidate should be accepted")
.expect("single global candidate should be returned");
assert_eq!(selected.0, PathBuf::from("/tmp/a"));
assert_eq!(selected.1.name, "state");
}
#[test]
fn global_plan_selection_prefers_current_module_match() {
let selected = DwarfAnalyzer::select_global_plan_with_preferred_module(
"state",
Path::new("/tmp/current"),
vec![
(PathBuf::from("/tmp/other"), global_plan("state", 0x2000)),
(PathBuf::from("/tmp/current"), global_plan("state", 0x1000)),
],
)
.expect("current module candidate should be accepted")
.expect("current module candidate should be returned");
assert_eq!(selected.0, PathBuf::from("/tmp/current"));
assert_eq!(
selected.1.location,
VariableLocation::Address(AddressExpr::constant(0x1000))
);
}
#[test]
fn global_plan_selection_rejects_ambiguous_current_module_matches() {
let err = DwarfAnalyzer::select_global_plan_with_preferred_module(
"state",
Path::new("/tmp/current"),
vec![
(PathBuf::from("/tmp/current"), global_plan("state", 0x1000)),
(PathBuf::from("/tmp/current"), global_plan("state", 0x1004)),
(PathBuf::from("/tmp/other"), global_plan("state", 0x2000)),
],
)
.expect_err("duplicate current-module candidates should be ambiguous");
assert!(err.to_string().contains("Ambiguous global 'state'"));
assert!(err.to_string().contains("2 matches"));
assert!(err.to_string().contains("/tmp/current"));
assert!(!err.to_string().contains("/tmp/other"));
}
}