gsym-rs 0.1.0

Pure-Rust reader, writer, and Linux ELF/DWARF converter for LLVM GSYM
Documentation
use std::collections::{HashMap, HashSet};

use gimli::{DebuggingInformationEntry, Dwarf, EntriesTreeNode, Reader, Unit};

use super::references::{absolute_entry_offset, resolve_name, resolve_reference_name};
use super::{file_index_attribute, gimli_error, unsigned_attribute};
use crate::convert::ConversionWarning;
use crate::model::{AddressRange, CallSite, CallSiteFlags, FileIndex, InlineNode};
use crate::{Error, Result};

struct Descendants {
    inlines: Vec<InlineNode>,
    call_sites: Vec<CallSite>,
    inline_count: usize,
}

pub(super) struct DetailOptions<'a> {
    pub(super) include_inlines: bool,
    pub(super) include_call_sites: bool,
    pub(super) warnings: &'a mut Vec<ConversionWarning>,
}

struct DetailContext<'data, 'warnings, R: Reader<Offset = usize>> {
    dwarf: &'data Dwarf<R>,
    unit: &'data Unit<R>,
    function_range: AddressRange,
    file_indices: &'data HashMap<u64, FileIndex>,
    include_inlines: bool,
    include_call_sites: bool,
    warnings: &'warnings mut Vec<ConversionWarning>,
}

pub(super) fn extract_subprogram_details<R: Reader<Offset = usize>>(
    dwarf: &Dwarf<R>,
    unit: &Unit<R>,
    offset: gimli::UnitOffset<usize>,
    function_range: AddressRange,
    function_name: &[u8],
    file_indices: &HashMap<u64, FileIndex>,
    options: &mut DetailOptions<'_>,
) -> Result<(Option<InlineNode>, Vec<CallSite>, usize)> {
    if !options.include_inlines && !options.include_call_sites {
        return Ok((None, Vec::new(), 0));
    }
    let mut tree = unit.entries_tree(Some(offset)).map_err(gimli_error)?;
    let root = tree.root().map_err(gimli_error)?;
    let mut context = DetailContext {
        dwarf,
        unit,
        function_range,
        file_indices,
        include_inlines: options.include_inlines,
        include_call_sites: options.include_call_sites,
        warnings: options.warnings,
    };
    let descendants = collect_descendants(&mut context, root, &[function_range], 0)?;
    let inline = (!descendants.inlines.is_empty()).then(|| InlineNode {
        ranges: vec![function_range],
        name: function_name.to_vec(),
        call_file: FileIndex::ZERO,
        call_line: 0,
        children: descendants.inlines,
    });
    Ok((inline, descendants.call_sites, descendants.inline_count))
}

fn collect_descendants<R: Reader<Offset = usize>>(
    context: &mut DetailContext<'_, '_, R>,
    node: EntriesTreeNode<'_, '_, R>,
    parent_ranges: &[AddressRange],
    depth: usize,
) -> Result<Descendants> {
    if depth > 256 {
        return Err(Error::InvalidModel("DWARF DIE nesting exceeds 256 levels"));
    }
    let mut result = Descendants {
        inlines: Vec::new(),
        call_sites: Vec::new(),
        inline_count: 0,
    };
    let mut children = node.children();
    while let Some(child) = children.next().map_err(gimli_error)? {
        let entry = child.entry();
        let tag = entry.tag();
        let inline_data =
            if context.include_inlines && tag == gimli::constants::DW_TAG_inlined_subroutine {
                make_inline_node(
                    context.dwarf,
                    context.unit,
                    entry,
                    parent_ranges,
                    context.file_indices,
                    context.warnings,
                )?
            } else {
                None
            };
        let call_site = if context.include_call_sites
            && depth == 0
            && tag == gimli::constants::DW_TAG_call_site
        {
            make_call_site(context.dwarf, context.unit, entry, context.function_range)?
        } else {
            None
        };
        let nested_parent = inline_data
            .as_ref()
            .map_or(parent_ranges, |inline| inline.ranges.as_slice());
        let nested = collect_descendants(context, child, nested_parent, depth.saturating_add(1))?;
        if depth == 0 {
            result.call_sites.extend(nested.call_sites);
        }
        result.inline_count = result.inline_count.saturating_add(nested.inline_count);
        if let Some(call_site) = call_site {
            result.call_sites.push(call_site);
        }
        if let Some(mut inline) = inline_data {
            inline.children = nested.inlines;
            result.inline_count = result.inline_count.saturating_add(1);
            result.inlines.push(inline);
        } else {
            result.inlines.extend(nested.inlines);
        }
    }
    Ok(result)
}

fn make_inline_node<R: Reader<Offset = usize>>(
    dwarf: &Dwarf<R>,
    unit: &Unit<R>,
    entry: &DebuggingInformationEntry<R>,
    parent_ranges: &[AddressRange],
    file_indices: &HashMap<u64, FileIndex>,
    warnings: &mut Vec<ConversionWarning>,
) -> Result<Option<InlineNode>> {
    let Some(name) = resolve_name(dwarf, unit, entry, 0)? else {
        return Ok(None);
    };
    let mut ranges = dwarf.die_ranges(unit, entry).map_err(gimli_error)?;
    let mut valid_ranges = Vec::new();
    while let Some(range) = ranges.next().map_err(gimli_error)? {
        let candidate = AddressRange::new(range.begin, range.end);
        if range.begin < range.end
            && parent_ranges
                .iter()
                .any(|parent| parent.contains_range(candidate))
        {
            valid_ranges.push(candidate);
        }
    }
    valid_ranges.sort_unstable();
    if valid_ranges.is_empty() {
        return Ok(None);
    }
    let call_file = match file_index_attribute(entry, gimli::constants::DW_AT_call_file) {
        Some(dwarf_index) => {
            if let Some(index) = file_indices.get(&dwarf_index).copied() {
                index
            } else {
                warnings.push(ConversionWarning::MissingInlineCallFile {
                    die_offset: absolute_entry_offset(unit, entry.offset())? as u64,
                    index: dwarf_index,
                });
                FileIndex::ZERO
            }
        }
        None => FileIndex::ZERO,
    };
    let call_line = unsigned_attribute(entry, gimli::constants::DW_AT_call_line).unwrap_or(0);
    let call_line = if let Ok(line) = u32::try_from(call_line) {
        line
    } else {
        warnings.push(ConversionWarning::InvalidInlineCallLine {
            die_offset: absolute_entry_offset(unit, entry.offset())? as u64,
            line: call_line,
        });
        0
    };
    Ok(Some(InlineNode {
        ranges: valid_ranges,
        name,
        call_file,
        call_line,
        children: Vec::new(),
    }))
}

fn make_call_site<R: Reader<Offset = usize>>(
    dwarf: &Dwarf<R>,
    unit: &Unit<R>,
    entry: &DebuggingInformationEntry<R>,
    function_range: AddressRange,
) -> Result<Option<CallSite>> {
    let Some(return_pc_value) = entry.attr_value(gimli::constants::DW_AT_call_return_pc) else {
        return Ok(None);
    };
    let Some(return_pc) = dwarf
        .attr_address(unit, return_pc_value)
        .map_err(gimli_error)?
    else {
        return Ok(None);
    };
    if !function_range.contains(return_pc) {
        return Ok(None);
    }
    let mut patterns = Vec::new();
    if let Some(origin) = entry.attr_value(gimli::constants::DW_AT_call_origin) {
        let mut visited = HashSet::new();
        if let Some(name) = resolve_reference_name(dwarf, unit, &origin, 0, &mut visited)? {
            patterns.push(name);
        }
    }
    Ok(Some(CallSite {
        return_offset: return_pc
            .checked_sub(function_range.start)
            .ok_or(Error::InvalidModel(
                "call-site return address precedes its function",
            ))?,
        flags: CallSiteFlags::default(),
        match_regex: patterns,
    }))
}