Skip to main content

gsym/convert/elf/
mod.rs

1mod discovery;
2mod image;
3
4use std::fmt;
5use std::path::{Path, PathBuf};
6
7use object::Object;
8
9use self::discovery::{
10    Discovery, discover_dwp, discover_separate_debug, discover_supplementary,
11    validated_gnu_debugdata,
12};
13pub(in crate::convert) use self::image::AddressLayout;
14use self::image::{
15    cross_check_elf_architecture, cross_check_elf_identity, import_symbols, malformed, parse_elf,
16    require_supported_kind,
17};
18use super::ConversionWarning;
19use crate::builder::{BuilderOptions, GsymBuilder};
20use crate::{ElfInputKind, Endian, Error, Result, WriterOptions};
21
22/// ELF inputs used to construct a GSYM file.
23///
24/// Only [`image`](Self::image) is required. Leaving a companion as `None` makes
25/// the converter fall back to the image itself for symbols and DWARF, so a
26/// single unstripped binary needs nothing else.
27///
28/// Passing inputs this way skips discovery entirely: nothing is read from the
29/// filesystem or the network. Use
30/// [`ElfConverter::convert_path`] when companion files should be searched for.
31///
32/// ```no_run
33/// use gsym::convert::ElfInputs;
34///
35/// let image = std::fs::read("app")?;
36/// let debug = std::fs::read("app.debug")?;
37/// let inputs = ElfInputs::new(&image).with_debug(&debug);
38/// # let _ = inputs;
39/// # Ok::<(), gsym::Error>(())
40/// ```
41#[derive(Clone, Copy, Eq, PartialEq)]
42pub struct ElfInputs<'data> {
43    /// Linked `ET_EXEC`, `ET_DYN`, or relocatable `ET_REL` ELF image.
44    pub image: &'data [u8],
45    /// Explicit separate DWARF ELF, if different from the image.
46    pub debug: Option<&'data [u8]>,
47    /// Extra symbol-table ELF read alongside the image's own tables.
48    pub symbols: Option<&'data [u8]>,
49    /// Supplementary DWARF ELF referenced by the main debug input.
50    pub supplementary: Option<&'data [u8]>,
51    /// Packaged split-DWARF input.
52    pub dwp: Option<&'data [u8]>,
53}
54
55impl fmt::Debug for ElfInputs<'_> {
56    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
57        formatter
58            .debug_struct("ElfInputs")
59            .field("image_len", &self.image.len())
60            .field("debug_len", &self.debug.map(<[u8]>::len))
61            .field("symbols_len", &self.symbols.map(<[u8]>::len))
62            .field("supplementary_len", &self.supplementary.map(<[u8]>::len))
63            .field("dwp_len", &self.dwp.map(<[u8]>::len))
64            .finish()
65    }
66}
67
68impl<'data> ElfInputs<'data> {
69    /// Creates inputs using one ELF for the image, symbols, and DWARF.
70    #[must_use]
71    pub const fn new(image: &'data [u8]) -> Self {
72        Self {
73            image,
74            debug: None,
75            symbols: None,
76            supplementary: None,
77            dwp: None,
78        }
79    }
80
81    /// Uses an explicit ELF containing the image's DWARF sections.
82    #[must_use]
83    pub const fn with_debug(mut self, debug: &'data [u8]) -> Self {
84        self.debug = Some(debug);
85        self
86    }
87
88    /// Uses an explicit ELF symbol table instead of the selected debug input.
89    ///
90    /// The image's own tables are read either way.
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
191impl Default for ConversionOptions {
192    fn default() -> Self {
193        Self {
194            writer: WriterOptions::default(),
195            include_symbols: true,
196            dwarf: Some(DwarfImportOptions::default()),
197            discovery: DiscoveryPolicy::Enabled,
198            debug_directories: vec![PathBuf::from("/usr/lib/debug")],
199            debuginfod_urls: std::env::var("DEBUGINFOD_URLS")
200                .ok()
201                .map(|urls| urls.split_whitespace().map(str::to_owned).collect())
202                .unwrap_or_default(),
203            debuginfod_cache: std::env::var_os("DEBUGINFOD_CACHE_PATH").map_or_else(
204                || std::env::temp_dir().join("gsym-rs-debuginfod"),
205                PathBuf::from,
206            ),
207            debuginfod_max_download_size: 1 << 30,
208            gnu_debugdata_max_decompressed_size: 1 << 30,
209        }
210    }
211}
212
213/// Counts describing one completed conversion.
214///
215/// `symbol_functions` and `dwarf_functions` overlap: a function present in both
216/// the symbol table and the DWARF is counted once in each, and the builder keeps
217/// the richer record. `rejected_ranges` is the number to watch, since a large
218/// fraction usually means the DWARF does not match the image.
219#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
220#[non_exhaustive]
221pub struct ConversionStats {
222    /// Functions imported from ELF symbol tables.
223    pub symbol_functions: usize,
224    /// Functions imported from DWARF DIEs.
225    pub dwarf_functions: usize,
226    /// Source-line rows attached to imported functions.
227    pub line_rows: usize,
228    /// Inline nodes attached to imported functions.
229    pub inline_nodes: usize,
230    /// Invalid, dead, or unrepresentable address ranges rejected.
231    pub rejected_ranges: usize,
232}
233
234/// Successful conversion output plus diagnostics and provenance.
235///
236/// [`builder`](Self::builder) is ready to encode, or to edit first. The
237/// `discovered_*` paths record which companion files were selected and are
238/// `None` when the input supplied them or none were found, which makes them
239/// worth logging when a conversion produces unexpected output.
240///
241/// A report can carry warnings and still be complete;
242/// [`warnings`](Self::warnings) describes records that were skipped, not a
243/// failed conversion.
244#[non_exhaustive]
245pub struct ConversionReport {
246    /// Populated builder ready for encoding or further modification.
247    pub builder: GsymBuilder,
248    /// Counts of imported and rejected records.
249    pub stats: ConversionStats,
250    /// Non-fatal issues encountered during conversion.
251    pub warnings: Vec<ConversionWarning>,
252    /// Automatically selected main debug file.
253    pub discovered_debug: Option<PathBuf>,
254    /// Automatically selected supplementary debug file.
255    pub discovered_supplementary: Option<PathBuf>,
256    /// Automatically selected DWP package.
257    pub discovered_dwp: Option<PathBuf>,
258}
259
260impl fmt::Debug for ConversionReport {
261    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
262        formatter
263            .debug_struct("ConversionReport")
264            .field("builder", &self.builder)
265            .field("stats", &self.stats)
266            .field("warning_count", &self.warnings.len())
267            .field("discovered_debug", &self.discovered_debug)
268            .field("discovered_supplementary", &self.discovered_supplementary)
269            .field("discovered_dwp", &self.discovered_dwp)
270            .finish_non_exhaustive()
271    }
272}
273
274/// Reusable ELF-to-GSYM converter with immutable options.
275///
276/// Options are fixed at construction, so one converter can be shared across
277/// many images. [`Default`] uses [`ConversionOptions::default`].
278///
279/// ```no_run
280/// use gsym::convert::ElfConverter;
281///
282/// let report = ElfConverter::default().convert_path("./app")?;
283/// for warning in &report.warnings {
284///     eprintln!("warning: {warning}");
285/// }
286/// report.builder.write_to(std::fs::File::create("app.gsym")?)?;
287/// # Ok::<(), gsym::Error>(())
288/// ```
289#[derive(Clone, Debug, Default, Eq, PartialEq)]
290pub struct ElfConverter {
291    options: ConversionOptions,
292}
293
294impl ElfConverter {
295    /// Creates a converter using `options`.
296    #[must_use]
297    pub const fn new(options: ConversionOptions) -> Self {
298        Self { options }
299    }
300
301    /// Returns this converter's immutable options.
302    #[must_use]
303    pub const fn options(&self) -> &ConversionOptions {
304        &self.options
305    }
306
307    /// Converts linked ELF image/debug inputs into a GSYM builder.
308    ///
309    /// Uses exactly the inputs given, with no filesystem or network access.
310    /// Companions supplied here are cross-checked against the image, so a debug
311    /// or symbol file from a different architecture or build is rejected.
312    ///
313    /// # Errors
314    ///
315    /// Returns an error for malformed inputs, incompatible companion files,
316    /// invalid DWARF, or unrepresentable GSYM records.
317    pub fn convert(&self, inputs: ElfInputs<'_>) -> Result<ConversionReport> {
318        self.convert_inner(inputs, None)
319    }
320
321    fn convert_inner(
322        &self,
323        inputs: ElfInputs<'_>,
324        dwo_base: Option<&Path>,
325    ) -> Result<ConversionReport> {
326        let image = parse_elf(inputs.image, ElfInputKind::Image)?;
327        require_supported_kind(&image)?;
328
329        let layout = AddressLayout::new(&image)?;
330        let base_address = layout.ranges.first().map_or(0, |range| range.start);
331        if layout.ranges.is_empty() {
332            return Err(Error::InvalidModel("ELF image has no executable sections"));
333        }
334
335        let build_id = image
336            .build_id()
337            .map_err(|error| malformed("ELF build ID", error))?
338            .unwrap_or_default()
339            .to_vec();
340        let mut warnings = Vec::new();
341        let mini_debug = if inputs.debug.is_none()
342            && (self.options.include_symbols || self.options.dwarf.is_some())
343        {
344            validated_gnu_debugdata(
345                &image,
346                self.options.gnu_debugdata_max_decompressed_size,
347                &mut warnings,
348            )
349        } else {
350            None
351        };
352        let debug_bytes = inputs
353            .debug
354            .or(mini_debug.as_deref())
355            .unwrap_or(inputs.image);
356        let separate_debug = inputs.debug.is_some() || mini_debug.is_some();
357        let debug = parse_elf(debug_bytes, ElfInputKind::Debug)?;
358        if inputs.debug.is_some() {
359            cross_check_elf_identity(&image, &debug, ElfInputKind::Debug)?;
360        }
361        let supplementary = inputs
362            .supplementary
363            .map(|bytes| parse_elf(bytes, ElfInputKind::Supplementary))
364            .transpose()?;
365        let dwp = inputs
366            .dwp
367            .map(|bytes| parse_elf(bytes, ElfInputKind::Dwp))
368            .transpose()?;
369        if let Some(dwp) = &dwp {
370            cross_check_elf_architecture(&image, dwp, ElfInputKind::Dwp)?;
371        }
372
373        let mut writer = self.options.writer.clone();
374        writer.endian = if image.is_little_endian() {
375            Endian::Little
376        } else {
377            Endian::Big
378        };
379        writer.base_address = Some(base_address);
380        if writer.build_id.is_empty() {
381            writer.build_id = build_id;
382        }
383        let mut builder = GsymBuilder::with_options(BuilderOptions {
384            writer,
385            executable_ranges: layout.ranges.clone().into_boxed_slice(),
386            repair_zero_sized_functions: true,
387            merge_equal_address_functions: false,
388        });
389        let mut stats = ConversionStats::default();
390
391        if self.options.include_symbols {
392            let mut imported = 0_usize;
393            if let Some(symbol_bytes) = inputs.symbols {
394                let symbol_file = parse_elf(symbol_bytes, ElfInputKind::Symbols)?;
395                cross_check_elf_identity(&image, &symbol_file, ElfInputKind::Symbols)?;
396                imported = import_symbols(
397                    &symbol_file,
398                    &layout,
399                    &mut builder,
400                    &mut stats.rejected_ranges,
401                )?;
402            } else if separate_debug {
403                imported =
404                    import_symbols(&debug, &layout, &mut builder, &mut stats.rejected_ranges)?;
405            }
406            stats.symbol_functions = imported.saturating_add(import_symbols(
407                &image,
408                &layout,
409                &mut builder,
410                &mut stats.rejected_ranges,
411            )?);
412        }
413        if let Some(dwarf_options) = self.options.dwarf {
414            super::dwarf::import_dwarf(super::dwarf::DwarfImport {
415                file: &debug,
416                supplementary: supplementary.as_ref(),
417                dwp: dwp.as_ref(),
418                dwo_base,
419                layout: &layout,
420                executable_ranges: &layout.ranges,
421                builder: &mut builder,
422                include_inlines: dwarf_options.inline_info,
423                include_call_sites: dwarf_options.call_sites,
424                stats: &mut stats,
425                warnings: &mut warnings,
426            })?;
427        }
428
429        Ok(ConversionReport {
430            builder,
431            stats,
432            warnings,
433            discovered_debug: None,
434            discovered_supplementary: None,
435            discovered_dwp: None,
436        })
437    }
438
439    /// Converts an ELF path and discovers supported companion debug files.
440    ///
441    /// Searches for separate debug information unless
442    /// [`DiscoveryPolicy::Disabled`] is set, then converts the image together
443    /// with whatever was found. The chosen paths are reported in
444    /// [`ConversionReport::discovered_debug`] and its siblings, and problems
445    /// encountered while searching appear as warnings rather than errors.
446    ///
447    /// With debuginfod configured, this may perform network requests. See
448    /// [`docs::conversion`](crate::docs::conversion).
449    ///
450    /// # Errors
451    ///
452    /// Returns an error when an input cannot be read or converted.
453    pub fn convert_path(&self, image_path: impl AsRef<Path>) -> Result<ConversionReport> {
454        let image_path = image_path.as_ref();
455        let image_bytes = read_file(image_path, "read ELF image")?;
456        let discovery = if self.options.discovery == DiscoveryPolicy::Enabled {
457            discover_separate_debug(image_path, &image_bytes, &self.options)?
458        } else {
459            Discovery::default()
460        };
461        let debug_path = discovery
462            .artifact
463            .as_ref()
464            .map_or(image_path, |artifact| artifact.path.as_path());
465        let debug_source = discovery
466            .artifact
467            .as_ref()
468            .map_or(image_bytes.as_slice(), |artifact| artifact.bytes.as_slice());
469        let supplementary_discovery =
470            discover_supplementary(debug_path, debug_source, &self.options)?;
471        let dwp = (self.options.discovery == DiscoveryPolicy::Enabled)
472            .then(|| discover_dwp(image_path, debug_path))
473            .flatten();
474        let dwo_base = discovery
475            .artifact
476            .as_ref()
477            .map(|artifact| artifact.path.as_path())
478            .or(Some(image_path))
479            .and_then(Path::parent);
480        let mut report = self.convert_inner(
481            ElfInputs {
482                image: &image_bytes,
483                debug: discovery
484                    .artifact
485                    .as_ref()
486                    .map(|artifact| artifact.bytes.as_slice()),
487                symbols: None,
488                supplementary: supplementary_discovery
489                    .artifact
490                    .as_ref()
491                    .map(|artifact| artifact.bytes.as_slice()),
492                dwp: dwp.as_ref().map(|artifact| artifact.bytes.as_slice()),
493            },
494            dwo_base,
495        )?;
496        report.warnings.extend(discovery.warnings);
497        report.warnings.extend(supplementary_discovery.warnings);
498        report.discovered_debug = discovery.artifact.map(|artifact| artifact.path);
499        report.discovered_supplementary = supplementary_discovery
500            .artifact
501            .map(|artifact| artifact.path);
502        report.discovered_dwp = dwp.map(|artifact| artifact.path);
503        Ok(report)
504    }
505}
506
507fn read_file(path: &Path, description: &'static str) -> Result<Vec<u8>> {
508    std::fs::read(path).map_err(|source| Error::IoAtPath {
509        operation: description,
510        path: path.to_path_buf(),
511        source,
512    })
513}