Skip to main content

ghostscope_dwarf/
analyzer.rs

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