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 three 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` turns off the search for a separate debug file
178/// and for a `.dwp` package. A `.gnu_debugaltlink` reference inside the debug
179/// data is still resolved, so a conversion that must not reach the network also
180/// needs [`ConversionOptions::debuginfod_urls`] cleared.
181#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
182#[non_exhaustive]
183pub enum DiscoveryPolicy {
184    /// Convert only the requested image path.
185    Disabled,
186    #[default]
187    /// Search debug links, build-ID roots, split-DWARF paths, and debuginfod.
188    Enabled,
189}
190
191/// A potentially slow operation reported during path-based debug discovery.
192///
193/// Pass an observer to [`ElfConverter::convert_path_with_observer`] to surface
194/// network activity in interactive tools.
195#[derive(Clone, Copy, Debug, Eq, PartialEq)]
196#[non_exhaustive]
197pub enum DiscoveryEvent<'path> {
198    /// A debuginfod request is about to be issued.
199    DebuginfodRequest {
200        /// Kind of companion being requested, such as `debug file`.
201        artifact: &'static str,
202        /// Hexadecimal ELF build ID.
203        build_id: &'path str,
204        /// Debuginfod server base URL.
205        endpoint: &'path str,
206        /// Image or debug file whose companion is being requested.
207        related_path: &'path Path,
208    },
209}
210
211impl Default for ConversionOptions {
212    fn default() -> Self {
213        Self {
214            writer: WriterOptions::default(),
215            include_symbols: true,
216            dwarf: Some(DwarfImportOptions::default()),
217            discovery: DiscoveryPolicy::Enabled,
218            debug_directories: vec![PathBuf::from("/usr/lib/debug")],
219            debuginfod_urls: std::env::var("DEBUGINFOD_URLS")
220                .ok()
221                .map(|urls| urls.split_whitespace().map(str::to_owned).collect())
222                .unwrap_or_default(),
223            debuginfod_cache: std::env::var_os("DEBUGINFOD_CACHE_PATH").map_or_else(
224                || std::env::temp_dir().join("gsym-rs-debuginfod"),
225                PathBuf::from,
226            ),
227            debuginfod_max_download_size: 1 << 30,
228            gnu_debugdata_max_decompressed_size: 1 << 30,
229        }
230    }
231}
232
233/// Counts describing one completed conversion.
234///
235/// `symbol_functions` and `dwarf_functions` overlap: a function present in both
236/// the symbol table and the DWARF is counted once in each, and the builder keeps
237/// the richer record. `rejected_ranges` includes expected linker tombstones as
238/// well as invalid or unrepresentable input ranges.
239#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
240#[non_exhaustive]
241pub struct ConversionStats {
242    /// Distinct functions imported from ELF symbol tables.
243    pub symbol_functions: usize,
244    /// Functions imported from DWARF DIEs.
245    pub dwarf_functions: 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, None)
354    }
355
356    fn convert_inner(
357        &self,
358        inputs: ElfInputs<'_>,
359        dwo_base: Option<&Path>,
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_base,
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 separate debug information unless
477    /// [`DiscoveryPolicy::Disabled`] is set, then converts the image together
478    /// with whatever was found. The chosen paths are reported in
479    /// [`ConversionReport::discovered_debug`] and its siblings, and problems
480    /// encountered while searching appear as warnings rather than errors.
481    ///
482    /// With debuginfod configured, this may perform network requests. See
483    /// [`docs::conversion`](crate::docs::conversion).
484    ///
485    /// # Errors
486    ///
487    /// Returns an error when an input cannot be read or converted.
488    pub fn convert_path(&self, image_path: impl AsRef<Path>) -> Result<ConversionReport> {
489        self.convert_path_with_observer(image_path, |_| {})
490    }
491
492    /// Converts an ELF path while reporting potentially slow discovery work.
493    ///
494    /// The observer is called immediately before each debuginfod request. It
495    /// may be called more than once when several servers or companions are
496    /// considered.
497    ///
498    /// # Errors
499    ///
500    /// Returns an error when an input cannot be read or converted.
501    pub fn convert_path_with_observer(
502        &self,
503        image_path: impl AsRef<Path>,
504        mut observer: impl FnMut(DiscoveryEvent<'_>),
505    ) -> Result<ConversionReport> {
506        let image_path = image_path.as_ref();
507        let image_bytes = read_file(image_path, "read ELF image")?;
508        let discovery_cache = self.discovery_cache.get_or_init(Arc::default);
509        let discovery = if self.options.discovery == DiscoveryPolicy::Enabled {
510            discover_separate_debug(
511                image_path,
512                &image_bytes,
513                &self.options,
514                discovery_cache,
515                &mut observer,
516            )?
517        } else {
518            Discovery::default()
519        };
520        let debug_path = discovery
521            .artifact
522            .as_ref()
523            .map_or(image_path, |artifact| artifact.path.as_path());
524        let debug_source = discovery
525            .artifact
526            .as_ref()
527            .map_or(image_bytes.as_slice(), |artifact| artifact.bytes.as_slice());
528        let supplementary_discovery = discover_supplementary(
529            debug_path,
530            debug_source,
531            &self.options,
532            discovery_cache,
533            &mut observer,
534        )?;
535        let dwp = (self.options.discovery == DiscoveryPolicy::Enabled)
536            .then(|| discover_dwp(image_path, debug_path))
537            .flatten();
538        let dwo_base = discovery
539            .artifact
540            .as_ref()
541            .map(|artifact| artifact.path.as_path())
542            .or(Some(image_path))
543            .and_then(Path::parent);
544        let mut report = self.convert_inner(
545            ElfInputs {
546                image: &image_bytes,
547                debug: discovery
548                    .artifact
549                    .as_ref()
550                    .map(|artifact| artifact.bytes.as_slice()),
551                symbols: None,
552                supplementary: supplementary_discovery
553                    .artifact
554                    .as_ref()
555                    .map(|artifact| artifact.bytes.as_slice()),
556                dwp: dwp.as_ref().map(|artifact| artifact.bytes.as_slice()),
557            },
558            dwo_base,
559        )?;
560        report.warnings.extend(discovery.warnings);
561        report.warnings.extend(supplementary_discovery.warnings);
562        report.discovered_debug = discovery.artifact.map(|artifact| artifact.path);
563        report.discovered_supplementary = supplementary_discovery
564            .artifact
565            .map(|artifact| artifact.path);
566        report.discovered_dwp = dwp.map(|artifact| artifact.path);
567        Ok(report)
568    }
569}
570
571fn import_distinct_symbols(
572    sources: &[(&[u8], &object::File<'_>)],
573    layout: &AddressLayout,
574    builder: &mut GsymBuilder,
575    rejected: &mut usize,
576) -> Result<usize> {
577    let mut visited: Vec<&[u8]> = Vec::with_capacity(sources.len());
578    let mut unique = Vec::with_capacity(sources.len());
579    for &(bytes, file) in sources {
580        if !visited.iter().any(|other| std::ptr::eq(*other, bytes)) {
581            visited.push(bytes);
582            unique.push(file);
583        }
584    }
585
586    if let [file] = unique.as_slice() {
587        let mut imported = 0_usize;
588        return image::visit_symbols(file, layout, |function, disposition| {
589            match disposition {
590                image::SymbolDisposition::Import => {
591                    builder.add_function(function)?;
592                    imported = imported.saturating_add(1);
593                }
594                image::SymbolDisposition::Reject => {
595                    *rejected = rejected.saturating_add(1);
596                }
597            }
598            Ok(())
599        })
600        .map(|()| imported);
601    }
602
603    let mut accepted = Vec::new();
604    let mut skipped = Vec::new();
605    for file in unique {
606        image::visit_symbols(file, layout, |function, disposition| {
607            match disposition {
608                image::SymbolDisposition::Import => accepted.push(function),
609                image::SymbolDisposition::Reject => skipped.push(function),
610            }
611            Ok(())
612        })?;
613    }
614    accepted.sort_unstable();
615    accepted.dedup();
616    skipped.sort_unstable();
617    skipped.dedup();
618    skipped.retain(|function| accepted.binary_search(function).is_err());
619
620    let imported = accepted.len();
621    *rejected = rejected.saturating_add(skipped.len());
622    for function in accepted {
623        builder.add_function(function)?;
624    }
625    Ok(imported)
626}
627
628fn read_file(path: &Path, description: &'static str) -> Result<Vec<u8>> {
629    std::fs::read(path).map_err(|source| Error::IoAtPath {
630        operation: description,
631        path: path.to_path_buf(),
632        source,
633    })
634}