Initial commit: xenia-rs workspace for Xbox 360 RE

Rust reimplementation of the xenia Xbox 360 emulator targeting reverse-
engineering and preservation, initially scoped to Project Sylpheed.

Includes:
- XEX2 loader (LZX decompression, AES decryption, PE parsing)
- XISO / XGD2 disc image VFS
- PPC interpreter with 200+ opcodes and VMX128 decoding
- Static analyzer: functions, cross-references, labels, asm + SQLite output
- HLE kernel covering the xboxkrnl/xam subset used by Sylpheed init
- Debugger with in-memory and SQLite-backed execution tracing
- `xenia-rs` CLI with extract/dis/exec commands that produce cumulative,
  superset SQLite databases and opt-in instruction/import/branch traces

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
MechaCat02
2026-04-16 23:11:49 +02:00
commit c694bb3f43
63 changed files with 13456 additions and 0 deletions

View File

@@ -0,0 +1,318 @@
//! Assembly text output formatter for Xbox 360 disassembly.
use std::collections::HashMap;
use std::io::Write;
use xenia_xex::header::ImportLibrary;
use xenia_xex::pe::PeSection;
use crate::func::FuncAnalysis;
use crate::xref::{XrefKind, Xref, XrefMap, section_for_addr, resolve_source_label};
/// Metadata passed to the formatter (avoids exposing full Xex2Header internals).
pub struct DisasmInfo<'a> {
pub image_base: u32,
pub entry_point: u32,
pub original_pe_name: Option<&'a str>,
pub title_id: Option<u32>,
pub media_id: Option<u32>,
pub sections: &'a [PeSection],
pub import_libraries: &'a [ImportLibrary],
}
/// Write full disassembly to the output stream.
pub fn write_asm(
out: &mut dyn Write,
pe: &[u8],
info: &DisasmInfo,
func_analysis: &FuncAnalysis,
labels: &HashMap<u32, String>,
import_map: &HashMap<u32, String>,
xrefs: &XrefMap,
data_annotations: &HashMap<u32, (u32, XrefKind)>,
) -> anyhow::Result<()> {
// Header
writeln!(out, "; ============================================================================")?;
writeln!(out, "; Xbox 360 Disassembly — generated by xenia-rs")?;
if let Some(name) = info.original_pe_name {
writeln!(out, "; Original PE: {name}")?;
}
if let (Some(title_id), Some(media_id)) = (info.title_id, info.media_id) {
writeln!(out, "; Title ID: 0x{title_id:08X} Media ID: 0x{media_id:08X}")?;
}
writeln!(out, "; Image base: 0x{:08X} Entry point: 0x{:08X}", info.image_base, info.entry_point)?;
writeln!(out, "; Functions detected: {}", func_analysis.functions.len())?;
writeln!(out, "; ============================================================================")?;
writeln!(out)?;
// Import declarations
if !info.import_libraries.is_empty() {
writeln!(out, "; ── Imports ─────────────────────────────────────────────────────────────────")?;
for lib in info.import_libraries {
writeln!(out, "; Library: {}", lib.name)?;
for imp in &lib.imports {
let resolved = crate::resolve_ordinal(&lib.name, imp.ordinal);
let name = resolved.unwrap_or("???");
let kind = if imp.record_type == 1 { "thunk" } else { "var" };
writeln!(out, "; [{kind}] 0x{:08X} ordinal 0x{:04X} = {}", imp.address, imp.ordinal, name)?;
}
}
writeln!(out)?;
}
// Disassemble each section
for section in info.sections {
writeln!(out, "; ── Section: {:8} VA=0x{:08X} Size=0x{:08X} Flags=0x{:08X} ──",
section.name, section.virtual_address, section.virtual_size, section.flags)?;
let va_start = section.virtual_address;
let va_end = va_start + section.virtual_size;
let file_start = section.virtual_address as usize;
// Pre-sort data labels in this section for break-at-label hex dump
let section_labels_sorted: Vec<u32> = if !section.is_code() {
let sec_start = info.image_base + va_start;
let sec_end = info.image_base + va_end;
let mut addrs: Vec<u32> = labels.keys()
.filter(|&&a| a >= sec_start && a < sec_end)
.copied()
.collect();
addrs.sort();
addrs
} else {
Vec::new()
};
if section.is_code() {
writeln!(out, ".text")?;
writeln!(out)?;
let mut in_function = false;
let mut addr = va_start;
while addr < va_end {
let abs_addr = info.image_base + addr;
let off = (addr - va_start) as usize + file_start;
if off + 4 > pe.len() { break; }
// Function start? Emit separator + header
if let Some(fi) = func_analysis.get(abs_addr) {
if in_function {
writeln!(out, "; end function")?;
}
writeln!(out)?;
writeln!(out, "; ──────────────────────────────────────────────────────────────────────────")?;
let lbl = labels.get(&abs_addr).cloned()
.unwrap_or_else(|| format!("sub_{abs_addr:08X}"));
if fi.is_saverestore {
writeln!(out, "; FUNCTION: {lbl} (save/restore GPR helper)")?;
} else if fi.is_leaf {
writeln!(out, "; FUNCTION: {lbl} (leaf)")?;
} else {
let mut details = Vec::new();
if fi.frame_size > 0 {
details.push(format!("frame={}", fi.frame_size));
}
if fi.saved_gprs > 0 {
let first_reg = 32 - fi.saved_gprs;
details.push(format!("saves r{first_reg}-r31"));
}
let detail_str = if details.is_empty() {
String::new()
} else {
format!(" ({})", details.join(", "))
};
writeln!(out, "; FUNCTION: {lbl}{detail_str}")?;
}
// Xrefs for function entry
if let Some(xref_lines) = format_xrefs(abs_addr, xrefs, func_analysis, labels) {
for line in &xref_lines {
writeln!(out, "{line}")?;
}
}
writeln!(out, "; ──────────────────────────────────────────────────────────────────────────")?;
in_function = true;
}
// Label
if let Some(lbl) = labels.get(&abs_addr) {
if !func_analysis.is_function_start(abs_addr) {
writeln!(out)?;
// Xrefs for local labels
if let Some(xref_lines) = format_xrefs(abs_addr, xrefs, func_analysis, labels) {
for line in &xref_lines {
writeln!(out, "{line}")?;
}
}
writeln!(out, "{lbl}:")?;
} else {
writeln!(out)?;
writeln!(out, "{lbl}:")?;
}
}
// Import thunk annotation
if let Some(imp_name) = import_map.get(&abs_addr) {
writeln!(out, " ; IMPORT: {imp_name}")?;
}
let instr = u32::from_be_bytes([
pe[off], pe[off+1], pe[off+2], pe[off+3]
]);
let decoded = crate::ppc::disasm(instr, abs_addr);
let disasm_text = decoded.display().to_string();
// Annotate branch targets with label names
let mut annotated = annotate_branch(&disasm_text, labels);
// Annotate data references
if let Some(&(data_addr, kind)) = data_annotations.get(&abs_addr) {
let tag = match kind {
XrefKind::DataRead => "[R]",
XrefKind::DataWrite => "[W]",
_ => "[&]",
};
let sec = section_for_addr(data_addr, info.sections, info.image_base)
.unwrap_or("?");
let data_lbl = labels.get(&data_addr)
.map(|s| format!(" = {s}"))
.unwrap_or_default();
if !annotated.contains("; ->") {
annotated = format!("{annotated:<40} ; {tag} 0x{data_addr:08X} ({sec}){data_lbl}");
} else {
annotated = format!("{annotated} {tag} 0x{data_addr:08X} ({sec}){data_lbl}");
}
}
writeln!(out, " {:08X}: {:08X} {}", abs_addr, instr, annotated)?;
addr += 4;
}
if in_function {
writeln!(out, "; end function")?;
}
} else {
// Data section: hex dump
writeln!(out, ".data")?;
writeln!(out)?;
let mut addr = va_start;
while addr < va_end {
let abs_addr = info.image_base + addr;
let off = (addr - va_start) as usize + file_start;
if let Some(lbl) = labels.get(&abs_addr) {
writeln!(out)?;
// Xrefs for data labels
if let Some(xref_lines) = format_xrefs(abs_addr, xrefs, func_analysis, labels) {
for line in &xref_lines {
writeln!(out, "{line}")?;
}
}
writeln!(out, "{lbl}:")?;
}
// Emit up to 16 bytes per line, but break at label boundaries
let mut line_end = std::cmp::min(addr + 16, va_end);
for &lbl_addr in &section_labels_sorted {
let lbl_va = lbl_addr - info.image_base;
if lbl_va > addr && lbl_va < line_end {
line_end = lbl_va;
break;
}
if lbl_va >= line_end { break; }
}
let byte_count = (line_end - addr) as usize;
if off + byte_count > pe.len() { break; }
write!(out, " {:08X}: ", abs_addr)?;
for i in 0..byte_count {
write!(out, "{:02X}", pe[off + i])?;
if i % 4 == 3 { write!(out, " ")?; }
}
// ASCII representation
let pad = (16 - byte_count) * 2 + (16 - byte_count) / 4;
write!(out, "{:>width$} |", "", width = pad)?;
for i in 0..byte_count {
let b = pe[off + i];
let ch = if b.is_ascii_graphic() || b == b' ' { b as char } else { '.' };
write!(out, "{ch}")?;
}
writeln!(out, "|")?;
addr = line_end;
}
}
writeln!(out)?;
}
Ok(())
}
const XREF_DISPLAY_LIMIT: usize = 8;
fn format_xrefs(
target: u32,
xrefs: &XrefMap,
func_analysis: &FuncAnalysis,
labels: &HashMap<u32, String>,
) -> Option<Vec<String>> {
let refs = xrefs.get(&target)?;
if refs.is_empty() { return None; }
let mut sorted: Vec<Xref> = refs.clone();
sorted.sort();
sorted.dedup();
let total = sorted.len();
let mut lines = Vec::new();
let calls = sorted.iter().filter(|x| x.kind == XrefKind::Call).count();
let jumps = sorted.iter().filter(|x| x.kind == XrefKind::Jump).count();
let branches = sorted.iter().filter(|x| x.kind == XrefKind::Branch).count();
let reads = sorted.iter().filter(|x| x.kind == XrefKind::DataRead).count();
let writes = sorted.iter().filter(|x| x.kind == XrefKind::DataWrite).count();
let data_refs = sorted.iter().filter(|x| x.kind == XrefKind::DataRef).count();
let mut summary_parts = Vec::new();
if calls > 0 { summary_parts.push(format!("{calls} call{}", if calls != 1 { "s" } else { "" })); }
if jumps > 0 { summary_parts.push(format!("{jumps} jump{}", if jumps != 1 { "s" } else { "" })); }
if branches > 0 { summary_parts.push(format!("{branches} branch{}", if branches != 1 { "es" } else { "" })); }
if reads > 0 { summary_parts.push(format!("{reads} read{}", if reads != 1 { "s" } else { "" })); }
if writes > 0 { summary_parts.push(format!("{writes} write{}", if writes != 1 { "s" } else { "" })); }
if data_refs > 0 { summary_parts.push(format!("{data_refs} ref{}", if data_refs != 1 { "s" } else { "" })); }
lines.push(format!("; XREF: {} ({})", summary_parts.join(", "), total));
for (i, xref) in sorted.iter().enumerate() {
if i >= XREF_DISPLAY_LIMIT {
lines.push(format!("; ... and {} more", total - XREF_DISPLAY_LIMIT));
break;
}
let source_label = resolve_source_label(xref.source, func_analysis, labels);
lines.push(format!("; {} from {}", xref.kind.tag(), source_label));
}
Some(lines)
}
fn annotate_branch(disasm: &str, labels: &HashMap<u32, String>) -> String {
if let Some(pos) = disasm.find("0x") {
let hex_start = pos + 2;
let hex_end = disasm[hex_start..].find(|c: char| !c.is_ascii_hexdigit())
.map(|i| hex_start + i)
.unwrap_or(disasm.len());
let hex_str = &disasm[hex_start..hex_end];
if hex_str.len() == 8 {
if let Ok(addr) = u32::from_str_radix(hex_str, 16) {
if let Some(lbl) = labels.get(&addr) {
return format!("{disasm:<40} ; -> {lbl}");
}
}
}
}
disasm.to_string()
}