use crate::{
binary::DwarfReader,
core::{FunctionDieKind, IndexEntry, LineEntry, Result},
index::{
directory_from_index, resolve_file_path, LightweightFileIndex, LightweightIndex,
LightweightIndexShard, LineMappingTable, ScopedFileIndexManager,
},
};
use gimli::Reader;
use rayon::prelude::*;
use std::{
collections::{HashMap, HashSet},
sync::Arc,
};
use tracing::debug;
#[derive(Clone, Default)]
struct FunctionMetadata {
name: Option<Arc<str>>,
has_inline_attribute: bool,
is_linkage_name: bool,
is_external: Option<bool>,
}
#[derive(Clone)]
struct FunctionEntrySeed {
die_offset: gimli::UnitOffset,
tag: gimli::DwTag,
unit_offset: gimli::DebugInfoOffset,
flags: crate::core::IndexFlags,
language: Option<gimli::DwLang>,
representative_addr: Option<u64>,
entry_pc: Option<u64>,
function_kind: FunctionDieKind,
}
#[derive(Clone, Default)]
struct RawDieAttrs {
name: Option<Arc<str>>,
linkage_name: Option<Arc<str>>,
has_inline_attribute: bool,
is_external: Option<bool>,
is_declaration: Option<bool>,
low_pc: Option<u64>,
high_pc: Option<u64>,
high_pc_offset: Option<u64>,
ranges_attr: Option<gimli::AttributeValue<DwarfReader>>,
entry_pc: Option<u64>,
location_attr: Option<gimli::AttributeValue<DwarfReader>>,
has_main_subprogram: bool,
abstract_origin: Option<DieRef>,
specification: Option<DieRef>,
}
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
enum DieRef {
Unit(gimli::UnitOffset),
DebugInfo(gimli::DebugInfoOffset),
}
struct ResolvedRawDieAttrs {
unit: Option<gimli::Unit<DwarfReader>>,
offset: gimli::UnitOffset,
absolute_offset: gimli::DebugInfoOffset,
attrs: RawDieAttrs,
}
#[derive(Debug, Clone)]
pub(crate) struct CompilationUnit {
pub base_directory: String,
pub include_directories: Vec<String>,
pub files: Vec<SourceFile>,
}
#[derive(Debug, Clone)]
pub(crate) struct SourceFile {
pub directory_path: String,
pub filename: String,
pub full_path: String,
}
type InfoShard = LightweightIndexShard;
#[derive(Default)]
struct LineShard {
line_entries: Vec<crate::core::LineEntry>,
compilation_units: HashMap<String, CompilationUnit>,
file_indices: Vec<(String, LightweightFileIndex)>,
files_count: usize,
}
pub(crate) struct DwarfParseResult {
pub lightweight_index: LightweightIndex,
pub line_mapping: LineMappingTable,
pub scoped_file_manager: ScopedFileIndexManager,
pub compilation_units: HashMap<String, CompilationUnit>,
pub stats: DwarfParseStats,
}
pub(crate) struct LineParseResult {
pub line_mapping: LineMappingTable,
pub scoped_file_manager: ScopedFileIndexManager,
pub compilation_units: HashMap<String, CompilationUnit>,
pub line_entries_count: usize,
pub files_count: usize,
}
pub(crate) struct DebugParseResult {
pub lightweight_index: LightweightIndex,
pub functions_count: usize,
pub variables_count: usize,
}
#[derive(Debug, Clone)]
pub(crate) struct DwarfParseStats {
pub total_functions: usize,
pub total_variables: usize,
pub total_line_entries: usize,
pub total_files: usize,
}
pub(crate) struct DwarfParser<'a> {
dwarf: &'a gimli::Dwarf<DwarfReader>,
}
impl<'a> DwarfParser<'a> {
fn process_unit_shard(
&self,
unit: &gimli::Unit<DwarfReader>,
unit_offset: gimli::DebugInfoOffset,
cu_language: Option<gimli::DwLang>,
) -> Result<InfoShard> {
let mut shard = InfoShard::default();
let mut entries = unit.entries_raw(None)?;
let mut metadata_cache: HashMap<gimli::DebugInfoOffset, FunctionMetadata> = HashMap::new();
let mut tag_stack: Vec<gimli::DwTag> = Vec::new();
while !entries.is_empty() {
let d: usize = entries.next_depth() as usize;
let entry_offset = entries.next_offset();
while tag_stack.len() > d {
tag_stack.pop();
}
let Some(abbrev) = entries.read_abbreviation()? else {
continue;
};
let tag = abbrev.tag();
let needs_full_entry = matches!(
tag,
gimli::constants::DW_TAG_subprogram
| gimli::constants::DW_TAG_inlined_subroutine
| gimli::constants::DW_TAG_variable
| gimli::constants::DW_TAG_structure_type
| gimli::constants::DW_TAG_class_type
| gimli::constants::DW_TAG_union_type
| gimli::constants::DW_TAG_enumeration_type
| gimli::constants::DW_TAG_typedef
);
if !needs_full_entry {
entries.skip_attributes(abbrev.attributes())?;
tag_stack.push(tag);
continue;
}
let raw_attrs = self.read_selected_raw_attrs(unit, &mut entries, abbrev)?;
match tag {
gimli::constants::DW_TAG_subprogram => {
let mut visited = HashSet::new();
let metadata = self.resolve_function_metadata_from_raw(
unit,
entry_offset,
&raw_attrs,
&mut metadata_cache,
&mut visited,
)?;
if let Some(name) = metadata.name.as_ref() {
let representative_addr =
self.extract_representative_address_from_raw(unit, &raw_attrs)?;
let entry_pc_cached = raw_attrs.entry_pc;
let function_kind =
Self::classify_function_kind(tag, representative_addr, entry_pc_cached);
let is_main = Self::is_main_function_from_raw(&raw_attrs, name.as_ref());
let is_static = metadata
.is_external
.map(|external| !external)
.unwrap_or_else(|| Self::is_static_symbol_from_raw(&raw_attrs));
let flags = crate::core::IndexFlags {
is_static,
is_main,
has_inline_attribute: metadata.has_inline_attribute,
is_linkage: metadata.is_linkage_name,
..Default::default()
};
let linkage_name = raw_attrs.linkage_name.as_ref().map(Arc::clone);
let seed = FunctionEntrySeed {
die_offset: entry_offset,
tag,
unit_offset,
flags,
language: cu_language,
representative_addr,
entry_pc: entry_pc_cached,
function_kind,
};
Self::push_function_entries(
&mut shard,
Arc::clone(name),
linkage_name,
&seed,
);
}
}
gimli::constants::DW_TAG_inlined_subroutine => {
let mut visited = HashSet::new();
let metadata = self.resolve_function_metadata_from_raw(
unit,
entry_offset,
&raw_attrs,
&mut metadata_cache,
&mut visited,
)?;
if let Some(name) = metadata.name.as_ref() {
let representative_addr =
self.extract_representative_address_from_raw(unit, &raw_attrs)?;
let entry_pc_cached = raw_attrs.entry_pc;
let function_kind =
Self::classify_function_kind(tag, representative_addr, entry_pc_cached);
let is_static = metadata
.is_external
.map(|external| !external)
.unwrap_or(false);
let flags = crate::core::IndexFlags {
is_static,
has_inline_attribute: metadata.has_inline_attribute,
is_linkage: metadata.is_linkage_name,
..Default::default()
};
let linkage_name = raw_attrs.linkage_name.as_ref().map(Arc::clone);
let seed = FunctionEntrySeed {
die_offset: entry_offset,
tag,
unit_offset,
flags,
language: cu_language,
representative_addr,
entry_pc: entry_pc_cached,
function_kind,
};
Self::push_function_entries(
&mut shard,
Arc::clone(name),
linkage_name,
&seed,
);
}
}
gimli::constants::DW_TAG_variable => {
tracing::trace!(
"Evaluating global variable DIE {:?} in CU {:?}",
entry_offset,
unit_offset
);
let var_addr = self.extract_variable_address_from_raw(unit, &raw_attrs)?;
let is_static_symbol =
Self::is_static_variable_symbol_from_raw(&raw_attrs, var_addr);
let in_function_scope = tag_stack.iter().any(|t| {
*t == gimli::constants::DW_TAG_subprogram
|| *t == gimli::constants::DW_TAG_inlined_subroutine
});
if in_function_scope && !is_static_symbol {
tracing::trace!(
"Skipping variable at {:?} (in function scope, stack={:?})",
entry_offset,
tag_stack
);
tag_stack.push(tag);
continue;
} else if in_function_scope {
tracing::trace!(
"Treating static variable at {:?} as global despite function scope (stack={:?})",
entry_offset,
tag_stack
);
}
if Self::is_declaration_from_raw(&raw_attrs) {
tracing::trace!(
"Skipping variable at {:?} (declaration-only DIE)",
entry_offset
);
tag_stack.push(tag);
continue;
}
let mut collected_names: Vec<(Arc<str>, bool)> = Vec::new();
let mut push_unique_name = |candidate: Arc<str>, is_linkage_alias: bool| {
if candidate.is_empty() {
return;
}
if collected_names
.iter()
.any(|(existing, _)| existing.as_ref() == candidate.as_ref())
{
return;
}
collected_names.push((candidate, is_linkage_alias));
};
let mut visited = HashSet::with_capacity(4);
if let Some(name) =
self.resolve_name_from_raw(unit, entry_offset, &raw_attrs, &mut visited)?
{
push_unique_name(name, false);
}
if let Some(linkage_name) = raw_attrs.linkage_name.as_ref() {
push_unique_name(Arc::clone(linkage_name), true);
}
if collected_names.is_empty() {
tracing::trace!(
"DWARF variable at {:?} missing usable name (CU lang={:?}); skipping alias registration",
entry_offset,
cu_language
);
tag_stack.push(tag);
continue;
}
let flags = crate::core::IndexFlags {
is_static: is_static_symbol,
..Default::default()
};
for (name, is_linkage_alias) in collected_names {
let mut entry_flags = flags;
entry_flags.is_linkage = is_linkage_alias;
let index_entry = IndexEntry {
name: Arc::clone(&name),
die_offset: entry_offset,
unit_offset,
tag,
flags: entry_flags,
language: cu_language,
representative_addr: var_addr,
entry_pc: None,
function_kind: FunctionDieKind::NotFunction,
};
tracing::trace!(
"Registering variable alias '{}' (linkage={}, lang={:?}, die={:?})",
name,
entry_flags.is_linkage,
cu_language,
entry_offset
);
shard.push_variable_entry(name.to_string(), index_entry);
}
}
gimli::constants::DW_TAG_structure_type
| gimli::constants::DW_TAG_class_type
| gimli::constants::DW_TAG_union_type
| gimli::constants::DW_TAG_enumeration_type
| gimli::constants::DW_TAG_typedef => {
let mut visited = HashSet::with_capacity(4);
if let Some(name) =
self.resolve_name_from_raw(unit, entry_offset, &raw_attrs, &mut visited)?
{
let is_decl = Self::is_declaration_from_raw(&raw_attrs);
let flags = crate::core::IndexFlags {
is_type_declaration: is_decl,
..Default::default()
};
let index_entry = IndexEntry {
name: Arc::clone(&name),
die_offset: entry_offset,
unit_offset,
tag,
flags,
language: cu_language,
representative_addr: None,
entry_pc: None,
function_kind: FunctionDieKind::NotFunction,
};
shard.push_type_entry(name.to_string(), index_entry);
}
}
_ => {}
}
tag_stack.push(tag);
}
Ok(shard)
}
pub fn new(dwarf: &'a gimli::Dwarf<DwarfReader>) -> Self {
Self { dwarf }
}
fn classify_function_kind(
tag: gimli::DwTag,
representative_addr: Option<u64>,
entry_pc: Option<u64>,
) -> FunctionDieKind {
match tag {
gimli::constants::DW_TAG_inlined_subroutine => FunctionDieKind::InlineInstance,
gimli::constants::DW_TAG_subprogram => {
if representative_addr.is_some() || entry_pc.is_some() {
FunctionDieKind::ConcreteSubprogram
} else {
FunctionDieKind::AbstractSubprogram
}
}
_ => FunctionDieKind::NotFunction,
}
}
fn build_function_index_entry(name: Arc<str>, seed: &FunctionEntrySeed) -> IndexEntry {
IndexEntry {
name,
die_offset: seed.die_offset,
unit_offset: seed.unit_offset,
tag: seed.tag,
flags: seed.flags,
language: seed.language,
representative_addr: seed.representative_addr,
entry_pc: seed.entry_pc,
function_kind: seed.function_kind,
}
}
fn push_function_entries(
shard: &mut InfoShard,
name: Arc<str>,
linkage_name: Option<Arc<str>>,
seed: &FunctionEntrySeed,
) {
shard.push_function_entry(
name.to_string(),
Self::build_function_index_entry(Arc::clone(&name), seed),
);
if let Some(linkage_name) =
linkage_name.filter(|linkage_name| linkage_name.as_ref() != name.as_ref())
{
let mut alias_seed = seed.clone();
let mut alias_flags = alias_seed.flags;
alias_flags.is_linkage = true;
alias_seed.flags = alias_flags;
shard.push_function_entry(
linkage_name.to_string(),
Self::build_function_index_entry(linkage_name, &alias_seed),
);
}
}
fn extract_attr_string(
dwarf: &gimli::Dwarf<DwarfReader>,
unit: &gimli::Unit<DwarfReader>,
attr_value: gimli::AttributeValue<DwarfReader>,
) -> Result<Option<Arc<str>>> {
if let Ok(string) = dwarf.attr_string(unit, attr_value) {
if let Ok(s_str) = string.to_string_lossy() {
return Ok(Some(Arc::<str>::from(s_str.into_owned())));
}
}
Ok(None)
}
fn die_ref(
unit: &gimli::Unit<DwarfReader>,
value: gimli::AttributeValue<DwarfReader>,
) -> Option<DieRef> {
match value {
gimli::AttributeValue::UnitRef(offset) => Some(DieRef::Unit(offset)),
gimli::AttributeValue::DebugInfoRef(offset) => {
if let Some(unit_offset) = offset.to_unit_offset(&unit.header) {
Some(DieRef::Unit(unit_offset))
} else {
Some(DieRef::DebugInfo(offset))
}
}
_ => None,
}
}
fn should_read_raw_attr(attr: gimli::DwAt) -> bool {
matches!(
attr,
gimli::constants::DW_AT_name
| gimli::constants::DW_AT_linkage_name
| gimli::constants::DW_AT_MIPS_linkage_name
| gimli::constants::DW_AT_inline
| gimli::constants::DW_AT_external
| gimli::constants::DW_AT_declaration
| gimli::constants::DW_AT_low_pc
| gimli::constants::DW_AT_high_pc
| gimli::constants::DW_AT_ranges
| gimli::constants::DW_AT_entry_pc
| gimli::constants::DW_AT_location
| gimli::constants::DW_AT_main_subprogram
| gimli::constants::DW_AT_abstract_origin
| gimli::constants::DW_AT_specification
)
}
fn read_selected_raw_attrs(
&self,
unit: &gimli::Unit<DwarfReader>,
entries: &mut gimli::EntriesRaw<'_, DwarfReader>,
abbrev: &gimli::Abbreviation,
) -> Result<RawDieAttrs> {
let mut attrs = RawDieAttrs::default();
for spec in abbrev.attributes() {
if !Self::should_read_raw_attr(spec.name()) {
entries.skip_attributes(std::slice::from_ref(spec))?;
continue;
}
let attr = entries.read_attribute(*spec)?;
let value = attr.value();
match attr.name() {
gimli::constants::DW_AT_name => {
attrs.name = Self::extract_attr_string(self.dwarf, unit, value)?;
}
gimli::constants::DW_AT_linkage_name
| gimli::constants::DW_AT_MIPS_linkage_name => {
if attrs.linkage_name.is_none() {
attrs.linkage_name = Self::extract_attr_string(self.dwarf, unit, value)?;
}
}
gimli::constants::DW_AT_inline => {
if let gimli::AttributeValue::Inline(inline_attr) = value {
attrs.has_inline_attribute = inline_attr == gimli::DW_INL_inlined
|| inline_attr == gimli::DW_INL_declared_inlined;
}
}
gimli::constants::DW_AT_external => {
attrs.is_external = Self::flag_attr_value(value);
}
gimli::constants::DW_AT_declaration => {
attrs.is_declaration = Self::flag_attr_value(value);
}
gimli::constants::DW_AT_low_pc => match value {
gimli::AttributeValue::Addr(addr) => attrs.low_pc = Some(addr),
gimli::AttributeValue::DebugAddrIndex(index) => {
attrs.low_pc = self.dwarf.address(unit, index).ok();
}
_ => {}
},
gimli::constants::DW_AT_high_pc => match value {
gimli::AttributeValue::Addr(addr) => attrs.high_pc = Some(addr),
_ => attrs.high_pc_offset = Self::high_pc_offset_value(value),
},
gimli::constants::DW_AT_ranges => {
attrs.ranges_attr = Some(value);
}
gimli::constants::DW_AT_entry_pc => match value {
gimli::AttributeValue::Addr(addr) => attrs.entry_pc = Some(addr),
gimli::AttributeValue::DebugAddrIndex(index) => {
attrs.entry_pc = self.dwarf.address(unit, index).ok();
}
_ => {}
},
gimli::constants::DW_AT_location => {
attrs.location_attr = Some(value);
}
gimli::constants::DW_AT_main_subprogram => {
attrs.has_main_subprogram = true;
}
gimli::constants::DW_AT_abstract_origin => {
attrs.abstract_origin = Self::die_ref(unit, value);
}
gimli::constants::DW_AT_specification => {
attrs.specification = Self::die_ref(unit, value);
}
_ => {}
}
}
Ok(attrs)
}
fn read_attrs_for_die_ref(
&self,
current_unit: &gimli::Unit<DwarfReader>,
reference: DieRef,
) -> Result<Option<ResolvedRawDieAttrs>> {
match reference {
DieRef::Unit(unit_offset) => {
let Some(debug_info_offset) =
unit_offset.to_debug_info_offset(¤t_unit.header)
else {
return Ok(None);
};
let attrs = self.read_selected_raw_attrs_at(current_unit, unit_offset)?;
Ok(Some(ResolvedRawDieAttrs {
unit: None,
offset: unit_offset,
absolute_offset: debug_info_offset,
attrs,
}))
}
DieRef::DebugInfo(debug_info_offset) => {
if let Some(unit_offset) = debug_info_offset.to_unit_offset(¤t_unit.header) {
let attrs = self.read_selected_raw_attrs_at(current_unit, unit_offset)?;
return Ok(Some(ResolvedRawDieAttrs {
unit: None,
offset: unit_offset,
absolute_offset: debug_info_offset,
attrs,
}));
}
let mut units = self.dwarf.units();
while let Some(header) = units.next()? {
if let Some(unit_offset) = debug_info_offset.to_unit_offset(&header) {
let unit = self.dwarf.unit(header)?;
let attrs = self.read_selected_raw_attrs_at(&unit, unit_offset)?;
return Ok(Some(ResolvedRawDieAttrs {
unit: Some(unit),
offset: unit_offset,
absolute_offset: debug_info_offset,
attrs,
}));
}
}
Ok(None)
}
}
}
fn read_selected_raw_attrs_at(
&self,
unit: &gimli::Unit<DwarfReader>,
offset: gimli::UnitOffset,
) -> Result<RawDieAttrs> {
let mut entries = unit.entries_raw(Some(offset))?;
let Some(abbrev) = entries.read_abbreviation()? else {
return Ok(RawDieAttrs::default());
};
self.read_selected_raw_attrs(unit, &mut entries, abbrev)
}
fn resolve_name_from_raw(
&self,
unit: &gimli::Unit<DwarfReader>,
entry_offset: gimli::UnitOffset,
attrs: &RawDieAttrs,
visited: &mut HashSet<gimli::DebugInfoOffset>,
) -> Result<Option<Arc<str>>> {
if let Some(name) = attrs.name.as_ref() {
return Ok(Some(Arc::clone(name)));
}
let Some(entry_abs) = entry_offset.to_debug_info_offset(&unit.header) else {
return Ok(None);
};
if !visited.insert(entry_abs) {
return Ok(None);
}
let mut resolved = None;
for origin_ref in [attrs.specification, attrs.abstract_origin]
.into_iter()
.flatten()
{
let Some(origin) = self.read_attrs_for_die_ref(unit, origin_ref)? else {
continue;
};
if visited.contains(&origin.absolute_offset) {
continue;
}
let origin_unit = origin.unit.as_ref().unwrap_or(unit);
if let Some(name) =
self.resolve_name_from_raw(origin_unit, origin.offset, &origin.attrs, visited)?
{
resolved = Some(name);
break;
}
}
visited.remove(&entry_abs);
Ok(resolved)
}
fn resolve_function_metadata_from_raw(
&self,
unit: &gimli::Unit<DwarfReader>,
entry_offset: gimli::UnitOffset,
attrs: &RawDieAttrs,
cache: &mut HashMap<gimli::DebugInfoOffset, FunctionMetadata>,
visited: &mut HashSet<gimli::DebugInfoOffset>,
) -> Result<FunctionMetadata> {
let Some(entry_abs) = entry_offset.to_debug_info_offset(&unit.header) else {
return Ok(FunctionMetadata::default());
};
if let Some(cached) = cache.get(&entry_abs) {
return Ok(cached.clone());
}
if !visited.insert(entry_abs) {
return Ok(FunctionMetadata::default());
}
let mut metadata = FunctionMetadata::default();
let mut name_visited = HashSet::with_capacity(4);
if let Some(name) =
self.resolve_name_from_raw(unit, entry_offset, attrs, &mut name_visited)?
{
metadata.name = Some(name);
} else if let Some(name) = attrs.linkage_name.as_ref() {
metadata.name = Some(Arc::clone(name));
metadata.is_linkage_name = true;
}
metadata.has_inline_attribute = attrs.has_inline_attribute;
metadata.is_external = attrs.is_external;
let mut merge_from_origin = |origin_ref: DieRef| -> Result<()> {
let Some(origin) = self.read_attrs_for_die_ref(unit, origin_ref)? else {
return Ok(());
};
if visited.contains(&origin.absolute_offset) {
return Ok(());
}
let origin_metadata = if let Some(cached) = cache.get(&origin.absolute_offset) {
cached.clone()
} else {
let origin_unit = origin.unit.as_ref().unwrap_or(unit);
self.resolve_function_metadata_from_raw(
origin_unit,
origin.offset,
&origin.attrs,
cache,
visited,
)?
};
if metadata.name.is_none() {
metadata.name = origin_metadata.name.as_ref().map(Arc::clone);
}
if metadata.is_external.is_none() {
metadata.is_external = origin_metadata.is_external;
}
metadata.is_linkage_name |= origin_metadata.is_linkage_name;
Ok(())
};
if let Some(origin_offset) = attrs.abstract_origin {
merge_from_origin(origin_offset)?;
}
if let Some(origin_offset) = attrs.specification {
merge_from_origin(origin_offset)?;
}
visited.remove(&entry_abs);
cache.insert(entry_abs, metadata.clone());
Ok(metadata)
}
fn flag_attr_value(value: gimli::AttributeValue<DwarfReader>) -> Option<bool> {
match value {
gimli::AttributeValue::Flag(v) => Some(v),
_ => None,
}
}
fn flush_pending_line_entries(
out: &mut Vec<LineEntry>,
pending: &mut Vec<LineEntry>,
end_address: Option<u64>,
) {
for mut entry in std::mem::take(pending) {
entry.end_address = end_address;
out.push(entry);
}
}
fn high_pc_offset_value(value: gimli::AttributeValue<DwarfReader>) -> Option<u64> {
match value {
gimli::AttributeValue::Udata(offset) => Some(offset),
gimli::AttributeValue::Data1(offset) => Some(offset as u64),
gimli::AttributeValue::Data2(offset) => Some(offset as u64),
gimli::AttributeValue::Data4(offset) => Some(offset as u64),
gimli::AttributeValue::Data8(offset) => Some(offset),
_ => None,
}
}
fn is_static_symbol_from_raw(attrs: &RawDieAttrs) -> bool {
attrs
.is_external
.map(|is_external| !is_external)
.unwrap_or(true)
}
fn is_static_variable_symbol_from_raw(
attrs: &RawDieAttrs,
absolute_address: Option<u64>,
) -> bool {
if let Some(is_external) = attrs.is_external {
return !is_external;
}
absolute_address.is_some()
}
fn is_declaration_from_raw(attrs: &RawDieAttrs) -> bool {
attrs.is_declaration.unwrap_or(false)
}
fn extract_representative_address_from_raw(
&self,
unit: &gimli::Unit<DwarfReader>,
attrs: &RawDieAttrs,
) -> Result<Option<u64>> {
if let Some(low_pc) = attrs.low_pc {
return Ok(Some(low_pc));
}
if let Some(entry_pc) = attrs.entry_pc {
return Ok(Some(entry_pc));
}
self.peek_first_range_start_from_raw(unit, attrs)
}
fn peek_first_range_start_from_raw(
&self,
unit: &gimli::Unit<DwarfReader>,
attrs: &RawDieAttrs,
) -> Result<Option<u64>> {
let Some(ranges_attr) = attrs.ranges_attr.clone() else {
return Ok(None);
};
let ranges_offset = match ranges_attr {
gimli::AttributeValue::RangeListsRef(offset) => {
self.dwarf.ranges_offset_from_raw(unit, offset)
}
gimli::AttributeValue::DebugRngListsIndex(index) => {
self.dwarf.ranges_offset(unit, index)?
}
gimli::AttributeValue::SecOffset(offset) => gimli::RangeListsOffset(offset),
_ => return Ok(None),
};
let mut ranges_iter = self.dwarf.ranges(unit, ranges_offset)?;
while let Some(range) = ranges_iter.next()? {
let begin = range.begin;
let end = range.end;
if begin > end {
continue;
}
return Ok(Some(begin));
}
Ok(None)
}
fn extract_variable_address_from_raw(
&self,
unit: &gimli::Unit<DwarfReader>,
attrs: &RawDieAttrs,
) -> Result<Option<u64>> {
let Some(location_attr) = attrs.location_attr.clone() else {
return Ok(None);
};
match location_attr {
gimli::AttributeValue::Addr(a) => Ok(Some(a)),
gimli::AttributeValue::Exprloc(expr) => Ok(
self.extract_absolute_storage_address_from_expr(unit, gimli::Expression(expr.0))
),
gimli::AttributeValue::LocationListsRef(offs) => {
if let Ok(mut locations) = self
.dwarf
.locations(unit, gimli::LocationListsOffset(offs.0))
{
while let Ok(Some(loc)) = locations.next() {
if let Some(address) = self.extract_absolute_storage_address_from_expr(
unit,
gimli::Expression(loc.data.0),
) {
return Ok(Some(address));
}
}
}
Ok(None)
}
gimli::AttributeValue::DebugLocListsIndex(index) => {
if let Ok(offset) = self.dwarf.locations_offset(unit, index) {
if let Ok(mut locations) = self.dwarf.locations(unit, offset) {
while let Ok(Some(loc)) = locations.next() {
if let Some(address) = self.extract_absolute_storage_address_from_expr(
unit,
gimli::Expression(loc.data.0),
) {
return Ok(Some(address));
}
}
}
}
Ok(None)
}
gimli::AttributeValue::SecOffset(off) => {
if let Ok(mut locations) =
self.dwarf.locations(unit, gimli::LocationListsOffset(off))
{
while let Ok(Some(loc)) = locations.next() {
if let Some(address) = self.extract_absolute_storage_address_from_expr(
unit,
gimli::Expression(loc.data.0),
) {
return Ok(Some(address));
}
}
}
Ok(None)
}
_ => Ok(None),
}
}
fn extract_absolute_storage_address_from_expr(
&self,
unit: &gimli::Unit<DwarfReader>,
expr: gimli::Expression<DwarfReader>,
) -> Option<u64> {
crate::dwarf_expr::storage::absolute_address(self.dwarf, unit, expr)
}
fn is_main_function_from_raw(attrs: &RawDieAttrs, name: &str) -> bool {
attrs.has_main_subprogram || name == "main" || name == "_main"
}
fn extract_language(
&self,
_dwarf: &gimli::Dwarf<DwarfReader>,
unit: &gimli::Unit<DwarfReader>,
) -> Option<gimli::DwLang> {
let mut entries = unit.entries();
if let Ok(Some(cu_entry)) = entries.next_dfs() {
if cu_entry.tag() == gimli::constants::DW_TAG_compile_unit {
if let Some(lang_attr) = cu_entry.attr(gimli::constants::DW_AT_language) {
if let gimli::AttributeValue::Language(lang) = lang_attr.value() {
return Some(lang);
}
}
}
}
None
}
pub fn parse_line_info(&self, module_path: &str) -> Result<LineParseResult> {
debug!("Starting debug_line-only parsing for: {}", module_path);
let mut headers: Vec<gimli::UnitHeader<DwarfReader>> = Vec::new();
let mut units = self.dwarf.units();
while let Some(header) = units.next()? {
headers.push(header);
}
headers.sort_by_key(|h| std::cmp::Reverse(h.length_including_self() as u64));
let shard_results: Vec<Result<LineShard>> = headers
.into_par_iter()
.map(|unit_header| -> Result<LineShard> {
let mut shard = LineShard::default();
let version = unit_header.version();
let unit = self.dwarf.unit(unit_header)?;
if let Some(ref line_program) = unit.line_program {
let cu_name = Self::extract_cu_name_from_dwarf(self.dwarf, &unit)
.unwrap_or_else(|| "unknown".to_string());
let comp_dir = Self::extract_comp_dir_from_dwarf(self.dwarf, &unit);
let mut file_index = LightweightFileIndex::new(comp_dir, version);
let header = line_program.header();
for dir_entry in header.include_directories() {
if let Ok(dir_path) = self.dwarf.attr_string(&unit, dir_entry.clone()) {
if let Ok(s_str) = dir_path.to_string_lossy() {
file_index.add_directory(s_str.into_owned());
}
}
}
for (file_idx, file_entry) in header.file_names().iter().enumerate() {
let file_index_value = if header.version() >= 5 {
file_idx as u64
} else {
(file_idx + 1) as u64
};
if let Ok(filename) = self.dwarf.attr_string(&unit, file_entry.path_name())
{
let dir_index = file_entry.directory_index();
if let Ok(s_str) = filename.to_string_lossy() {
file_index.add_file_entry(
file_index_value,
dir_index,
s_str.into_owned(),
);
}
}
}
let compilation_unit = Self::extract_file_info_from_line_program_static(
self.dwarf,
&unit,
line_program,
)?;
shard.files_count += compilation_unit.files.len();
shard
.compilation_units
.insert(cu_name.clone(), compilation_unit);
shard.file_indices.push((cu_name.clone(), file_index));
let (line_program, sequences) = line_program.clone().sequences()?;
for seq in sequences {
let mut rows = line_program.resume_from(&seq);
let mut pending_entries: Vec<LineEntry> = Vec::new();
let mut pending_address: Option<u64> = None;
while let Some((_, line_row)) = rows.next_row()? {
let row_address = line_row.address();
if line_row.end_sequence() {
Self::flush_pending_line_entries(
&mut shard.line_entries,
&mut pending_entries,
Some(row_address),
);
pending_address = None;
continue;
}
if let Some(address) = pending_address {
if row_address > address {
Self::flush_pending_line_entries(
&mut shard.line_entries,
&mut pending_entries,
Some(row_address),
);
pending_address = Some(row_address);
} else if row_address < address {
Self::flush_pending_line_entries(
&mut shard.line_entries,
&mut pending_entries,
None,
);
pending_address = Some(row_address);
}
} else {
pending_address = Some(row_address);
}
let column = match line_row.column() {
gimli::ColumnType::LeftEdge => 0,
gimli::ColumnType::Column(x) => x.get(),
};
pending_entries.push(LineEntry {
address: row_address,
end_address: None,
file_path: String::new(),
file_index: line_row.file_index(),
compilation_unit: std::sync::Arc::from(cu_name.as_str()),
line: line_row.line().map(|l| l.get()).unwrap_or(0),
column,
is_stmt: line_row.is_stmt(),
prologue_end: line_row.prologue_end(),
});
}
Self::flush_pending_line_entries(
&mut shard.line_entries,
&mut pending_entries,
None,
);
}
}
Ok(shard)
})
.collect();
let mut scoped_file_manager = ScopedFileIndexManager::new();
let mut line_entries = Vec::new();
let mut compilation_units: HashMap<String, CompilationUnit> = HashMap::new();
let mut total_files = 0usize;
for sr in shard_results {
let shard = sr?;
total_files += shard.files_count;
line_entries.extend(shard.line_entries);
for (cu, cuinfo) in shard.compilation_units {
compilation_units.insert(cu, cuinfo);
}
for (cu, fi) in shard.file_indices {
scoped_file_manager.add_compilation_unit(cu, fi);
}
}
let total_line_entries = line_entries.len();
let line_mapping =
LineMappingTable::from_entries_with_scoped_manager(line_entries, &scoped_file_manager);
debug!(
"Completed debug_line parsing for {}: {} line entries, {} files, {} compilation units",
module_path,
total_line_entries,
total_files,
compilation_units.len()
);
Ok(LineParseResult {
line_mapping,
scoped_file_manager,
compilation_units,
line_entries_count: total_line_entries,
files_count: total_files,
})
}
pub fn parse_debug_info(&self, module_path: &str) -> Result<DebugParseResult> {
debug!("Starting debug_info-only parsing for: {}", module_path);
let mut headers: Vec<gimli::UnitHeader<DwarfReader>> = Vec::new();
let mut units = self.dwarf.units();
while let Some(header) = units.next()? {
headers.push(header);
}
let shard_results: Vec<Result<InfoShard>> = headers
.into_par_iter()
.map(|header| -> Result<InfoShard> {
match header.debug_info_offset() {
Some(unit_off) => {
let unit = self.dwarf.unit(header)?;
let cu_lang = self.extract_language(self.dwarf, &unit);
self.process_unit_shard(&unit, unit_off, cu_lang)
}
_ => Ok(InfoShard::default()),
}
})
.collect();
let mut shards = Vec::with_capacity(shard_results.len());
for sr in shard_results {
shards.push(sr?);
}
let mut lightweight_index = LightweightIndex::from_shards(shards);
let functions_count = lightweight_index.get_function_names().len();
let variables_count = lightweight_index.get_variable_names().len();
lightweight_index.build_cu_maps(self.dwarf);
let _ = lightweight_index.build_cu_maps_from_aranges(self.dwarf);
debug!(
"Completed debug_info parsing for {}: {} functions, {} variables",
module_path, functions_count, variables_count
);
Ok(DebugParseResult {
lightweight_index,
functions_count,
variables_count,
})
}
pub fn combine_parallel_results(
line_result: LineParseResult,
info_result: DebugParseResult,
module_path: String,
) -> DwarfParseResult {
debug!("Assembling parallel parse results for: {}", module_path);
let LineParseResult {
line_mapping,
scoped_file_manager,
compilation_units,
line_entries_count,
files_count,
} = line_result;
let stats = DwarfParseStats {
total_functions: info_result.functions_count,
total_variables: info_result.variables_count,
total_line_entries: line_entries_count,
total_files: files_count,
};
DwarfParseResult {
lightweight_index: info_result.lightweight_index,
line_mapping,
scoped_file_manager,
compilation_units,
stats,
}
}
fn extract_file_info_from_line_program_static(
dwarf: &gimli::Dwarf<DwarfReader>,
unit: &gimli::Unit<DwarfReader>,
line_program: &gimli::IncompleteLineProgram<DwarfReader>,
) -> Result<CompilationUnit> {
let cu_name = Self::extract_cu_name_from_dwarf(dwarf, unit)
.unwrap_or_else(|| format!("unknown_cu_{:?}", unit.header.offset()));
debug!("Extracting files from compilation unit: {}", cu_name);
let header = line_program.header();
let mut compilation_unit = CompilationUnit {
base_directory: Self::extract_comp_dir_from_dwarf(dwarf, unit)
.unwrap_or_else(|| ".".to_string()),
include_directories: Vec::new(),
files: Vec::new(),
};
compilation_unit.include_directories = header
.include_directories()
.iter()
.enumerate()
.map(|(i, path)| {
let path_str = dwarf
.attr_string(unit, path.clone())
.ok()
.and_then(|s| s.to_string_lossy().ok().map(|cow| cow.into_owned()))
.unwrap_or_else(|| format!("unknown_dir_{i}"));
tracing::trace!("Include directory [{}]: '{}'", i + 1, path_str);
path_str
})
.collect();
for (file_index, file_entry) in header.file_names().iter().enumerate() {
match Self::extract_source_file_static(
dwarf,
unit,
file_index as u64,
file_entry,
&cu_name,
compilation_unit.base_directory.as_str(),
&compilation_unit.include_directories,
) {
Ok(source_file) => {
compilation_unit.files.push(source_file);
}
Err(e) => {
tracing::trace!("Skipping file entry {}: {}", file_index, e);
}
}
}
tracing::trace!(
"Extracted {} files from compilation unit {}",
compilation_unit.files.len(),
cu_name
);
Ok(compilation_unit)
}
fn extract_cu_name_from_dwarf(
dwarf: &gimli::Dwarf<DwarfReader>,
unit: &gimli::Unit<DwarfReader>,
) -> Option<String> {
let mut entries = unit.entries();
let entry = entries.next_dfs().ok()??;
if let Some(name_attr) = entry.attr_value(gimli::constants::DW_AT_name) {
if let Ok(name) = dwarf.attr_string(unit, name_attr) {
if let Ok(s_str) = name.to_string_lossy() {
return Some(s_str.into_owned());
}
}
}
None
}
fn extract_comp_dir_from_dwarf(
dwarf: &gimli::Dwarf<DwarfReader>,
unit: &gimli::Unit<DwarfReader>,
) -> Option<String> {
let mut entries = unit.entries();
let entry = entries.next_dfs().ok()??;
if let Some(comp_dir_attr) = entry.attr_value(gimli::constants::DW_AT_comp_dir) {
if let Ok(comp_dir) = dwarf.attr_string(unit, comp_dir_attr) {
if let Ok(s_str) = comp_dir.to_string_lossy() {
return Some(s_str.into_owned());
}
}
}
None
}
fn extract_source_file_static(
dwarf: &gimli::Dwarf<DwarfReader>,
unit: &gimli::Unit<DwarfReader>,
file_index: u64,
file_entry: &gimli::FileEntry<DwarfReader>,
compilation_unit: &str,
base_directory: &str,
include_directories: &[String],
) -> anyhow::Result<SourceFile> {
let dir_index = file_entry.directory_index();
let directory_path = directory_from_index(
unit.header.version(),
base_directory,
include_directories,
dir_index,
);
let filename = dwarf
.attr_string(unit, file_entry.path_name())
.ok()
.and_then(|s| s.to_string_lossy().ok().map(|cow| cow.into_owned()))
.unwrap_or_else(|| "unknown".to_string());
if filename == "<built-in>" {
return Err(anyhow::anyhow!("Skipping system file"));
}
let full_path = resolve_file_path(
unit.header.version(),
base_directory,
include_directories,
dir_index,
&filename,
);
tracing::trace!(
"extract_source_file_static: cu='{}', file_index={}, dir_index={}, directory_path='{}', filename='{}', full_path='{}'",
compilation_unit, file_index, dir_index, directory_path, filename, full_path
);
Ok(SourceFile {
directory_path,
filename,
full_path,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::binary::dwarf_reader_from_arc;
use gimli::write::{
Address, AttributeValue as WriteAttributeValue, DebugInfoRef as WriteDebugInfoRef,
Dwarf as WriteDwarf, EndianVec, Expression as WriteExpression, LineProgram, Sections, Unit,
};
use gimli::{Format, LittleEndian};
use object::{Object, ObjectSection};
use std::os::unix::fs::PermissionsExt;
use std::path::{Path, PathBuf};
use std::process::Command;
use std::sync::Arc;
use tempfile::TempPath;
fn test_line_entry(address: u64) -> LineEntry {
LineEntry {
address,
end_address: None,
file_path: "/src/main.c".to_string(),
file_index: 1,
compilation_unit: Arc::from("main.c"),
line: 10,
column: 0,
is_stmt: true,
prologue_end: false,
}
}
fn write_dwarf_to_read(mut dwarf: WriteDwarf) -> gimli::Dwarf<DwarfReader> {
let mut sections = Sections::new(EndianVec::new(LittleEndian));
dwarf.write(&mut sections).unwrap();
let dwarf_sections: gimli::DwarfSections<Vec<u8>> = gimli::DwarfSections::load(|id| {
Ok::<_, gimli::Error>(
sections
.get(id)
.map(|section| section.slice().to_vec())
.unwrap_or_default(),
)
})
.unwrap();
dwarf_sections
.borrow(|section| dwarf_reader_from_arc(Arc::<[u8]>::from(section.as_slice())))
}
fn dwarf_with_debug_info_bytes(debug_info: Vec<u8>) -> gimli::Dwarf<DwarfReader> {
gimli::Dwarf::load(|id| {
let data = if id.name() == ".debug_info" {
debug_info.clone()
} else {
Vec::new()
};
Ok::<_, gimli::Error>(dwarf_reader_from_arc(Arc::<[u8]>::from(data)))
})
.unwrap()
}
#[test]
fn flush_pending_line_entries_keeps_equal_end_bounded() {
let mut out = Vec::new();
let mut pending = vec![test_line_entry(0x1000)];
DwarfParser::flush_pending_line_entries(&mut out, &mut pending, Some(0x1000));
assert!(pending.is_empty());
assert_eq!(out.len(), 1);
assert_eq!(out[0].end_address, Some(0x1000));
assert!(!out[0].contains_address(0x1000));
assert!(!out[0].contains_address(0x1001));
}
#[test]
fn parse_line_info_propagates_unit_header_errors() {
let dwarf = dwarf_with_debug_info_bytes(vec![0x0b, 0x00, 0x00, 0x00, 0x04]);
let parser = DwarfParser { dwarf: &dwarf };
let err = match parser.parse_line_info("malformed") {
Ok(_) => panic!("malformed unit header must not be treated as end-of-units"),
Err(err) => err,
};
assert!(
err.downcast_ref::<gimli::Error>().is_some(),
"expected gimli parse error, got: {err}"
);
}
#[test]
fn parse_debug_info_propagates_unit_header_errors() {
let dwarf = dwarf_with_debug_info_bytes(vec![0x0b, 0x00, 0x00, 0x00, 0x04]);
let parser = DwarfParser { dwarf: &dwarf };
let err = match parser.parse_debug_info("malformed") {
Ok(_) => panic!("malformed unit header must not be treated as end-of-units"),
Err(err) => err,
};
assert!(
err.downcast_ref::<gimli::Error>().is_some(),
"expected gimli parse error, got: {err}"
);
}
fn build_variable_index_fixture() -> gimli::Dwarf<DwarfReader> {
let encoding = gimli::Encoding {
format: Format::Dwarf32,
version: 4,
address_size: 8,
};
let mut dwarf = WriteDwarf::new();
let unit_id = dwarf.units.add(Unit::new(encoding, LineProgram::none()));
let unit = dwarf.units.get_mut(unit_id);
let root = unit.root();
let global_id = unit.add(root, gimli::constants::DW_TAG_variable);
let global = unit.get_mut(global_id);
global.set(
gimli::constants::DW_AT_name,
WriteAttributeValue::String(b"real_global".to_vec()),
);
global.set(
gimli::constants::DW_AT_external,
WriteAttributeValue::Flag(true),
);
let mut global_loc = WriteExpression::new();
global_loc.op_addr(Address::Constant(0x401000));
global.set(
gimli::constants::DW_AT_location,
WriteAttributeValue::Exprloc(global_loc),
);
let subprogram_id = unit.add(root, gimli::constants::DW_TAG_subprogram);
unit.get_mut(subprogram_id).set(
gimli::constants::DW_AT_name,
WriteAttributeValue::String(b"touch".to_vec()),
);
let local_static_id = unit.add(subprogram_id, gimli::constants::DW_TAG_variable);
let local_static = unit.get_mut(local_static_id);
local_static.set(
gimli::constants::DW_AT_name,
WriteAttributeValue::String(b"local_static".to_vec()),
);
let mut local_static_loc = WriteExpression::new();
local_static_loc.op_addr(Address::Constant(0x402000));
local_static.set(
gimli::constants::DW_AT_location,
WriteAttributeValue::Exprloc(local_static_loc),
);
let local_ptr_id = unit.add(subprogram_id, gimli::constants::DW_TAG_variable);
let local_ptr = unit.get_mut(local_ptr_id);
local_ptr.set(
gimli::constants::DW_AT_name,
WriteAttributeValue::String(b"p".to_vec()),
);
let mut local_ptr_loc = WriteExpression::new();
local_ptr_loc.op_addr(Address::Constant(0x403000));
local_ptr_loc.op(gimli::constants::DW_OP_stack_value);
local_ptr.set(
gimli::constants::DW_AT_location,
WriteAttributeValue::Exprloc(local_ptr_loc),
);
let mut sections = Sections::new(EndianVec::new(LittleEndian));
dwarf.write(&mut sections).unwrap();
let dwarf_sections: gimli::DwarfSections<Vec<u8>> = gimli::DwarfSections::load(|id| {
Ok::<_, gimli::Error>(
sections
.get(id)
.map(|section| section.slice().to_vec())
.unwrap_or_default(),
)
})
.unwrap();
dwarf_sections
.borrow(|section| dwarf_reader_from_arc(Arc::<[u8]>::from(section.as_slice())))
}
fn build_inline_origin_fixture() -> gimli::Dwarf<DwarfReader> {
let encoding = gimli::Encoding {
format: Format::Dwarf32,
version: 4,
address_size: 8,
};
let mut dwarf = WriteDwarf::new();
let unit_id = dwarf.units.add(Unit::new(encoding, LineProgram::none()));
let unit = dwarf.units.get_mut(unit_id);
let root = unit.root();
let abstract_id = unit.add(root, gimli::constants::DW_TAG_subprogram);
let abstract_fn = unit.get_mut(abstract_id);
abstract_fn.set(
gimli::constants::DW_AT_name,
WriteAttributeValue::String(b"CGPsend".to_vec()),
);
abstract_fn.set(
gimli::constants::DW_AT_inline,
WriteAttributeValue::Inline(gimli::DW_INL_inlined),
);
abstract_fn.set(
gimli::constants::DW_AT_external,
WriteAttributeValue::Flag(true),
);
let concrete_id = unit.add(root, gimli::constants::DW_TAG_subprogram);
let concrete_fn = unit.get_mut(concrete_id);
concrete_fn.set(
gimli::constants::DW_AT_abstract_origin,
WriteAttributeValue::UnitRef(abstract_id),
);
concrete_fn.set(
gimli::constants::DW_AT_low_pc,
WriteAttributeValue::Address(Address::Constant(0x8e97c0)),
);
concrete_fn.set(
gimli::constants::DW_AT_high_pc,
WriteAttributeValue::Udata(0x420),
);
let inlined_id = unit.add(root, gimli::constants::DW_TAG_inlined_subroutine);
unit.get_mut(inlined_id).set(
gimli::constants::DW_AT_abstract_origin,
WriteAttributeValue::UnitRef(abstract_id),
);
let mut sections = Sections::new(EndianVec::new(LittleEndian));
dwarf.write(&mut sections).unwrap();
let dwarf_sections: gimli::DwarfSections<Vec<u8>> = gimli::DwarfSections::load(|id| {
Ok::<_, gimli::Error>(
sections
.get(id)
.map(|section| section.slice().to_vec())
.unwrap_or_default(),
)
})
.unwrap();
dwarf_sections
.borrow(|section| dwarf_reader_from_arc(Arc::<[u8]>::from(section.as_slice())))
}
fn build_cross_cu_origin_fixture() -> gimli::Dwarf<DwarfReader> {
let encoding = gimli::Encoding {
format: Format::Dwarf32,
version: 4,
address_size: 8,
};
let mut dwarf = WriteDwarf::new();
let decl_unit_id = dwarf.units.add(Unit::new(encoding, LineProgram::none()));
let decl_unit = dwarf.units.get_mut(decl_unit_id);
let decl_root = decl_unit.root();
decl_unit.get_mut(decl_root).set(
gimli::constants::DW_AT_name,
WriteAttributeValue::String(b"decl_unit.c".to_vec()),
);
let abstract_fn_id = decl_unit.add(decl_root, gimli::constants::DW_TAG_subprogram);
let abstract_fn = decl_unit.get_mut(abstract_fn_id);
abstract_fn.set(
gimli::constants::DW_AT_name,
WriteAttributeValue::String(b"cross_cu_origin_fn".to_vec()),
);
abstract_fn.set(
gimli::constants::DW_AT_external,
WriteAttributeValue::Flag(true),
);
let spec_type_id = decl_unit.add(decl_root, gimli::constants::DW_TAG_structure_type);
let spec_type = decl_unit.get_mut(spec_type_id);
spec_type.set(
gimli::constants::DW_AT_name,
WriteAttributeValue::String(b"CrossCuOriginType".to_vec()),
);
spec_type.set(
gimli::constants::DW_AT_declaration,
WriteAttributeValue::Flag(true),
);
let concrete_unit_id = dwarf.units.add(Unit::new(encoding, LineProgram::none()));
let concrete_unit = dwarf.units.get_mut(concrete_unit_id);
let concrete_root = concrete_unit.root();
concrete_unit.get_mut(concrete_root).set(
gimli::constants::DW_AT_name,
WriteAttributeValue::String(b"concrete_unit.c".to_vec()),
);
let concrete_fn_id = concrete_unit.add(concrete_root, gimli::constants::DW_TAG_subprogram);
let concrete_fn = concrete_unit.get_mut(concrete_fn_id);
concrete_fn.set(
gimli::constants::DW_AT_abstract_origin,
WriteAttributeValue::DebugInfoRef(WriteDebugInfoRef::Entry(
decl_unit_id,
abstract_fn_id,
)),
);
concrete_fn.set(
gimli::constants::DW_AT_low_pc,
WriteAttributeValue::Address(Address::Constant(0x501000)),
);
concrete_fn.set(
gimli::constants::DW_AT_high_pc,
WriteAttributeValue::Udata(0x40),
);
let concrete_type_id =
concrete_unit.add(concrete_root, gimli::constants::DW_TAG_structure_type);
concrete_unit.get_mut(concrete_type_id).set(
gimli::constants::DW_AT_specification,
WriteAttributeValue::DebugInfoRef(WriteDebugInfoRef::Entry(decl_unit_id, spec_type_id)),
);
write_dwarf_to_read(dwarf)
}
fn build_multi_cu_shared_function_fixture() -> gimli::Dwarf<DwarfReader> {
let encoding = gimli::Encoding {
format: Format::Dwarf32,
version: 4,
address_size: 8,
};
let mut dwarf = WriteDwarf::new();
for (cu_name, low_pc) in [("unit_one.rs", 0x401000_u64), ("unit_two.rs", 0x402000_u64)] {
let unit_id = dwarf.units.add(Unit::new(encoding, LineProgram::none()));
let unit = dwarf.units.get_mut(unit_id);
let root = unit.root();
unit.get_mut(root).set(
gimli::constants::DW_AT_name,
WriteAttributeValue::String(cu_name.as_bytes().to_vec()),
);
let subprogram_id = unit.add(root, gimli::constants::DW_TAG_subprogram);
let subprogram = unit.get_mut(subprogram_id);
subprogram.set(
gimli::constants::DW_AT_name,
WriteAttributeValue::String(b"shared".to_vec()),
);
subprogram.set(
gimli::constants::DW_AT_low_pc,
WriteAttributeValue::Address(Address::Constant(low_pc)),
);
subprogram.set(
gimli::constants::DW_AT_high_pc,
WriteAttributeValue::Udata(0x20),
);
}
let mut sections = Sections::new(EndianVec::new(LittleEndian));
dwarf.write(&mut sections).unwrap();
let dwarf_sections: gimli::DwarfSections<Vec<u8>> = gimli::DwarfSections::load(|id| {
Ok::<_, gimli::Error>(
sections
.get(id)
.map(|section| section.slice().to_vec())
.unwrap_or_default(),
)
})
.unwrap();
dwarf_sections
.borrow(|section| dwarf_reader_from_arc(Arc::<[u8]>::from(section.as_slice())))
}
fn build_cu_body_lookup_fixture() -> gimli::Dwarf<DwarfReader> {
let encoding = gimli::Encoding {
format: Format::Dwarf32,
version: 4,
address_size: 8,
};
let mut dwarf = WriteDwarf::new();
let unit_id = dwarf.units.add(Unit::new(encoding, LineProgram::none()));
let unit = dwarf.units.get_mut(unit_id);
let root = unit.root();
unit.get_mut(root).set(
gimli::constants::DW_AT_name,
WriteAttributeValue::String(b"body_lookup_unit".to_vec()),
);
let subprogram_id = unit.add(root, gimli::constants::DW_TAG_subprogram);
let subprogram = unit.get_mut(subprogram_id);
subprogram.set(
gimli::constants::DW_AT_name,
WriteAttributeValue::String(b"body_lookup".to_vec()),
);
subprogram.set(
gimli::constants::DW_AT_low_pc,
WriteAttributeValue::Address(Address::Constant(0x401000)),
);
subprogram.set(
gimli::constants::DW_AT_high_pc,
WriteAttributeValue::Udata(0x40),
);
let mut sections = Sections::new(EndianVec::new(LittleEndian));
dwarf.write(&mut sections).unwrap();
let dwarf_sections: gimli::DwarfSections<Vec<u8>> = gimli::DwarfSections::load(|id| {
Ok::<_, gimli::Error>(
sections
.get(id)
.map(|section| section.slice().to_vec())
.unwrap_or_default(),
)
})
.unwrap();
dwarf_sections
.borrow(|section| dwarf_reader_from_arc(Arc::<[u8]>::from(section.as_slice())))
}
fn clang_available() -> bool {
Command::new("clang")
.arg("--version")
.status()
.map(|status| status.success())
.unwrap_or(false)
}
fn compile_inline_callsite_fixture_with_clang_dwarf5() -> anyhow::Result<TempPath> {
let workspace_root = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.parent()
.ok_or_else(|| anyhow::anyhow!("ghostscope-dwarf has no workspace parent"))?
.to_path_buf();
let source = workspace_root
.join("e2e-tests/tests/fixtures/inline_callsite_program/inline_callsite_program.c");
let binary = tempfile::Builder::new()
.prefix("ghostscope-fast-parser-")
.tempfile()?
.into_temp_path();
let binary_path = binary.to_path_buf();
let compile_output = Command::new("clang")
.args(["-Wall", "-Wextra", "-gdwarf-5", "-O3"])
.arg("-o")
.arg(&binary_path)
.arg(&source)
.output()
.map_err(|e| anyhow::anyhow!("Failed to run clang for {}: {}", source.display(), e))?;
if compile_output.status.success() {
Ok(binary)
} else {
let stderr = String::from_utf8_lossy(&compile_output.stderr);
Err(anyhow::anyhow!(
"Failed to compile {} with clang -gdwarf-5 -O3: {}",
source.display(),
stderr
))
}
}
fn load_dwarf_from_binary(path: &Path) -> anyhow::Result<gimli::Dwarf<DwarfReader>> {
let bytes = std::fs::read(path)
.map_err(|e| anyhow::anyhow!("Failed to read {}: {}", path.display(), e))?;
let object = object::File::parse(&*bytes)
.map_err(|e| anyhow::anyhow!("Failed to parse {}: {}", path.display(), e))?;
let dwarf = gimli::Dwarf::load(|id| {
let section_data = object
.section_by_name(id.name())
.and_then(|section| section.uncompressed_data().ok())
.map(|data| data.into_owned())
.unwrap_or_default();
Ok::<_, gimli::Error>(dwarf_reader_from_arc(Arc::<[u8]>::from(section_data)))
})?;
Ok(dwarf)
}
fn read_uleb128(input: &[u8], offset: &mut usize) -> anyhow::Result<u64> {
let mut value = 0_u64;
let mut shift = 0_u32;
loop {
let byte = *input
.get(*offset)
.ok_or_else(|| anyhow::anyhow!("Unexpected EOF while reading ULEB128"))?;
*offset += 1;
let low_bits = u64::from(byte & 0x7f);
anyhow::ensure!(
shift < 64 && !(shift == 63 && low_bits > 1),
"ULEB128 value exceeds u64"
);
value |= low_bits << shift;
if byte & 0x80 == 0 {
return Ok(value);
}
shift += 7;
}
}
#[test]
fn read_uleb128_rejects_values_that_overflow_u64() {
let overflow = [0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x02];
let mut offset = 0;
let err = read_uleb128(&overflow, &mut offset).expect_err("overflow should be rejected");
assert!(
err.to_string().contains("exceeds u64"),
"unexpected overflow error: {err}"
);
}
fn patch_inlined_subroutine_low_pc_to_entry_pc(abbrev: &mut [u8]) -> anyhow::Result<usize> {
let mut offset = 0;
let mut patched = 0;
while offset < abbrev.len() {
let code = read_uleb128(abbrev, &mut offset)?;
if code == 0 {
continue;
}
let tag = read_uleb128(abbrev, &mut offset)?;
let _has_children = *abbrev
.get(offset)
.ok_or_else(|| anyhow::anyhow!("Missing abbrev children byte"))?;
offset += 1;
loop {
let name_offset = offset;
let name = read_uleb128(abbrev, &mut offset)?;
let form = read_uleb128(abbrev, &mut offset)?;
if name == 0 && form == 0 {
break;
}
let is_addrx_form = form == u64::from(gimli::constants::DW_FORM_addrx.0)
|| form == u64::from(gimli::constants::DW_FORM_addrx1.0)
|| form == u64::from(gimli::constants::DW_FORM_addrx2.0)
|| form == u64::from(gimli::constants::DW_FORM_addrx3.0)
|| form == u64::from(gimli::constants::DW_FORM_addrx4.0);
if tag == u64::from(gimli::constants::DW_TAG_inlined_subroutine.0)
&& name == u64::from(gimli::constants::DW_AT_low_pc.0)
&& is_addrx_form
{
*abbrev
.get_mut(name_offset)
.ok_or_else(|| anyhow::anyhow!("Invalid abbrev attribute offset"))? =
gimli::constants::DW_AT_entry_pc.0 as u8;
patched += 1;
}
}
}
Ok(patched)
}
fn rewrite_inline_fixture_entry_pc_attr(input_path: &Path) -> anyhow::Result<TempPath> {
let mut bytes = std::fs::read(input_path)
.map_err(|e| anyhow::anyhow!("Failed to read {}: {}", input_path.display(), e))?;
let (abbrev_offset, abbrev_size) = {
let object = object::File::parse(&*bytes)
.map_err(|e| anyhow::anyhow!("Failed to parse {}: {}", input_path.display(), e))?;
let section = object.section_by_name(".debug_abbrev").ok_or_else(|| {
anyhow::anyhow!("{} is missing .debug_abbrev", input_path.display())
})?;
section.file_range().ok_or_else(|| {
anyhow::anyhow!(
"{} has no file range for .debug_abbrev",
input_path.display()
)
})?
};
let patched = patch_inlined_subroutine_low_pc_to_entry_pc(
&mut bytes[abbrev_offset as usize..(abbrev_offset + abbrev_size) as usize],
)?;
anyhow::ensure!(
patched > 0,
"Expected to patch at least one inline low_pc abbrev in {}",
input_path.display()
);
let output = tempfile::Builder::new()
.prefix(".ghostscope-fast-parser-patched-")
.tempfile()?
.into_temp_path();
std::fs::write(&output, &bytes)?;
let perms = std::fs::metadata(input_path)?.permissions().mode();
std::fs::set_permissions(&output, std::fs::Permissions::from_mode(perms))?;
Ok(output)
}
fn has_inline_entry_pc_debug_addr_index(dwarf: &gimli::Dwarf<DwarfReader>) -> bool {
let mut units = dwarf.units();
while let Ok(Some(header)) = units.next() {
let Ok(unit) = dwarf.unit(header) else {
continue;
};
let mut entries = unit.entries();
while let Ok(Some(entry)) = entries.next_dfs() {
if entry.tag() != gimli::constants::DW_TAG_inlined_subroutine {
continue;
}
if let Some(attr) = entry.attr(gimli::constants::DW_AT_entry_pc) {
if matches!(attr.value(), gimli::AttributeValue::DebugAddrIndex(_)) {
return true;
}
}
}
}
false
}
#[test]
fn parse_debug_info_skips_stack_value_address_locals_from_global_index() {
let dwarf = build_variable_index_fixture();
let parser = DwarfParser { dwarf: &dwarf };
let result = parser.parse_debug_info("synthetic").unwrap();
let real_global = result
.lightweight_index
.find_variables_by_name("real_global");
let local_static = result
.lightweight_index
.find_variables_by_name("local_static");
let optimized_local = result.lightweight_index.find_variables_by_name("p");
assert_eq!(
real_global.len(),
1,
"real global should remain indexed: {real_global:?}"
);
assert_eq!(
local_static.len(),
1,
"function-scoped static with real storage should remain indexed: {local_static:?}"
);
assert!(
optimized_local.is_empty(),
"address-valued optimized local must not be indexed as a global: {optimized_local:?}"
);
}
#[test]
fn parse_debug_info_keeps_concrete_abstract_origin_subprogram_non_inline() {
let dwarf = build_inline_origin_fixture();
let parser = DwarfParser { dwarf: &dwarf };
let result = parser.parse_debug_info("synthetic").unwrap();
let entries = result
.lightweight_index
.find_dies_by_function_name("CGPsend");
let concrete_entries: Vec<_> = entries
.iter()
.copied()
.filter(|entry| {
entry.function_kind() == crate::core::FunctionDieKind::ConcreteSubprogram
})
.collect();
let abstract_entries: Vec<_> = entries
.iter()
.copied()
.filter(|entry| {
entry.function_kind() == crate::core::FunctionDieKind::AbstractSubprogram
})
.collect();
let inlined_entries: Vec<_> = entries
.iter()
.copied()
.filter(|entry| entry.function_kind() == crate::core::FunctionDieKind::InlineInstance)
.collect();
assert_eq!(
concrete_entries.len(),
1,
"concrete out-of-line subprogram should stay non-inline: {entries:?}"
);
assert_eq!(
abstract_entries.len(),
1,
"only the abstract inline definition should carry the inline flag: {entries:?}"
);
assert_eq!(
inlined_entries.len(),
1,
"expected one inlined subroutine instance: {entries:?}"
);
assert!(
inlined_entries[0].is_inline_instance(),
"DW_TAG_inlined_subroutine must remain an inline instance: {entries:?}"
);
assert!(
!concrete_entries[0].flags.has_inline_attribute,
"concrete out-of-line body should not inherit the abstract inline attribute: {entries:?}"
);
assert!(
abstract_entries[0].flags.has_inline_attribute,
"abstract definition should retain its original DW_AT_inline attribute: {entries:?}"
);
}
#[test]
fn parse_debug_info_resolves_cross_cu_origin_names_for_index_entries() {
let dwarf = build_cross_cu_origin_fixture();
let parser = DwarfParser { dwarf: &dwarf };
let result = parser.parse_debug_info("synthetic").unwrap();
let function_entries = result
.lightweight_index
.find_dies_by_function_name("cross_cu_origin_fn");
assert!(
function_entries.iter().any(|entry| {
entry.function_kind() == crate::core::FunctionDieKind::ConcreteSubprogram
&& entry.representative_addr == Some(0x501000)
}),
"concrete function should inherit its name through a cross-CU origin: {function_entries:?}"
);
let mut has_concrete_type = false;
result
.lightweight_index
.for_each_type_map_entry(|name, entry_base, indices| {
if name != "CrossCuOriginType" {
return;
}
has_concrete_type |= indices.iter().any(|local_idx| {
result
.lightweight_index
.entry(entry_base + *local_idx)
.is_some_and(|entry| !entry.flags.is_type_declaration)
});
});
assert!(
has_concrete_type,
"concrete type should inherit its name through a cross-CU specification"
);
}
#[test]
fn parse_debug_info_keeps_sharded_name_lookup_without_duplicate_keys() {
let dwarf = build_multi_cu_shared_function_fixture();
let parser = DwarfParser { dwarf: &dwarf };
let result = parser.parse_debug_info("synthetic").unwrap();
let entries = result
.lightweight_index
.find_dies_by_function_name("shared");
let names = result.lightweight_index.get_function_names();
assert_eq!(
result.functions_count, 1,
"unique function-name count should stay deduplicated across shards"
);
assert_eq!(
entries.len(),
2,
"function lookup should fan out across CU shards: {entries:?}"
);
assert_eq!(
names
.iter()
.filter(|name| name.as_str() == "shared")
.count(),
1,
"function name listing should not duplicate shard-local keys: {names:?}"
);
}
#[test]
fn parse_debug_info_builds_fallback_cu_map_for_function_body_addresses() {
let dwarf = build_cu_body_lookup_fixture();
let parser = DwarfParser { dwarf: &dwarf };
let result = parser.parse_debug_info("synthetic").unwrap();
let entry = result
.lightweight_index
.find_function_by_address(0x401020, |entry| {
let header = dwarf.unit_header(entry.unit_offset).ok()?;
let unit = dwarf.unit(header).ok()?;
let die = unit.entry(entry.die_offset).ok()?;
crate::parser::RangeExtractor::extract_all_ranges(&die, &unit, &dwarf).ok()
})
.expect("function body address should resolve through fallback CU map");
assert_eq!(entry.name.as_ref(), "body_lookup");
assert_eq!(
result.lightweight_index.find_cu_by_address(0x401020),
Some(entry.unit_offset),
"fallback CU map should cover addresses inside the function body, not just the representative address"
);
}
#[test]
fn parse_debug_info_resolves_debug_addr_index_entry_pc_for_inline_instances() {
if !clang_available() {
eprintln!("Skipping fast_parser DWARF5 entry_pc regression: clang is unavailable");
return;
}
let patched_binary = {
let binary = compile_inline_callsite_fixture_with_clang_dwarf5()
.expect("clang dwarf5 inline fixture should compile");
rewrite_inline_fixture_entry_pc_attr(binary.as_ref())
.expect("inline fixture abbrev should rewrite low_pc addrx into entry_pc addrx")
};
let dwarf = load_dwarf_from_binary(patched_binary.as_ref())
.expect("compiled inline fixture should load as DWARF");
assert!(
has_inline_entry_pc_debug_addr_index(&dwarf),
"patched inline fixture should expose an inlined_subroutine DW_AT_entry_pc via DW_FORM_addrx/.debug_addr"
);
let parser = DwarfParser { dwarf: &dwarf };
let result = parser
.parse_debug_info(patched_binary.to_string_lossy().as_ref())
.expect("fast parser should index patched clang dwarf5 inline fixture");
let inline_entries: Vec<_> = ["add3", "consume_state"]
.into_iter()
.flat_map(|name| {
result
.lightweight_index
.find_dies_by_function_name(name)
.iter()
.copied()
.filter(|entry| {
entry.function_kind() == crate::core::FunctionDieKind::InlineInstance
})
.collect::<Vec<_>>()
})
.collect();
assert!(
!inline_entries.is_empty(),
"expected inline entries for clang dwarf5 inline fixture"
);
assert!(
inline_entries.iter().any(|entry| entry.entry_pc.is_some()),
"fast parser should resolve DW_FORM_addrx-backed DW_AT_entry_pc values for inline instances: {inline_entries:?}"
);
}
}