use std::{borrow::Cow, path::Path, sync::Arc}; use anyhow::{Result, bail}; use fxprof_processed_profile::{LibraryInfo, Symbol, SymbolTable, debugid::DebugId}; use object::{Object, ObjectSection, ObjectSymbol}; use wholesym::samply_symbols::{DebugIdExt, demangle_any}; use crate::emulator::inline_stack_map::{InlineStackMap, InlineStackMapBuilder}; #[derive(Debug)] pub struct GameInfo { library_info: LibraryInfo, inline_stack_map: InlineStackMap, } impl GameInfo { pub fn new(file_path: &Path, input: &[u8]) -> Result { let file = object::File::parse(input)?; let (name, path) = name_and_path(file_path); let debug_id = file .build_id()? .map(|id| DebugId::from_identifier(id, true)) .unwrap_or_default(); let code_id = file.build_id()?.map(hex::encode); let mut symbols = vec![]; for sym in file.symbols() { symbols.push(Symbol { address: sym.address() as u32, size: Some(sym.size() as u32), name: demangle_any(sym.name()?), }); } let inline_stack_map = build_inline_stack_map(file).unwrap_or_else(|_| InlineStackMap::empty()); let library_info = LibraryInfo { name: name.clone(), debug_name: name, path: path.clone(), debug_path: path, debug_id, code_id, arch: None, symbol_table: Some(Arc::new(SymbolTable::new(symbols))), }; Ok(Self { library_info, inline_stack_map, }) } pub fn empty(file_path: &Path) -> Self { let (name, path) = name_and_path(file_path); let library_info = LibraryInfo { name: name.clone(), debug_name: name, path: path.clone(), debug_path: path, debug_id: DebugId::default(), code_id: None, arch: None, symbol_table: None, }; let inline_stack_map = InlineStackMap::empty(); Self { library_info, inline_stack_map, } } pub fn name(&self) -> &str { &self.library_info.name } pub fn library_info(&self) -> &LibraryInfo { &self.library_info } pub fn inline_stack_map(&self) -> &InlineStackMap { &self.inline_stack_map } } fn build_inline_stack_map(file: object::File) -> Result { let endian = if file.is_little_endian() { gimli::RunTimeEndian::Little } else { gimli::RunTimeEndian::Big }; fn load_section<'a>(file: &'a object::File, id: gimli::SectionId) -> Result> { let input = match file.section_by_name(id.name()) { Some(section) => section.uncompressed_data()?, None => Cow::Owned(vec![]), }; Ok(input) } let dorf = gimli::DwarfSections::load(|id| load_section(&file, id))?; let dorf = dorf.borrow(|sec| gimli::EndianSlice::new(sec, endian)); let mut units = dorf.units(); let mut frames = InlineStackMap::builder(); while let Some(header) = units.next()? { let unit = dorf.unit(header)?; let mut entree = unit.entries_tree(None)?; let root = entree.root()?; let mut ctx = ParseContext { dorf: &dorf, unit: &unit, frames: &mut frames, }; parse_inline(&mut ctx, root)?; } Ok(frames.build()) } type Reader<'a> = gimli::EndianSlice<'a, gimli::RunTimeEndian>; struct ParseContext<'a> { dorf: &'a gimli::Dwarf>, unit: &'a gimli::Unit>, frames: &'a mut InlineStackMapBuilder, } impl ParseContext<'_> { fn name_attr(&self, attr: gimli::AttributeValue) -> Result> { match attr { gimli::AttributeValue::DebugInfoRef(offset) => { let mut units = self.dorf.units(); while let Some(header) = units.next()? { if let Some(offset) = offset.to_unit_offset(&header) { let unit = self.dorf.unit(header)?; return self.name_entry(&unit, offset); } } Ok(None) } gimli::AttributeValue::UnitRef(offset) => self.name_entry(self.unit, offset), other => { bail!("unrecognized attr {other:?}"); } } } fn name_entry( &self, unit: &gimli::Unit, offset: gimli::UnitOffset, ) -> Result> { let abbreviations = self.dorf.abbreviations(&unit.header)?; let mut entries = unit.header.entries_raw(&abbreviations, Some(offset))?; let Some(abbrev) = entries.read_abbreviation()? else { return Ok(None); }; let mut name = None; for spec in abbrev.attributes() { let attr = entries.read_attribute(*spec)?; if attr.name() == gimli::DW_AT_linkage_name || (attr.name() == gimli::DW_AT_name && name.is_none()) { name = Some(self.dorf.attr_string(unit, attr.value())?) } } Ok(name.map(|n| demangle_any(&String::from_utf8_lossy(&n)))) } } fn parse_inline(ctx: &mut ParseContext, node: gimli::EntriesTreeNode) -> Result<()> { if node.entry().tag() == gimli::DW_TAG_inlined_subroutine && let Some(attr) = node.entry().attr_value(gimli::DW_AT_abstract_origin)? && let Some(name) = ctx.name_attr(attr)? { let name = Arc::new(name); let mut ranges = ctx.dorf.die_ranges(ctx.unit, node.entry())?; while let Some(range) = ranges.next()? { let start = range.begin as u32; let end = range.end as u32; ctx.frames.add(start, end, name.clone()); } } let mut children = node.children(); while let Some(child) = children.next()? { parse_inline(ctx, child)?; } Ok(()) } fn name_and_path(file_path: &Path) -> (String, String) { let normalized = normpath::PathExt::normalize(file_path); let path = normalized .as_ref() .map(|n| n.as_path()) .unwrap_or(file_path); let name = match path.file_stem() { Some(s) => s.to_string_lossy().into_owned(), None => "game".to_string(), }; let path = path.to_string_lossy().into_owned(); (name, path) }