ghostscope_dwarf/
analyzer.rs

1//! Main DWARF analyzer - unified entry point for all DWARF operations
2
3use crate::{
4    core::{mapping::ModuleMapping, GlobalVariableInfo, ModuleAddress, Result, SourceLocation},
5    module::ModuleData,
6};
7use object::{Object, ObjectSection};
8use std::collections::HashMap;
9use std::path::{Path, PathBuf};
10
11/// Events emitted during module loading process
12#[derive(Debug, Clone)]
13pub enum ModuleLoadingEvent {
14    /// Module discovered during process scanning
15    Discovered {
16        module_path: String,
17        current: usize,
18        total: usize,
19    },
20    /// Module loading started
21    LoadingStarted {
22        module_path: String,
23        current: usize,
24        total: usize,
25    },
26    /// Module loading completed successfully
27    LoadingCompleted {
28        module_path: String,
29        stats: ModuleLoadingStats,
30        current: usize,
31        total: usize,
32    },
33    /// Module loading failed
34    LoadingFailed {
35        module_path: String,
36        error: String,
37        current: usize,
38        total: usize,
39    },
40}
41
42/// Statistics for a loaded module
43#[derive(Debug, Clone)]
44pub struct ModuleLoadingStats {
45    pub functions: usize,
46    pub variables: usize,
47    pub types: usize,
48    pub load_time_ms: u64,
49}
50
51/// DWARF analyzer - unified entry point for all DWARF analysis
52#[derive(Debug)]
53pub struct DwarfAnalyzer {
54    /// Process ID
55    pid: u32,
56    /// Module path -> module data mapping
57    modules: HashMap<PathBuf, ModuleData>,
58}
59
60impl DwarfAnalyzer {
61    /// Create DWARF analyzer from PID (now uses parallel loading)
62    pub async fn from_pid(pid: u32) -> Result<Self> {
63        Self::from_pid_parallel(pid).await
64    }
65
66    /// Classify whether an address is inside an inlined subroutine instance
67    /// Returns Some(true) if inline, Some(false) if a normal (non-inline) context,
68    /// or None if the module/address cannot be resolved.
69    pub fn is_inline_at(&mut self, module_address: &ModuleAddress) -> Option<bool> {
70        if let Some(module_data) = self.modules.get_mut(&module_address.module_path) {
71            module_data.is_inline_at(module_address.address)
72        } else {
73            None
74        }
75    }
76
77    /// Resolve struct/class by name (shallow) in a specific module using only indexes
78    pub fn resolve_struct_type_shallow_by_name_in_module<P: AsRef<Path>>(
79        &mut self,
80        module_path: P,
81        name: &str,
82    ) -> Option<crate::TypeInfo> {
83        let path_buf = module_path.as_ref().to_path_buf();
84        if let Some(module_data) = self.modules.get_mut(&path_buf) {
85            return module_data.resolve_struct_type_shallow_by_name(name);
86        }
87        None
88    }
89
90    /// Resolve struct/class by name (shallow) across modules (first match)
91    pub fn resolve_struct_type_shallow_by_name(&mut self, name: &str) -> Option<crate::TypeInfo> {
92        for module_data in self.modules.values_mut() {
93            if let Some(t) = module_data.resolve_struct_type_shallow_by_name(name) {
94                return Some(t);
95            }
96        }
97        None
98    }
99
100    /// Resolve union by name (shallow) in a specific module
101    pub fn resolve_union_type_shallow_by_name_in_module<P: AsRef<Path>>(
102        &mut self,
103        module_path: P,
104        name: &str,
105    ) -> Option<crate::TypeInfo> {
106        let path_buf = module_path.as_ref().to_path_buf();
107        if let Some(module_data) = self.modules.get_mut(&path_buf) {
108            return module_data.resolve_union_type_shallow_by_name(name);
109        }
110        None
111    }
112
113    /// Resolve union by name (shallow) across modules (first match)
114    pub fn resolve_union_type_shallow_by_name(&mut self, name: &str) -> Option<crate::TypeInfo> {
115        for module_data in self.modules.values_mut() {
116            if let Some(t) = module_data.resolve_union_type_shallow_by_name(name) {
117                return Some(t);
118            }
119        }
120        None
121    }
122
123    /// Resolve enum by name (shallow) in a specific module
124    pub fn resolve_enum_type_shallow_by_name_in_module<P: AsRef<Path>>(
125        &mut self,
126        module_path: P,
127        name: &str,
128    ) -> Option<crate::TypeInfo> {
129        let path_buf = module_path.as_ref().to_path_buf();
130        if let Some(module_data) = self.modules.get_mut(&path_buf) {
131            return module_data.resolve_enum_type_shallow_by_name(name);
132        }
133        None
134    }
135
136    /// Resolve enum by name (shallow) across modules (first match)
137    pub fn resolve_enum_type_shallow_by_name(&mut self, name: &str) -> Option<crate::TypeInfo> {
138        for module_data in self.modules.values_mut() {
139            if let Some(t) = module_data.resolve_enum_type_shallow_by_name(name) {
140                return Some(t);
141            }
142        }
143        None
144    }
145
146    /// Create DWARF analyzer from PID using parallel loading
147    pub async fn from_pid_parallel(pid: u32) -> Result<Self> {
148        Self::from_pid_parallel_with_config(pid, &[], false, |_event| {}).await
149    }
150
151    /// Create DWARF analyzer from PID using parallel loading with progress callback
152    pub async fn from_pid_parallel_with_progress<F>(pid: u32, progress_callback: F) -> Result<Self>
153    where
154        F: Fn(ModuleLoadingEvent) + Send + Sync + 'static,
155    {
156        Self::from_pid_parallel_with_config(pid, &[], false, progress_callback).await
157    }
158
159    /// Create DWARF analyzer from PID using parallel loading with debug search paths and progress callback
160    pub async fn from_pid_parallel_with_config<F>(
161        pid: u32,
162        debug_search_paths: &[String],
163        allow_loose_debug_match: bool,
164        progress_callback: F,
165    ) -> Result<Self>
166    where
167        F: Fn(ModuleLoadingEvent) + Send + Sync + 'static,
168    {
169        tracing::info!("Creating DWARF analyzer for PID {} (parallel)", pid);
170
171        // Discover all modules for this process using coordinator
172        let mut coord = ghostscope_process::ProcessManager::new();
173        coord.ensure_prefill_pid(pid)?;
174        let mut module_mappings: Vec<crate::core::mapping::ModuleMapping> = Vec::new();
175        if let Some(entries) = coord.cached_offsets_with_paths_for_pid(pid) {
176            use std::collections::HashSet;
177            let mut seen = HashSet::new();
178            for e in entries {
179                if seen.insert(e.module_path.clone()) {
180                    let mut mm = crate::core::mapping::ModuleMapping::from_path(
181                        std::path::PathBuf::from(&e.module_path),
182                    );
183                    mm.loaded_address = Some(e.base);
184                    mm.size = e.size;
185                    module_mappings.push(mm);
186                }
187            }
188        }
189
190        tracing::info!(
191            "Discovered {} modules for PID {}",
192            module_mappings.len(),
193            pid
194        );
195
196        // Notify discovery completion
197        for (index, mapping) in module_mappings.iter().enumerate() {
198            progress_callback(ModuleLoadingEvent::Discovered {
199                module_path: mapping.path.to_string_lossy().to_string(),
200                current: index + 1,
201                total: module_mappings.len(),
202            });
203        }
204
205        // Load all modules in parallel with progress tracking
206        let mut loader = crate::loader::ModuleLoader::new(module_mappings).parallel();
207
208        // Configure debug search paths if provided
209        if !debug_search_paths.is_empty() {
210            loader = loader.with_debug_search_paths(debug_search_paths.to_vec());
211        }
212        loader = loader.with_loose_debug_match(allow_loose_debug_match);
213
214        let modules = loader
215            .with_progress_callback(progress_callback)
216            .load()
217            .await?;
218
219        tracing::info!(
220            "Created DWARF analyzer for PID {} with {} modules (parallel)",
221            pid,
222            modules.len()
223        );
224
225        Ok(Self::from_modules(pid, modules))
226    }
227
228    /// Create DWARF analyzer from executable path (single module mode, now async parallel)
229    pub async fn from_exec_path<P: AsRef<std::path::Path>>(exec_path: P) -> Result<Self> {
230        Self::from_exec_path_with_config(exec_path, &[], false).await
231    }
232
233    /// Create DWARF analyzer from executable path with debug search paths
234    pub async fn from_exec_path_with_config<P: AsRef<std::path::Path>>(
235        exec_path: P,
236        debug_search_paths: &[String],
237        allow_loose_debug_match: bool,
238    ) -> Result<Self> {
239        let exec_path = exec_path.as_ref().to_path_buf();
240        tracing::info!(
241            "Creating DWARF analyzer for executable: {}",
242            exec_path.display()
243        );
244
245        let mut analyzer = Self {
246            pid: 0, // No specific PID in exec mode
247            modules: HashMap::new(),
248        };
249
250        // Create a single module mapping for the executable
251        // No loaded address since we're not analyzing a running process
252        let module_mapping = ModuleMapping {
253            path: exec_path.clone(),
254            loaded_address: None, // No process mapping in exec path mode
255            size: 0,              // Will be determined from file size if needed
256        };
257
258        // Load the single module using parallel loading
259        match ModuleData::load_parallel(module_mapping, debug_search_paths, allow_loose_debug_match)
260            .await
261        {
262            Ok(module_data) => {
263                analyzer.modules.insert(exec_path.clone(), module_data);
264                tracing::info!(
265                    "Created DWARF analyzer for executable {} with 1 module",
266                    exec_path.display()
267                );
268            }
269            Err(e) => {
270                return Err(crate::DwarfError::ModuleLoadError(format!(
271                    "Failed to load executable {}: {}",
272                    exec_path.display(),
273                    e
274                ))
275                .into());
276            }
277        }
278
279        Ok(analyzer)
280    }
281
282    /// Create analyzer from pre-loaded modules (for Builder pattern)
283    pub(crate) fn from_modules(pid: u32, modules: Vec<ModuleData>) -> Self {
284        let mut analyzer = Self {
285            pid,
286            modules: HashMap::new(),
287        };
288
289        for module in modules {
290            let module_path = module.module_path().clone();
291            analyzer.modules.insert(module_path, module);
292        }
293
294        tracing::info!(
295            "Created DWARF analyzer for PID {} with {} pre-loaded modules",
296            pid,
297            analyzer.modules.len()
298        );
299
300        analyzer
301    }
302
303    /// Lookup function addresses across all modules
304    /// Returns: Vec<ModuleAddress> - one for each address where the function is found
305    pub fn lookup_function_addresses(&self, name: &str) -> Vec<ModuleAddress> {
306        let mut results = Vec::new();
307
308        for (module_path, module_data) in &self.modules {
309            let addresses = module_data.lookup_function_addresses_any(name);
310
311            // Create a ModuleAddress for each address found in this module
312            for address in addresses {
313                tracing::debug!(
314                    "Function '{}' found in module {} at address: 0x{:x}",
315                    name,
316                    module_path.display(),
317                    address
318                );
319                results.push(ModuleAddress::new(module_path.clone(), address));
320            }
321        }
322
323        // Deterministic ordering: module path asc, then address asc
324        results.sort_by(|a, b| {
325            let pa = a.module_path.to_string_lossy();
326            let pb = b.module_path.to_string_lossy();
327            match pa.cmp(&pb) {
328                std::cmp::Ordering::Equal => a.address.cmp(&b.address),
329                other => other,
330            }
331        });
332        results
333    }
334
335    /// Convert a module-relative virtual address (DWARF PC) to an ELF file offset
336    /// Returns None if the module is unknown or the address is not within a PT_LOAD segment
337    pub fn vaddr_to_file_offset<P: AsRef<std::path::Path>>(
338        &self,
339        module_path: P,
340        vaddr: u64,
341    ) -> Option<u64> {
342        let path_buf = module_path.as_ref().to_path_buf();
343        if let Some(module_data) = self.modules.get(&path_buf) {
344            module_data.vaddr_to_file_offset(vaddr)
345        } else {
346            None
347        }
348    }
349
350    /// Get all variables visible at the given module address with EvaluationResult
351    ///
352    /// # Arguments
353    /// * `module_address` - Module address containing both module path and address offset
354    pub fn get_all_variables_at_address(
355        &mut self,
356        module_address: &ModuleAddress,
357    ) -> Result<Vec<crate::data::VariableWithEvaluation>> {
358        tracing::info!(
359            "Looking up variables at address 0x{:x} in module {}",
360            module_address.address,
361            module_address.module_display()
362        );
363
364        if let Some(module_data) = self.modules.get_mut(&module_address.module_path) {
365            module_data.get_all_variables_at_address(module_address.address)
366        } else {
367            tracing::warn!(
368                "Module {} not found in loaded modules",
369                module_address.module_display()
370            );
371            Err(anyhow::anyhow!(
372                "Module {} not loaded",
373                module_address.module_display()
374            ))
375        }
376    }
377
378    /// Plan a chain access (e.g., r.headers_in) and synthesize a VariableWithEvaluation
379    pub fn plan_chain_access(
380        &mut self,
381        module_address: &ModuleAddress,
382        base_var: &str,
383        chain: &[String],
384    ) -> Result<Option<crate::data::VariableWithEvaluation>> {
385        if let Some(module_data) = self.modules.get_mut(&module_address.module_path) {
386            module_data.plan_chain_access(module_address.address, base_var, chain)
387        } else {
388            Ok(None)
389        }
390    }
391
392    /// Get all loaded module paths
393    pub fn get_loaded_modules(&self) -> Vec<&PathBuf> {
394        self.modules.keys().collect()
395    }
396
397    /// Find global/static variables by name across all loaded modules
398    pub fn find_global_variables_by_name(&self, name: &str) -> Vec<(PathBuf, GlobalVariableInfo)> {
399        let mut results = Vec::new();
400        for (module_path, module_data) in &self.modules {
401            let vars = module_data.find_global_variables_by_name_any(name);
402            for v in vars {
403                results.push((module_path.clone(), v));
404            }
405        }
406        if !results.is_empty() {
407            return results;
408        }
409
410        // Fallback: scan all globals in each module and match by exact or leaf name
411        for (module_path, module_data) in &self.modules {
412            let all = module_data.list_all_global_variables();
413            for v in all {
414                let leaf = v.name.rsplit("::").next().unwrap_or(&v.name).to_string();
415                if v.name == name || leaf == name {
416                    results.push((module_path.clone(), v));
417                }
418            }
419        }
420
421        results
422    }
423
424    /// Plan a member/chain access across modules focusing on global/static variables.
425    /// Strict policy and order:
426    /// 1) Query globals index by base name (prefer current module first).
427    /// 2) For each candidate: try static-offset lowering when link-time address exists.
428    /// 3) Fallback to per-module planner at addr=0.
429    ///
430    ///    Returns None if unresolved; never falls back to unrelated globals.
431    pub fn plan_global_chain_access(
432        &mut self,
433        prefer_module: &PathBuf,
434        base: &str,
435        fields: &[String],
436    ) -> Result<Option<(PathBuf, crate::data::VariableWithEvaluation)>> {
437        // 1) Globals across modules (strict)
438        let matches = self.find_global_variables_by_name(base);
439        if matches.is_empty() {
440            // Strict policy: if no global/base by name exists anywhere, stop here
441            return Ok(None);
442        }
443
444        // Build preferred order: prefer current module first
445        let mut ordered: Vec<(PathBuf, GlobalVariableInfo)> = Vec::new();
446        for (mpath, info) in matches.iter() {
447            if *mpath == *prefer_module {
448                ordered.push((mpath.clone(), info.clone()));
449            }
450        }
451        for (mpath, info) in matches.into_iter() {
452            if mpath != *prefer_module {
453                ordered.push((mpath, info));
454            }
455        }
456
457        for (mpath, info) in ordered.into_iter() {
458            // 2a) Static-offset lowering when link-time address is available
459            if let Some(link) = info.link_address {
460                if let Ok(Some((off, final_ty))) = self.compute_global_member_static_offset(
461                    &mpath,
462                    link,
463                    info.unit_offset,
464                    info.die_offset,
465                    fields,
466                ) {
467                    let name = if fields.is_empty() {
468                        base.to_string()
469                    } else {
470                        format!("{base}.{}", fields.join("."))
471                    };
472                    let var = crate::data::VariableWithEvaluation {
473                        name,
474                        type_name: final_ty.type_name(),
475                        dwarf_type: Some(final_ty),
476                        evaluation_result: crate::core::EvaluationResult::MemoryLocation(
477                            crate::core::LocationResult::Address(link + off),
478                        ),
479                        scope_depth: 0,
480                        is_parameter: false,
481                        is_artificial: false,
482                    };
483                    tracing::info!(
484                        "plan_global_chain_access: resolved '{}' in module '{}' via static-offset",
485                        base,
486                        mpath.display()
487                    );
488                    return Ok(Some((mpath, var)));
489                }
490            }
491
492            // 2b) Module planner fallback at addr=0
493            let ma = ModuleAddress::new(mpath.clone(), 0);
494            match self.plan_chain_access(&ma, base, fields) {
495                Ok(Some(v)) => {
496                    tracing::info!(
497                        "plan_global_chain_access: resolved '{}' in module '{}' via planner",
498                        base,
499                        ma.module_display()
500                    );
501                    return Ok(Some((mpath, v)));
502                }
503                Ok(None) => {}
504                Err(e) => {
505                    tracing::debug!(
506                        "plan_global_chain_access: planner miss in module '{}': {}",
507                        ma.module_display(),
508                        e
509                    );
510                }
511            }
512        }
513
514        Ok(None)
515    }
516
517    /// Resolve a variable by CU/DIE offsets in a specific module at an arbitrary address context (for globals)
518    pub fn resolve_variable_by_offsets_in_module<P: AsRef<Path>>(
519        &mut self,
520        module_path: P,
521        cu_off: gimli::DebugInfoOffset,
522        die_off: gimli::UnitOffset,
523    ) -> Result<crate::data::VariableWithEvaluation> {
524        let path_buf = module_path.as_ref().to_path_buf();
525        if let Some(module_data) = self.modules.get_mut(&path_buf) {
526            let items = vec![(cu_off, die_off)];
527            let vars = module_data.resolve_variables_by_offsets_at_address(0, &items)?;
528            let mut var = vars.into_iter().next().ok_or_else(|| {
529                anyhow::anyhow!(
530                    "Failed to resolve variable at offsets {:?}/{:?} in module {}",
531                    cu_off,
532                    die_off,
533                    path_buf.display()
534                )
535            })?;
536            if var.dwarf_type.is_none() {
537                if let Some(ti) = module_data.shallow_type_for_variable_offsets(cu_off, die_off) {
538                    var.type_name = ti.type_name();
539                    var.dwarf_type = Some(ti);
540                }
541            }
542            Ok(var)
543        } else {
544            Err(anyhow::anyhow!(
545                "Module {} not loaded",
546                module_path.as_ref().display()
547            ))
548        }
549    }
550
551    /// List all global/static variables with usable addresses across all loaded modules
552    pub fn list_all_global_variables(&self) -> Vec<(PathBuf, GlobalVariableInfo)> {
553        let mut results = Vec::new();
554        for (module_path, module_data) in &self.modules {
555            for v in module_data.list_all_global_variables() {
556                results.push((module_path.clone(), v));
557            }
558        }
559        results
560    }
561
562    /// Classify the section type for a link-time virtual address in a specific module
563    pub fn classify_section_for_address<P: AsRef<Path>>(
564        &self,
565        module_path: P,
566        vaddr: u64,
567    ) -> Option<crate::core::SectionType> {
568        let path = module_path.as_ref();
569        if let Some(module_data) = self.modules.get(path) {
570            module_data.classify_section_for_vaddr(vaddr)
571        } else {
572            None
573        }
574    }
575
576    /// Compute static offset for a global variable member chain
577    pub fn compute_global_member_static_offset<P: AsRef<Path>>(
578        &mut self,
579        module_path: P,
580        link_address: u64,
581        cu_off: gimli::DebugInfoOffset,
582        var_die: gimli::UnitOffset,
583        fields: &[String],
584    ) -> Result<Option<(u64, crate::TypeInfo)>> {
585        let path_buf = module_path.as_ref().to_path_buf();
586        if let Some(module_data) = self.modules.get_mut(&path_buf) {
587            module_data.compute_global_member_static_offset(cu_off, var_die, link_address, fields)
588        } else {
589            Err(anyhow::anyhow!(
590                "Module {} not loaded",
591                module_path.as_ref().display()
592            ))
593        }
594    }
595
596    /// Lookup function address by name - returns first match
597    /// Returns ModuleAddress for the first function found
598    pub fn lookup_function_address_by_name(&self, function_name: &str) -> Option<ModuleAddress> {
599        let module_addresses = self.lookup_function_addresses(function_name);
600
601        if let Some(first_module_address) = module_addresses.first() {
602            tracing::info!(
603                "Found function '{}' in module '{}' at address 0x{:x}",
604                function_name,
605                first_module_address.module_display(),
606                first_module_address.address
607            );
608            Some(first_module_address.clone())
609        } else {
610            tracing::warn!("Function '{}' not found in any module", function_name);
611            None
612        }
613    }
614
615    /// Lookup source location by module address
616    /// Returns source location for the given module address
617    pub fn lookup_source_location(
618        &mut self,
619        module_address: &ModuleAddress,
620    ) -> Option<SourceLocation> {
621        if let Some(module_data) = self.modules.get_mut(&module_address.module_path) {
622            module_data.lookup_source_location(module_address.address)
623        } else {
624            tracing::warn!("Module {} not found", module_address.module_display());
625            None
626        }
627    }
628
629    /// Lookup addresses by source line (cross-module)
630    /// Returns: Vec<ModuleAddress> for all matches
631    pub fn lookup_addresses_by_source_line(
632        &self,
633        file_path: &str,
634        line_number: u32,
635    ) -> Vec<ModuleAddress> {
636        let mut results = Vec::new();
637
638        // Check each module for this source:line combination
639        for (module_path, module_data) in &self.modules {
640            let addresses = module_data.lookup_addresses_by_source_line(file_path, line_number);
641
642            // Add all addresses from this module
643            for address in addresses {
644                results.push(ModuleAddress::new(module_path.clone(), address));
645            }
646        }
647
648        if !results.is_empty() {
649            tracing::info!(
650                "Found {} addresses for {}:{} across {} modules",
651                results.len(),
652                file_path,
653                line_number,
654                self.modules.len()
655            );
656        }
657
658        results.sort_by(|a, b| {
659            let pa = a.module_path.to_string_lossy();
660            let pb = b.module_path.to_string_lossy();
661            match pa.cmp(&pb) {
662                std::cmp::Ordering::Equal => a.address.cmp(&b.address),
663                other => other,
664            }
665        });
666        results
667    }
668
669    /// Get all function names (cross-module)
670    pub fn get_all_function_names(&self) -> Vec<String> {
671        let mut all_names = std::collections::HashSet::new();
672        for module_data in self.modules.values() {
673            for name in module_data.get_function_names() {
674                all_names.insert(name.clone());
675            }
676        }
677        all_names.into_iter().collect()
678    }
679
680    /// Get statistics for debugging
681    pub fn get_stats(&self) -> AnalyzerStats {
682        let mut total_functions = 0;
683        let mut total_variables = 0;
684        let mut total_line_headers = 0;
685        let mut total_cache_entries = 0;
686
687        for module_data in self.modules.values() {
688            total_functions += module_data.get_function_names().len();
689            total_variables += module_data.get_variable_names().len();
690            total_line_headers += module_data.get_line_header_count();
691            let (cache_entries, _) = module_data.get_cache_stats();
692            total_cache_entries += cache_entries;
693        }
694
695        AnalyzerStats {
696            pid: self.pid,
697            module_count: self.modules.len(),
698            total_functions,
699            total_variables,
700            total_line_headers,
701            total_cache_entries,
702        }
703    }
704
705    /// Get module statistics (compatible with ghostscope-binary's ModuleStats)
706    pub fn get_module_stats(&self) -> ModuleStats {
707        let mut total_symbols = 0;
708        let mut executable_modules = 0;
709        let mut library_modules = 0;
710
711        for (module_path, module_data) in &self.modules {
712            let function_names = module_data.get_function_names();
713            total_symbols += function_names.len();
714
715            // Check if module is executable (main binary) or library
716            if self.is_main_executable_module(module_path) {
717                executable_modules += 1;
718            } else {
719                library_modules += 1;
720            }
721        }
722
723        ModuleStats {
724            total_modules: self.modules.len(),
725            executable_modules,
726            library_modules,
727            total_symbols,
728            modules_with_debug_info: self.modules.len(), // All DWARF modules have debug info
729        }
730    }
731
732    /// Get main executable module information
733    pub fn get_main_executable(&self) -> Option<MainExecutableInfo> {
734        // Find the main executable module (usually the first non-library module)
735        for module_path in self.modules.keys() {
736            if self.is_main_executable_module(module_path) {
737                return Some(MainExecutableInfo {
738                    path: module_path.to_string_lossy().to_string(),
739                });
740            }
741        }
742        None
743    }
744
745    /// Check if a module is the main executable (not a shared library)
746    fn is_main_executable_module(&self, module_path: &Path) -> bool {
747        // Heuristic: main executable usually doesn't have .so extension and contains the process name
748        let filename = module_path
749            .file_name()
750            .and_then(|name| name.to_str())
751            .unwrap_or("");
752
753        // Not a shared library
754        !filename.contains(".so") &&
755        // Not a system library path
756        !module_path.to_string_lossy().starts_with("/lib") &&
757        !module_path.to_string_lossy().starts_with("/usr/lib")
758    }
759
760    /// Get list of all function names across all modules
761    pub fn list_functions(&self) -> Vec<String> {
762        let mut all_functions = Vec::new();
763
764        for module_data in self.modules.values() {
765            let function_names = module_data.get_function_names();
766            for name in function_names {
767                all_functions.push(name.clone());
768            }
769        }
770
771        // Remove duplicates and sort
772        all_functions.sort();
773        all_functions.dedup();
774
775        tracing::debug!(
776            "Listed {} unique functions across {} modules",
777            all_functions.len(),
778            self.modules.len()
779        );
780
781        all_functions
782    }
783
784    /// Lookup functions by pattern (simplified - exact match only for now)
785    pub fn lookup_functions_by_pattern(&self, pattern: &str) -> Vec<String> {
786        let all_functions = self.list_functions();
787        all_functions
788            .into_iter()
789            .filter(|name| name.contains(pattern))
790            .collect()
791    }
792
793    /// Get all function names (alias for compatibility)
794    pub fn lookup_all_function_names(&self) -> Vec<String> {
795        self.list_functions()
796    }
797
798    /// Get PID (accessor for private field)
799    pub fn get_pid(&self) -> u32 {
800        self.pid
801    }
802
803    /// Get shared library information (compatibility method)
804    pub fn get_shared_library_info(&self) -> Vec<SharedLibraryInfo> {
805        self.modules
806            .iter()
807            .filter(|(path, _)| self.is_shared_library(path))
808            .map(|(path, module_data)| {
809                let mapping = module_data.module_mapping();
810                let debug_file_path = module_data
811                    .get_debug_file_path()
812                    .map(|p| p.to_string_lossy().to_string());
813
814                SharedLibraryInfo {
815                    from_address: mapping.loaded_address.unwrap_or(0),
816                    to_address: mapping.loaded_address.map_or(0, |addr| addr + mapping.size),
817                    symbols_read: !module_data.get_function_names().is_empty(),
818                    // Reflect actual DWARF availability (embedded or via .gnu_debuglink)
819                    debug_info_available: module_data.has_dwarf_info(),
820                    library_path: path.to_string_lossy().to_string(),
821                    size: mapping.size,
822                    debug_file_path,
823                }
824            })
825            .collect()
826    }
827
828    /// Get executable file information (for "info file" command)
829    pub fn get_executable_file_info(&self) -> Option<ExecutableFileInfo> {
830        // Find the primary executable (not a shared library)
831        let executable = self
832            .modules
833            .iter()
834            .find(|(path, _)| !self.is_shared_library(path))?;
835
836        let (exe_path, module_data) = executable;
837        let file_path = exe_path.to_string_lossy().to_string();
838
839        // Parse the ELF file to get detailed information
840        let file_bytes = std::fs::read(exe_path).ok()?;
841        let obj = object::File::parse(&file_bytes[..]).ok()?;
842
843        // Get file type
844        let file_type = match obj.format() {
845            object::BinaryFormat::Elf => {
846                if obj.is_64() {
847                    "ELF 64-bit executable"
848                } else {
849                    "ELF 32-bit executable"
850                }
851            }
852            _ => "Unknown format",
853        }
854        .to_string();
855
856        // Check if has symbols
857        let has_symbols = !module_data.get_function_names().is_empty()
858            || obj.symbols().count() > 0
859            || obj.dynamic_symbols().count() > 0;
860
861        // Check if has debug info - check if DWARF was successfully loaded
862        // This includes both embedded DWARF and debug link external files
863        let has_debug_info = module_data.has_dwarf_info();
864
865        // Get debug file path if using separate debug file (e.g., via .gnu_debuglink)
866        let debug_file_path = module_data.get_debug_file_path();
867
868        // Load bias for PID mode from module mapping (if available)
869        let load_bias = if self.pid != 0 {
870            module_data.module_mapping().loaded_address.unwrap_or(0)
871        } else {
872            0
873        };
874
875        // Get entry point (add load bias in PID mode)
876        let entry_point = Some(obj.entry() + load_bias);
877
878        // Get .text section info (add load bias in PID mode)
879        let text_section = obj.section_by_name(".text").map(|section| {
880            let addr = section.address() + load_bias;
881            let size = section.size();
882            SectionInfo {
883                start_address: addr,
884                end_address: addr + size,
885                size,
886            }
887        });
888
889        // Get .data section info (add load bias in PID mode)
890        let data_section = obj.section_by_name(".data").map(|section| {
891            let addr = section.address() + load_bias;
892            let size = section.size();
893            SectionInfo {
894                start_address: addr,
895                end_address: addr + size,
896                size,
897            }
898        });
899
900        // Determine mode description based on pid
901        let mode_description = if self.pid != 0 {
902            format!("Attached to process {} (PID mode)", self.pid)
903        } else {
904            "Static analysis mode (target file specified with -t)".to_string()
905        };
906
907        Some(ExecutableFileInfo {
908            file_path,
909            file_type,
910            entry_point,
911            has_symbols,
912            has_debug_info,
913            debug_file_path: debug_file_path.map(|p| p.to_string_lossy().to_string()),
914            text_section,
915            data_section,
916            mode_description,
917        })
918    }
919
920    // NOTE: Runtime section offsets are handled by ghostscope-coordinator.
921
922    /// Check if a module is a shared library
923    fn is_shared_library(&self, module_path: &Path) -> bool {
924        let filename = module_path
925            .file_name()
926            .and_then(|name| name.to_str())
927            .unwrap_or("");
928
929        // Shared libraries typically have .so extension or contain .so
930        filename.contains(".so")
931            || module_path.to_string_lossy().starts_with("/lib")
932            || module_path.to_string_lossy().starts_with("/usr/lib")
933    }
934
935    /// Find symbol by module address
936    pub fn find_symbol_by_module_address(&self, module_address: &ModuleAddress) -> Option<String> {
937        if let Some(module_data) = self.modules.get(&module_address.module_path) {
938            module_data.find_symbol_by_address(module_address.address)
939        } else {
940            None
941        }
942    }
943
944    /// Get grouped file info by module (compatibility method)
945    pub fn get_grouped_file_info_by_module(&self) -> Result<Vec<(String, Vec<SimpleFileInfo>)>> {
946        let mut grouped = Vec::new();
947
948        for (module_path, module_data) in &self.modules {
949            let files = module_data.get_all_files();
950            if !files.is_empty() {
951                let simple_files: Vec<SimpleFileInfo> = files
952                    .into_iter()
953                    .map(|source_file| SimpleFileInfo {
954                        full_path: source_file.full_path,
955                        basename: source_file.filename,
956                        directory: source_file.directory_path,
957                    })
958                    .collect();
959
960                grouped.push((module_path.to_string_lossy().to_string(), simple_files));
961            }
962        }
963
964        Ok(grouped)
965    }
966}
967
968///
969/// Module statistics compatible with ghostscope-binary
970#[derive(Debug, Clone)]
971pub struct ModuleStats {
972    pub total_modules: usize,
973    pub executable_modules: usize,
974    pub library_modules: usize,
975    pub total_symbols: usize,
976    pub modules_with_debug_info: usize,
977}
978
979/// Main executable information
980#[derive(Debug, Clone)]
981pub struct MainExecutableInfo {
982    pub path: String,
983}
984
985/// Statistics for debugging and monitoring
986#[derive(Debug, Clone)]
987pub struct AnalyzerStats {
988    pub pid: u32,
989    pub module_count: usize,
990    pub total_functions: usize,
991    pub total_variables: usize,
992    pub total_line_headers: usize,
993    pub total_cache_entries: usize,
994}
995
996/// Shared library information (compatible with ghostscope-ui)
997#[derive(Debug, Clone)]
998pub struct SharedLibraryInfo {
999    pub from_address: u64,               // Starting address in memory
1000    pub to_address: u64,                 // Ending address in memory
1001    pub symbols_read: bool,              // Whether symbols were successfully read
1002    pub debug_info_available: bool,      // Whether debug information is available
1003    pub library_path: String,            // Full path to the library file
1004    pub size: u64,                       // Size of the library in memory
1005    pub debug_file_path: Option<String>, // Path to separate debug file (if via .gnu_debuglink)
1006}
1007
1008/// Executable file information (for "info file" command)
1009#[derive(Debug, Clone)]
1010pub struct ExecutableFileInfo {
1011    pub file_path: String,
1012    pub file_type: String,
1013    pub entry_point: Option<u64>,
1014    pub has_symbols: bool,
1015    pub has_debug_info: bool,
1016    pub debug_file_path: Option<String>,
1017    pub text_section: Option<SectionInfo>,
1018    pub data_section: Option<SectionInfo>,
1019    pub mode_description: String,
1020}
1021
1022/// Section information for executable files
1023#[derive(Debug, Clone)]
1024pub struct SectionInfo {
1025    pub start_address: u64,
1026    pub end_address: u64,
1027    pub size: u64,
1028}
1029
1030/// Simple file information compatible with ghostscope-binary
1031#[derive(Debug, Clone)]
1032pub struct SimpleFileInfo {
1033    pub full_path: String,
1034    pub basename: String,
1035    pub directory: String,
1036}