diff --git a/ghostscope-dwarf/src/analyzer/mod.rs b/ghostscope-dwarf/src/analyzer/mod.rs index 0c31d0d1..2a275ba4 100644 --- a/ghostscope-dwarf/src/analyzer/mod.rs +++ b/ghostscope-dwarf/src/analyzer/mod.rs @@ -50,6 +50,9 @@ pub struct DwarfAnalyzer { pid: u32, /// Module path -> module data mapping modules: HashMap, + /// The explicit target, or the loaded module identified by /proc/PID/exe. + /// Keep this identity stable as shared libraries are discovered later. + main_module: Option, /// Bounded ELF symbol indexes for runtime modules loaded without full DWARF. /// Cookies preserve reusable module identity across paths and PID roots. runtime_text_symbols: HashMap>, @@ -465,6 +468,7 @@ impl DwarfAnalyzer { } if new_runtime_modules.is_empty() { + self.resolve_main_module_if_missing(); return Ok(0); } @@ -501,6 +505,7 @@ impl DwarfAnalyzer { let module_path = module.module_path().clone(); self.modules.insert(module_path, module); } + self.resolve_main_module_if_missing(); if loaded_count > 0 { self.clear_pc_context_cache(); @@ -704,6 +709,7 @@ impl DwarfAnalyzer { let mut analyzer = Self { pid: 0, // No specific PID in exec mode modules: HashMap::new(), + main_module: Some(exec_path.clone()), runtime_text_symbols: HashMap::new(), pc_context_cache: RwLock::new(PcContextCache::default()), }; @@ -790,6 +796,7 @@ impl DwarfAnalyzer { let mut analyzer = Self { pid, modules: HashMap::new(), + main_module: None, runtime_text_symbols: HashMap::new(), pc_context_cache: RwLock::new(PcContextCache::default()), }; @@ -799,6 +806,8 @@ impl DwarfAnalyzer { analyzer.modules.insert(module_path, module); } + analyzer.resolve_main_module_if_missing(); + tracing::info!( "Created DWARF analyzer for PID {} with {} pre-loaded modules", pid, @@ -808,6 +817,16 @@ impl DwarfAnalyzer { analyzer } + /// Retry unresolved PID identity while preserving an already selected target. + fn resolve_main_module_if_missing(&mut self) { + if self.pid == 0 || self.main_module.is_some() { + return; + } + + let executable = PathBuf::from(format!("/proc/{}/exe", self.pid)); + self.main_module = self.loaded_module_path_for(&executable).cloned(); + } + fn clear_pc_context_cache(&self) { if let Ok(mut cache) = self.pc_context_cache.write() { *cache = PcContextCache::default(); @@ -1273,32 +1292,16 @@ impl DwarfAnalyzer { } } - /// Get main executable module information + /// Get the process executable, or the explicitly selected target in -t mode. pub fn get_main_executable(&self) -> Option { - // Find the main executable module (usually the first non-library module) - 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 + self.main_module.as_ref().map(|path| MainExecutableInfo { + path: path.to_string_lossy().to_string(), + }) } - /// Check if a module is the main executable (not a shared library) + /// Check the identity captured at construction, without guessing from names. fn is_main_executable_module(&self, module_path: &Path) -> bool { - // Heuristic: main executable usually doesn't have .so extension and contains the process name - let filename = module_path - .file_name() - .and_then(|name| name.to_str()) - .unwrap_or(""); - - // Not a shared library - !filename.contains(".so") && - // Not a system library path - !module_path.to_string_lossy().starts_with("/lib") && - !module_path.to_string_lossy().starts_with("/usr/lib") + self.main_module.as_deref() == Some(module_path) } /// Get list of all function names across all modules @@ -1371,13 +1374,8 @@ impl DwarfAnalyzer { /// Get executable file information (for "info file" command) pub fn get_executable_file_info(&self) -> Option { - // Find the primary executable (not a shared library) - let executable = self - .modules - .iter() - .find(|(path, _)| !self.is_shared_library(path))?; - - let (exe_path, module_data) = executable; + let exe_path = self.main_module.as_ref()?; + let module_data = self.modules.get(exe_path)?; let file_path = exe_path.to_string_lossy().to_string(); // Parse the ELF file to get detailed information @@ -1411,7 +1409,7 @@ impl DwarfAnalyzer { // Load bias for PID mode from module mapping (if available) let load_bias = if self.pid != 0 { - module_data.module_mapping().loaded_address.unwrap_or(0) + module_data.module_mapping().load_bias.unwrap_or(0) } else { 0 }; @@ -1463,17 +1461,9 @@ impl DwarfAnalyzer { // NOTE: Runtime section offsets are handled by ghostscope-coordinator. - /// Check if a module is a shared library + /// Modules other than the selected target are displayed as dependencies. fn is_shared_library(&self, module_path: &Path) -> bool { - let filename = module_path - .file_name() - .and_then(|name| name.to_str()) - .unwrap_or(""); - - // Shared libraries typically have .so extension or contain .so - filename.contains(".so") - || module_path.to_string_lossy().starts_with("/lib") - || module_path.to_string_lossy().starts_with("/usr/lib") + !self.is_main_executable_module(module_path) } /// Get grouped file info by module (compatibility method) diff --git a/ghostscope-dwarf/tests/main_module_identity.rs b/ghostscope-dwarf/tests/main_module_identity.rs new file mode 100644 index 00000000..25bcf68d --- /dev/null +++ b/ghostscope-dwarf/tests/main_module_identity.rs @@ -0,0 +1,161 @@ +use ghostscope_dwarf::{DwarfAnalyzer, ModuleDefaultPolicy, ModuleLoadingEvent}; +use object::Object; +use std::io::{BufRead, BufReader}; +use std::path::Path; +use std::process::{Command, Stdio}; + +struct Target(std::process::Child); + +impl Drop for Target { + fn drop(&mut self) { + let _ = self.0.kill(); + let _ = self.0.wait(); + } +} + +fn compile_target(directory: &Path, name: &str, shared: bool) -> std::path::PathBuf { + let source = directory.join("target.c"); + std::fs::write( + &source, + "#include \nint value = 7;\nint main(void) { write(STDOUT_FILENO, \"ready\\n\", 6); for (;;) pause(); }\n", + ) + .unwrap(); + let binary = directory.join(name); + let mut compiler = Command::new("cc"); + compiler.args(["-g", "-O0"]); + if shared { + compiler.args(["-shared", "-fPIC"]); + } else { + compiler.arg("-no-pie"); + } + let output = compiler + .arg(&source) + .arg("-o") + .arg(&binary) + .output() + .unwrap(); + assert!( + output.status.success(), + "{}", + String::from_utf8_lossy(&output.stderr) + ); + binary +} + +fn start_target(binary: &Path) -> Target { + let mut target = Target( + Command::new(binary) + .stdout(Stdio::piped()) + .stderr(Stdio::null()) + .spawn() + .unwrap(), + ); + let mut ready = String::new(); + BufReader::new(target.0.stdout.take().unwrap()) + .read_line(&mut ready) + .unwrap(); + assert_eq!( + ready, "ready\n", + "target should finish startup before discovery" + ); + target +} + +fn assert_default_module(analyzer: &DwarfAnalyzer, binary: &Path) { + assert_eq!( + Path::new(&analyzer.get_main_executable().unwrap().path), + binary + ); + assert_eq!( + analyzer + .resolve_address_module(None, None, ModuleDefaultPolicy::MainExecutableOnly) + .unwrap(), + binary + ); + assert!(analyzer.get_executable_file_info().is_some()); + assert!(analyzer + .get_shared_library_info() + .iter() + .all(|module| Path::new(&module.library_path) != binary)); +} + +#[tokio::test] +async fn explicit_target_identity_does_not_depend_on_filename() { + let directory = tempfile::tempdir().unwrap(); + for (name, shared) in [("worker.software", false), ("plugin.so", true)] { + let binary = compile_target(directory.path(), name, shared); + let analyzer = DwarfAnalyzer::from_exec_path(&binary).await.unwrap(); + assert_default_module(&analyzer, &binary); + } +} + +#[tokio::test] +async fn pid_default_module_comes_from_the_process_executable() { + let directory = tempfile::tempdir().unwrap(); + let binary = compile_target(directory.path(), "worker.software", false); + let target = start_target(&binary); + let analyzer = DwarfAnalyzer::from_pid(target.0.id()).await.unwrap(); + assert_default_module(&analyzer, &binary); + assert_eq!(analyzer.get_module_stats().executable_modules, 1); + let bytes = std::fs::read(&binary).unwrap(); + let elf = object::File::parse(bytes.as_slice()).unwrap(); + assert_eq!( + analyzer.get_executable_file_info().unwrap().entry_point, + Some(elf.entry()), + "a non-PIE executable has zero load bias, despite its nonzero mapping base" + ); +} + +#[tokio::test] +async fn pid_default_module_recovers_after_its_path_is_restored() { + for remove_before_load in [true, false] { + let directory = tempfile::tempdir().unwrap(); + let binary = compile_target(directory.path(), "worker.software", false); + let backup = directory.path().join("backup"); + std::fs::hard_link(&binary, &backup).unwrap(); + let target = start_target(&binary); + + // Remove the path either before discovery or just after loading its ELF. + // Both leave main identity unresolved, but only the first needs a new module. + if remove_before_load { + std::fs::remove_file(&binary).unwrap(); + } + let callback_binary = binary.clone(); + let mut analyzer = + DwarfAnalyzer::from_pid_parallel_with_progress(target.0.id(), move |event| { + if let ModuleLoadingEvent::LoadingCompleted { module_path, .. } = event { + if !remove_before_load && Path::new(&module_path) == callback_binary { + std::fs::remove_file(&callback_binary).unwrap(); + } + } + }) + .await + .unwrap(); + assert!(analyzer.get_main_executable().is_none()); + assert_eq!( + analyzer.module_paths().contains(&binary), + !remove_before_load + ); + + // Restore the same inode while the original process continues running. + std::fs::hard_link(&backup, &binary).unwrap(); + assert!(DwarfAnalyzer::module_paths_equivalent( + &binary, + format!("/proc/{}/exe", target.0.id()) + )); + let runtime_modules = DwarfAnalyzer::discover_pid_runtime_modules(target.0.id()).unwrap(); + let loaded = analyzer + .refresh_pid_runtime_modules_with_config_and_debuginfod( + runtime_modules, + &[], + false, + None, + |_| {}, + ) + .await + .unwrap(); + assert_eq!(loaded, usize::from(remove_before_load)); + assert_default_module(&analyzer, &binary); + assert_eq!(analyzer.get_module_stats().executable_modules, 1); + } +}