mirror of
https://github.com/italicsjenga/agb.git
synced 2024-12-23 08:11:33 +11:00
Crash page (#644)
* Uses agb_debug in wasm to show addresses and more with additional debug information!
This commit is contained in:
commit
8c8581e19c
2
.github/workflows/build-site.yml
vendored
2
.github/workflows/build-site.yml
vendored
|
@ -33,6 +33,8 @@ jobs:
|
||||||
uses: peaceiris/actions-mdbook@v2
|
uses: peaceiris/actions-mdbook@v2
|
||||||
with:
|
with:
|
||||||
mdbook-version: "0.4.13"
|
mdbook-version: "0.4.13"
|
||||||
|
- name: Setup wasm
|
||||||
|
run: just setup-cargo-wasm
|
||||||
- name: Build website
|
- name: Build website
|
||||||
run: just build-site
|
run: just build-site
|
||||||
- name: Upload artifact
|
- name: Upload artifact
|
||||||
|
|
|
@ -26,6 +26,7 @@ members = [
|
||||||
"emulator/mgba",
|
"emulator/mgba",
|
||||||
"emulator/mgba-sys",
|
"emulator/mgba-sys",
|
||||||
"emulator/test-runner",
|
"emulator/test-runner",
|
||||||
|
"website/backtrace",
|
||||||
]
|
]
|
||||||
|
|
||||||
exclude = [
|
exclude = [
|
||||||
|
|
|
@ -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;
|
||||||
|
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,18 +1,15 @@
|
||||||
use std::{
|
use std::{
|
||||||
borrow::Cow,
|
borrow::Cow,
|
||||||
|
error::Error,
|
||||||
fs::{self, File},
|
fs::{self, File},
|
||||||
io::Read,
|
io::Read,
|
||||||
path::PathBuf,
|
path::PathBuf,
|
||||||
time::SystemTime,
|
time::SystemTime,
|
||||||
};
|
};
|
||||||
|
|
||||||
use addr2line::gimli;
|
use agb_debug::{address_info, AddressInfo, 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 +21,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,52 +29,28 @@ 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)?;
|
let infos = address_info(&ctx, address.into())?;
|
||||||
|
for info in infos {
|
||||||
|
print_address_info(&info, i, modification_time)?;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn print_address(
|
fn print_address_info(
|
||||||
ctx: &addr2line::Context<impl gimli::Reader>,
|
info: &AddressInfo,
|
||||||
index: usize,
|
index: usize,
|
||||||
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 function_name_to_print = &info.function;
|
||||||
|
|
||||||
let mut is_first = true;
|
if !info.is_inline {
|
||||||
|
|
||||||
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);
|
|
||||||
let function_name_to_print = if is_interesting {
|
|
||||||
function_name.bold()
|
|
||||||
} else {
|
|
||||||
function_name.normal()
|
|
||||||
};
|
|
||||||
|
|
||||||
if is_first {
|
|
||||||
print!("{index}:\t{function_name_to_print}");
|
print!("{index}:\t{function_name_to_print}");
|
||||||
} else {
|
} else {
|
||||||
print!("\t(inlined into) {function_name_to_print}");
|
print!("\t(inlined into) {function_name_to_print}");
|
||||||
|
@ -101,29 +58,22 @@ fn print_address(
|
||||||
|
|
||||||
println!(
|
println!(
|
||||||
" {}:{}",
|
" {}:{}",
|
||||||
prettify_path(&location.filename).green(),
|
prettify_path(&info.location.filename).green(),
|
||||||
location.line.to_string().green()
|
info.location.line.to_string().green()
|
||||||
);
|
);
|
||||||
|
|
||||||
if location.line != 0 && is_interesting {
|
if info.location.line != 0 && info.is_interesting {
|
||||||
print_line_of_code(&frame, location, elf_modification_time)?;
|
print_line_of_code(&info.location, elf_modification_time)?;
|
||||||
}
|
|
||||||
|
|
||||||
is_first = false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn print_line_of_code(
|
fn print_line_of_code(
|
||||||
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 filename = &location.filename;
|
||||||
return Ok(());
|
|
||||||
};
|
|
||||||
|
|
||||||
let Ok(mut file) = File::open(filename) else {
|
let Ok(mut file) = File::open(filename) else {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
};
|
};
|
||||||
|
@ -171,15 +121,3 @@ fn prettify_path(path: &str) -> Cow<'_, str> {
|
||||||
Cow::Borrowed(path)
|
Cow::Borrowed(path)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
17
justfile
17
justfile
|
@ -87,6 +87,15 @@ release +args: (_run-tool "release" args)
|
||||||
miri:
|
miri:
|
||||||
(cd agb-hashmap && cargo miri test)
|
(cd agb-hashmap && cargo miri test)
|
||||||
|
|
||||||
|
setup-cargo-wasm:
|
||||||
|
cargo install wasm-pack
|
||||||
|
|
||||||
|
build-website-backtrace:
|
||||||
|
(cd website/backtrace && wasm-pack build --target web)
|
||||||
|
rm -rf website/agb/src/app/vendor/backtrace
|
||||||
|
mkdir -p website/agb/src/app/vendor
|
||||||
|
cp website/backtrace/pkg website/agb/src/app/vendor/backtrace -r
|
||||||
|
|
||||||
build-mgba-wasm:
|
build-mgba-wasm:
|
||||||
rm -rf website/agb/src/app/mgba/vendor
|
rm -rf website/agb/src/app/mgba/vendor
|
||||||
mkdir website/agb/src/app/mgba/vendor
|
mkdir website/agb/src/app/mgba/vendor
|
||||||
|
@ -96,10 +105,16 @@ build-combo-rom-site:
|
||||||
just _build-rom "examples/combo" "AGBGAMES"
|
just _build-rom "examples/combo" "AGBGAMES"
|
||||||
gzip -9 -c examples/target/examples/combo.gba > website/agb/src/app/combo.gba.gz
|
gzip -9 -c examples/target/examples/combo.gba > website/agb/src/app/combo.gba.gz
|
||||||
|
|
||||||
build-site-app: build-mgba-wasm build-combo-rom-site
|
|
||||||
|
setup-app-build: build-mgba-wasm build-combo-rom-site build-website-backtrace
|
||||||
(cd website/agb && npm install --no-save --prefer-offline --no-audit)
|
(cd website/agb && npm install --no-save --prefer-offline --no-audit)
|
||||||
|
|
||||||
|
build-site-app: setup-app-build
|
||||||
(cd website/agb && npm run build)
|
(cd website/agb && npm run build)
|
||||||
|
|
||||||
|
serve-site-dev: setup-app-build
|
||||||
|
(cd website/agb && npm run dev)
|
||||||
|
|
||||||
build-site: build-site-app build-book
|
build-site: build-site-app build-book
|
||||||
rm -rf website/build
|
rm -rf website/build
|
||||||
cp website/agb/out website/build -r
|
cp website/agb/out website/build -r
|
||||||
|
|
|
@ -1,33 +1,93 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { FC } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useClientValue } from "../useClientValue.hook";
|
|
||||||
|
import { ContentBlock } from "../contentBlock";
|
||||||
|
import { GameDeveloperSummary } from "./gameDeveloperSummary";
|
||||||
import { styled } from "styled-components";
|
import { styled } from "styled-components";
|
||||||
|
import { Debug } from "./debug";
|
||||||
|
|
||||||
const BacktraceWrapper = styled.section`
|
export function BacktracePage() {
|
||||||
display: flex;
|
const [backtrace, setBacktrace] = useState("");
|
||||||
gap: 10px;
|
useEffect(() => {
|
||||||
justify-content: center;
|
setBacktrace(getBacktrace());
|
||||||
`;
|
}, []);
|
||||||
|
|
||||||
function getBacktrace() {
|
|
||||||
return window.location.hash.slice(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function BacktraceDisplay() {
|
|
||||||
const backtrace = useClientValue(getBacktrace) ?? "";
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ContentBlock>
|
||||||
|
<h1>agbrs crash backtrace viewer</h1>
|
||||||
|
<p>
|
||||||
|
You likely got here from the link / QR code that was displayed when a
|
||||||
|
game you were playing crashed. This is the default crash page for games
|
||||||
|
made using the agb library.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
The creator of the game is <em>very</em> likely interested in the code
|
||||||
|
below <em>along with</em> a description of what you were doing at the
|
||||||
|
time.{" "}
|
||||||
|
<strong>Send these to the creator of the game you are playing.</strong>
|
||||||
|
</p>
|
||||||
|
<BacktraceCopyDisplay backtrace={backtrace} setBacktrace={setBacktrace} />
|
||||||
|
<p>
|
||||||
|
<em>
|
||||||
|
The owners of this website are not necessarily the creators of the
|
||||||
|
game you are playing.
|
||||||
|
</em>
|
||||||
|
</p>
|
||||||
|
<h2>Backtrace</h2>
|
||||||
|
{backtrace && <Debug encodedBacktrace={backtrace} />}
|
||||||
|
<GameDeveloperSummary />
|
||||||
|
</ContentBlock>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function BacktraceCopyDisplay({
|
||||||
|
backtrace,
|
||||||
|
setBacktrace,
|
||||||
|
}: {
|
||||||
|
backtrace: string;
|
||||||
|
setBacktrace: (newValue: string) => void;
|
||||||
|
}) {
|
||||||
return (
|
return (
|
||||||
<BacktraceWrapper>
|
<BacktraceWrapper>
|
||||||
<label>Backtrace:</label>
|
<BacktraceInputBox
|
||||||
<input type="text" value={backtrace} />
|
type="text"
|
||||||
<button
|
placeholder="Enter the backtrace code here"
|
||||||
|
onChange={(e) => setBacktrace(e.target.value)}
|
||||||
|
value={backtrace}
|
||||||
|
/>
|
||||||
|
<BacktraceCopyButton
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
navigator.clipboard.writeText(backtrace);
|
navigator.clipboard.writeText(backtrace);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Copy
|
Copy
|
||||||
</button>
|
</BacktraceCopyButton>
|
||||||
</BacktraceWrapper>
|
</BacktraceWrapper>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const BacktraceInputBox = styled.input`
|
||||||
|
font-size: larger;
|
||||||
|
background-color: #eee;
|
||||||
|
border: 1px solid #aaa;
|
||||||
|
border-radius: 4px;
|
||||||
|
min-width: 0;
|
||||||
|
|
||||||
|
flex-grow: 999;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const BacktraceWrapper = styled.section`
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const BacktraceCopyButton = styled.button`
|
||||||
|
padding: 10px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
function getBacktrace() {
|
||||||
|
return window.location.hash.slice(1);
|
||||||
|
}
|
||||||
|
|
210
website/agb/src/app/crash/debug.tsx
Normal file
210
website/agb/src/app/crash/debug.tsx
Normal file
|
@ -0,0 +1,210 @@
|
||||||
|
import { styled } from "styled-components";
|
||||||
|
import { AddressInfo, AgbDebug, useAgbDebug } from "../useAgbDebug.hook";
|
||||||
|
import { ReactNode, useMemo, useState } from "react";
|
||||||
|
|
||||||
|
const BacktraceListWrapper = styled.div`
|
||||||
|
font-size: 1rem;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const BacktraceList = styled.ol`
|
||||||
|
overflow-x: scroll;
|
||||||
|
white-space: nowrap;
|
||||||
|
`;
|
||||||
|
|
||||||
|
interface DebugProps {
|
||||||
|
encodedBacktrace: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Debug(props: DebugProps) {
|
||||||
|
const debug = useAgbDebug();
|
||||||
|
if (debug) {
|
||||||
|
return <DebugBacktraceDecode debug={debug} {...props} />;
|
||||||
|
} else {
|
||||||
|
return <p>Loading debug viewer...</p>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DebugBacktraceDecodeProps extends DebugProps {
|
||||||
|
debug: AgbDebug;
|
||||||
|
}
|
||||||
|
|
||||||
|
const NonWrapCode = styled.code`
|
||||||
|
white-space: nowrap;
|
||||||
|
`;
|
||||||
|
|
||||||
|
function DebugBacktraceDecode({
|
||||||
|
encodedBacktrace,
|
||||||
|
debug,
|
||||||
|
}: DebugBacktraceDecodeProps) {
|
||||||
|
const backtraceAddresses = useBacktraceData(debug, encodedBacktrace);
|
||||||
|
const [backtraceLocations, setBacktraceLocations] = useState<AddressInfo[][]>(
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
const [backtraceLocationsError, setBacktraceLocationsError] =
|
||||||
|
useState<string>("");
|
||||||
|
|
||||||
|
if (typeof backtraceAddresses === "string") {
|
||||||
|
return (
|
||||||
|
<DebugError>
|
||||||
|
Something went wrong decoding the backtrace: {backtraceAddresses}
|
||||||
|
</DebugError>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<BacktraceListWrapper>
|
||||||
|
<BacktraceList>
|
||||||
|
{backtraceAddresses.map((x, idx) => (
|
||||||
|
<li key={idx}>
|
||||||
|
<BacktraceAddressInfo
|
||||||
|
address={x}
|
||||||
|
info={backtraceLocations[idx]}
|
||||||
|
/>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</BacktraceList>
|
||||||
|
</BacktraceListWrapper>
|
||||||
|
<p>
|
||||||
|
If you add the elf file used to make the GBA file, or the GBA file
|
||||||
|
itself if it was made with <NonWrapCode>agb-gbafix --debug</NonWrapCode>
|
||||||
|
, you can see: function names, file names, line numbers, and column
|
||||||
|
numbers.
|
||||||
|
</p>
|
||||||
|
<label>
|
||||||
|
Elf file or GBA file with debug information:{" "}
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
onChange={(evt) => {
|
||||||
|
const files = evt.target.files;
|
||||||
|
if (!files) return;
|
||||||
|
const file = files[0];
|
||||||
|
if (!file) return;
|
||||||
|
setBacktraceLocationsError("");
|
||||||
|
loadLocations(debug, backtraceAddresses, file)
|
||||||
|
.then((data) => setBacktraceLocations(data))
|
||||||
|
.catch((e) => setBacktraceLocationsError(`${e}`));
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
{backtraceLocationsError && (
|
||||||
|
<DebugError>
|
||||||
|
Something went wrong looking up the addresses in the file provided:{" "}
|
||||||
|
{backtraceLocationsError}
|
||||||
|
</DebugError>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const ErrorBlock = styled.div`
|
||||||
|
background-color: #f78f8f;
|
||||||
|
border: 2px solid #9c0a0a;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 20px;
|
||||||
|
margin-top: 10px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
function DebugError({ children }: { children: ReactNode }) {
|
||||||
|
return <ErrorBlock>{children}</ErrorBlock>;
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeNicePath(path: string) {
|
||||||
|
const srcIndex = path.lastIndexOf("/src/");
|
||||||
|
if (srcIndex < 0) return path;
|
||||||
|
|
||||||
|
const crateNameStartIndex = path.slice(0, srcIndex).lastIndexOf("/");
|
||||||
|
const crateName =
|
||||||
|
crateNameStartIndex < 0
|
||||||
|
? "<crate>"
|
||||||
|
: path.slice(crateNameStartIndex + 1, srcIndex);
|
||||||
|
|
||||||
|
return `<${crateName}>/${path.slice(srcIndex + 5)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const GreenSpan = styled.span`
|
||||||
|
color: green;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const BacktraceAddressLine = styled.ul`
|
||||||
|
list-style-type: none;
|
||||||
|
padding-left: 20px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
function BacktraceAddressInfo({
|
||||||
|
address,
|
||||||
|
info,
|
||||||
|
}: {
|
||||||
|
address: number;
|
||||||
|
info: AddressInfo[] | undefined;
|
||||||
|
}) {
|
||||||
|
const formattedAddress = `0x${address.toString(16).padStart(8, "0")}`;
|
||||||
|
if (!info) {
|
||||||
|
return <code>{formattedAddress}</code>;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (info.length === 0) {
|
||||||
|
return (
|
||||||
|
<BacktraceAddressLine>
|
||||||
|
<li>
|
||||||
|
<code>(no info) {formattedAddress}</code>
|
||||||
|
</li>
|
||||||
|
</BacktraceAddressLine>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function FunctionName({
|
||||||
|
interesting,
|
||||||
|
functionName,
|
||||||
|
}: {
|
||||||
|
interesting: boolean;
|
||||||
|
functionName: string;
|
||||||
|
}) {
|
||||||
|
if (interesting) {
|
||||||
|
return <strong>{functionName}</strong>;
|
||||||
|
}
|
||||||
|
return functionName;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(info);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<BacktraceAddressLine>
|
||||||
|
{info.map((x, idx) => (
|
||||||
|
<li key={idx}>
|
||||||
|
<code>
|
||||||
|
{x.is_inline && "(inlined into)"}{" "}
|
||||||
|
<FunctionName
|
||||||
|
interesting={x.is_interesting}
|
||||||
|
functionName={x.function_name}
|
||||||
|
/>{" "}
|
||||||
|
<GreenSpan>
|
||||||
|
{makeNicePath(x.filename)}:{x.line_number}:{x.column}
|
||||||
|
</GreenSpan>
|
||||||
|
</code>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</BacktraceAddressLine>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadLocations(debug: AgbDebug, addresses: number[], file: File) {
|
||||||
|
const buf = await file.arrayBuffer();
|
||||||
|
const view = new Uint8Array(buf);
|
||||||
|
|
||||||
|
const agbDebugFile = debug.debug_file(view);
|
||||||
|
const debugInfo = addresses.map((x) => agbDebugFile.address_info(x));
|
||||||
|
return debugInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
function useBacktraceData(debug: AgbDebug, trace: string) {
|
||||||
|
return useMemo(() => {
|
||||||
|
try {
|
||||||
|
const addresses = debug?.decode_backtrace(trace);
|
||||||
|
return addresses && Array.from(addresses);
|
||||||
|
} catch (e: unknown) {
|
||||||
|
return `${e}`;
|
||||||
|
}
|
||||||
|
}, [debug, trace]);
|
||||||
|
}
|
23
website/agb/src/app/crash/gameDeveloperSummary.tsx
Normal file
23
website/agb/src/app/crash/gameDeveloperSummary.tsx
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
import { styled } from "styled-components";
|
||||||
|
|
||||||
|
export function GameDeveloperSummary() {
|
||||||
|
return (
|
||||||
|
<Details>
|
||||||
|
<Summary>For game developers</Summary>
|
||||||
|
<p>If you don't want players to be sent to this page, you can:</p>
|
||||||
|
<ol>
|
||||||
|
<li>Configure the backtrace page to point to your own site</li>
|
||||||
|
<li>Configure the backtrace page to not point to a site at all</li>
|
||||||
|
<li>Not use the backtrace feature</li>
|
||||||
|
</ol>
|
||||||
|
</Details>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const Details = styled.details`
|
||||||
|
margin-top: 10px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const Summary = styled.summary`
|
||||||
|
font-weight: bold;
|
||||||
|
`;
|
|
@ -1,21 +1,10 @@
|
||||||
import { Metadata } from "next";
|
import { Metadata } from "next";
|
||||||
import { BacktraceDisplay } from "./backtrace";
|
import { BacktracePage } from "./backtrace";
|
||||||
import { ContentBlock } from "../contentBlock";
|
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "agbrs crash backtrace",
|
title: "agbrs crash backtrace",
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function Crash() {
|
export default function Backtrace() {
|
||||||
return (
|
return <BacktracePage />;
|
||||||
<ContentBlock>
|
|
||||||
<h1>agbrs crash backtrace viewer</h1>
|
|
||||||
<p>This page will eventually let you view backtraces in the browser.</p>
|
|
||||||
<p>
|
|
||||||
For now you can copy the backtrace code here and use it with{" "}
|
|
||||||
<code>agb-addr2line</code>
|
|
||||||
</p>
|
|
||||||
<BacktraceDisplay />
|
|
||||||
</ContentBlock>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
37
website/agb/src/app/useAgbDebug.hook.ts
Normal file
37
website/agb/src/app/useAgbDebug.hook.ts
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import debugInit, {
|
||||||
|
decode_backtrace,
|
||||||
|
DebugFile,
|
||||||
|
InitOutput,
|
||||||
|
AddressInfo,
|
||||||
|
} from "./vendor/backtrace/backtrace";
|
||||||
|
|
||||||
|
let agbDebug: Promise<InitOutput> | undefined;
|
||||||
|
|
||||||
|
export { AddressInfo };
|
||||||
|
|
||||||
|
export interface AgbDebug {
|
||||||
|
decode_backtrace: (backtrace: string) => Uint32Array;
|
||||||
|
debug_file: (file: Uint8Array) => DebugFile;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useAgbDebug(): AgbDebug | undefined {
|
||||||
|
const [debug, setDebug] = useState<AgbDebug>();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
(async () => {
|
||||||
|
if (agbDebug === undefined) {
|
||||||
|
agbDebug = debugInit();
|
||||||
|
}
|
||||||
|
|
||||||
|
await agbDebug;
|
||||||
|
|
||||||
|
setDebug({
|
||||||
|
decode_backtrace,
|
||||||
|
debug_file: (file: Uint8Array) => new DebugFile(file),
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return debug;
|
||||||
|
}
|
29
website/backtrace/Cargo.toml
Normal file
29
website/backtrace/Cargo.toml
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
[package]
|
||||||
|
name = "backtrace"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
crate-type = ["cdylib", "rlib"]
|
||||||
|
|
||||||
|
[features]
|
||||||
|
default = ["console_error_panic_hook"]
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
wasm-bindgen = "0.2.84"
|
||||||
|
agb-debug = { path="../../agb-debug" }
|
||||||
|
|
||||||
|
# The `console_error_panic_hook` crate provides better debugging of panics by
|
||||||
|
# logging them with `console.error`. This is great for development, but requires
|
||||||
|
# all the `std::fmt` and `std::panicking` infrastructure, so isn't great for
|
||||||
|
# code size when deploying.
|
||||||
|
console_error_panic_hook = { version = "0.1.7", optional = true }
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
wasm-bindgen-test = "0.3.34"
|
||||||
|
|
||||||
|
[profile.release]
|
||||||
|
# Tell `rustc` to optimize for small code size.
|
||||||
|
opt-level = "s"
|
49
website/backtrace/src/lib.rs
Normal file
49
website/backtrace/src/lib.rs
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
use agb_debug::{addr2line::Context, load_dwarf, Addr2LineContext};
|
||||||
|
use wasm_bindgen::prelude::*;
|
||||||
|
|
||||||
|
#[wasm_bindgen]
|
||||||
|
pub fn decode_backtrace(backtrace: &str) -> Result<Vec<u32>, JsError> {
|
||||||
|
Ok(agb_debug::gwilym_decode(backtrace)?.collect())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen]
|
||||||
|
pub struct DebugFile {
|
||||||
|
dwarf: Addr2LineContext,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen(getter_with_clone)]
|
||||||
|
pub struct AddressInfo {
|
||||||
|
pub filename: String,
|
||||||
|
pub function_name: String,
|
||||||
|
pub line_number: u32,
|
||||||
|
pub column: u32,
|
||||||
|
pub is_interesting: bool,
|
||||||
|
pub is_inline: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen]
|
||||||
|
impl DebugFile {
|
||||||
|
#[wasm_bindgen(constructor)]
|
||||||
|
pub fn new(data: &[u8]) -> Result<DebugFile, JsError> {
|
||||||
|
Ok(DebugFile {
|
||||||
|
dwarf: Context::from_dwarf(load_dwarf(data)?)?,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn address_info(&self, address: u32) -> Result<Vec<AddressInfo>, JsError> {
|
||||||
|
let info = agb_debug::address_info(&self.dwarf, address.into())?;
|
||||||
|
let address_infos = info
|
||||||
|
.into_iter()
|
||||||
|
.map(|x| AddressInfo {
|
||||||
|
filename: x.location.filename,
|
||||||
|
line_number: x.location.line,
|
||||||
|
column: x.location.col,
|
||||||
|
is_interesting: x.is_interesting,
|
||||||
|
is_inline: x.is_inline,
|
||||||
|
function_name: x.function,
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
Ok(address_infos)
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue