From f269e6364a9fcf18d6f655616f0a97a86806f862 Mon Sep 17 00:00:00 2001 From: Corwin Date: Thu, 18 Apr 2024 00:43:47 +0100 Subject: [PATCH] thiserror + expose some as lib --- agb-debug/Cargo.toml | 2 +- agb-debug/src/gwilym_encoding.rs | 30 +++++++---- agb-debug/src/lib.rs | 93 ++++++++++++++++++++++++++++++++ agb-debug/src/load_dwarf.rs | 38 ++++++++----- agb-debug/src/main.rs | 32 +++-------- 5 files changed, 148 insertions(+), 47 deletions(-) create mode 100644 agb-debug/src/lib.rs diff --git a/agb-debug/Cargo.toml b/agb-debug/Cargo.toml index 4c3f41f9..12977469 100644 --- a/agb-debug/Cargo.toml +++ b/agb-debug/Cargo.toml @@ -8,7 +8,7 @@ description = "CLI utility to convert agb stack trace dumps into human readable repository = "https://github.com/agbrs/agb" [dependencies] -anyhow = "1" +thiserror = "1" clap = { version = "4", features = ["derive"] } addr2line = { version = "0.22", default-features = false, features = [ "rustc-demangle", diff --git a/agb-debug/src/gwilym_encoding.rs b/agb-debug/src/gwilym_encoding.rs index 1ce957f5..f192c28a 100644 --- a/agb-debug/src/gwilym_encoding.rs +++ b/agb-debug/src/gwilym_encoding.rs @@ -1,8 +1,20 @@ use std::{slice::ChunksExact, sync::OnceLock}; +use thiserror::Error; + const ALPHABET: &[u8] = b"0123456789=ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz"; -pub fn gwilym_decode(input: &str) -> anyhow::Result> { +#[derive(Debug, Error)] +pub enum GwilymDecodeError { + #[error("Does not contain version")] + NoVersion, + #[error("Only version 1 is supported")] + WrongVersion, + #[error("Input must be a multiple of 3 but have {0}")] + LengthWrong(usize), +} + +pub fn gwilym_decode(input: &str) -> Result, GwilymDecodeError> { GwilymDecodeIter::new(input) } @@ -11,21 +23,21 @@ pub struct GwilymDecodeIter<'a> { } impl<'a> GwilymDecodeIter<'a> { - fn new(input: &'a str) -> anyhow::Result { + fn new(input: &'a str) -> Result { let input = input .strip_prefix("https://agbrs.dev/crash#") .unwrap_or(input); let Some((input, version)) = input.rsplit_once('v') else { - anyhow::bail!("Does not contain version"); + return Err(GwilymDecodeError::NoVersion); }; if version != "1" { - anyhow::bail!("Only version 1 is supported"); + return Err(GwilymDecodeError::WrongVersion); } if input.len() % 3 != 0 { - anyhow::bail!("Input string must have length a multiple of 3"); + return Err(GwilymDecodeError::LengthWrong(input.len())); } Ok(Self { @@ -75,11 +87,11 @@ fn get_value_for_char(input: u8) -> u32 { #[cfg(test)] mod test { - use super::{gwilym_decode, ALPHABET}; + use super::*; use std::fmt::Write; #[test] - fn should_correctly_decode_16s() -> anyhow::Result<()> { + fn should_correctly_decode_16s() -> Result<(), GwilymDecodeError> { assert_eq!( &gwilym_decode("2QI65Q69306Kv1")?.collect::>(), &[0x0800_16d3, 0x0800_315b, 0x0800_3243, 0x0800_0195] @@ -112,7 +124,7 @@ mod test { } #[test] - fn should_correctly_decode_16s_and_32s() -> anyhow::Result<()> { + fn should_correctly_decode_16s_and_32s() -> Result<(), Box> { let trace: &[u32] = &[ 0x0300_2990, 0x0800_3289, @@ -143,7 +155,7 @@ mod test { } #[test] - fn should_strip_the_agbrsdev_prefix() -> anyhow::Result<()> { + fn should_strip_the_agbrsdev_prefix() -> Result<(), Box> { assert_eq!( &gwilym_decode("https://agbrs.dev/crash#2QI65Q69306Kv1")?.collect::>(), &[0x0800_16d3, 0x0800_315b, 0x0800_3243, 0x0800_0195] diff --git a/agb-debug/src/lib.rs b/agb-debug/src/lib.rs new file mode 100644 index 00000000..e353a0ca --- /dev/null +++ b/agb-debug/src/lib.rs @@ -0,0 +1,93 @@ +mod gwilym_encoding; +mod load_dwarf; + +use addr2line::gimli::{self, EndianReader}; +pub use gwilym_encoding::{gwilym_decode, GwilymDecodeError}; +pub use load_dwarf::{load_dwarf, GimliDwarf, LoadDwarfError}; +use thiserror::Error; + +pub use addr2line; + +pub struct AddressInfo { + pub location: Location, + pub is_interesting: bool, + pub is_inline: bool, + pub function: String, +} + +#[derive(Debug, Error)] +pub enum AddressInfoError { + #[error(transparent)] + Gimli(#[from] gimli::Error), +} + +pub struct Location { + pub filename: String, + pub line: u32, + pub col: u32, +} + +pub type Addr2LineContext = addr2line::Context>; + +impl Default for Location { + fn default() -> Self { + Self { + filename: "??".to_string(), + line: 0, + col: 0, + } + } +} + +pub fn address_info( + ctx: &Addr2LineContext, + address: u64, +) -> Result, AddressInfoError> { + let mut frames = ctx.find_frames(address).skip_all_loads()?; + + let mut is_first = true; + + let mut infos = Vec::new(); + + while let Some(frame) = frames.next()? { + let function_name = if let Some(ref func) = frame.function { + func.demangle()?.into_owned() + } else { + "unknown function".to_string() + }; + + let location = frame + .location + .as_ref() + .map(|location| Location { + filename: location.file.unwrap_or("??").to_owned(), + line: location.line.unwrap_or(0), + col: location.column.unwrap_or(0), + }) + .unwrap_or_default(); + + let is_interesting = is_interesting_function(&function_name, &location.filename); + + infos.push(AddressInfo { + location, + is_interesting, + is_inline: is_first, + function: function_name, + }); + is_first = false; + } + + Ok(infos) +} + +fn is_interesting_function(function_name: &str, path: &str) -> bool { + if function_name == "rust_begin_unwind" { + return false; // this is the unwind exception call + } + + if path.ends_with("panicking.rs") { + return false; // probably part of rust's internal panic mechanisms + } + + true +} diff --git a/agb-debug/src/load_dwarf.rs b/agb-debug/src/load_dwarf.rs index 394e5e57..d4b13ab1 100644 --- a/agb-debug/src/load_dwarf.rs +++ b/agb-debug/src/load_dwarf.rs @@ -4,38 +4,52 @@ use addr2line::{ gimli, object::{self, Object}, }; -use anyhow::bail; +use thiserror::Error; -pub fn load_dwarf( - file_content: &[u8], -) -> anyhow::Result>> { +#[derive(Debug, Error)] +pub enum LoadDwarfError { + #[error("Gba file is empty")] + GbaFileEmpty, + #[error("Failed to load debug information from ROM file, it might not have been included?")] + NoDebugInformation, + #[error("Failed to load debug information: {0}")] + DeserializationError(#[from] rmp_serde::decode::Error), + #[error(transparent)] + GimliError(#[from] gimli::Error), +} + +pub type GimliDwarf = gimli::Dwarf>; + +pub fn load_dwarf(file_content: &[u8]) -> Result { if let Ok(object) = object::File::parse(file_content) { - return load_from_object(&object); + return Ok(load_from_object(&object)?); } // the file might have been padded, so ensure we skip any padding before continuing let last_non_zero_byte = file_content .iter() .rposition(|&b| b != 0) - .ok_or_else(|| anyhow::anyhow!("Gba file is empty"))?; + .ok_or_else(|| LoadDwarfError::GbaFileEmpty)?; let file_content = &file_content[..last_non_zero_byte + 1]; let last_8_bytes = &file_content[file_content.len() - 8..]; - let len = u32::from_le_bytes(last_8_bytes[0..4].try_into()?) as usize; + let len = u32::from_le_bytes( + last_8_bytes[0..4] + .try_into() + .or(Err(LoadDwarfError::NoDebugInformation))?, + ) as usize; let version = &last_8_bytes[4..]; if version != b"agb1" { - bail!("Failed to load debug information from ROM file, it might not have been included?"); + return Err(LoadDwarfError::NoDebugInformation); } let compressed_debug_data = &file_content[file_content.len() - len - 8..file_content.len() - 8]; let decompressing_reader = lz4_flex::frame::FrameDecoder::new(Cursor::new(compressed_debug_data)); - let debug_info: HashMap> = - rmp_serde::decode::from_read(decompressing_reader) - .map_err(|e| anyhow::anyhow!("Failed to load debug information: {e}"))?; + let debug_info: HashMap> = rmp_serde::decode::from_read(decompressing_reader)?; let dwarf = gimli::Dwarf::load(|id| { let data = debug_info @@ -54,7 +68,7 @@ pub fn load_dwarf( fn load_from_object<'file>( object: &object::File<'file, &'file [u8]>, -) -> anyhow::Result>> { +) -> Result { let endian = if object.is_little_endian() { gimli::RunTimeEndian::Little } else { diff --git a/agb-debug/src/main.rs b/agb-debug/src/main.rs index efb5bd62..00611148 100644 --- a/agb-debug/src/main.rs +++ b/agb-debug/src/main.rs @@ -1,5 +1,6 @@ use std::{ borrow::Cow, + error::Error, fs::{self, File}, io::Read, path::PathBuf, @@ -7,12 +8,9 @@ use std::{ }; use addr2line::gimli; +use agb_debug::Location; use clap::Parser; use colored::Colorize; -use load_dwarf::load_dwarf; - -mod gwilym_encoding; -mod load_dwarf; #[derive(Parser, Debug)] #[command(version, about, long_about = None)] @@ -24,23 +22,7 @@ struct Args { dump: String, } -struct Location { - filename: String, - line: u32, - col: u32, -} - -impl Default for Location { - fn default() -> Self { - Self { - filename: "??".to_string(), - line: 0, - col: 0, - } - } -} - -fn main() -> anyhow::Result<()> { +fn main() -> Result<(), Box> { let cli = Args::parse(); let modification_time = fs::metadata(&cli.elf_path)? @@ -48,11 +30,11 @@ fn main() -> anyhow::Result<()> { .unwrap_or(SystemTime::UNIX_EPOCH); let file = fs::read(&cli.elf_path)?; - let dwarf = load_dwarf(&file)?; + let dwarf = agb_debug::load_dwarf(&file)?; let ctx = addr2line::Context::from_dwarf(dwarf)?; - for (i, address) in gwilym_encoding::gwilym_decode(&cli.dump)?.enumerate() { + for (i, address) in agb_debug::gwilym_decode(&cli.dump)?.enumerate() { print_address(&ctx, i, address.into(), modification_time)?; } @@ -64,7 +46,7 @@ fn print_address( index: usize, address: u64, elf_modification_time: SystemTime, -) -> anyhow::Result<()> { +) -> Result<(), Box> { let mut frames = ctx.find_frames(address).skip_all_loads()?; let mut is_first = true; @@ -119,7 +101,7 @@ fn print_line_of_code( frame: &addr2line::Frame<'_, impl gimli::Reader>, location: Location, elf_modification_time: SystemTime, -) -> anyhow::Result<()> { +) -> Result<(), Box> { let Some(filename) = frame.location.as_ref().and_then(|location| location.file) else { return Ok(()); };