lemur/src/emulator/game_info.rs

202 lines
6.4 KiB
Rust

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<Self> {
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<InlineStackMap> {
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<Cow<'a, [u8]>> {
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<Reader<'a>>,
unit: &'a gimli::Unit<Reader<'a>>,
frames: &'a mut InlineStackMapBuilder,
}
impl ParseContext<'_> {
fn name_attr(&self, attr: gimli::AttributeValue<Reader>) -> Result<Option<String>> {
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<Reader>,
offset: gimli::UnitOffset,
) -> Result<Option<String>> {
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<Reader>) -> 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)
}