mirror of
https://github.com/italicsjenga/agb.git
synced 2024-12-23 08:11:33 +11:00
thiserror + expose some as lib
This commit is contained in:
parent
72e7850152
commit
f269e6364a
|
@ -8,7 +8,7 @@ description = "CLI utility to convert agb stack trace dumps into human readable
|
||||||
repository = "https://github.com/agbrs/agb"
|
repository = "https://github.com/agbrs/agb"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
anyhow = "1"
|
thiserror = "1"
|
||||||
clap = { version = "4", features = ["derive"] }
|
clap = { version = "4", features = ["derive"] }
|
||||||
addr2line = { version = "0.22", default-features = false, features = [
|
addr2line = { version = "0.22", default-features = false, features = [
|
||||||
"rustc-demangle",
|
"rustc-demangle",
|
||||||
|
|
|
@ -1,8 +1,20 @@
|
||||||
use std::{slice::ChunksExact, sync::OnceLock};
|
use std::{slice::ChunksExact, sync::OnceLock};
|
||||||
|
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
const ALPHABET: &[u8] = b"0123456789=ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz";
|
const ALPHABET: &[u8] = b"0123456789=ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz";
|
||||||
|
|
||||||
pub fn gwilym_decode(input: &str) -> anyhow::Result<GwilymDecodeIter<'_>> {
|
#[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<GwilymDecodeIter<'_>, GwilymDecodeError> {
|
||||||
GwilymDecodeIter::new(input)
|
GwilymDecodeIter::new(input)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -11,21 +23,21 @@ pub struct GwilymDecodeIter<'a> {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> GwilymDecodeIter<'a> {
|
impl<'a> GwilymDecodeIter<'a> {
|
||||||
fn new(input: &'a str) -> anyhow::Result<Self> {
|
fn new(input: &'a str) -> Result<Self, GwilymDecodeError> {
|
||||||
let input = input
|
let input = input
|
||||||
.strip_prefix("https://agbrs.dev/crash#")
|
.strip_prefix("https://agbrs.dev/crash#")
|
||||||
.unwrap_or(input);
|
.unwrap_or(input);
|
||||||
|
|
||||||
let Some((input, version)) = input.rsplit_once('v') else {
|
let Some((input, version)) = input.rsplit_once('v') else {
|
||||||
anyhow::bail!("Does not contain version");
|
return Err(GwilymDecodeError::NoVersion);
|
||||||
};
|
};
|
||||||
|
|
||||||
if version != "1" {
|
if version != "1" {
|
||||||
anyhow::bail!("Only version 1 is supported");
|
return Err(GwilymDecodeError::WrongVersion);
|
||||||
}
|
}
|
||||||
|
|
||||||
if input.len() % 3 != 0 {
|
if input.len() % 3 != 0 {
|
||||||
anyhow::bail!("Input string must have length a multiple of 3");
|
return Err(GwilymDecodeError::LengthWrong(input.len()));
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
|
@ -75,11 +87,11 @@ fn get_value_for_char(input: u8) -> u32 {
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod test {
|
||||||
use super::{gwilym_decode, ALPHABET};
|
use super::*;
|
||||||
use std::fmt::Write;
|
use std::fmt::Write;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn should_correctly_decode_16s() -> anyhow::Result<()> {
|
fn should_correctly_decode_16s() -> Result<(), GwilymDecodeError> {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
&gwilym_decode("2QI65Q69306Kv1")?.collect::<Vec<_>>(),
|
&gwilym_decode("2QI65Q69306Kv1")?.collect::<Vec<_>>(),
|
||||||
&[0x0800_16d3, 0x0800_315b, 0x0800_3243, 0x0800_0195]
|
&[0x0800_16d3, 0x0800_315b, 0x0800_3243, 0x0800_0195]
|
||||||
|
@ -112,7 +124,7 @@ mod test {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn should_correctly_decode_16s_and_32s() -> anyhow::Result<()> {
|
fn should_correctly_decode_16s_and_32s() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
let trace: &[u32] = &[
|
let trace: &[u32] = &[
|
||||||
0x0300_2990,
|
0x0300_2990,
|
||||||
0x0800_3289,
|
0x0800_3289,
|
||||||
|
@ -143,7 +155,7 @@ mod test {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn should_strip_the_agbrsdev_prefix() -> anyhow::Result<()> {
|
fn should_strip_the_agbrsdev_prefix() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
&gwilym_decode("https://agbrs.dev/crash#2QI65Q69306Kv1")?.collect::<Vec<_>>(),
|
&gwilym_decode("https://agbrs.dev/crash#2QI65Q69306Kv1")?.collect::<Vec<_>>(),
|
||||||
&[0x0800_16d3, 0x0800_315b, 0x0800_3243, 0x0800_0195]
|
&[0x0800_16d3, 0x0800_315b, 0x0800_3243, 0x0800_0195]
|
||||||
|
|
93
agb-debug/src/lib.rs
Normal file
93
agb-debug/src/lib.rs
Normal file
|
@ -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<gimli::EndianRcSlice<gimli::RunTimeEndian>>;
|
||||||
|
|
||||||
|
impl Default for Location {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
filename: "??".to_string(),
|
||||||
|
line: 0,
|
||||||
|
col: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn address_info(
|
||||||
|
ctx: &Addr2LineContext,
|
||||||
|
address: u64,
|
||||||
|
) -> Result<Vec<AddressInfo>, 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
|
||||||
|
}
|
|
@ -4,38 +4,52 @@ use addr2line::{
|
||||||
gimli,
|
gimli,
|
||||||
object::{self, Object},
|
object::{self, Object},
|
||||||
};
|
};
|
||||||
use anyhow::bail;
|
use thiserror::Error;
|
||||||
|
|
||||||
pub fn load_dwarf(
|
#[derive(Debug, Error)]
|
||||||
file_content: &[u8],
|
pub enum LoadDwarfError {
|
||||||
) -> anyhow::Result<gimli::Dwarf<gimli::EndianRcSlice<gimli::RunTimeEndian>>> {
|
#[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<gimli::EndianRcSlice<gimli::RunTimeEndian>>;
|
||||||
|
|
||||||
|
pub fn load_dwarf(file_content: &[u8]) -> Result<GimliDwarf, LoadDwarfError> {
|
||||||
if let Ok(object) = object::File::parse(file_content) {
|
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
|
// the file might have been padded, so ensure we skip any padding before continuing
|
||||||
let last_non_zero_byte = file_content
|
let last_non_zero_byte = file_content
|
||||||
.iter()
|
.iter()
|
||||||
.rposition(|&b| b != 0)
|
.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 file_content = &file_content[..last_non_zero_byte + 1];
|
||||||
|
|
||||||
let last_8_bytes = &file_content[file_content.len() - 8..];
|
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..];
|
let version = &last_8_bytes[4..];
|
||||||
|
|
||||||
if version != b"agb1" {
|
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 compressed_debug_data = &file_content[file_content.len() - len - 8..file_content.len() - 8];
|
||||||
|
|
||||||
let decompressing_reader =
|
let decompressing_reader =
|
||||||
lz4_flex::frame::FrameDecoder::new(Cursor::new(compressed_debug_data));
|
lz4_flex::frame::FrameDecoder::new(Cursor::new(compressed_debug_data));
|
||||||
let debug_info: HashMap<String, Vec<u8>> =
|
let debug_info: HashMap<String, Vec<u8>> = rmp_serde::decode::from_read(decompressing_reader)?;
|
||||||
rmp_serde::decode::from_read(decompressing_reader)
|
|
||||||
.map_err(|e| anyhow::anyhow!("Failed to load debug information: {e}"))?;
|
|
||||||
|
|
||||||
let dwarf = gimli::Dwarf::load(|id| {
|
let dwarf = gimli::Dwarf::load(|id| {
|
||||||
let data = debug_info
|
let data = debug_info
|
||||||
|
@ -54,7 +68,7 @@ pub fn load_dwarf(
|
||||||
|
|
||||||
fn load_from_object<'file>(
|
fn load_from_object<'file>(
|
||||||
object: &object::File<'file, &'file [u8]>,
|
object: &object::File<'file, &'file [u8]>,
|
||||||
) -> anyhow::Result<gimli::Dwarf<gimli::EndianRcSlice<gimli::RunTimeEndian>>> {
|
) -> Result<GimliDwarf, gimli::Error> {
|
||||||
let endian = if object.is_little_endian() {
|
let endian = if object.is_little_endian() {
|
||||||
gimli::RunTimeEndian::Little
|
gimli::RunTimeEndian::Little
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
use std::{
|
use std::{
|
||||||
borrow::Cow,
|
borrow::Cow,
|
||||||
|
error::Error,
|
||||||
fs::{self, File},
|
fs::{self, File},
|
||||||
io::Read,
|
io::Read,
|
||||||
path::PathBuf,
|
path::PathBuf,
|
||||||
|
@ -7,12 +8,9 @@ use std::{
|
||||||
};
|
};
|
||||||
|
|
||||||
use addr2line::gimli;
|
use addr2line::gimli;
|
||||||
|
use agb_debug::Location;
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
use colored::Colorize;
|
use colored::Colorize;
|
||||||
use load_dwarf::load_dwarf;
|
|
||||||
|
|
||||||
mod gwilym_encoding;
|
|
||||||
mod load_dwarf;
|
|
||||||
|
|
||||||
#[derive(Parser, Debug)]
|
#[derive(Parser, Debug)]
|
||||||
#[command(version, about, long_about = None)]
|
#[command(version, about, long_about = None)]
|
||||||
|
@ -24,23 +22,7 @@ struct Args {
|
||||||
dump: String,
|
dump: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
struct Location {
|
fn main() -> Result<(), Box<dyn Error>> {
|
||||||
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<()> {
|
|
||||||
let cli = Args::parse();
|
let cli = Args::parse();
|
||||||
|
|
||||||
let modification_time = fs::metadata(&cli.elf_path)?
|
let modification_time = fs::metadata(&cli.elf_path)?
|
||||||
|
@ -48,11 +30,11 @@ fn main() -> anyhow::Result<()> {
|
||||||
.unwrap_or(SystemTime::UNIX_EPOCH);
|
.unwrap_or(SystemTime::UNIX_EPOCH);
|
||||||
|
|
||||||
let file = fs::read(&cli.elf_path)?;
|
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)?;
|
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)?;
|
print_address(&ctx, i, address.into(), modification_time)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -64,7 +46,7 @@ fn print_address(
|
||||||
index: usize,
|
index: usize,
|
||||||
address: u64,
|
address: u64,
|
||||||
elf_modification_time: SystemTime,
|
elf_modification_time: SystemTime,
|
||||||
) -> anyhow::Result<()> {
|
) -> Result<(), Box<dyn Error>> {
|
||||||
let mut frames = ctx.find_frames(address).skip_all_loads()?;
|
let mut frames = ctx.find_frames(address).skip_all_loads()?;
|
||||||
|
|
||||||
let mut is_first = true;
|
let mut is_first = true;
|
||||||
|
@ -119,7 +101,7 @@ fn print_line_of_code(
|
||||||
frame: &addr2line::Frame<'_, impl gimli::Reader>,
|
frame: &addr2line::Frame<'_, impl gimli::Reader>,
|
||||||
location: Location,
|
location: Location,
|
||||||
elf_modification_time: SystemTime,
|
elf_modification_time: SystemTime,
|
||||||
) -> anyhow::Result<()> {
|
) -> Result<(), Box<dyn Error>> {
|
||||||
let Some(filename) = frame.location.as_ref().and_then(|location| location.file) else {
|
let Some(filename) = frame.location.as_ref().and_then(|location| location.file) else {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
};
|
};
|
||||||
|
|
Loading…
Reference in a new issue