1use crate::{
4 core::{
5 mapping::ModuleMapping, CallerFrameRecovery, DebugInfoSource, ModuleAddress, Result,
6 SectionType, SourceLocation,
7 },
8 loader::ExplicitDebugFile,
9 objfile::LoadedObjfile,
10 semantics::{CompactUnwindRow, CompactUnwindTable, PcContext, VisibleVariable},
11};
12use ghostscope_debuginfod::DebuginfodClient;
13use object::{Object, ObjectSection};
14use std::collections::{HashMap, VecDeque};
15use std::path::{Path, PathBuf};
16use std::sync::{Arc, RwLock};
17
18mod module_resolution;
19mod plan_global;
20mod plan_pc;
21mod source_resolution;
22mod type_lookup;
23
24pub use module_resolution::ModuleDefaultPolicy;
25pub use source_resolution::{SourceLineAddressSearch, SourceLineQuerySearch};
26pub use type_lookup::TypeLookupAmbiguity;
27
28#[cfg(test)]
29use crate::{
30 core::{AddressExpr, Availability, Provenance, VariableLocation},
31 semantics::VariableReadPlan,
32};
33
34#[derive(Debug, Clone)]
36pub enum ModuleLoadingEvent {
37 Discovered {
39 module_path: String,
40 current: usize,
41 total: usize,
42 },
43 LoadingStarted {
45 module_path: String,
46 current: usize,
47 total: usize,
48 },
49 LoadingCompleted {
51 module_path: String,
52 stats: ModuleLoadingStats,
53 current: usize,
54 total: usize,
55 },
56 LoadingFailed {
58 module_path: String,
59 error: String,
60 current: usize,
61 total: usize,
62 },
63}
64
65#[derive(Debug, Clone)]
67pub struct ModuleLoadingStats {
68 pub functions: usize,
69 pub variables: usize,
70 pub types: usize,
71 pub debug_info_source: DebugInfoSource,
72 pub load_time_ms: u64,
73 pub parse_time_ms: u64,
74 pub index_time_ms: u64,
75 pub module_total_time_ms: u64,
76}
77
78#[derive(Debug, Clone)]
80pub struct AddressQueryResult {
81 pub module_path: PathBuf,
82 pub address: u64,
83 pub source_file: Option<String>,
84 pub source_line: Option<u32>,
85 pub source_column: Option<u32>,
86 pub function_name: Option<String>,
87 pub is_inline: Option<bool>,
88 pub variables: Vec<VisibleVariable>,
89 pub parameters: Vec<VisibleVariable>,
90}
91
92#[derive(Debug, Clone)]
94pub struct LoadedModuleRuntimeInfo {
95 pub module_path: PathBuf,
96 pub loaded_address: Option<u64>,
97 pub load_bias: Option<u64>,
98 pub size: u64,
99}
100
101#[derive(Debug, Clone)]
103pub struct FunctionQueryResult {
104 pub function_name: String,
105 pub addresses: Vec<AddressQueryResult>,
106}
107
108#[derive(Debug)]
110pub struct DwarfAnalyzer {
111 pid: u32,
113 modules: HashMap<PathBuf, LoadedObjfile>,
115 pc_context_cache: RwLock<PcContextCache>,
117}
118
119const PC_CONTEXT_CACHE_MAX_ENTRIES: usize = 8192;
120
121#[derive(Debug, Clone, PartialEq, Eq, Hash)]
122struct PcContextCacheKey {
123 module_path: PathBuf,
124 address: u64,
125}
126
127#[derive(Debug)]
128struct PcContextCache {
129 entries: HashMap<PathBuf, HashMap<u64, PcContext>>,
130 insertion_order: VecDeque<PcContextCacheKey>,
131 len: usize,
132 max_entries: usize,
133}
134
135impl Default for PcContextCache {
136 fn default() -> Self {
137 Self {
138 entries: HashMap::new(),
139 insertion_order: VecDeque::new(),
140 len: 0,
141 max_entries: PC_CONTEXT_CACHE_MAX_ENTRIES,
142 }
143 }
144}
145
146impl PcContextCache {
147 fn get(&self, module_path: &Path, address: u64) -> Option<PcContext> {
148 self.entries
149 .get(module_path)
150 .and_then(|entries| entries.get(&address))
151 .cloned()
152 }
153
154 fn insert(&mut self, module_path: PathBuf, address: u64, context: PcContext) {
155 if self.max_entries == 0 {
156 return;
157 }
158
159 let key = PcContextCacheKey {
160 module_path,
161 address,
162 };
163 let module_entries = self.entries.entry(key.module_path.clone()).or_default();
164 if module_entries.insert(address, context).is_none() {
165 self.insertion_order.push_back(key.clone());
166 self.len += 1;
167 }
168
169 while self.len > self.max_entries {
170 let Some(expired) = self.insertion_order.pop_front() else {
171 break;
172 };
173 if let Some(module_entries) = self.entries.get_mut(&expired.module_path) {
174 if module_entries.remove(&expired.address).is_some() {
175 self.len -= 1;
176 }
177 if module_entries.is_empty() {
178 self.entries.remove(&expired.module_path);
179 }
180 }
181 }
182 }
183}
184
185impl DwarfAnalyzer {
186 fn build_address_query_result(
187 &self,
188 module_address: &ModuleAddress,
189 ) -> Result<AddressQueryResult> {
190 self.build_address_query_result_with_source_hint(module_address, None)
191 }
192
193 fn build_address_query_result_with_source_hint(
194 &self,
195 module_address: &ModuleAddress,
196 source_hint: Option<(&str, u32)>,
197 ) -> Result<AddressQueryResult> {
198 let mut variables = Vec::new();
199 let mut parameters = Vec::new();
200
201 for variable in self.visible_variables_at_address(module_address)? {
202 if variable.is_parameter {
203 parameters.push(variable);
204 } else {
205 variables.push(variable);
206 }
207 }
208
209 let source_location = if let Some((file_path, line_number)) = source_hint {
210 self.modules
211 .get(&module_address.module_path)
212 .and_then(|module_data| {
213 module_data.lookup_source_location_for_source_line(
214 module_address.address,
215 file_path,
216 line_number,
217 )
218 })
219 } else {
220 self.lookup_source_location(module_address)
221 };
222 let function_name = self.find_function_name_by_module_address(module_address);
223 let is_inline = self.is_inline_at(module_address);
224
225 Ok(AddressQueryResult {
226 module_path: module_address.module_path.clone(),
227 address: module_address.address,
228 source_file: source_location.as_ref().map(|sl| sl.file_path.clone()),
229 source_line: source_location.as_ref().map(|sl| sl.line_number),
230 source_column: source_location.as_ref().and_then(|sl| sl.column),
231 function_name,
232 is_inline,
233 variables,
234 parameters,
235 })
236 }
237
238 fn query_module_addresses(
239 &self,
240 module_addresses: Vec<ModuleAddress>,
241 ) -> Result<Vec<AddressQueryResult>> {
242 module_addresses
243 .iter()
244 .map(|module_address| self.build_address_query_result(module_address))
245 .collect()
246 }
247
248 fn query_module_addresses_for_source_line(
249 &self,
250 module_addresses: Vec<ModuleAddress>,
251 file_path: &str,
252 line_number: u32,
253 ) -> Result<Vec<AddressQueryResult>> {
254 module_addresses
255 .iter()
256 .map(|module_address| {
257 self.build_address_query_result_with_source_hint(
258 module_address,
259 Some((file_path, line_number)),
260 )
261 })
262 .collect()
263 }
264
265 fn query_module_addresses_best_effort(
266 &self,
267 module_addresses: Vec<ModuleAddress>,
268 query_label: &str,
269 ) -> Result<Vec<AddressQueryResult>> {
270 let mut results = Vec::new();
271 let mut first_error: Option<(ModuleAddress, String)> = None;
272
273 for module_address in &module_addresses {
274 match self.build_address_query_result(module_address) {
275 Ok(result) => results.push(result),
276 Err(error) => {
277 let error_string = error.to_string();
278 tracing::warn!(
279 "Skipping failed address query for {} at {}:0x{:x}: {}",
280 query_label,
281 module_address.module_display(),
282 module_address.address,
283 error_string
284 );
285
286 if first_error.is_none() {
287 first_error = Some((module_address.clone(), error_string));
288 }
289 }
290 }
291 }
292
293 if results.is_empty() {
294 if let Some((module_address, error)) = first_error {
295 return Err(anyhow::anyhow!(
296 "Failed to analyze any address for {} (first failure at {}:0x{:x}: {})",
297 query_label,
298 module_address.module_display(),
299 module_address.address,
300 error
301 ));
302 }
303 }
304
305 Ok(results)
306 }
307
308 fn query_module_addresses_for_source_line_best_effort(
309 &self,
310 module_addresses: Vec<ModuleAddress>,
311 file_path: &str,
312 line_number: u32,
313 query_label: &str,
314 ) -> Result<Vec<AddressQueryResult>> {
315 let mut results = Vec::new();
316 let mut first_error: Option<(ModuleAddress, String)> = None;
317
318 for module_address in &module_addresses {
319 match self.build_address_query_result_with_source_hint(
320 module_address,
321 Some((file_path, line_number)),
322 ) {
323 Ok(result) => results.push(result),
324 Err(error) => {
325 let error_string = error.to_string();
326 tracing::warn!(
327 "Skipping failed address query for {} at {}:0x{:x}: {}",
328 query_label,
329 module_address.module_display(),
330 module_address.address,
331 error_string
332 );
333
334 if first_error.is_none() {
335 first_error = Some((module_address.clone(), error_string));
336 }
337 }
338 }
339 }
340
341 if results.is_empty() {
342 if let Some((module_address, error)) = first_error {
343 return Err(anyhow::anyhow!(
344 "Failed to analyze any address for {} (first failure at {}:0x{:x}: {})",
345 query_label,
346 module_address.module_display(),
347 module_address.address,
348 error
349 ));
350 }
351 }
352
353 Ok(results)
354 }
355
356 fn find_function_name_by_module_address(
357 &self,
358 module_address: &ModuleAddress,
359 ) -> Option<String> {
360 self.loaded_module_path_for(&module_address.module_path)
361 .and_then(|module_path| self.modules.get(module_path))
362 .and_then(|module_data| {
363 module_data.find_function_name_by_address(module_address.address)
364 })
365 }
366
367 fn sorted_module_paths(&self) -> Vec<&PathBuf> {
368 let mut paths: Vec<&PathBuf> = self.modules.keys().collect();
369 paths.sort();
370 paths
371 }
372
373 pub(crate) fn loaded_module_path_for<P: AsRef<Path>>(
374 &self,
375 module_path: P,
376 ) -> Option<&PathBuf> {
377 let module_path = module_path.as_ref();
378 if let Some((path, _)) = self.modules.get_key_value(module_path) {
379 return Some(path);
380 }
381
382 self.sorted_module_paths()
383 .into_iter()
384 .find(|path| Self::module_paths_equivalent(path.as_path(), module_path))
385 }
386
387 pub fn module_id_for_path<P: AsRef<Path>>(&self, module_path: P) -> Option<crate::ModuleId> {
389 let module_path = self.loaded_module_path_for(module_path)?;
390 self.sorted_module_paths()
391 .into_iter()
392 .position(|path| path.as_path() == module_path.as_path())
393 .map(|index| crate::ModuleId(index as u32))
394 }
395
396 pub fn module_path_for_id(&self, module: crate::ModuleId) -> Option<&Path> {
398 self.sorted_module_paths()
399 .get(module.0 as usize)
400 .map(|path| path.as_path())
401 }
402
403 pub async fn from_pid(pid: u32) -> Result<Self> {
405 Self::from_pid_parallel(pid).await
406 }
407
408 pub fn is_inline_at(&self, module_address: &ModuleAddress) -> Option<bool> {
412 if let Some(module_data) = self
413 .loaded_module_path_for(&module_address.module_path)
414 .and_then(|module_path| self.modules.get(module_path))
415 {
416 module_data.is_inline_at(module_address.address)
417 } else {
418 None
419 }
420 }
421
422 pub async fn from_pid_parallel(pid: u32) -> Result<Self> {
424 Self::from_pid_parallel_with_config(pid, &[], false, |_event| {}).await
425 }
426
427 pub async fn from_pid_parallel_with_progress<F>(pid: u32, progress_callback: F) -> Result<Self>
429 where
430 F: Fn(ModuleLoadingEvent) + Send + Sync + 'static,
431 {
432 Self::from_pid_parallel_with_config(pid, &[], false, progress_callback).await
433 }
434
435 pub async fn from_pid_parallel_with_config<F>(
437 pid: u32,
438 debug_search_paths: &[String],
439 allow_loose_debug_match: bool,
440 progress_callback: F,
441 ) -> Result<Self>
442 where
443 F: Fn(ModuleLoadingEvent) + Send + Sync + 'static,
444 {
445 Self::from_pid_parallel_with_config_and_debuginfod(
446 pid,
447 debug_search_paths,
448 allow_loose_debug_match,
449 None,
450 progress_callback,
451 )
452 .await
453 }
454
455 pub async fn from_pid_parallel_with_config_and_debuginfod<F>(
457 pid: u32,
458 debug_search_paths: &[String],
459 allow_loose_debug_match: bool,
460 debuginfod_client: Option<Arc<DebuginfodClient>>,
461 progress_callback: F,
462 ) -> Result<Self>
463 where
464 F: Fn(ModuleLoadingEvent) + Send + Sync + 'static,
465 {
466 tracing::info!("Creating DWARF analyzer for PID {} (parallel)", pid);
467
468 let module_runtime_info = Self::discover_pid_runtime_modules(pid)?;
469
470 Self::from_pid_runtime_modules_with_config_and_debuginfod(
471 pid,
472 module_runtime_info,
473 debug_search_paths,
474 allow_loose_debug_match,
475 debuginfod_client,
476 progress_callback,
477 )
478 .await
479 }
480
481 pub fn discover_pid_runtime_modules(pid: u32) -> Result<Vec<LoadedModuleRuntimeInfo>> {
483 let mut coord = ghostscope_process::ProcessManager::new();
484 coord.ensure_prefill_pid(pid)?;
485 Ok(coord
486 .cached_offsets_with_paths_for_pid(pid)
487 .map(Self::runtime_modules_from_pid_offsets)
488 .unwrap_or_default())
489 }
490
491 pub fn runtime_modules_from_pid_offsets(
493 entries: &[ghostscope_process::PidOffsetsEntry],
494 ) -> Vec<LoadedModuleRuntimeInfo> {
495 let mut seen = std::collections::HashSet::new();
496 entries
497 .iter()
498 .filter(|entry| seen.insert(entry.module_path.clone()))
499 .map(|entry| LoadedModuleRuntimeInfo {
500 module_path: PathBuf::from(&entry.module_path),
501 loaded_address: Some(entry.base),
502 load_bias: Some(entry.offsets.text),
503 size: entry.size,
504 })
505 .collect()
506 }
507
508 fn runtime_modules_to_module_mappings(
509 runtime_modules: Vec<LoadedModuleRuntimeInfo>,
510 ) -> Vec<ModuleMapping> {
511 runtime_modules
512 .into_iter()
513 .map(|module| {
514 let mut mapping = ModuleMapping::from_path(module.module_path);
515 mapping.loaded_address = module.loaded_address;
516 mapping.load_bias = module.load_bias;
517 mapping.size = module.size;
518 mapping
519 })
520 .collect()
521 }
522
523 pub async fn refresh_pid_runtime_modules_with_config_and_debuginfod<F>(
525 &mut self,
526 runtime_modules: Vec<LoadedModuleRuntimeInfo>,
527 debug_search_paths: &[String],
528 allow_loose_debug_match: bool,
529 debuginfod_client: Option<Arc<DebuginfodClient>>,
530 progress_callback: F,
531 ) -> Result<usize>
532 where
533 F: Fn(ModuleLoadingEvent) + Send + Sync + 'static,
534 {
535 let mut new_runtime_modules = Vec::new();
536 let mut updated_existing = 0usize;
537
538 for runtime_module in runtime_modules {
539 let existing = self.modules.iter_mut().find(|(path, _)| {
540 Self::module_paths_equivalent(path.as_path(), &runtime_module.module_path)
541 });
542
543 if let Some((_path, loaded)) = existing {
544 let mapping = loaded.module_mapping();
545 if mapping.loaded_address != runtime_module.loaded_address
546 || mapping.load_bias != runtime_module.load_bias
547 || mapping.size != runtime_module.size
548 {
549 loaded.update_runtime_mapping(
550 runtime_module.loaded_address,
551 runtime_module.load_bias,
552 runtime_module.size,
553 );
554 updated_existing += 1;
555 }
556 } else {
557 new_runtime_modules.push(runtime_module);
558 }
559 }
560
561 if updated_existing > 0 {
562 self.clear_pc_context_cache();
563 tracing::debug!(
564 "Updated runtime mapping metadata for {} loaded module(s)",
565 updated_existing
566 );
567 }
568
569 if new_runtime_modules.is_empty() {
570 return Ok(0);
571 }
572
573 tracing::info!(
574 "Refreshing DWARF analyzer for PID {} with {} newly mapped module(s)",
575 self.pid,
576 new_runtime_modules.len()
577 );
578
579 let module_mappings = Self::runtime_modules_to_module_mappings(new_runtime_modules);
580
581 for (index, mapping) in module_mappings.iter().enumerate() {
582 progress_callback(ModuleLoadingEvent::Discovered {
583 module_path: mapping.path.to_string_lossy().to_string(),
584 current: index + 1,
585 total: module_mappings.len(),
586 });
587 }
588
589 let mut loader = crate::loader::ModuleLoader::new(module_mappings).parallel();
590 if !debug_search_paths.is_empty() {
591 loader = loader.with_debug_search_paths(debug_search_paths.to_vec());
592 }
593 loader = loader.with_loose_debug_match(allow_loose_debug_match);
594 loader = loader.with_debuginfod_client(debuginfod_client);
595
596 let modules = loader
597 .with_progress_callback(progress_callback)
598 .load()
599 .await?;
600 let loaded_count = modules.len();
601
602 for module in modules {
603 let module_path = module.module_path().clone();
604 self.modules.insert(module_path, module);
605 }
606
607 if loaded_count > 0 {
608 self.clear_pc_context_cache();
609 tracing::info!(
610 "DWARF analyzer for PID {} loaded {} new module(s)",
611 self.pid,
612 loaded_count
613 );
614 }
615
616 Ok(loaded_count)
617 }
618
619 pub async fn from_pid_runtime_modules_with_config_and_debuginfod<F>(
621 pid: u32,
622 runtime_modules: Vec<LoadedModuleRuntimeInfo>,
623 debug_search_paths: &[String],
624 allow_loose_debug_match: bool,
625 debuginfod_client: Option<Arc<DebuginfodClient>>,
626 progress_callback: F,
627 ) -> Result<Self>
628 where
629 F: Fn(ModuleLoadingEvent) + Send + Sync + 'static,
630 {
631 Self::from_pid_runtime_modules_with_config_debuginfod_and_explicit_debug_file(
632 pid,
633 runtime_modules,
634 debug_search_paths,
635 allow_loose_debug_match,
636 debuginfod_client,
637 None,
638 progress_callback,
639 )
640 .await
641 }
642
643 pub async fn from_pid_runtime_modules_with_config_debuginfod_and_explicit_debug_file<F>(
646 pid: u32,
647 runtime_modules: Vec<LoadedModuleRuntimeInfo>,
648 debug_search_paths: &[String],
649 allow_loose_debug_match: bool,
650 debuginfod_client: Option<Arc<DebuginfodClient>>,
651 explicit_debug_file: Option<ExplicitDebugFile>,
652 progress_callback: F,
653 ) -> Result<Self>
654 where
655 F: Fn(ModuleLoadingEvent) + Send + Sync + 'static,
656 {
657 tracing::info!(
658 "Creating DWARF analyzer for PID {} from {} runtime module mappings",
659 pid,
660 runtime_modules.len()
661 );
662
663 let module_mappings = Self::runtime_modules_to_module_mappings(runtime_modules);
664
665 tracing::info!(
666 "Discovered {} modules for PID {}",
667 module_mappings.len(),
668 pid
669 );
670
671 for (index, mapping) in module_mappings.iter().enumerate() {
673 progress_callback(ModuleLoadingEvent::Discovered {
674 module_path: mapping.path.to_string_lossy().to_string(),
675 current: index + 1,
676 total: module_mappings.len(),
677 });
678 }
679
680 let mut loader = crate::loader::ModuleLoader::new(module_mappings).parallel();
682
683 if !debug_search_paths.is_empty() {
685 loader = loader.with_debug_search_paths(debug_search_paths.to_vec());
686 }
687 loader = loader.with_loose_debug_match(allow_loose_debug_match);
688 loader = loader.with_explicit_debug_file(explicit_debug_file);
689 loader = loader.with_debuginfod_client(debuginfod_client);
690
691 let modules = loader
692 .with_progress_callback(progress_callback)
693 .load()
694 .await?;
695
696 tracing::info!(
697 "Created DWARF analyzer for PID {} with {} modules (parallel)",
698 pid,
699 modules.len()
700 );
701
702 Ok(Self::from_modules(pid, modules))
703 }
704
705 pub async fn from_exec_path<P: AsRef<std::path::Path>>(exec_path: P) -> Result<Self> {
707 Self::from_exec_path_with_config(exec_path, &[], false).await
708 }
709
710 pub async fn from_exec_path_with_config<P: AsRef<std::path::Path>>(
712 exec_path: P,
713 debug_search_paths: &[String],
714 allow_loose_debug_match: bool,
715 ) -> Result<Self> {
716 Self::from_exec_path_with_config_and_debuginfod(
717 exec_path,
718 debug_search_paths,
719 allow_loose_debug_match,
720 None,
721 )
722 .await
723 }
724
725 pub async fn from_exec_path_with_config_and_debuginfod<P: AsRef<std::path::Path>>(
727 exec_path: P,
728 debug_search_paths: &[String],
729 allow_loose_debug_match: bool,
730 debuginfod_client: Option<Arc<DebuginfodClient>>,
731 ) -> Result<Self> {
732 Self::from_exec_path_with_config_and_debuginfod_and_progress(
733 exec_path,
734 debug_search_paths,
735 allow_loose_debug_match,
736 debuginfod_client,
737 |_event| {},
738 )
739 .await
740 }
741
742 pub async fn from_exec_path_with_config_and_progress<P, F>(
744 exec_path: P,
745 debug_search_paths: &[String],
746 allow_loose_debug_match: bool,
747 progress_callback: F,
748 ) -> Result<Self>
749 where
750 P: AsRef<std::path::Path>,
751 F: Fn(ModuleLoadingEvent) + Send + Sync + 'static,
752 {
753 Self::from_exec_path_with_config_and_debuginfod_and_progress(
754 exec_path,
755 debug_search_paths,
756 allow_loose_debug_match,
757 None,
758 progress_callback,
759 )
760 .await
761 }
762
763 pub async fn from_exec_path_with_config_and_debuginfod_and_progress<P, F>(
765 exec_path: P,
766 debug_search_paths: &[String],
767 allow_loose_debug_match: bool,
768 debuginfod_client: Option<Arc<DebuginfodClient>>,
769 progress_callback: F,
770 ) -> Result<Self>
771 where
772 P: AsRef<std::path::Path>,
773 F: Fn(ModuleLoadingEvent) + Send + Sync + 'static,
774 {
775 Self::from_exec_path_with_config_debuginfod_explicit_debug_file_and_progress(
776 exec_path,
777 debug_search_paths,
778 allow_loose_debug_match,
779 debuginfod_client,
780 None,
781 progress_callback,
782 )
783 .await
784 }
785
786 pub async fn from_exec_path_with_config_debuginfod_explicit_debug_file_and_progress<P, F>(
789 exec_path: P,
790 debug_search_paths: &[String],
791 allow_loose_debug_match: bool,
792 debuginfod_client: Option<Arc<DebuginfodClient>>,
793 explicit_debug_file: Option<PathBuf>,
794 progress_callback: F,
795 ) -> Result<Self>
796 where
797 P: AsRef<std::path::Path>,
798 F: Fn(ModuleLoadingEvent) + Send + Sync + 'static,
799 {
800 let exec_path = exec_path.as_ref().to_path_buf();
801 tracing::info!(
802 "Creating DWARF analyzer for executable: {}",
803 exec_path.display()
804 );
805
806 let mut analyzer = Self {
807 pid: 0, modules: HashMap::new(),
809 pc_context_cache: RwLock::new(PcContextCache::default()),
810 };
811
812 let module_mapping = ModuleMapping {
815 path: exec_path.clone(),
816 loaded_address: None, load_bias: None,
818 size: 0, };
820 let module_path = exec_path.to_string_lossy().to_string();
821
822 progress_callback(ModuleLoadingEvent::Discovered {
823 module_path: module_path.clone(),
824 current: 1,
825 total: 1,
826 });
827 progress_callback(ModuleLoadingEvent::LoadingStarted {
828 module_path: module_path.clone(),
829 current: 1,
830 total: 1,
831 });
832
833 let start_time = std::time::Instant::now();
835 match LoadedObjfile::load_parallel(
836 module_mapping,
837 debug_search_paths,
838 allow_loose_debug_match,
839 explicit_debug_file,
840 debuginfod_client,
841 )
842 .await
843 {
844 Ok(module_data) => {
845 let (functions, variables, types) = module_data.get_lightweight_index().get_stats();
846 let (parse_time_ms, index_time_ms, module_total_time_ms) =
847 module_data.get_load_timing_ms();
848 progress_callback(ModuleLoadingEvent::LoadingCompleted {
849 module_path,
850 stats: ModuleLoadingStats {
851 functions,
852 variables,
853 types,
854 debug_info_source: module_data.get_debug_info_source().clone(),
855 load_time_ms: start_time.elapsed().as_millis() as u64,
856 parse_time_ms,
857 index_time_ms,
858 module_total_time_ms,
859 },
860 current: 1,
861 total: 1,
862 });
863 analyzer.modules.insert(exec_path.clone(), module_data);
864 tracing::info!(
865 "Created DWARF analyzer for executable {} with 1 module",
866 exec_path.display()
867 );
868 }
869 Err(e) => {
870 progress_callback(ModuleLoadingEvent::LoadingFailed {
871 module_path,
872 error: e.to_string(),
873 current: 1,
874 total: 1,
875 });
876 return Err(crate::DwarfError::ModuleLoadError(format!(
877 "Failed to load executable {}: {}",
878 exec_path.display(),
879 e
880 ))
881 .into());
882 }
883 }
884
885 Ok(analyzer)
886 }
887
888 pub(crate) fn from_modules(pid: u32, modules: Vec<LoadedObjfile>) -> Self {
890 let mut analyzer = Self {
891 pid,
892 modules: HashMap::new(),
893 pc_context_cache: RwLock::new(PcContextCache::default()),
894 };
895
896 for module in modules {
897 let module_path = module.module_path().clone();
898 analyzer.modules.insert(module_path, module);
899 }
900
901 tracing::info!(
902 "Created DWARF analyzer for PID {} with {} pre-loaded modules",
903 pid,
904 analyzer.modules.len()
905 );
906
907 analyzer
908 }
909
910 fn clear_pc_context_cache(&self) {
911 if let Ok(mut cache) = self.pc_context_cache.write() {
912 *cache = PcContextCache::default();
913 }
914 }
915
916 pub fn lookup_function_addresses(&self, name: &str) -> Vec<ModuleAddress> {
919 let mut results = Vec::new();
920
921 for (module_path, module_data) in &self.modules {
922 let addresses = module_data.lookup_function_addresses_any(name);
923
924 for address in addresses {
926 tracing::debug!(
927 "Function '{}' found in module {} at address: 0x{:x}",
928 name,
929 module_path.display(),
930 address
931 );
932 results.push(ModuleAddress::new(module_path.clone(), address));
933 }
934 }
935
936 results.sort_by(|a, b| {
938 let pa = a.module_path.to_string_lossy();
939 let pb = b.module_path.to_string_lossy();
940 match pa.cmp(&pb) {
941 std::cmp::Ordering::Equal => a.address.cmp(&b.address),
942 other => other,
943 }
944 });
945 results
946 }
947
948 pub fn query_function(&self, name: &str) -> Result<FunctionQueryResult> {
950 let module_addresses = self.lookup_function_addresses(name);
951 let addresses = self.query_module_addresses(module_addresses)?;
952 Ok(FunctionQueryResult {
953 function_name: name.to_string(),
954 addresses,
955 })
956 }
957
958 pub fn query_function_best_effort(&self, name: &str) -> Result<FunctionQueryResult> {
961 let module_addresses = self.lookup_function_addresses(name);
962 let addresses = self
963 .query_module_addresses_best_effort(module_addresses, &format!("function '{name}'"))?;
964 Ok(FunctionQueryResult {
965 function_name: name.to_string(),
966 addresses,
967 })
968 }
969
970 pub fn vaddr_to_file_offset<P: AsRef<std::path::Path>>(
973 &self,
974 module_path: P,
975 vaddr: u64,
976 ) -> Option<u64> {
977 let path_buf = module_path.as_ref().to_path_buf();
978 if let Some(module_data) = self.modules.get(&path_buf) {
979 module_data.vaddr_to_file_offset(vaddr)
980 } else {
981 None
982 }
983 }
984
985 pub fn recover_caller_frame(
987 &self,
988 module_address: &ModuleAddress,
989 registers: &[u16],
990 ) -> Result<Option<CallerFrameRecovery>> {
991 if let Some(module_data) = self
992 .loaded_module_path_for(&module_address.module_path)
993 .and_then(|module_path| self.modules.get(module_path))
994 {
995 module_data.recover_caller_frame(module_address.address, registers)
996 } else {
997 Ok(None)
998 }
999 }
1000
1001 pub fn recover_caller_frame_for_context(
1003 &self,
1004 ctx: &PcContext,
1005 registers: &[u16],
1006 ) -> Result<Option<CallerFrameRecovery>> {
1007 let module_address = self.module_address_for_context(ctx)?;
1008 self.recover_caller_frame(&module_address, registers)
1009 }
1010
1011 pub fn compact_unwind_table_for_context(
1013 &self,
1014 ctx: &PcContext,
1015 ) -> Result<Option<Arc<CompactUnwindTable>>> {
1016 let module_path = self
1017 .module_path_for_id(ctx.module)
1018 .ok_or_else(|| anyhow::anyhow!("Semantic module id {:?} is not loaded", ctx.module))?;
1019 self.modules
1020 .get(module_path)
1021 .ok_or_else(|| anyhow::anyhow!("Module {} not loaded", module_path.display()))?
1022 .compact_unwind_table(ctx.module)
1023 }
1024
1025 pub fn compact_unwind_row_for_context(
1027 &self,
1028 ctx: &PcContext,
1029 ) -> Result<Option<CompactUnwindRow>> {
1030 let module_path = self
1031 .module_path_for_id(ctx.module)
1032 .ok_or_else(|| anyhow::anyhow!("Semantic module id {:?} is not loaded", ctx.module))?;
1033 self.modules
1034 .get(module_path)
1035 .ok_or_else(|| anyhow::anyhow!("Module {} not loaded", module_path.display()))?
1036 .compact_unwind_row(ctx.module, ctx.normalized_pc)
1037 }
1038
1039 pub fn compact_unwind_table_for_module(
1041 &self,
1042 module: crate::ModuleId,
1043 ) -> Result<Option<Arc<CompactUnwindTable>>> {
1044 let module_path = self
1045 .module_path_for_id(module)
1046 .ok_or_else(|| anyhow::anyhow!("Semantic module id {:?} is not loaded", module))?;
1047 self.modules
1048 .get(module_path)
1049 .ok_or_else(|| anyhow::anyhow!("Module {} not loaded", module_path.display()))?
1050 .compact_unwind_table(module)
1051 }
1052
1053 pub fn get_loaded_modules(&self) -> Vec<&PathBuf> {
1055 self.modules.keys().collect()
1056 }
1057
1058 pub fn loaded_module_runtime_info(&self) -> Vec<LoadedModuleRuntimeInfo> {
1060 let mut modules: Vec<_> = self
1061 .modules
1062 .values()
1063 .map(|module| {
1064 let mapping = module.module_mapping();
1065 LoadedModuleRuntimeInfo {
1066 module_path: mapping.path.clone(),
1067 loaded_address: mapping.loaded_address,
1068 load_bias: mapping.load_bias,
1069 size: mapping.size,
1070 }
1071 })
1072 .collect();
1073 modules.sort_by(|left, right| left.module_path.cmp(&right.module_path));
1074 modules
1075 }
1076
1077 pub fn module_entry_address<P: AsRef<Path>>(&self, module_path: P) -> Option<u64> {
1079 self.modules
1080 .get(module_path.as_ref())
1081 .and_then(|module| module.entry_address())
1082 }
1083
1084 pub fn classify_section_for_address<P: AsRef<Path>>(
1086 &self,
1087 module_path: P,
1088 vaddr: u64,
1089 ) -> Option<SectionType> {
1090 let path = module_path.as_ref();
1091 if let Some(module_data) = self.modules.get(path) {
1092 module_data.classify_section_for_vaddr(vaddr)
1093 } else {
1094 None
1095 }
1096 }
1097
1098 pub fn lookup_function_address_by_name(&self, function_name: &str) -> Option<ModuleAddress> {
1101 let module_addresses = self.lookup_function_addresses(function_name);
1102
1103 if let Some(first_module_address) = module_addresses.first() {
1104 tracing::info!(
1105 "Found function '{}' in module '{}' at address 0x{:x}",
1106 function_name,
1107 first_module_address.module_display(),
1108 first_module_address.address
1109 );
1110 Some(first_module_address.clone())
1111 } else {
1112 tracing::warn!("Function '{}' not found in any module", function_name);
1113 None
1114 }
1115 }
1116
1117 pub fn lookup_source_location(&self, module_address: &ModuleAddress) -> Option<SourceLocation> {
1120 if let Some(module_data) = self
1121 .loaded_module_path_for(&module_address.module_path)
1122 .and_then(|module_path| self.modules.get(module_path))
1123 {
1124 module_data.lookup_source_location(module_address.address)
1125 } else {
1126 tracing::warn!("Module {} not found", module_address.module_display());
1127 None
1128 }
1129 }
1130
1131 pub fn lookup_addresses_by_source_line(
1134 &self,
1135 file_path: &str,
1136 line_number: u32,
1137 ) -> Vec<ModuleAddress> {
1138 let mut results = Vec::new();
1139
1140 for (module_path, module_data) in &self.modules {
1142 let addresses = module_data.lookup_addresses_by_source_line(file_path, line_number);
1143
1144 for address in addresses {
1146 results.push(ModuleAddress::new(module_path.clone(), address));
1147 }
1148 }
1149
1150 if !results.is_empty() {
1151 tracing::info!(
1152 "Found {} addresses for {}:{} across {} modules",
1153 results.len(),
1154 file_path,
1155 line_number,
1156 self.modules.len()
1157 );
1158 }
1159
1160 results.sort_by(|a, b| {
1161 let pa = a.module_path.to_string_lossy();
1162 let pb = b.module_path.to_string_lossy();
1163 match pa.cmp(&pb) {
1164 std::cmp::Ordering::Equal => a.address.cmp(&b.address),
1165 other => other,
1166 }
1167 });
1168 results
1169 }
1170
1171 pub fn query_source_line(
1173 &self,
1174 file_path: &str,
1175 line_number: u32,
1176 ) -> Result<Vec<AddressQueryResult>> {
1177 let module_addresses = self.lookup_addresses_by_source_line(file_path, line_number);
1178 self.query_module_addresses_for_source_line(module_addresses, file_path, line_number)
1179 }
1180
1181 pub fn query_source_line_best_effort(
1185 &self,
1186 file_path: &str,
1187 line_number: u32,
1188 ) -> Result<Vec<AddressQueryResult>> {
1189 let module_addresses = self.lookup_addresses_by_source_line(file_path, line_number);
1190 self.query_module_addresses_for_source_line_best_effort(
1191 module_addresses,
1192 file_path,
1193 line_number,
1194 &format!("source line '{file_path}:{line_number}'"),
1195 )
1196 }
1197
1198 pub fn query_address<P: AsRef<Path>>(
1200 &self,
1201 module_path: P,
1202 address: u64,
1203 ) -> Result<AddressQueryResult> {
1204 let module_address = ModuleAddress::new(module_path.as_ref().to_path_buf(), address);
1205 self.build_address_query_result(&module_address)
1206 }
1207
1208 pub fn get_all_function_names(&self) -> Vec<String> {
1210 let mut all_names = std::collections::HashSet::new();
1211 for module_data in self.modules.values() {
1212 for name in module_data.get_function_names() {
1213 all_names.insert(name.clone());
1214 }
1215 }
1216 all_names.into_iter().collect()
1217 }
1218
1219 pub fn get_stats(&self) -> AnalyzerStats {
1221 let mut total_functions = 0;
1222 let mut total_variables = 0;
1223 let mut total_line_headers = 0;
1224
1225 for module_data in self.modules.values() {
1226 total_functions += module_data.get_function_names().len();
1227 total_variables += module_data.get_variable_names().len();
1228 total_line_headers += module_data.get_line_header_count();
1229 }
1230
1231 AnalyzerStats {
1232 pid: self.pid,
1233 module_count: self.modules.len(),
1234 total_functions,
1235 total_variables,
1236 total_line_headers,
1237 }
1238 }
1239
1240 pub fn get_module_stats(&self) -> ModuleStats {
1242 let mut total_symbols = 0;
1243 let mut executable_modules = 0;
1244 let mut library_modules = 0;
1245 let mut modules_with_debug_info = 0;
1246
1247 for (module_path, module_data) in &self.modules {
1248 let function_names = module_data.get_function_names();
1249 total_symbols += function_names.len();
1250 if !matches!(
1251 module_data.get_debug_info_source(),
1252 DebugInfoSource::Missing
1253 ) {
1254 modules_with_debug_info += 1;
1255 }
1256
1257 if self.is_main_executable_module(module_path) {
1259 executable_modules += 1;
1260 } else {
1261 library_modules += 1;
1262 }
1263 }
1264
1265 ModuleStats {
1266 total_modules: self.modules.len(),
1267 executable_modules,
1268 library_modules,
1269 total_symbols,
1270 modules_with_debug_info,
1271 }
1272 }
1273
1274 pub fn get_main_executable(&self) -> Option<MainExecutableInfo> {
1276 for module_path in self.modules.keys() {
1278 if self.is_main_executable_module(module_path) {
1279 return Some(MainExecutableInfo {
1280 path: module_path.to_string_lossy().to_string(),
1281 });
1282 }
1283 }
1284 None
1285 }
1286
1287 fn is_main_executable_module(&self, module_path: &Path) -> bool {
1289 let filename = module_path
1291 .file_name()
1292 .and_then(|name| name.to_str())
1293 .unwrap_or("");
1294
1295 !filename.contains(".so") &&
1297 !module_path.to_string_lossy().starts_with("/lib") &&
1299 !module_path.to_string_lossy().starts_with("/usr/lib")
1300 }
1301
1302 pub fn list_functions(&self) -> Vec<String> {
1304 let mut all_functions = Vec::new();
1305
1306 for module_data in self.modules.values() {
1307 let function_names = module_data.get_function_names();
1308 for name in function_names {
1309 all_functions.push(name.clone());
1310 }
1311 }
1312
1313 all_functions.sort();
1315 all_functions.dedup();
1316
1317 tracing::debug!(
1318 "Listed {} unique functions across {} modules",
1319 all_functions.len(),
1320 self.modules.len()
1321 );
1322
1323 all_functions
1324 }
1325
1326 pub fn lookup_functions_by_pattern(&self, pattern: &str) -> Vec<String> {
1328 let all_functions = self.list_functions();
1329 all_functions
1330 .into_iter()
1331 .filter(|name| name.contains(pattern))
1332 .collect()
1333 }
1334
1335 pub fn lookup_all_function_names(&self) -> Vec<String> {
1337 self.list_functions()
1338 }
1339
1340 pub fn get_pid(&self) -> u32 {
1342 self.pid
1343 }
1344
1345 pub fn get_shared_library_info(&self) -> Vec<SharedLibraryInfo> {
1347 self.modules
1348 .iter()
1349 .filter(|(path, _)| self.is_shared_library(path))
1350 .map(|(path, module_data)| {
1351 let mapping = module_data.module_mapping();
1352 let debug_file_path = module_data
1353 .get_debug_file_path()
1354 .map(|p| p.to_string_lossy().to_string());
1355
1356 SharedLibraryInfo {
1357 from_address: mapping.loaded_address.unwrap_or(0),
1358 to_address: mapping.loaded_address.map_or(0, |addr| addr + mapping.size),
1359 symbols_read: !module_data.get_function_names().is_empty(),
1360 debug_info_available: module_data.has_dwarf_info(),
1362 library_path: path.to_string_lossy().to_string(),
1363 size: mapping.size,
1364 debug_file_path,
1365 }
1366 })
1367 .collect()
1368 }
1369
1370 pub fn get_executable_file_info(&self) -> Option<ExecutableFileInfo> {
1372 let executable = self
1374 .modules
1375 .iter()
1376 .find(|(path, _)| !self.is_shared_library(path))?;
1377
1378 let (exe_path, module_data) = executable;
1379 let file_path = exe_path.to_string_lossy().to_string();
1380
1381 let file_bytes = std::fs::read(exe_path).ok()?;
1383 let obj = object::File::parse(&file_bytes[..]).ok()?;
1384
1385 let file_type = match obj.format() {
1387 object::BinaryFormat::Elf => {
1388 if obj.is_64() {
1389 "ELF 64-bit executable"
1390 } else {
1391 "ELF 32-bit executable"
1392 }
1393 }
1394 _ => "Unknown format",
1395 }
1396 .to_string();
1397
1398 let has_symbols = !module_data.get_function_names().is_empty()
1400 || obj.symbols().count() > 0
1401 || obj.dynamic_symbols().count() > 0;
1402
1403 let has_debug_info = module_data.has_dwarf_info();
1406
1407 let debug_file_path = module_data.get_debug_file_path();
1409
1410 let load_bias = if self.pid != 0 {
1412 module_data.module_mapping().loaded_address.unwrap_or(0)
1413 } else {
1414 0
1415 };
1416
1417 let entry_point = Some(obj.entry() + load_bias);
1419
1420 let text_section = obj.section_by_name(".text").map(|section| {
1422 let addr = section.address() + load_bias;
1423 let size = section.size();
1424 SectionInfo {
1425 start_address: addr,
1426 end_address: addr + size,
1427 size,
1428 }
1429 });
1430
1431 let data_section = obj.section_by_name(".data").map(|section| {
1433 let addr = section.address() + load_bias;
1434 let size = section.size();
1435 SectionInfo {
1436 start_address: addr,
1437 end_address: addr + size,
1438 size,
1439 }
1440 });
1441
1442 let mode_description = if self.pid != 0 {
1444 format!("Attached to process {} (PID mode)", self.pid)
1445 } else {
1446 "Static analysis mode (target file specified with -t)".to_string()
1447 };
1448
1449 Some(ExecutableFileInfo {
1450 file_path,
1451 file_type,
1452 entry_point,
1453 has_symbols,
1454 has_debug_info,
1455 debug_file_path: debug_file_path.map(|p| p.to_string_lossy().to_string()),
1456 text_section,
1457 data_section,
1458 mode_description,
1459 })
1460 }
1461
1462 fn is_shared_library(&self, module_path: &Path) -> bool {
1466 let filename = module_path
1467 .file_name()
1468 .and_then(|name| name.to_str())
1469 .unwrap_or("");
1470
1471 filename.contains(".so")
1473 || module_path.to_string_lossy().starts_with("/lib")
1474 || module_path.to_string_lossy().starts_with("/usr/lib")
1475 }
1476
1477 pub fn get_grouped_file_info_by_module(&self) -> Result<Vec<(String, Vec<SimpleFileInfo>)>> {
1479 let mut grouped = Vec::new();
1480
1481 for (module_path, module_data) in &self.modules {
1482 let files = module_data.get_all_files();
1483 if !files.is_empty() {
1484 let simple_files: Vec<SimpleFileInfo> = files
1485 .into_iter()
1486 .map(|source_file| SimpleFileInfo {
1487 full_path: source_file.full_path,
1488 basename: source_file.filename,
1489 directory: source_file.directory_path,
1490 })
1491 .collect();
1492
1493 grouped.push((module_path.to_string_lossy().to_string(), simple_files));
1494 }
1495 }
1496
1497 Ok(grouped)
1498 }
1499}
1500
1501#[derive(Debug, Clone)]
1504pub struct ModuleStats {
1505 pub total_modules: usize,
1506 pub executable_modules: usize,
1507 pub library_modules: usize,
1508 pub total_symbols: usize,
1509 pub modules_with_debug_info: usize,
1510}
1511
1512#[derive(Debug, Clone)]
1514pub struct MainExecutableInfo {
1515 pub path: String,
1516}
1517
1518#[derive(Debug, Clone)]
1520pub struct AnalyzerStats {
1521 pub pid: u32,
1522 pub module_count: usize,
1523 pub total_functions: usize,
1524 pub total_variables: usize,
1525 pub total_line_headers: usize,
1526}
1527
1528#[derive(Debug, Clone)]
1530pub struct SharedLibraryInfo {
1531 pub from_address: u64, pub to_address: u64, pub symbols_read: bool, pub debug_info_available: bool, pub library_path: String, pub size: u64, pub debug_file_path: Option<String>, }
1539
1540#[derive(Debug, Clone)]
1542pub struct ExecutableFileInfo {
1543 pub file_path: String,
1544 pub file_type: String,
1545 pub entry_point: Option<u64>,
1546 pub has_symbols: bool,
1547 pub has_debug_info: bool,
1548 pub debug_file_path: Option<String>,
1549 pub text_section: Option<SectionInfo>,
1550 pub data_section: Option<SectionInfo>,
1551 pub mode_description: String,
1552}
1553
1554#[derive(Debug, Clone)]
1556pub struct SectionInfo {
1557 pub start_address: u64,
1558 pub end_address: u64,
1559 pub size: u64,
1560}
1561
1562#[derive(Debug, Clone)]
1564pub struct SimpleFileInfo {
1565 pub full_path: String,
1566 pub basename: String,
1567 pub directory: String,
1568}
1569
1570#[cfg(test)]
1571mod tests {
1572 use super::*;
1573
1574 fn global_plan(name: &str, address: u64) -> VariableReadPlan {
1575 VariableReadPlan {
1576 name: name.to_string(),
1577 type_name: "int".to_string(),
1578 access_path: crate::VariableAccessPath::default(),
1579 module_path: None,
1580 dwarf_type: Some(crate::TypeInfo::BaseType {
1581 name: "int".to_string(),
1582 size: 4,
1583 encoding: gimli::constants::DW_ATE_signed.0 as u16,
1584 }),
1585 declaration: None,
1586 type_id: None,
1587 location: VariableLocation::Address(AddressExpr::constant(address)),
1588 availability: Availability::Available,
1589 scope_depth: 0,
1590 is_parameter: false,
1591 is_artificial: false,
1592 pc_range: None,
1593 inline_context: None,
1594 provenance: Provenance::Synthesized {
1595 detail: "test".to_string(),
1596 },
1597 }
1598 }
1599
1600 fn visible_var(name: &str, scope_depth: usize) -> VisibleVariable {
1601 VisibleVariable {
1602 name: name.to_string(),
1603 type_name: "int".to_string(),
1604 dwarf_type: Some(crate::TypeInfo::BaseType {
1605 name: "int".to_string(),
1606 size: 4,
1607 encoding: gimli::constants::DW_ATE_signed.0 as u16,
1608 }),
1609 declaration: None,
1610 type_id: None,
1611 location: VariableLocation::RegisterValue { dwarf_reg: 0 },
1612 availability: Availability::Available,
1613 scope_depth,
1614 is_parameter: false,
1615 is_artificial: false,
1616 }
1617 }
1618
1619 fn diagnostic(
1620 name: &str,
1621 scope_depth: usize,
1622 detail: &str,
1623 ) -> crate::semantics::VariableQueryDiagnostic {
1624 crate::semantics::VariableQueryDiagnostic {
1625 pc: 0x1234,
1626 name: Some(name.to_string()),
1627 scope_depth,
1628 availability: Availability::Unsupported(crate::UnsupportedReason::ExpressionShape {
1629 detail: detail.to_string(),
1630 }),
1631 detail: detail.to_string(),
1632 }
1633 }
1634
1635 #[test]
1636 fn variable_selection_rejects_inner_diagnostic_over_outer_match() {
1637 let err = DwarfAnalyzer::select_visible_variable_by_name(
1638 0x1234,
1639 "state",
1640 vec![visible_var("state", 1)],
1641 &[diagnostic("state", 2, "DW_OP_bad is unsupported")],
1642 )
1643 .expect_err("inner unavailable variable should block outer fallback");
1644
1645 assert!(err.to_string().contains("Unavailable variable 'state'"));
1646 assert!(err.to_string().contains("DW_OP_bad is unsupported"));
1647 }
1648
1649 #[test]
1650 fn variable_selection_keeps_inner_match_over_outer_diagnostic() {
1651 let selected = DwarfAnalyzer::select_visible_variable_by_name(
1652 0x1234,
1653 "state",
1654 vec![visible_var("state", 2)],
1655 &[diagnostic("state", 1, "outer variable is unavailable")],
1656 )
1657 .expect("outer diagnostic should not block inner match")
1658 .expect("inner match should be returned");
1659
1660 assert_eq!(selected.name, "state");
1661 assert_eq!(selected.scope_depth, 2);
1662 }
1663
1664 #[test]
1665 fn global_plan_selection_rejects_ambiguous_matches() {
1666 let err = DwarfAnalyzer::select_unambiguous_global_plan(
1667 "state",
1668 vec![
1669 (PathBuf::from("/tmp/a"), global_plan("state", 0x1000)),
1670 (PathBuf::from("/tmp/b"), global_plan("state", 0x2000)),
1671 ],
1672 )
1673 .expect_err("multiple global candidates should be ambiguous");
1674
1675 assert!(err.to_string().contains("Ambiguous global 'state'"));
1676 assert!(err.to_string().contains("2 matches"));
1677 }
1678
1679 #[test]
1680 fn global_plan_selection_accepts_single_match() {
1681 let selected = DwarfAnalyzer::select_unambiguous_global_plan(
1682 "state",
1683 vec![(PathBuf::from("/tmp/a"), global_plan("state", 0x1000))],
1684 )
1685 .expect("single global candidate should be accepted")
1686 .expect("single global candidate should be returned");
1687
1688 assert_eq!(selected.0, PathBuf::from("/tmp/a"));
1689 assert_eq!(selected.1.name, "state");
1690 }
1691
1692 #[test]
1693 fn global_plan_selection_prefers_current_module_match() {
1694 let selected = DwarfAnalyzer::select_global_plan_with_preferred_module(
1695 "state",
1696 Path::new("/tmp/current"),
1697 vec![
1698 (PathBuf::from("/tmp/other"), global_plan("state", 0x2000)),
1699 (PathBuf::from("/tmp/current"), global_plan("state", 0x1000)),
1700 ],
1701 )
1702 .expect("current module candidate should be accepted")
1703 .expect("current module candidate should be returned");
1704
1705 assert_eq!(selected.0, PathBuf::from("/tmp/current"));
1706 assert_eq!(
1707 selected.1.location,
1708 VariableLocation::Address(AddressExpr::constant(0x1000))
1709 );
1710 }
1711
1712 #[test]
1713 fn global_plan_selection_rejects_ambiguous_current_module_matches() {
1714 let err = DwarfAnalyzer::select_global_plan_with_preferred_module(
1715 "state",
1716 Path::new("/tmp/current"),
1717 vec![
1718 (PathBuf::from("/tmp/current"), global_plan("state", 0x1000)),
1719 (PathBuf::from("/tmp/current"), global_plan("state", 0x1004)),
1720 (PathBuf::from("/tmp/other"), global_plan("state", 0x2000)),
1721 ],
1722 )
1723 .expect_err("duplicate current-module candidates should be ambiguous");
1724
1725 assert!(err.to_string().contains("Ambiguous global 'state'"));
1726 assert!(err.to_string().contains("2 matches"));
1727 assert!(err.to_string().contains("/tmp/current"));
1728 assert!(!err.to_string().contains("/tmp/other"));
1729 }
1730}