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:
28
crates/xenia-app/Cargo.toml
Normal file
28
crates/xenia-app/Cargo.toml
Normal file
@@ -0,0 +1,28 @@
|
||||
[package]
|
||||
name = "xenia-app"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
|
||||
[[bin]]
|
||||
name = "xenia-rs"
|
||||
path = "src/main.rs"
|
||||
|
||||
[dependencies]
|
||||
xenia-types = { workspace = true }
|
||||
xenia-memory = { workspace = true }
|
||||
xenia-cpu = { workspace = true }
|
||||
xenia-xex = { workspace = true }
|
||||
xenia-vfs = { workspace = true }
|
||||
xenia-kernel = { workspace = true }
|
||||
xenia-gpu = { workspace = true }
|
||||
xenia-apu = { workspace = true }
|
||||
xenia-hid = { workspace = true }
|
||||
xenia-debugger = { workspace = true }
|
||||
xenia-analysis = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
tracing-subscriber = { workspace = true }
|
||||
anyhow = { workspace = true }
|
||||
clap = { version = "4", features = ["derive"] }
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
812
crates/xenia-app/src/main.rs
Normal file
812
crates/xenia-app/src/main.rs
Normal file
@@ -0,0 +1,812 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use anyhow::Result;
|
||||
use clap::{Parser, Subcommand};
|
||||
use tracing_subscriber::EnvFilter;
|
||||
|
||||
use xenia_kernel::ModuleId;
|
||||
use xenia_memory::MemoryAccess;
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(name = "xenia-rs")]
|
||||
#[command(about = "Xbox 360 emulator for reverse engineering and preservation")]
|
||||
struct Cli {
|
||||
#[command(subcommand)]
|
||||
command: Commands,
|
||||
}
|
||||
|
||||
#[derive(Subcommand)]
|
||||
enum Commands {
|
||||
/// Disassemble a XEX file from its entry point
|
||||
Disasm {
|
||||
/// Path to XEX file
|
||||
path: String,
|
||||
/// Number of instructions to disassemble
|
||||
#[arg(short = 'n', default_value = "64")]
|
||||
count: usize,
|
||||
},
|
||||
/// Load and execute a XEX file with tracing
|
||||
Exec {
|
||||
/// Path to XEX file
|
||||
path: String,
|
||||
/// Maximum instructions to execute before stopping (unlimited if omitted)
|
||||
#[arg(short = 'n')]
|
||||
max_instructions: Option<u64>,
|
||||
/// SQLite database to write to. Includes the full static analysis
|
||||
/// that `dis --db` would produce, plus any opt-in trace tables.
|
||||
#[arg(long)]
|
||||
db: Option<String>,
|
||||
/// Log each executed instruction to the `exec_trace` table
|
||||
#[arg(long)]
|
||||
trace_instructions: bool,
|
||||
/// Log kernel/import calls to the `import_calls` table
|
||||
#[arg(long)]
|
||||
trace_imports: bool,
|
||||
/// Log taken branches (calls, returns, jumps) to the `branch_trace` table
|
||||
#[arg(long)]
|
||||
trace_branches: bool,
|
||||
/// Suppress banners, kernel-call logs, and final register dump
|
||||
/// (only errors, faults, halts, and the summary line are printed)
|
||||
#[arg(long)]
|
||||
quiet: bool,
|
||||
},
|
||||
/// Browse XISO disc image contents
|
||||
Browse {
|
||||
/// Path to XISO file
|
||||
path: String,
|
||||
},
|
||||
/// Display XEX header information
|
||||
Info {
|
||||
/// Path to XEX file
|
||||
path: String,
|
||||
},
|
||||
/// Extract PE image and metadata from a XEX file
|
||||
Extract {
|
||||
/// Path to XEX or ISO file
|
||||
path: String,
|
||||
/// Output directory (default: same directory as input)
|
||||
#[arg(short, long)]
|
||||
output: Option<String>,
|
||||
/// Write base tables (metadata, sections, imports) to a SQLite database
|
||||
#[arg(long)]
|
||||
db: Option<String>,
|
||||
},
|
||||
/// Full disassembly with function detection, cross-references, and optional database
|
||||
Dis {
|
||||
/// Path to XEX or ISO file
|
||||
path: String,
|
||||
/// Output .asm file (default: stdout)
|
||||
#[arg(short, long)]
|
||||
output: Option<String>,
|
||||
/// Output SQLite database (also includes the base extract tables)
|
||||
#[arg(long)]
|
||||
db: Option<String>,
|
||||
/// Suppress assembly text output (DB-only mode)
|
||||
#[arg(long)]
|
||||
quiet: bool,
|
||||
},
|
||||
}
|
||||
|
||||
fn main() -> Result<()> {
|
||||
let cli = Cli::parse();
|
||||
|
||||
// Bump default log level to `warn` for quiet exec runs so kernel-call
|
||||
// tracing::info! spam is filtered out. RUST_LOG still wins if set.
|
||||
let exec_quiet = matches!(&cli.command, Commands::Exec { quiet: true, .. });
|
||||
let default_level = if exec_quiet { "warn" } else { "info" };
|
||||
tracing_subscriber::fmt()
|
||||
.with_env_filter(EnvFilter::from_default_env().add_directive(default_level.parse()?))
|
||||
.init();
|
||||
|
||||
match cli.command {
|
||||
Commands::Disasm { path, count } => cmd_disasm(&path, count),
|
||||
Commands::Exec {
|
||||
path,
|
||||
max_instructions,
|
||||
db,
|
||||
trace_instructions,
|
||||
trace_imports,
|
||||
trace_branches,
|
||||
quiet,
|
||||
} => cmd_exec(
|
||||
&path,
|
||||
max_instructions,
|
||||
db.as_deref(),
|
||||
trace_instructions,
|
||||
trace_imports,
|
||||
trace_branches,
|
||||
quiet,
|
||||
),
|
||||
Commands::Browse { path } => cmd_browse(&path),
|
||||
Commands::Info { path } => cmd_info(&path),
|
||||
Commands::Extract { path, output, db } => cmd_extract(&path, output.as_deref(), db.as_deref()),
|
||||
Commands::Dis { path, output, db, quiet } => cmd_dis(&path, output.as_deref(), db.as_deref(), quiet),
|
||||
}
|
||||
}
|
||||
|
||||
/// Load XEX data from a path. If the path is an ISO, extract default.xex from it.
|
||||
fn load_xex_data(path: &str) -> Result<Vec<u8>> {
|
||||
let lower = path.to_lowercase();
|
||||
if lower.ends_with(".iso") || lower.ends_with(".xiso") {
|
||||
use xenia_vfs::VfsDevice;
|
||||
println!("Detected disc image, extracting default.xex...");
|
||||
let disc = xenia_vfs::disc_image::DiscImageDevice::open("disc", std::path::Path::new(path))
|
||||
.map_err(|e| anyhow::anyhow!("Failed to open disc image: {}", e))?;
|
||||
disc.read_file("default.xex")
|
||||
.map_err(|e| anyhow::anyhow!("Failed to extract default.xex from disc image: {}", e))
|
||||
} else {
|
||||
Ok(std::fs::read(path)?)
|
||||
}
|
||||
}
|
||||
|
||||
fn cmd_info(path: &str) -> Result<()> {
|
||||
let data = load_xex_data(path)?;
|
||||
let header = xenia_xex::loader::parse_xex2_header(&data)?;
|
||||
|
||||
println!("=== XEX2 Header ===");
|
||||
println!("Magic: {:#010x}", header.magic);
|
||||
println!("Module Flags: {:#010x}", header.module_flags);
|
||||
println!("Header Size: {:#x}", header.header_size);
|
||||
println!("Headers: {}", header.header_count);
|
||||
|
||||
if let Some(entry) = xenia_xex::loader::get_entry_point(&header) {
|
||||
println!("Entry Point: {:#010x}", entry);
|
||||
}
|
||||
if let Some(base) = xenia_xex::loader::get_image_base(&header) {
|
||||
println!("Image Base: {:#010x}", base);
|
||||
}
|
||||
|
||||
println!("\n=== Optional Headers ===");
|
||||
for h in &header.optional_headers {
|
||||
println!(" Key: {:#010x} Value: {:#010x}", h.key, h.value);
|
||||
}
|
||||
|
||||
if let Some(ref sec) = header.security_info {
|
||||
println!("\n=== Security Info ===");
|
||||
println!("Image Size: {:#x}", sec.image_size);
|
||||
println!("Load Address: {:#010x}", sec.load_address);
|
||||
println!("Image Flags: {:#010x}", sec.image_flags);
|
||||
println!("Page Descs: {}", sec.page_descriptors.len());
|
||||
}
|
||||
|
||||
if let Some(ref ffi) = header.file_format_info {
|
||||
println!("\n=== File Format ===");
|
||||
println!("Encryption: {}", match ffi.encryption_type {
|
||||
0 => "None", 1 => "Normal (AES)", _ => "Unknown"
|
||||
});
|
||||
println!("Compression: {}", match ffi.compression_type {
|
||||
0 => "None", 1 => "Basic", 2 => "Normal (LZX)", _ => "Unknown"
|
||||
});
|
||||
if !ffi.basic_blocks.is_empty() {
|
||||
println!("Basic blocks: {}", ffi.basic_blocks.len());
|
||||
}
|
||||
if ffi.normal_window_size != 0 {
|
||||
println!("LZX Window: {:#x}", ffi.normal_window_size);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(ref name) = header.original_pe_name {
|
||||
println!("\nOriginal PE: {}", name);
|
||||
}
|
||||
|
||||
if let Some(ref ei) = header.execution_info {
|
||||
println!("\n=== Execution Info ===");
|
||||
println!("Title ID: {:#010x}", ei.title_id);
|
||||
println!("Media ID: {:#010x}", ei.media_id);
|
||||
println!("Disc: {} of {}", ei.disc_number, ei.disc_count);
|
||||
}
|
||||
|
||||
if !header.import_libraries.is_empty() {
|
||||
println!("\n=== Import Libraries ===");
|
||||
for lib in &header.import_libraries {
|
||||
println!(" {} (v{:#010x}, {} imports)", lib.name, lib.version_cur, lib.imports.len());
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn cmd_disasm(path: &str, count: usize) -> Result<()> {
|
||||
let data = load_xex_data(path)?;
|
||||
let header = xenia_xex::loader::parse_xex2_header(&data)?;
|
||||
|
||||
let entry = xenia_xex::loader::get_entry_point(&header)
|
||||
.ok_or_else(|| anyhow::anyhow!("No entry point found in XEX2 header"))?;
|
||||
let base = xenia_xex::loader::get_image_base(&header)
|
||||
.ok_or_else(|| anyhow::anyhow!("No image base found in XEX2 header"))?;
|
||||
|
||||
println!("Entry point: {:#010x}, Image base: {:#010x}", entry, base);
|
||||
|
||||
// Load and decompress the image
|
||||
let image_data = xenia_xex::loader::load_image(&data, &header)?;
|
||||
println!("Image loaded: {} bytes decompressed", image_data.len());
|
||||
println!("Disassembly from entry point ({} instructions):\n", count);
|
||||
|
||||
let entry_offset = (entry - base) as usize;
|
||||
if entry_offset + count * 4 <= image_data.len() {
|
||||
let block = xenia_cpu::disasm::disassemble_block(&image_data[entry_offset..], entry, count);
|
||||
for (addr, text) in block {
|
||||
println!(" {:#010x}: {}", addr, text);
|
||||
}
|
||||
} else {
|
||||
println!(" (entry point offset {:#x} is outside image bounds, image is {:#x} bytes)", entry_offset, image_data.len());
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn cmd_exec(
|
||||
path: &str,
|
||||
max_instructions: Option<u64>,
|
||||
db_path: Option<&str>,
|
||||
trace_instructions: bool,
|
||||
trace_imports: bool,
|
||||
trace_branches: bool,
|
||||
quiet: bool,
|
||||
) -> Result<()> {
|
||||
let data = load_xex_data(path)?;
|
||||
let mut header = xenia_xex::loader::parse_xex2_header(&data)?;
|
||||
|
||||
let entry = xenia_xex::loader::get_entry_point(&header)
|
||||
.ok_or_else(|| anyhow::anyhow!("No entry point found"))?;
|
||||
let base = xenia_xex::loader::get_image_base(&header)
|
||||
.ok_or_else(|| anyhow::anyhow!("No image base found"))?;
|
||||
|
||||
if !quiet {
|
||||
if let Some(ref ffi) = header.file_format_info {
|
||||
println!("Compression: {} (encryption: {})",
|
||||
match ffi.compression_type {
|
||||
0 => "none", 1 => "basic", 2 => "normal (LZX)", _ => "unknown"
|
||||
},
|
||||
match ffi.encryption_type {
|
||||
0 => "none", 1 => "normal (AES)", _ => "unknown"
|
||||
});
|
||||
}
|
||||
if !header.import_libraries.is_empty() {
|
||||
println!("Import libraries:");
|
||||
for lib in &header.import_libraries {
|
||||
println!(" {} ({} imports)", lib.name, lib.imports.len());
|
||||
}
|
||||
}
|
||||
println!("Loading XEX: entry={:#010x} base={:#010x}", entry, base);
|
||||
}
|
||||
|
||||
// Allocate guest memory
|
||||
let mut mem = xenia_memory::GuestMemory::new()
|
||||
.map_err(|e| anyhow::anyhow!("Failed to allocate guest memory: {}", e))?;
|
||||
|
||||
// Load and decompress the XEX image
|
||||
let image_data = xenia_xex::loader::load_image(&data, &header)?;
|
||||
|
||||
// Resolve import ordinals from PE image
|
||||
xenia_xex::loader::resolve_imports(&mut header, &image_data);
|
||||
|
||||
let alloc_size = ((image_data.len() + 4095) & !4095) as u32;
|
||||
let rw = xenia_memory::page_table::MemoryProtect::READ
|
||||
| xenia_memory::page_table::MemoryProtect::WRITE;
|
||||
mem.alloc(base, alloc_size, rw)
|
||||
.map_err(|e| anyhow::anyhow!("Failed to allocate guest memory region: {}", e))?;
|
||||
mem.write_bulk(base, &image_data);
|
||||
|
||||
// ── Phase 1: Build import thunk map ──────────────────────────────────
|
||||
let mut thunk_map: HashMap<u32, (ModuleId, u16, String)> = HashMap::new();
|
||||
for lib in &header.import_libraries {
|
||||
let module = match lib.name.as_str() {
|
||||
"xboxkrnl.exe" => ModuleId::Xboxkrnl,
|
||||
"xam.xex" => ModuleId::Xam,
|
||||
_ => continue,
|
||||
};
|
||||
for imp in &lib.imports {
|
||||
if imp.record_type == 1 {
|
||||
let name = xenia_analysis::resolve_ordinal(&lib.name, imp.ordinal)
|
||||
.map(|s| s.to_string())
|
||||
.unwrap_or_else(|| format!("ordinal_{:#06X}", imp.ordinal));
|
||||
thunk_map.insert(imp.address, (module, imp.ordinal, name));
|
||||
}
|
||||
}
|
||||
}
|
||||
if !quiet {
|
||||
println!("Import thunks mapped: {}", thunk_map.len());
|
||||
}
|
||||
|
||||
// ── Phase 2: CPU initialization per xenia-canary ─────────────────────
|
||||
// Allocate stack (1MB at 0x70000000)
|
||||
let stack_base = 0x7000_0000u32;
|
||||
let stack_size = 0x10_0000u32;
|
||||
mem.alloc(stack_base, stack_size, rw)
|
||||
.map_err(|e| anyhow::anyhow!("Failed to allocate stack: {}", e))?;
|
||||
|
||||
// Allocate PCR (Processor Control Region) and TLS
|
||||
let pcr_addr = 0x7FFF_0000u32;
|
||||
let tls_addr = 0x7FFE_0000u32;
|
||||
mem.alloc(pcr_addr, 0x1000, rw)?;
|
||||
mem.alloc(tls_addr, 0x1000, rw)?;
|
||||
|
||||
// Initialize PCR structure
|
||||
mem.write_u32(pcr_addr, tls_addr); // PCR->tls_ptr
|
||||
mem.write_u32(pcr_addr + 0x100, 0x1000); // PCR->current_thread (fake)
|
||||
mem.write_u32(pcr_addr + 0x150, 0); // PCR->dpc_active
|
||||
|
||||
// Set up CPU context per xenia-canary/cpu/thread_state.cc
|
||||
let mut ctx = xenia_cpu::PpcContext::new();
|
||||
ctx.pc = entry;
|
||||
ctx.gpr[1] = (stack_base + stack_size - 0x80) as u64; // Stack pointer
|
||||
ctx.gpr[2] = 0x2000_0000; // RTOC
|
||||
ctx.gpr[13] = pcr_addr as u64; // PCR/TLS pointer
|
||||
ctx.msr = 0x9030; // Hardware-dumped MSR
|
||||
|
||||
// ── Phase 3: Data export patching (variable imports) ─────────────────
|
||||
for lib in &header.import_libraries {
|
||||
for imp in &lib.imports {
|
||||
if imp.record_type != 0 { continue; } // Only variable entries
|
||||
let addr = imp.address;
|
||||
match (lib.name.as_str(), imp.ordinal) {
|
||||
("xboxkrnl.exe", 0x0158) => {
|
||||
// XboxKrnlVersion: {major=2, minor=0, build=20000, qfe=0}
|
||||
mem.write_u16(addr, 2);
|
||||
mem.write_u16(addr + 2, 0);
|
||||
mem.write_u16(addr + 4, 20000);
|
||||
mem.write_u16(addr + 6, 0);
|
||||
}
|
||||
("xboxkrnl.exe", 0x0193) => {
|
||||
// XexExecutableModuleHandle -> image base
|
||||
mem.write_u32(addr, base);
|
||||
}
|
||||
("xboxkrnl.exe", 0x01C0) => {
|
||||
// VdGpuClockInMHz
|
||||
mem.write_u32(addr, 500);
|
||||
}
|
||||
_ => {
|
||||
// All other variable exports: write 0
|
||||
mem.write_u32(addr, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Phase 4: Set up kernel ───────────────────────────────────────────
|
||||
let mut kernel = xenia_kernel::KernelState::new();
|
||||
kernel.image_base = base;
|
||||
|
||||
// ── Phase 5: Set up SQLite DB with full static analysis + opt-in traces ──
|
||||
let mut db_writer: Option<xenia_analysis::DbWriter> = None;
|
||||
if let Some(db) = db_path {
|
||||
use std::collections::HashMap;
|
||||
|
||||
let sections = xenia_xex::pe::parse_sections(&image_data)?;
|
||||
|
||||
// Build import address -> name map (for xref analysis)
|
||||
let mut import_map: HashMap<u32, String> = HashMap::new();
|
||||
for lib in &header.import_libraries {
|
||||
for imp in &lib.imports {
|
||||
let resolved = xenia_analysis::resolve_ordinal(&lib.name, imp.ordinal);
|
||||
let name = match resolved {
|
||||
Some(n) => format!("{}::{}", lib.name, n),
|
||||
None => format!("{}::ordinal_{:#06X}", lib.name, imp.ordinal),
|
||||
};
|
||||
import_map.insert(imp.address, name);
|
||||
}
|
||||
}
|
||||
|
||||
// Function + xref analysis
|
||||
let code_sections: Vec<(u32, u32, u32)> = sections.iter()
|
||||
.filter(|s| s.is_code())
|
||||
.map(|s| (s.virtual_address, s.virtual_size, s.flags))
|
||||
.collect();
|
||||
let func_analysis = xenia_analysis::func::analyze(&image_data, base, entry, &code_sections);
|
||||
eprintln!("Functions detected: {}", func_analysis.functions.len());
|
||||
|
||||
let xref_result = xenia_analysis::xref::analyze_xrefs(
|
||||
&image_data, base, entry, §ions, &func_analysis, &import_map,
|
||||
);
|
||||
let total_xrefs: usize = xref_result.xrefs.values().map(|v| v.len()).sum();
|
||||
eprintln!("Labels: {}, Cross-references: {}", xref_result.labels.len(), total_xrefs);
|
||||
|
||||
let disasm_info = xenia_analysis::formatter::DisasmInfo {
|
||||
image_base: base,
|
||||
entry_point: entry,
|
||||
original_pe_name: header.original_pe_name.as_deref(),
|
||||
title_id: header.execution_info.as_ref().map(|e| e.title_id),
|
||||
media_id: header.execution_info.as_ref().map(|e| e.media_id),
|
||||
sections: §ions,
|
||||
import_libraries: &header.import_libraries,
|
||||
};
|
||||
|
||||
eprintln!("Writing database to {db}...");
|
||||
let mut w = xenia_analysis::DbWriter::open_fresh(std::path::Path::new(db))?;
|
||||
w.write_base(&disasm_info)?;
|
||||
w.write_disasm(&image_data, &disasm_info, &func_analysis, &xref_result.labels, &xref_result.xrefs)?;
|
||||
w.prepare_trace_tables(trace_instructions, trace_imports, trace_branches)?;
|
||||
db_writer = Some(w);
|
||||
}
|
||||
|
||||
// Set up debugger (in-memory trace disabled when any file-based trace is on)
|
||||
let mut debugger = xenia_debugger::Debugger::new();
|
||||
debugger.paused = false;
|
||||
debugger.step_mode = xenia_debugger::StepMode::Run;
|
||||
debugger.trace_enabled = !trace_instructions;
|
||||
|
||||
if !quiet {
|
||||
match max_instructions {
|
||||
Some(n) => println!("Starting execution (max {n} instructions)...\n"),
|
||||
None => println!("Starting execution (unlimited)...\n"),
|
||||
}
|
||||
}
|
||||
|
||||
// ── Phase 6: Execution loop with thunk interception ──────────────────
|
||||
use xenia_cpu::interpreter::{step, StepResult};
|
||||
use xenia_cpu::PpcOpcode;
|
||||
|
||||
let mut instruction_count: u64 = 0;
|
||||
let mut unimpl_count: u64 = 0;
|
||||
let mut import_count: u64 = 0;
|
||||
|
||||
loop {
|
||||
if let Some(limit) = max_instructions {
|
||||
if instruction_count >= limit {
|
||||
println!("\nReached max instruction count ({limit})");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// ── Import thunk interception ──
|
||||
if let Some((module, ordinal, name)) = thunk_map.get(&ctx.pc) {
|
||||
let module = *module;
|
||||
let ordinal_u32 = *ordinal as u32;
|
||||
|
||||
let thunk_pc = ctx.pc;
|
||||
let args = [ctx.gpr[3], ctx.gpr[4], ctx.gpr[5], ctx.gpr[6]];
|
||||
|
||||
kernel.call_export(module, ordinal_u32, &mut ctx, &mut mem);
|
||||
|
||||
if let Some(ref mut db) = db_writer {
|
||||
db.log_import_call(xenia_analysis::ImportCallEntry {
|
||||
address: thunk_pc,
|
||||
cycle: ctx.cycle_count,
|
||||
module: match module {
|
||||
ModuleId::Xboxkrnl => "xboxkrnl.exe".to_string(),
|
||||
ModuleId::Xam => "xam.xex".to_string(),
|
||||
ModuleId::Xbdm => "xbdm.xex".to_string(),
|
||||
},
|
||||
ordinal: *ordinal,
|
||||
name: name.clone(),
|
||||
arg_r3: args[0],
|
||||
arg_r4: args[1],
|
||||
arg_r5: args[2],
|
||||
arg_r6: args[3],
|
||||
return_value: ctx.gpr[3],
|
||||
});
|
||||
}
|
||||
|
||||
// Simulate blr (return to caller)
|
||||
ctx.pc = ctx.lr as u32;
|
||||
ctx.cycle_count += 1;
|
||||
instruction_count += 1;
|
||||
import_count += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check if PC is in mapped memory
|
||||
if !mem.is_mapped(ctx.pc) {
|
||||
println!("[{:>8}] FAULT: PC {:#010x} is in unmapped memory", instruction_count, ctx.pc);
|
||||
break;
|
||||
}
|
||||
|
||||
// Pre-step debugger
|
||||
debugger.pre_step(&ctx, &mem);
|
||||
|
||||
let pc_before = ctx.pc;
|
||||
// Decode the instruction word before step() so we can classify branches
|
||||
let raw_before = mem.read_u32(pc_before);
|
||||
let opcode_before = xenia_cpu::decode(raw_before, pc_before).opcode;
|
||||
|
||||
let result = step(&mut ctx, &mut mem);
|
||||
instruction_count += 1;
|
||||
|
||||
if let Some(ref mut db) = db_writer {
|
||||
db.log_instruction(xenia_analysis::ExecTraceEntry {
|
||||
address: pc_before,
|
||||
cycle: ctx.cycle_count,
|
||||
r3: ctx.gpr[3],
|
||||
r4: ctx.gpr[4],
|
||||
lr: ctx.lr,
|
||||
sp: ctx.gpr[1],
|
||||
});
|
||||
|
||||
// Branch detection — fallthrough (pc_before + 4) means untaken conditional
|
||||
if opcode_before.is_branch() && ctx.pc != pc_before.wrapping_add(4) {
|
||||
let lk = (raw_before & 1) == 1;
|
||||
let kind: &'static str = if lk {
|
||||
"call"
|
||||
} else if opcode_before == PpcOpcode::bclrx {
|
||||
"return"
|
||||
} else if opcode_before == PpcOpcode::bcctrx {
|
||||
"jump"
|
||||
} else {
|
||||
"branch"
|
||||
};
|
||||
db.log_branch(xenia_analysis::BranchTraceEntry {
|
||||
cycle: ctx.cycle_count,
|
||||
source: pc_before,
|
||||
target: ctx.pc,
|
||||
kind,
|
||||
lr: ctx.lr,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Post-step debugger
|
||||
debugger.post_step(&ctx, &mem);
|
||||
|
||||
match result {
|
||||
StepResult::Continue => {}
|
||||
StepResult::SystemCall => {
|
||||
tracing::warn!("SYSCALL at {:#010x}", pc_before);
|
||||
}
|
||||
StepResult::Unimplemented(op) => {
|
||||
unimpl_count += 1;
|
||||
if unimpl_count <= 50 {
|
||||
println!("[{:>8}] UNIMPL: {:?} at {:#010x}", instruction_count, op, pc_before);
|
||||
} else if unimpl_count == 51 {
|
||||
println!(" (suppressing further UNIMPL messages)");
|
||||
}
|
||||
}
|
||||
StepResult::Trap => {
|
||||
tracing::warn!("TRAP at {:#010x}", pc_before);
|
||||
}
|
||||
StepResult::Halted => {
|
||||
println!("[{:>8}] HALTED", instruction_count);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if debugger.should_break() {
|
||||
println!("[{:>8}] BREAK at {:#010x}", instruction_count, ctx.pc);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Finalize trace DB (flush buffers + create indices)
|
||||
if let Some(ref mut db) = db_writer {
|
||||
db.finalize_traces()?;
|
||||
}
|
||||
|
||||
if !quiet {
|
||||
println!("\n=== Final State ===");
|
||||
println!("PC: {:#010x}", ctx.pc);
|
||||
println!("LR: {:#010x}", ctx.lr as u32);
|
||||
println!("CTR: {:#010x}", ctx.ctr as u32);
|
||||
println!("CR: {:#010x}", ctx.cr());
|
||||
println!("XER: CA={} OV={} SO={}", ctx.xer_ca, ctx.xer_ov, ctx.xer_so);
|
||||
for i in 0..32 {
|
||||
if ctx.gpr[i] != 0 {
|
||||
println!("r{:<2}: {:#018x}", i, ctx.gpr[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
println!("Executed {} instructions ({} import calls, {} unimplemented)",
|
||||
instruction_count, import_count, unimpl_count);
|
||||
if !quiet && db_writer.is_none() {
|
||||
println!("Trace log: {} entries", debugger.trace_log.len());
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn cmd_browse(path: &str) -> Result<()> {
|
||||
use xenia_vfs::VfsDevice;
|
||||
|
||||
let disc = xenia_vfs::disc_image::DiscImageDevice::open("disc", std::path::Path::new(path))
|
||||
.map_err(|e| anyhow::anyhow!("Failed to open disc image: {}", e))?;
|
||||
|
||||
println!("=== XISO Contents: {} ===", path);
|
||||
match disc.list_root() {
|
||||
Ok(entries) => {
|
||||
for entry in entries {
|
||||
let kind = if entry.is_directory { "DIR " } else { "FILE" };
|
||||
println!(" {} {:>10} {}", kind, entry.size, entry.name);
|
||||
}
|
||||
}
|
||||
Err(e) => println!(" Error listing contents: {}", e),
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Helper: load XEX, parse header, decompress PE, resolve imports, parse sections.
|
||||
fn load_and_prepare(path: &str) -> Result<(xenia_xex::Xex2Header, Vec<u8>, Vec<xenia_xex::pe::PeSection>)> {
|
||||
let data = load_xex_data(path)?;
|
||||
let mut header = xenia_xex::loader::parse_xex2_header(&data)?;
|
||||
|
||||
let entry = xenia_xex::loader::get_entry_point(&header)
|
||||
.ok_or_else(|| anyhow::anyhow!("No entry point found in XEX2 header"))?;
|
||||
let base = xenia_xex::loader::get_image_base(&header)
|
||||
.ok_or_else(|| anyhow::anyhow!("No image base found in XEX2 header"))?;
|
||||
|
||||
eprintln!("Entry point: {:#010x}, Image base: {:#010x}", entry, base);
|
||||
|
||||
let pe_image = xenia_xex::loader::load_image(&data, &header)?;
|
||||
eprintln!("Image loaded: {} bytes decompressed", pe_image.len());
|
||||
|
||||
// Resolve import ordinals and record types from the PE image
|
||||
xenia_xex::loader::resolve_imports(&mut header, &pe_image);
|
||||
|
||||
// Parse PE sections
|
||||
let sections = xenia_xex::pe::parse_sections(&pe_image)?;
|
||||
eprintln!("PE sections: {}", sections.len());
|
||||
|
||||
Ok((header, pe_image, sections))
|
||||
}
|
||||
|
||||
fn cmd_extract(path: &str, output_dir: Option<&str>, db_path: Option<&str>) -> Result<()> {
|
||||
use serde::Serialize;
|
||||
|
||||
let (header, pe_image, sections) = load_and_prepare(path)?;
|
||||
|
||||
let entry = xenia_xex::loader::get_entry_point(&header).unwrap();
|
||||
let base = xenia_xex::loader::get_image_base(&header).unwrap();
|
||||
let image_size = header.security_info.as_ref().map(|s| s.image_size).unwrap_or(0);
|
||||
|
||||
// Build JSON-serializable info struct
|
||||
#[derive(Serialize)]
|
||||
struct Xex2Info<'a> {
|
||||
module_flags: u32,
|
||||
image_base: u32,
|
||||
entry_point: u32,
|
||||
image_size: u32,
|
||||
original_pe_name: Option<&'a str>,
|
||||
execution_info: &'a Option<xenia_xex::header::ExecutionInfo>,
|
||||
import_libraries: &'a [xenia_xex::header::ImportLibrary],
|
||||
sections: &'a [xenia_xex::pe::PeSection],
|
||||
}
|
||||
|
||||
let info = Xex2Info {
|
||||
module_flags: header.module_flags,
|
||||
image_base: base,
|
||||
entry_point: entry,
|
||||
image_size,
|
||||
original_pe_name: header.original_pe_name.as_deref(),
|
||||
execution_info: &header.execution_info,
|
||||
import_libraries: &header.import_libraries,
|
||||
sections: §ions,
|
||||
};
|
||||
|
||||
// Determine output directory
|
||||
let input_path = std::path::Path::new(path);
|
||||
let out_dir = match output_dir {
|
||||
Some(d) => std::path::PathBuf::from(d),
|
||||
None => input_path.parent().unwrap_or(std::path::Path::new(".")).to_path_buf(),
|
||||
};
|
||||
std::fs::create_dir_all(&out_dir)?;
|
||||
|
||||
let stem = input_path.file_stem()
|
||||
.and_then(|s| s.to_str())
|
||||
.unwrap_or("output");
|
||||
|
||||
// Write PE image
|
||||
let pe_path = out_dir.join(format!("{stem}.pe"));
|
||||
std::fs::write(&pe_path, &pe_image)?;
|
||||
eprintln!("Wrote PE image: {} ({} bytes)", pe_path.display(), pe_image.len());
|
||||
|
||||
// Write JSON metadata
|
||||
let json_path = out_dir.join(format!("{stem}.xex.json"));
|
||||
let json = serde_json::to_string_pretty(&info)?;
|
||||
std::fs::write(&json_path, &json)?;
|
||||
eprintln!("Wrote metadata: {}", json_path.display());
|
||||
|
||||
// Print summary
|
||||
let total_imports: usize = header.import_libraries.iter().map(|l| l.imports.len()).sum();
|
||||
println!("Extracted: {} sections, {} import libraries ({} imports)",
|
||||
sections.len(), header.import_libraries.len(), total_imports);
|
||||
if let Some(ref ei) = header.execution_info {
|
||||
println!("Title ID: 0x{:08X} Media ID: 0x{:08X}", ei.title_id, ei.media_id);
|
||||
}
|
||||
|
||||
// Write base tables to SQLite if requested
|
||||
if let Some(db) = db_path {
|
||||
let disasm_info = xenia_analysis::formatter::DisasmInfo {
|
||||
image_base: base,
|
||||
entry_point: entry,
|
||||
original_pe_name: header.original_pe_name.as_deref(),
|
||||
title_id: header.execution_info.as_ref().map(|e| e.title_id),
|
||||
media_id: header.execution_info.as_ref().map(|e| e.media_id),
|
||||
sections: §ions,
|
||||
import_libraries: &header.import_libraries,
|
||||
};
|
||||
eprintln!("Writing base tables to {db}...");
|
||||
let mut w = xenia_analysis::DbWriter::open_fresh(std::path::Path::new(db))?;
|
||||
w.write_base(&disasm_info)?;
|
||||
eprintln!("Database written: {db}");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn cmd_dis(path: &str, output: Option<&str>, db_path: Option<&str>, quiet: bool) -> Result<()> {
|
||||
use std::collections::HashMap;
|
||||
|
||||
let (header, pe_image, sections) = load_and_prepare(path)?;
|
||||
|
||||
let entry = xenia_xex::loader::get_entry_point(&header).unwrap();
|
||||
let base = xenia_xex::loader::get_image_base(&header).unwrap();
|
||||
|
||||
// Build import address -> name map
|
||||
let mut import_map: HashMap<u32, String> = HashMap::new();
|
||||
for lib in &header.import_libraries {
|
||||
for imp in &lib.imports {
|
||||
let resolved = xenia_analysis::resolve_ordinal(&lib.name, imp.ordinal);
|
||||
let name = match resolved {
|
||||
Some(n) => format!("{}::{}", lib.name, n),
|
||||
None => format!("{}::ordinal_{:#06X}", lib.name, imp.ordinal),
|
||||
};
|
||||
import_map.insert(imp.address, name);
|
||||
}
|
||||
}
|
||||
eprintln!("Resolved {} import thunks", import_map.len());
|
||||
|
||||
// Function analysis
|
||||
let code_sections: Vec<(u32, u32, u32)> = sections.iter()
|
||||
.filter(|s| s.is_code())
|
||||
.map(|s| (s.virtual_address, s.virtual_size, s.flags))
|
||||
.collect();
|
||||
let func_analysis = xenia_analysis::func::analyze(&pe_image, base, entry, &code_sections);
|
||||
eprintln!("Functions detected: {}", func_analysis.functions.len());
|
||||
|
||||
// Cross-reference analysis
|
||||
let xref_result = xenia_analysis::xref::analyze_xrefs(
|
||||
&pe_image, base, entry, §ions, &func_analysis, &import_map,
|
||||
);
|
||||
let total_xrefs: usize = xref_result.xrefs.values().map(|v| v.len()).sum();
|
||||
eprintln!("Labels: {}, Cross-references: {}", xref_result.labels.len(), total_xrefs);
|
||||
|
||||
// Build DisasmInfo
|
||||
let disasm_info = xenia_analysis::formatter::DisasmInfo {
|
||||
image_base: base,
|
||||
entry_point: entry,
|
||||
original_pe_name: header.original_pe_name.as_deref(),
|
||||
title_id: header.execution_info.as_ref().map(|e| e.title_id),
|
||||
media_id: header.execution_info.as_ref().map(|e| e.media_id),
|
||||
sections: §ions,
|
||||
import_libraries: &header.import_libraries,
|
||||
};
|
||||
|
||||
// SQLite database output (base + disasm layers)
|
||||
if let Some(db) = db_path {
|
||||
eprintln!("Writing database to {db}...");
|
||||
let mut w = xenia_analysis::DbWriter::open_fresh(std::path::Path::new(db))?;
|
||||
w.write_base(&disasm_info)?;
|
||||
w.write_disasm(
|
||||
&pe_image,
|
||||
&disasm_info,
|
||||
&func_analysis,
|
||||
&xref_result.labels,
|
||||
&xref_result.xrefs,
|
||||
)?;
|
||||
eprintln!("Database written: {db}");
|
||||
}
|
||||
|
||||
// Assembly output (skipped when --quiet and no --output specified)
|
||||
if !quiet || output.is_some() {
|
||||
let mut out: Box<dyn std::io::Write> = match output {
|
||||
Some(path) => Box::new(std::io::BufWriter::new(std::fs::File::create(path)?)),
|
||||
None => Box::new(std::io::BufWriter::new(std::io::stdout().lock())),
|
||||
};
|
||||
|
||||
xenia_analysis::formatter::write_asm(
|
||||
&mut *out,
|
||||
&pe_image,
|
||||
&disasm_info,
|
||||
&func_analysis,
|
||||
&xref_result.labels,
|
||||
&import_map,
|
||||
&xref_result.xrefs,
|
||||
&xref_result.data_annotations,
|
||||
)?;
|
||||
|
||||
if let Some(path) = output {
|
||||
eprintln!("Wrote disassembly: {path}");
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
Reference in New Issue
Block a user