Skip to main content

gsym/convert/elf/
mod.rs

1mod discovery;
2mod image;
3
4use std::fmt;
5use std::path::{Path, PathBuf};
6use std::sync::{Arc, OnceLock};
7
8use object::Object;
9
10use self::discovery::{
11    Discovery, DiscoveryCache, discover_dwp, discover_separate_debug, discover_supplementary,
12    validated_gnu_debugdata,
13};
14pub(in crate::convert) use self::image::AddressLayout;
15use self::image::{
16    cross_check_elf_architecture, cross_check_elf_identity, malformed, parse_elf,
17    require_supported_kind,
18};
19use super::ConversionWarning;
20use crate::builder::{BuilderOptions, GsymBuilder};
21use crate::{ElfInputKind, Endian, Error, Result, WriterOptions};
22
23/// ELF inputs used to construct a GSYM file.
24///
25/// Only [`image`](Self::image) is required. Leaving a companion as `None` makes
26/// the converter fall back to the image itself for symbols and DWARF, so a
27/// single unstripped binary needs nothing else.
28///
29/// Passing inputs this way skips discovery entirely: nothing is read from the
30/// filesystem or the network. Use
31/// [`ElfConverter::convert_path`] when companion files should be searched for.
32///
33/// ```no_run
34/// use gsym::convert::ElfInputs;
35///
36/// let image = std::fs::read("app")?;
37/// let debug = std::fs::read("app.debug")?;
38/// let inputs = ElfInputs::new(&image).with_debug(&debug);
39/// # let _ = inputs;
40/// # Ok::<(), gsym::Error>(())
41/// ```
42#[derive(Clone, Copy, Eq, PartialEq)]
43pub struct ElfInputs<'data> {
44    /// Linked `ET_EXEC`, `ET_DYN`, or relocatable `ET_REL` ELF image.
45    pub image: &'data [u8],
46    /// Explicit separate DWARF ELF, if different from the image.
47    pub debug: Option<&'data [u8]>,
48    /// Extra symbol-table ELF read alongside the image's and debug input's own
49    /// tables.
50    pub symbols: Option<&'data [u8]>,
51    /// Supplementary DWARF ELF referenced by the main debug input.
52    pub supplementary: Option<&'data [u8]>,
53    /// Packaged split-DWARF input.
54    pub dwp: Option<&'data [u8]>,
55}
56
57impl fmt::Debug for ElfInputs<'_> {
58    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
59        formatter
60            .debug_struct("ElfInputs")
61            .field("image_len", &self.image.len())
62            .field("debug_len", &self.debug.map(<[u8]>::len))
63            .field("symbols_len", &self.symbols.map(<[u8]>::len))
64            .field("supplementary_len", &self.supplementary.map(<[u8]>::len))
65            .field("dwp_len", &self.dwp.map(<[u8]>::len))
66            .finish()
67    }
68}
69
70impl<'data> ElfInputs<'data> {
71    /// Creates inputs using one ELF for the image, symbols, and DWARF.
72    #[must_use]
73    pub const fn new(image: &'data [u8]) -> Self {
74        Self {
75            image,
76            debug: None,
77            symbols: None,
78            supplementary: None,
79            dwp: None,
80        }
81    }
82
83    /// Uses an explicit ELF containing the image's DWARF sections.
84    #[must_use]
85    pub const fn with_debug(mut self, debug: &'data [u8]) -> Self {
86        self.debug = Some(debug);
87        self
88    }
89
90    /// Adds an explicit ELF symbol table to the ones already imported.
91    #[must_use]
92    pub const fn with_symbols(mut self, symbols: &'data [u8]) -> Self {
93        self.symbols = Some(symbols);
94        self
95    }
96
97    /// Supplies the supplementary DWARF ELF referenced by the main debug input.
98    #[must_use]
99    pub const fn with_supplementary(mut self, supplementary: &'data [u8]) -> Self {
100        self.supplementary = Some(supplementary);
101        self
102    }
103
104    /// Supplies a packaged split-DWARF file.
105    #[must_use]
106    pub const fn with_dwp(mut self, dwp: &'data [u8]) -> Self {
107        self.dwp = Some(dwp);
108        self
109    }
110}
111
112/// Controls ELF/DWARF import and optional companion discovery.
113///
114/// Build from [`Default`] and adjust the fields you care about. The defaults
115/// import symbols and DWARF with inline information, search for companion debug
116/// files, and read two environment variables: `DEBUGINFOD_URLS` for the
117/// servers to try, which is empty unless set, and `DEBUGINFOD_CACHE_PATH` for
118/// the download cache. Clear
119/// [`debuginfod_urls`](Self::debuginfod_urls) or set
120/// [`discovery`](Self::discovery) to [`DiscoveryPolicy::Disabled`] for a
121/// conversion that must not touch the network.
122///
123/// [`writer`](Self::writer) selects the output version, but its byte order,
124/// base address, and build ID are overwritten from the image.
125///
126/// See [`docs::conversion`](crate::docs::conversion) for the discovery order
127/// and what each limit bounds.
128#[derive(Clone, Debug, Eq, PartialEq)]
129pub struct ConversionOptions {
130    /// GSYM writer settings.
131    pub writer: WriterOptions,
132    /// Import ELF `STT_FUNC` symbols.
133    pub include_symbols: bool,
134    /// DWARF import settings, or `None` to disable DWARF import.
135    pub dwarf: Option<DwarfImportOptions>,
136    /// Whether path conversion searches for companion debug files.
137    pub discovery: DiscoveryPolicy,
138    /// Roots searched for build-ID and mirrored-path debug files.
139    pub debug_directories: Vec<PathBuf>,
140    /// Debuginfod server URLs tried in order.
141    pub debuginfod_urls: Vec<String>,
142    /// Local debuginfod cache root.
143    pub debuginfod_cache: PathBuf,
144    /// Maximum accepted debuginfod response size in bytes.
145    pub debuginfod_max_download_size: u64,
146    /// Maximum decompressed `.gnu_debugdata` size in bytes.
147    pub gnu_debugdata_max_decompressed_size: u64,
148}
149
150/// Selects optional DWARF records to import.
151///
152/// Inline information is on by default because it is what distinguishes GSYM
153/// from a symbol table. Call sites are off, matching `llvm-gsymutil`.
154///
155/// Line rows are always imported when DWARF import is enabled. To skip DWARF
156/// altogether, set [`ConversionOptions::dwarf`] to `None`.
157#[derive(Clone, Copy, Debug, Eq, PartialEq)]
158pub struct DwarfImportOptions {
159    /// Import inline-call trees.
160    pub inline_info: bool,
161    /// Import DWARF call-site records.
162    pub call_sites: bool,
163}
164
165impl Default for DwarfImportOptions {
166    fn default() -> Self {
167        Self {
168            inline_info: true,
169            call_sites: false,
170        }
171    }
172}
173
174/// Companion-file discovery policy for path-based conversion.
175///
176/// Applies to [`ElfConverter::convert_path`] only; [`ElfConverter::convert`]
177/// never searches. `Disabled` prevents filesystem and network searches for
178/// separate debug files, supplementary files, and split-DWARF data.
179#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
180#[non_exhaustive]
181pub enum DiscoveryPolicy {
182    /// Convert only the requested image path.
183    Disabled,
184    #[default]
185    /// Search separate, supplementary, split-DWARF, and remote debug sources.
186    Enabled,
187}
188
189/// A potentially slow operation reported during path-based debug discovery.
190///
191/// Pass an observer to [`ElfConverter::convert_path_with_observer`] to surface
192/// network activity in interactive tools.
193#[derive(Clone, Copy, Debug, Eq, PartialEq)]
194#[non_exhaustive]
195pub enum DiscoveryEvent<'path> {
196    /// A debuginfod request is about to be issued.
197    DebuginfodRequest {
198        /// Kind of companion being requested, such as `debug file`.
199        artifact: &'static str,
200        /// Hexadecimal ELF build ID.
201        build_id: &'path str,
202        /// Debuginfod server base URL.
203        endpoint: &'path str,
204        /// Image or debug file whose companion is being requested.
205        related_path: &'path Path,
206    },
207}
208
209impl Default for ConversionOptions {
210    fn default() -> Self {
211        Self {
212            writer: WriterOptions::default(),
213            include_symbols: true,
214            dwarf: Some(DwarfImportOptions::default()),
215            discovery: DiscoveryPolicy::Enabled,
216            debug_directories: vec![PathBuf::from("/usr/lib/debug")],
217            debuginfod_urls: std::env::var("DEBUGINFOD_URLS")
218                .ok()
219                .map(|urls| urls.split_whitespace().map(str::to_owned).collect())
220                .unwrap_or_default(),
221            debuginfod_cache: std::env::var_os("DEBUGINFOD_CACHE_PATH").map_or_else(
222                || std::env::temp_dir().join("gsym-rs-debuginfod"),
223                PathBuf::from,
224            ),
225            debuginfod_max_download_size: 1 << 30,
226            gnu_debugdata_max_decompressed_size: 1 << 30,
227        }
228    }
229}
230
231/// Counts describing one completed conversion.
232///
233/// `symbol_functions` and `dwarf_functions` overlap: a function present in both
234/// the symbol table and the DWARF is counted once in each, and the builder keeps
235/// the richer record. `rejected_ranges` includes expected linker tombstones as
236/// well as invalid or unrepresentable input ranges.
237#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
238#[non_exhaustive]
239pub struct ConversionStats {
240    /// Distinct functions imported from ELF symbol tables.
241    pub symbol_functions: usize,
242    /// Functions imported from DWARF DIEs.
243    pub dwarf_functions: usize,
244    /// Skeleton units whose matching DWO or DWP data was imported.
245    pub split_dwarf_units: usize,
246    /// Source-line rows attached to imported functions.
247    pub line_rows: usize,
248    /// Inline nodes attached to imported functions.
249    pub inline_nodes: usize,
250    /// Dead, invalid, or unrepresentable address ranges rejected.
251    ///
252    /// Expected zero, `-1`, or `-2` linker tombstones are counted here but do not
253    /// produce individual conversion warnings.
254    pub rejected_ranges: usize,
255}
256
257/// Successful conversion output plus diagnostics and provenance.
258///
259/// [`builder`](Self::builder) is ready to encode, or to edit first. The
260/// `discovered_*` paths record which companion files were selected and are
261/// `None` when the input supplied them or none were found, which makes them
262/// worth logging when a conversion produces unexpected output.
263///
264/// A report can carry warnings and still be complete;
265/// [`warnings`](Self::warnings) describes records that were skipped, not a
266/// failed conversion.
267#[non_exhaustive]
268pub struct ConversionReport {
269    /// Populated builder ready for encoding or further modification.
270    pub builder: GsymBuilder,
271    /// Counts of imported and rejected records.
272    pub stats: ConversionStats,
273    /// Non-fatal issues encountered during conversion.
274    pub warnings: Vec<ConversionWarning>,
275    /// Automatically selected main debug file.
276    pub discovered_debug: Option<PathBuf>,
277    /// Automatically selected supplementary debug file.
278    pub discovered_supplementary: Option<PathBuf>,
279    /// Automatically selected DWP package.
280    pub discovered_dwp: Option<PathBuf>,
281}
282
283impl fmt::Debug for ConversionReport {
284    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
285        formatter
286            .debug_struct("ConversionReport")
287            .field("builder", &self.builder)
288            .field("stats", &self.stats)
289            .field("warning_count", &self.warnings.len())
290            .field("discovered_debug", &self.discovered_debug)
291            .field("discovered_supplementary", &self.discovered_supplementary)
292            .field("discovered_dwp", &self.discovered_dwp)
293            .finish_non_exhaustive()
294    }
295}
296
297/// Reusable ELF-to-GSYM converter with immutable options.
298///
299/// Options are fixed at construction, so one converter can be shared across
300/// many images. [`Default`] uses [`ConversionOptions::default`].
301///
302/// ```no_run
303/// use gsym::convert::ElfConverter;
304///
305/// let report = ElfConverter::default().convert_path("./app")?;
306/// for warning in &report.warnings {
307///     eprintln!("warning: {warning}");
308/// }
309/// report.builder.write_to(std::fs::File::create("app.gsym")?)?;
310/// # Ok::<(), gsym::Error>(())
311/// ```
312#[derive(Clone, Debug, Default)]
313pub struct ElfConverter {
314    options: ConversionOptions,
315    discovery_cache: OnceLock<Arc<DiscoveryCache>>,
316}
317
318impl PartialEq for ElfConverter {
319    fn eq(&self, other: &Self) -> bool {
320        self.options == other.options
321    }
322}
323
324impl Eq for ElfConverter {}
325
326impl ElfConverter {
327    /// Creates a converter using `options`.
328    #[must_use]
329    pub const fn new(options: ConversionOptions) -> Self {
330        Self {
331            options,
332            discovery_cache: OnceLock::new(),
333        }
334    }
335
336    /// Returns this converter's immutable options.
337    #[must_use]
338    pub const fn options(&self) -> &ConversionOptions {
339        &self.options
340    }
341
342    /// Converts linked ELF image/debug inputs into a GSYM builder.
343    ///
344    /// Uses exactly the inputs given, with no filesystem or network access.
345    /// Companions supplied here are cross-checked against the image, so a debug
346    /// or symbol file from a different architecture or build is rejected.
347    ///
348    /// # Errors
349    ///
350    /// Returns an error for malformed inputs, incompatible companion files,
351    /// invalid DWARF, or unrepresentable GSYM records.
352    pub fn convert(&self, inputs: ElfInputs<'_>) -> Result<ConversionReport> {
353        self.convert_inner(inputs, super::dwarf::DwoResolver::Disabled)
354    }
355
356    fn convert_inner(
357        &self,
358        inputs: ElfInputs<'_>,
359        dwo_resolver: super::dwarf::DwoResolver<'_>,
360    ) -> Result<ConversionReport> {
361        let image = parse_elf(inputs.image, ElfInputKind::Image)?;
362        require_supported_kind(&image)?;
363
364        let layout = AddressLayout::new(&image)?;
365        let base_address = layout.ranges.first().map_or(0, |range| range.start);
366        if layout.ranges.is_empty() {
367            return Err(Error::InvalidModel("ELF image has no executable sections"));
368        }
369
370        let build_id = image
371            .build_id()
372            .map_err(|error| malformed("ELF build ID", error))?
373            .unwrap_or_default()
374            .to_vec();
375        let mut warnings = Vec::new();
376        let mini_debug = if inputs.debug.is_none()
377            && (self.options.include_symbols || self.options.dwarf.is_some())
378        {
379            validated_gnu_debugdata(
380                &image,
381                self.options.gnu_debugdata_max_decompressed_size,
382                &mut warnings,
383            )
384        } else {
385            None
386        };
387        let debug_bytes = inputs
388            .debug
389            .or(mini_debug.as_deref())
390            .unwrap_or(inputs.image);
391        let separate_debug = inputs.debug.is_some() || mini_debug.is_some();
392        let debug = parse_elf(debug_bytes, ElfInputKind::Debug)?;
393        if inputs.debug.is_some() {
394            cross_check_elf_identity(&image, &debug, ElfInputKind::Debug)?;
395        }
396        let supplementary = inputs
397            .supplementary
398            .map(|bytes| parse_elf(bytes, ElfInputKind::Supplementary))
399            .transpose()?;
400        let dwp = inputs
401            .dwp
402            .map(|bytes| parse_elf(bytes, ElfInputKind::Dwp))
403            .transpose()?;
404        if let Some(dwp) = &dwp {
405            cross_check_elf_architecture(&image, dwp, ElfInputKind::Dwp)?;
406        }
407
408        let mut writer = self.options.writer.clone();
409        writer.endian = if image.is_little_endian() {
410            Endian::Little
411        } else {
412            Endian::Big
413        };
414        writer.base_address = Some(base_address);
415        if writer.build_id.is_empty() {
416            writer.build_id = build_id;
417        }
418        let mut builder = GsymBuilder::with_options(BuilderOptions {
419            writer,
420            executable_ranges: layout.ranges.clone().into_boxed_slice(),
421            ..BuilderOptions::default()
422        });
423        let mut stats = ConversionStats::default();
424
425        if self.options.include_symbols {
426            let symbol_file = inputs
427                .symbols
428                .map(|bytes| parse_elf(bytes, ElfInputKind::Symbols))
429                .transpose()?;
430            if let Some(symbol_file) = &symbol_file {
431                cross_check_elf_identity(&image, symbol_file, ElfInputKind::Symbols)?;
432            }
433            let mut sources: Vec<(&[u8], &object::File<'_>)> = Vec::with_capacity(3);
434            if let (Some(bytes), Some(file)) = (inputs.symbols, symbol_file.as_ref()) {
435                sources.push((bytes, file));
436            }
437            if separate_debug {
438                sources.push((debug_bytes, &debug));
439            }
440            sources.push((inputs.image, &image));
441            stats.symbol_functions = import_distinct_symbols(
442                &sources,
443                &layout,
444                &mut builder,
445                &mut stats.rejected_ranges,
446            )?;
447        }
448        if let Some(dwarf_options) = self.options.dwarf {
449            super::dwarf::import_dwarf(super::dwarf::DwarfImport {
450                file: &debug,
451                supplementary: supplementary.as_ref(),
452                dwp: dwp.as_ref(),
453                dwo_resolver,
454                layout: &layout,
455                executable_ranges: &layout.ranges,
456                builder: &mut builder,
457                include_inlines: dwarf_options.inline_info,
458                include_call_sites: dwarf_options.call_sites,
459                stats: &mut stats,
460                warnings: &mut warnings,
461            })?;
462        }
463
464        Ok(ConversionReport {
465            builder,
466            stats,
467            warnings,
468            discovered_debug: None,
469            discovered_supplementary: None,
470            discovered_dwp: None,
471        })
472    }
473
474    /// Converts an ELF path and discovers supported companion debug files.
475    ///
476    /// Searches for companion debug information unless
477    /// [`DiscoveryPolicy::Disabled`] is set. The chosen paths are reported in
478    /// [`ConversionReport::discovered_debug`] and its siblings, and problems
479    /// encountered while searching appear as warnings rather than errors.
480    ///
481    /// With debuginfod configured, this may perform network requests. See
482    /// [`docs::conversion`](crate::docs::conversion).
483    ///
484    /// # Errors
485    ///
486    /// Returns an error when an input cannot be read or converted.
487    pub fn convert_path(&self, image_path: impl AsRef<Path>) -> Result<ConversionReport> {
488        self.convert_path_with_observer(image_path, |_| {})
489    }
490
491    /// Converts an ELF path while reporting potentially slow discovery work.
492    ///
493    /// The observer is called immediately before each debuginfod request. It
494    /// may be called more than once when several servers or companions are
495    /// considered.
496    ///
497    /// # Errors
498    ///
499    /// Returns an error when an input cannot be read or converted.
500    pub fn convert_path_with_observer(
501        &self,
502        image_path: impl AsRef<Path>,
503        mut observer: impl FnMut(DiscoveryEvent<'_>),
504    ) -> Result<ConversionReport> {
505        let image_path = image_path.as_ref();
506        let image_bytes = read_file(image_path, "read ELF image")?;
507        let discovery_cache = self.discovery_cache.get_or_init(Arc::default);
508        let discovery_enabled = self.options.discovery == DiscoveryPolicy::Enabled;
509        let companion_discovery_enabled =
510            discovery_enabled && (self.options.include_symbols || self.options.dwarf.is_some());
511        let discovery = if companion_discovery_enabled {
512            discover_separate_debug(
513                image_path,
514                &image_bytes,
515                &self.options,
516                discovery_cache,
517                &mut observer,
518            )?
519        } else {
520            Discovery::default()
521        };
522        let debug_path = discovery
523            .artifact
524            .as_ref()
525            .map_or(image_path, |artifact| artifact.path.as_path());
526        let debug_source = discovery
527            .artifact
528            .as_ref()
529            .map_or(image_bytes.as_slice(), |artifact| artifact.bytes.as_slice());
530        let dwarf_discovery_enabled = discovery_enabled && self.options.dwarf.is_some();
531        let supplementary_discovery = if dwarf_discovery_enabled {
532            discover_supplementary(
533                debug_path,
534                debug_source,
535                &self.options,
536                discovery_cache,
537                &mut observer,
538            )?
539        } else {
540            Discovery::default()
541        };
542        let dwp = dwarf_discovery_enabled
543            .then(|| discover_dwp(image_path, debug_path))
544            .flatten();
545        let dwo_resolver = if dwarf_discovery_enabled {
546            debug_path
547                .parent()
548                .map_or(super::dwarf::DwoResolver::Disabled, |base| {
549                    super::dwarf::DwoResolver::Filesystem { base }
550                })
551        } else {
552            super::dwarf::DwoResolver::Disabled
553        };
554        let mut report = self.convert_inner(
555            ElfInputs {
556                image: &image_bytes,
557                debug: discovery
558                    .artifact
559                    .as_ref()
560                    .map(|artifact| artifact.bytes.as_slice()),
561                symbols: None,
562                supplementary: supplementary_discovery
563                    .artifact
564                    .as_ref()
565                    .map(|artifact| artifact.bytes.as_slice()),
566                dwp: dwp.as_ref().map(|artifact| artifact.bytes.as_slice()),
567            },
568            dwo_resolver,
569        )?;
570        report.warnings.extend(discovery.warnings);
571        report.warnings.extend(supplementary_discovery.warnings);
572        report.discovered_debug = discovery.artifact.map(|artifact| artifact.path);
573        report.discovered_supplementary = supplementary_discovery
574            .artifact
575            .map(|artifact| artifact.path);
576        report.discovered_dwp = dwp.map(|artifact| artifact.path);
577        Ok(report)
578    }
579}
580
581fn import_distinct_symbols(
582    sources: &[(&[u8], &object::File<'_>)],
583    layout: &AddressLayout,
584    builder: &mut GsymBuilder,
585    rejected: &mut usize,
586) -> Result<usize> {
587    let mut visited: Vec<&[u8]> = Vec::with_capacity(sources.len());
588    let mut unique = Vec::with_capacity(sources.len());
589    for &(bytes, file) in sources {
590        if !visited.iter().any(|other| std::ptr::eq(*other, bytes)) {
591            visited.push(bytes);
592            unique.push(file);
593        }
594    }
595
596    if let [file] = unique.as_slice() {
597        let mut imported = 0_usize;
598        return image::visit_symbols(file, layout, |function, disposition| {
599            match disposition {
600                image::SymbolDisposition::Import => {
601                    builder.add_function(function)?;
602                    imported = imported.saturating_add(1);
603                }
604                image::SymbolDisposition::Reject => {
605                    *rejected = rejected.saturating_add(1);
606                }
607            }
608            Ok(())
609        })
610        .map(|()| imported);
611    }
612
613    let mut accepted = Vec::new();
614    let mut skipped = Vec::new();
615    for file in unique {
616        image::visit_symbols(file, layout, |function, disposition| {
617            match disposition {
618                image::SymbolDisposition::Import => accepted.push(function),
619                image::SymbolDisposition::Reject => skipped.push(function),
620            }
621            Ok(())
622        })?;
623    }
624    accepted.sort_unstable();
625    accepted.dedup();
626    skipped.sort_unstable();
627    skipped.dedup();
628    skipped.retain(|function| accepted.binary_search(function).is_err());
629
630    let imported = accepted.len();
631    *rejected = rejected.saturating_add(skipped.len());
632    for function in accepted {
633        builder.add_function(function)?;
634    }
635    Ok(imported)
636}
637
638fn read_file(path: &Path, description: &'static str) -> Result<Vec<u8>> {
639    std::fs::read(path).map_err(|source| Error::IoAtPath {
640        operation: description,
641        path: path.to_path_buf(),
642        source,
643    })
644}