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 from_elf(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 from_isx(file_path: &Path, input: &[u8]) -> Self { let (name, path) = name_and_path(file_path); let symbols = extract_isx_symbols(input); 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: symbols.map(|syms| Arc::new(SymbolTable::new(syms))), }; let inline_stack_map = InlineStackMap::empty(); 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()) } fn extract_isx_symbols(input: &[u8]) -> Option> { let mut syms = vec![]; let (_, mut buf) = input.split_at_checked(32)?; while !buf.is_empty() { let typ; (typ, buf) = buf.split_first()?; match typ { 0x11 => { // Code (Virtual Boy) (_, buf) = buf.split_at_checked(4)?; let len_bytes; (len_bytes, buf) = buf.split_first_chunk()?; let len = u32::from_le_bytes(*len_bytes); (_, buf) = buf.split_at_checked(len as usize)?; } 0x13 => { // Range (Virtual Boy) let count_bytes; (count_bytes, buf) = buf.split_first_chunk()?; let count = u16::from_le_bytes(*count_bytes) + 1; (_, buf) = buf.split_at_checked(count as usize * 9)?; } 0x14 => { // Symbol (Virtual Boy) let count_bytes; (count_bytes, buf) = buf.split_first_chunk()?; let count = u16::from_le_bytes(*count_bytes) + 1; for _ in 0..count { let name_len; (name_len, buf) = buf.split_first()?; let name_bytes; (name_bytes, buf) = buf.split_at_checked(*name_len as usize)?; (_, buf) = buf.split_at_checked(2)?; let address_bytes; (address_bytes, buf) = buf.split_first_chunk()?; let name_str = String::from_utf8_lossy(name_bytes); let address = u32::from_le_bytes(*address_bytes); syms.push(Symbol { address, size: Some(4), name: demangle_any(&name_str), }); } } 0x20 | 0x21 | 0x22 => { // System (undocumented) let length_bytes; (length_bytes, buf) = buf.split_first_chunk()?; let length = u32::from_le_bytes(*length_bytes); (_, buf) = buf.split_at_checked(length as usize)?; } _ => { return None; } } } Some(syms) } 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) }