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,13 @@
[package]
name = "xenia-analysis"
version.workspace = true
edition.workspace = true
license.workspace = true
build = "build.rs"
[dependencies]
xenia-xex = { workspace = true }
serde = { workspace = true }
anyhow = { workspace = true }
tracing = { workspace = true }
rusqlite = { workspace = true }

View File

@@ -0,0 +1,87 @@
//! Build script: parse xenia's xboxkrnl_table.inc and xam_table.inc to generate
//! ordinal->name lookup tables at compile time.
use std::env;
use std::fs;
use std::io::Write;
use std::path::Path;
fn parse_table(path: &Path) -> Vec<(u32, String, String)> {
let content = match fs::read_to_string(path) {
Ok(c) => c,
Err(e) => {
eprintln!("cargo:warning=could not read {}: {}", path.display(), e);
return Vec::new();
}
};
let mut entries = Vec::new();
for line in content.lines() {
let line = line.trim();
// XE_EXPORT(module, 0xNNNNNNNN, Name, kType),
if !line.starts_with("XE_EXPORT(") { continue; }
let inner = match line.strip_prefix("XE_EXPORT(").and_then(|s| s.strip_suffix("),")) {
Some(s) => s,
None => continue,
};
let parts: Vec<&str> = inner.splitn(4, ',').map(|s| s.trim()).collect();
if parts.len() < 4 { continue; }
let module = parts[0].to_string();
let ordinal = match u32::from_str_radix(parts[1].trim_start_matches("0x").trim_start_matches("0X"), 16) {
Ok(n) => n,
Err(_) => continue,
};
let name = parts[2].to_string();
entries.push((ordinal, name, module));
}
entries
}
fn main() {
let out_dir = env::var("OUT_DIR").unwrap();
let dest = Path::new(&out_dir).join("ordinals.rs");
let mut f = fs::File::create(&dest).unwrap();
// Locate xenia tables relative to the workspace root
// crates/xenia-analysis/ -> ../../ -> workspace root -> ../xenia-canary/
let manifest = env::var("CARGO_MANIFEST_DIR").unwrap();
let workspace_root = Path::new(&manifest).parent().unwrap().parent().unwrap();
let project_root = workspace_root.parent().unwrap();
let krnl_path = project_root
.join("xenia-canary/src/xenia/kernel/xboxkrnl/xboxkrnl_table.inc");
let xam_path = project_root
.join("xenia-canary/src/xenia/kernel/xam/xam_table.inc");
println!("cargo:rerun-if-changed={}", krnl_path.display());
println!("cargo:rerun-if-changed={}", xam_path.display());
let krnl = parse_table(&krnl_path);
let xam = parse_table(&xam_path);
writeln!(f, "/// Auto-generated from xenia's export tables.").unwrap();
writeln!(f, "pub fn resolve_ordinal(lib: &str, ordinal: u16) -> Option<&'static str> {{").unwrap();
writeln!(f, " match lib {{").unwrap();
// xboxkrnl.exe
writeln!(f, " \"xboxkrnl.exe\" => match ordinal {{").unwrap();
for (ord, name, _) in &krnl {
writeln!(f, " 0x{ord:04X} => Some(\"{name}\"),").unwrap();
}
writeln!(f, " _ => None,").unwrap();
writeln!(f, " }},").unwrap();
// xam.xex
writeln!(f, " \"xam.xex\" => match ordinal {{").unwrap();
for (ord, name, _) in &xam {
writeln!(f, " 0x{ord:04X} => Some(\"{name}\"),").unwrap();
}
writeln!(f, " _ => None,").unwrap();
writeln!(f, " }},").unwrap();
writeln!(f, " _ => None,").unwrap();
writeln!(f, " }}").unwrap();
writeln!(f, "}}").unwrap();
eprintln!("ordinals.rs: {} xboxkrnl + {} xam entries", krnl.len(), xam.len());
}

View File

@@ -0,0 +1,727 @@
//! SQLite database writer for xenia-rs.
//!
//! Layered, streaming writes shared by `extract`, `dis`, and `exec`.
//! Each command's output is a superset of the previous:
//! - `extract --db` -> base tables (metadata, sections, imports)
//! - `dis --db` -> base + disasm tables (functions, labels, instructions, xrefs)
//! - `exec --db` -> base + disasm + opt-in trace tables (exec_trace, import_calls, branch_trace)
//!
//! Performance: streaming commits every 100k rows, no end-of-run ANALYZE,
//! progress messages before each index build.
//!
//! Trace kind values for `branch_trace.kind`:
//! - "call" : any branch with LK set (raw & 1 == 1)
//! - "return" : bclrx without LK
//! - "jump" : bcctrx without LK
//! - "branch" : bx/bcx without LK
use std::collections::HashMap;
use std::path::Path;
use rusqlite::{Connection, params};
use crate::func::FuncAnalysis;
use crate::xref::{XrefMap, resolve_source_label};
use crate::formatter::DisasmInfo;
const DEFAULT_BATCH_SIZE: u64 = 100_000;
/// Number of rows per DB commit / trace buffer flush.
/// Configurable via the `XENIA_DB_BATCH_SIZE` env var (default 100_000).
/// Used for:
/// - `instructions` and `xrefs` streaming commits in `write_disasm`
/// - `exec_trace` and `branch_trace` buffer thresholds during exec
/// (`import_calls` always flushes at 1000 — low volume, not worth scaling.)
fn batch_size() -> u64 {
use std::sync::OnceLock;
static CACHED: OnceLock<u64> = OnceLock::new();
*CACHED.get_or_init(|| {
std::env::var("XENIA_DB_BATCH_SIZE")
.ok()
.and_then(|s| s.parse::<u64>().ok())
.filter(|&n| n > 0)
.unwrap_or(DEFAULT_BATCH_SIZE)
})
}
pub struct ExecTraceEntry {
pub address: u32,
pub cycle: u64,
pub r3: u64,
pub r4: u64,
pub lr: u64,
pub sp: u64,
}
pub struct ImportCallEntry {
pub address: u32,
pub cycle: u64,
pub module: String,
pub ordinal: u16,
pub name: String,
pub arg_r3: u64,
pub arg_r4: u64,
pub arg_r5: u64,
pub arg_r6: u64,
pub return_value: u64,
}
pub struct BranchTraceEntry {
pub source: u32,
pub target: u32,
pub cycle: u64,
pub kind: &'static str,
pub lr: u64,
}
pub struct DbWriter {
conn: Connection,
exec_buffer: Vec<ExecTraceEntry>,
import_buffer: Vec<ImportCallEntry>,
branch_buffer: Vec<BranchTraceEntry>,
exec_count: u64,
import_count: u64,
branch_count: u64,
trace_instructions: bool,
trace_imports: bool,
trace_branches: bool,
}
impl DbWriter {
/// Open a fresh database at `path`, removing any existing file first.
pub fn open_fresh(path: &Path) -> anyhow::Result<Self> {
if path.exists() {
std::fs::remove_file(path)?;
}
let conn = Connection::open(path)?;
conn.execute_batch("
PRAGMA journal_mode = OFF;
PRAGMA synchronous = OFF;
PRAGMA locking_mode = EXCLUSIVE;
PRAGMA temp_store = MEMORY;
")?;
let cap = batch_size() as usize;
Ok(Self {
conn,
exec_buffer: Vec::with_capacity(cap),
import_buffer: Vec::with_capacity(1024),
branch_buffer: Vec::with_capacity(cap),
exec_count: 0,
import_count: 0,
branch_count: 0,
trace_instructions: false,
trace_imports: false,
trace_branches: false,
})
}
// ── Base layer (written by extract/dis/exec) ─────────────────────────────
/// Write metadata, sections, imports tables and their indices.
pub fn write_base(&mut self, info: &DisasmInfo) -> anyhow::Result<()> {
self.conn.execute_batch("
CREATE TABLE metadata (
key TEXT PRIMARY KEY,
value TEXT NOT NULL
);
CREATE TABLE sections (
name TEXT NOT NULL,
virtual_address INTEGER NOT NULL,
virtual_size INTEGER NOT NULL,
raw_offset INTEGER NOT NULL,
raw_size INTEGER NOT NULL,
flags INTEGER NOT NULL,
is_code BOOLEAN NOT NULL
);
CREATE TABLE imports (
library TEXT NOT NULL,
ordinal INTEGER NOT NULL,
name TEXT,
record_type INTEGER NOT NULL,
address INTEGER NOT NULL
);
")?;
insert_metadata(&self.conn, info)?;
insert_sections(&self.conn, info.sections)?;
insert_imports(&self.conn, info)?;
self.conn.execute_batch("
CREATE INDEX idx_imports_library ON imports(library);
CREATE INDEX idx_imports_name ON imports(name);
")?;
Ok(())
}
// ── Disasm layer (written by dis/exec) ───────────────────────────────────
/// Write functions, labels, instructions, xrefs tables and indices.
pub fn write_disasm(
&mut self,
pe: &[u8],
info: &DisasmInfo,
func_analysis: &FuncAnalysis,
labels: &HashMap<u32, String>,
xrefs: &XrefMap,
) -> anyhow::Result<()> {
self.conn.execute_batch("
CREATE TABLE functions (
address INTEGER PRIMARY KEY,
name TEXT NOT NULL,
end_address INTEGER NOT NULL,
frame_size INTEGER NOT NULL,
saved_gprs INTEGER NOT NULL,
is_leaf BOOLEAN NOT NULL,
is_saverestore BOOLEAN NOT NULL
);
CREATE TABLE labels (
address INTEGER PRIMARY KEY,
name TEXT NOT NULL,
kind TEXT NOT NULL
);
CREATE TABLE instructions (
address INTEGER PRIMARY KEY,
raw INTEGER NOT NULL,
mnemonic TEXT NOT NULL,
operands TEXT NOT NULL,
disasm TEXT NOT NULL,
ext_mnemonic TEXT,
ext_operands TEXT,
ext_disasm TEXT,
section TEXT NOT NULL,
function INTEGER,
label TEXT
);
CREATE TABLE xrefs (
source INTEGER NOT NULL,
target INTEGER NOT NULL,
kind TEXT NOT NULL,
instruction TEXT,
source_func INTEGER,
source_label TEXT,
target_label TEXT
);
")?;
insert_functions(&self.conn, func_analysis, labels)?;
insert_labels(&self.conn, labels)?;
insert_instructions_streaming(&self.conn, pe, info, func_analysis, labels)?;
insert_xrefs_streaming(&self.conn, xrefs, pe, info.image_base, func_analysis, labels)?;
let indices = [
("idx_functions_name", "CREATE INDEX idx_functions_name ON functions(name)"),
("idx_labels_kind", "CREATE INDEX idx_labels_kind ON labels(kind)"),
("idx_labels_name", "CREATE INDEX idx_labels_name ON labels(name)"),
("idx_instructions_function", "CREATE INDEX idx_instructions_function ON instructions(function)"),
("idx_instructions_mnemonic", "CREATE INDEX idx_instructions_mnemonic ON instructions(mnemonic)"),
("idx_instructions_ext_mnemonic","CREATE INDEX idx_instructions_ext_mnemonic ON instructions(ext_mnemonic)"),
("idx_instructions_section", "CREATE INDEX idx_instructions_section ON instructions(section)"),
("idx_instructions_label", "CREATE INDEX idx_instructions_label ON instructions(label)"),
("idx_xrefs_target", "CREATE INDEX idx_xrefs_target ON xrefs(target)"),
("idx_xrefs_source", "CREATE INDEX idx_xrefs_source ON xrefs(source)"),
("idx_xrefs_source_func", "CREATE INDEX idx_xrefs_source_func ON xrefs(source_func)"),
("idx_xrefs_kind", "CREATE INDEX idx_xrefs_kind ON xrefs(kind)"),
("idx_xrefs_instruction", "CREATE INDEX idx_xrefs_instruction ON xrefs(instruction)"),
("idx_xrefs_target_label", "CREATE INDEX idx_xrefs_target_label ON xrefs(target_label)"),
];
for (name, sql) in indices {
eprintln!("[db] creating {name}...");
self.conn.execute_batch(sql)?;
}
Ok(())
}
// ── Trace layer (written by exec when flags enabled) ─────────────────────
/// Create the opt-in trace tables. No-op if all flags are false.
pub fn prepare_trace_tables(
&mut self,
trace_instructions: bool,
trace_imports: bool,
trace_branches: bool,
) -> anyhow::Result<()> {
self.trace_instructions = trace_instructions;
self.trace_imports = trace_imports;
self.trace_branches = trace_branches;
if trace_instructions {
self.conn.execute_batch("
CREATE TABLE IF NOT EXISTS exec_trace (
id INTEGER PRIMARY KEY,
address INTEGER NOT NULL,
cycle INTEGER NOT NULL,
r3 INTEGER NOT NULL,
r4 INTEGER NOT NULL,
lr INTEGER NOT NULL,
sp INTEGER NOT NULL
);
DELETE FROM exec_trace;
")?;
}
if trace_imports {
self.conn.execute_batch("
CREATE TABLE IF NOT EXISTS import_calls (
id INTEGER PRIMARY KEY,
address INTEGER NOT NULL,
cycle INTEGER NOT NULL,
module TEXT NOT NULL,
ordinal INTEGER NOT NULL,
name TEXT NOT NULL,
arg_r3 INTEGER NOT NULL,
arg_r4 INTEGER NOT NULL,
arg_r5 INTEGER NOT NULL,
arg_r6 INTEGER NOT NULL,
return_value INTEGER NOT NULL
);
DELETE FROM import_calls;
")?;
}
if trace_branches {
self.conn.execute_batch("
CREATE TABLE IF NOT EXISTS branch_trace (
id INTEGER PRIMARY KEY,
cycle INTEGER NOT NULL,
source INTEGER NOT NULL,
target INTEGER NOT NULL,
kind TEXT NOT NULL,
lr INTEGER NOT NULL
);
DELETE FROM branch_trace;
")?;
}
Ok(())
}
pub fn log_instruction(&mut self, entry: ExecTraceEntry) {
if !self.trace_instructions { return; }
self.exec_buffer.push(entry);
if self.exec_buffer.len() as u64 >= batch_size() {
self.flush_exec();
}
}
pub fn log_import_call(&mut self, entry: ImportCallEntry) {
if !self.trace_imports { return; }
self.import_buffer.push(entry);
if self.import_buffer.len() >= 1000 {
self.flush_imports();
}
}
pub fn log_branch(&mut self, entry: BranchTraceEntry) {
if !self.trace_branches { return; }
self.branch_buffer.push(entry);
if self.branch_buffer.len() as u64 >= batch_size() {
self.flush_branches();
}
}
fn flush_exec(&mut self) {
if self.exec_buffer.is_empty() { return; }
let tx = self.conn.unchecked_transaction().unwrap();
{
let mut stmt = tx.prepare_cached(
"INSERT INTO exec_trace (address, cycle, r3, r4, lr, sp) VALUES (?1, ?2, ?3, ?4, ?5, ?6)"
).unwrap();
for e in &self.exec_buffer {
stmt.execute(params![
e.address as i64,
e.cycle as i64,
e.r3 as i64,
e.r4 as i64,
e.lr as i64,
e.sp as i64,
]).ok();
}
}
tx.commit().ok();
self.exec_count += self.exec_buffer.len() as u64;
self.exec_buffer.clear();
}
fn flush_imports(&mut self) {
if self.import_buffer.is_empty() { return; }
let tx = self.conn.unchecked_transaction().unwrap();
{
let mut stmt = tx.prepare_cached(
"INSERT INTO import_calls (address, cycle, module, ordinal, name, arg_r3, arg_r4, arg_r5, arg_r6, return_value)
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10)"
).unwrap();
for e in &self.import_buffer {
stmt.execute(params![
e.address as i64,
e.cycle as i64,
e.module,
e.ordinal as i64,
e.name,
e.arg_r3 as i64,
e.arg_r4 as i64,
e.arg_r5 as i64,
e.arg_r6 as i64,
e.return_value as i64,
]).ok();
}
}
tx.commit().ok();
self.import_count += self.import_buffer.len() as u64;
self.import_buffer.clear();
}
fn flush_branches(&mut self) {
if self.branch_buffer.is_empty() { return; }
let tx = self.conn.unchecked_transaction().unwrap();
{
let mut stmt = tx.prepare_cached(
"INSERT INTO branch_trace (cycle, source, target, kind, lr) VALUES (?1, ?2, ?3, ?4, ?5)"
).unwrap();
for e in &self.branch_buffer {
stmt.execute(params![
e.cycle as i64,
e.source as i64,
e.target as i64,
e.kind,
e.lr as i64,
]).ok();
}
}
tx.commit().ok();
self.branch_count += self.branch_buffer.len() as u64;
self.branch_buffer.clear();
}
/// Flush remaining trace buffers and create their indices.
pub fn finalize_traces(&mut self) -> anyhow::Result<()> {
self.flush_exec();
self.flush_imports();
self.flush_branches();
if self.trace_instructions {
eprintln!("[db] creating idx_exec_trace_address...");
self.conn.execute_batch("CREATE INDEX IF NOT EXISTS idx_exec_trace_address ON exec_trace(address);")?;
eprintln!("[db] creating idx_exec_trace_cycle...");
self.conn.execute_batch("CREATE INDEX IF NOT EXISTS idx_exec_trace_cycle ON exec_trace(cycle);")?;
}
if self.trace_imports {
eprintln!("[db] creating idx_import_calls_name...");
self.conn.execute_batch("CREATE INDEX IF NOT EXISTS idx_import_calls_name ON import_calls(name);")?;
eprintln!("[db] creating idx_import_calls_cycle...");
self.conn.execute_batch("CREATE INDEX IF NOT EXISTS idx_import_calls_cycle ON import_calls(cycle);")?;
}
if self.trace_branches {
eprintln!("[db] creating idx_branch_trace_source...");
self.conn.execute_batch("CREATE INDEX IF NOT EXISTS idx_branch_trace_source ON branch_trace(source);")?;
eprintln!("[db] creating idx_branch_trace_target...");
self.conn.execute_batch("CREATE INDEX IF NOT EXISTS idx_branch_trace_target ON branch_trace(target);")?;
eprintln!("[db] creating idx_branch_trace_kind...");
self.conn.execute_batch("CREATE INDEX IF NOT EXISTS idx_branch_trace_kind ON branch_trace(kind);")?;
eprintln!("[db] creating idx_branch_trace_cycle...");
self.conn.execute_batch("CREATE INDEX IF NOT EXISTS idx_branch_trace_cycle ON branch_trace(cycle);")?;
}
eprintln!(
"[db] trace totals: {} instructions, {} imports, {} branches",
self.exec_count, self.import_count, self.branch_count
);
Ok(())
}
}
/// Backwards-compatible wrapper that writes the full base + disasm layers.
pub fn write_db(
path: &Path,
pe: &[u8],
info: &DisasmInfo,
func_analysis: &FuncAnalysis,
labels: &HashMap<u32, String>,
_import_map: &HashMap<u32, String>,
xrefs: &XrefMap,
) -> anyhow::Result<()> {
let mut w = DbWriter::open_fresh(path)?;
w.write_base(info)?;
w.write_disasm(pe, info, func_analysis, labels, xrefs)?;
Ok(())
}
// ── Helpers ────────────────────────────────────────────────────────────────
fn insert_metadata(conn: &Connection, info: &DisasmInfo) -> anyhow::Result<()> {
let mut stmt = conn.prepare("INSERT INTO metadata (key, value) VALUES (?1, ?2)")?;
stmt.execute(params!["image_base", format!("0x{:08X}", info.image_base)])?;
stmt.execute(params!["entry_point", format!("0x{:08X}", info.entry_point)])?;
if let Some(name) = info.original_pe_name {
stmt.execute(params!["original_pe_name", name])?;
}
if let Some(title_id) = info.title_id {
stmt.execute(params!["title_id", format!("0x{:08X}", title_id)])?;
}
if let Some(media_id) = info.media_id {
stmt.execute(params!["media_id", format!("0x{:08X}", media_id)])?;
}
Ok(())
}
fn insert_sections(conn: &Connection, sections: &[xenia_xex::pe::PeSection]) -> anyhow::Result<()> {
let mut stmt = conn.prepare(
"INSERT INTO sections (name, virtual_address, virtual_size, raw_offset, raw_size, flags, is_code)
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)"
)?;
for s in sections {
stmt.execute(params![
s.name,
s.virtual_address as i64,
s.virtual_size as i64,
s.raw_offset as i64,
s.raw_size as i64,
s.flags as i64,
s.is_code() as i32,
])?;
}
Ok(())
}
fn insert_imports(conn: &Connection, info: &DisasmInfo) -> anyhow::Result<()> {
let mut stmt = conn.prepare(
"INSERT INTO imports (library, ordinal, name, record_type, address)
VALUES (?1, ?2, ?3, ?4, ?5)"
)?;
for lib in info.import_libraries {
for imp in &lib.imports {
let resolved = crate::resolve_ordinal(&lib.name, imp.ordinal);
stmt.execute(params![
lib.name,
imp.ordinal as i64,
resolved,
imp.record_type as i64,
imp.address as i64,
])?;
}
}
Ok(())
}
fn insert_functions(
conn: &Connection,
func_analysis: &FuncAnalysis,
labels: &HashMap<u32, String>,
) -> anyhow::Result<()> {
let mut stmt = conn.prepare(
"INSERT INTO functions (address, name, end_address, frame_size, saved_gprs, is_leaf, is_saverestore)
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)"
)?;
for (&addr, fi) in &func_analysis.functions {
let name = labels.get(&addr)
.cloned()
.unwrap_or_else(|| format!("sub_{addr:08X}"));
stmt.execute(params![
addr as i64,
name,
fi.end as i64,
fi.frame_size as i64,
fi.saved_gprs as i64,
fi.is_leaf as i32,
fi.is_saverestore as i32,
])?;
}
Ok(())
}
fn insert_labels(
conn: &Connection,
labels: &HashMap<u32, String>,
) -> anyhow::Result<()> {
let mut stmt = conn.prepare(
"INSERT OR IGNORE INTO labels (address, name, kind) VALUES (?1, ?2, ?3)"
)?;
for (&addr, name) in labels {
let kind = if name.starts_with("sub_") || name == "entry_point" {
"function"
} else if name.starts_with("__imp_") {
"import"
} else if name.starts_with("__savegprlr_") || name.starts_with("__restgprlr_") {
"saverestore"
} else if name.starts_with("loc_") {
"local"
} else if name.starts_with("dat_") {
"data"
} else {
"other"
};
stmt.execute(params![addr as i64, name, kind])?;
}
Ok(())
}
fn insert_instructions_streaming(
conn: &Connection,
pe: &[u8],
info: &DisasmInfo,
func_analysis: &FuncAnalysis,
labels: &HashMap<u32, String>,
) -> anyhow::Result<()> {
let mut tx = conn.unchecked_transaction()?;
let mut count: u64 = 0;
let mut since_commit: u64 = 0;
for section in info.sections {
if !section.is_code() { continue; }
let va_start = section.virtual_address;
let va_end = va_start + section.virtual_size;
let file_start = section.virtual_address as usize;
let mut current_func: Option<u32> = None;
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; }
if func_analysis.is_function_start(abs_addr) {
current_func = Some(abs_addr);
}
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 (mnemonic, operands) = split_disasm(&decoded.base);
let (ext_mnemonic, ext_operands, ext_disasm): (Option<&str>, Option<&str>, Option<&str>) =
match &decoded.ext {
Some(ext) => {
let (em, eo) = split_disasm(ext);
(Some(em), Some(eo), Some(ext.as_str()))
}
None => (None, None, None),
};
let label = labels.get(&abs_addr).map(|s| s.as_str());
{
let mut stmt = tx.prepare_cached(
"INSERT INTO instructions (address, raw, mnemonic, operands, disasm, ext_mnemonic, ext_operands, ext_disasm, section, function, label)
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11)"
)?;
stmt.execute(params![
abs_addr as i64,
instr as i64,
mnemonic,
operands,
decoded.base,
ext_mnemonic,
ext_operands,
ext_disasm,
section.name,
current_func.map(|a| a as i64),
label,
])?;
}
count += 1;
since_commit += 1;
addr += 4;
if since_commit >= batch_size() {
tx.commit()?;
eprintln!("[db] instructions: {count} committed");
tx = conn.unchecked_transaction()?;
since_commit = 0;
}
}
}
tx.commit()?;
eprintln!("[db] inserted {count} instructions");
Ok(())
}
fn insert_xrefs_streaming(
conn: &Connection,
xrefs: &XrefMap,
pe: &[u8],
image_base: u32,
func_analysis: &FuncAnalysis,
labels: &HashMap<u32, String>,
) -> anyhow::Result<()> {
let mut tx = conn.unchecked_transaction()?;
let mut count: u64 = 0;
let mut since_commit: u64 = 0;
for (&target, refs) in xrefs {
let target_label = labels.get(&target).map(|s| s.as_str());
for xref in refs {
let kind = xref.kind.db_tag();
let instruction: Option<String> = {
let off = xref.source.wrapping_sub(image_base) as usize;
if off + 4 <= pe.len() {
let raw = u32::from_be_bytes([pe[off], pe[off+1], pe[off+2], pe[off+3]]);
let decoded = crate::ppc::disasm(raw, xref.source);
let display = decoded.display().to_string();
let (mnem, _) = split_disasm(&display);
Some(mnem.to_string())
} else {
None
}
};
let source_func = func_analysis.functions
.range(..=xref.source)
.next_back()
.map(|(&a, _)| a as i64);
let source_label = resolve_source_label(
xref.source, func_analysis, labels,
);
{
let mut stmt = tx.prepare_cached(
"INSERT INTO xrefs (source, target, kind, instruction, source_func, source_label, target_label)
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)"
)?;
stmt.execute(params![
xref.source as i64,
target as i64,
kind,
instruction,
source_func,
source_label,
target_label,
])?;
}
count += 1;
since_commit += 1;
if since_commit >= batch_size() {
tx.commit()?;
eprintln!("[db] xrefs: {count} committed");
tx = conn.unchecked_transaction()?;
since_commit = 0;
}
}
}
tx.commit()?;
eprintln!("[db] inserted {count} xrefs");
Ok(())
}
/// Split "mnemonic operands" into (mnemonic, operands).
fn split_disasm(disasm: &str) -> (&str, &str) {
let trimmed = disasm.trim();
if let Some(pos) = trimmed.find(|c: char| c.is_whitespace()) {
let mnemonic = &trimmed[..pos];
let operands = trimmed[pos..].trim_start();
(mnemonic, operands)
} else {
(trimmed, "")
}
}

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()
}

View File

@@ -0,0 +1,444 @@
//! Function boundary detection via PPC prologue/epilogue pattern matching.
//!
//! Strategy (multi-pass):
//! 1. Identify all `bl` (branch-and-link) targets — these are call sites,
//! hence very likely function entry points.
//! 2. Scan the save/restore GPR helper region and label it.
//! 3. For each candidate entry, look for prologue patterns:
//! a) `mfspr rN, LR` (typically r0 or r12)
//! b) `bl __savegprlr_NN` (call into save stub)
//! c) `stwu r1, -N(r1)` (allocate stack frame)
//! If a prologue is confirmed, record the function and its stack frame size.
//! 4. Walk forward from each function entry to find the epilogue:
//! a) `blr` (return)
//! b) `b __restgprlr_NN` (tail-branch into restore stub which returns)
//! Mark the function's end address.
//! 5. Detect leaf functions: `bl` targets that lack a prologue but eventually `blr`.
use std::collections::{HashMap, HashSet, BTreeMap};
/// Information about a detected function.
#[derive(Debug, Clone)]
pub struct FuncInfo {
/// Absolute start address.
pub start: u32,
/// Absolute end address (exclusive — one past last instruction).
pub end: u32,
/// Stack frame size (0 if unknown / leaf).
pub frame_size: u32,
/// Number of saved GPRs (via __savegprlr helper), 0 if unknown.
pub saved_gprs: u32,
/// True if this is a leaf function (no bl, no frame setup).
pub is_leaf: bool,
/// True if this is a save/restore GPR helper stub.
pub is_saverestore: bool,
}
/// Result of the function analysis pass.
pub struct FuncAnalysis {
/// address → FuncInfo for every detected function, sorted by address.
pub functions: BTreeMap<u32, FuncInfo>,
/// Addresses in the save-GPR region (start of __savegprlr block).
pub save_gpr_base: Option<u32>,
/// Addresses in the restore-GPR region (start of __restgprlr block).
pub restore_gpr_base: Option<u32>,
}
// ── Instruction field helpers ──────────────────────────────────────────────
fn op(instr: u32) -> u32 { (instr >> 26) & 0x3F }
fn bits(instr: u32, hi: u32, lo: u32) -> u32 {
(instr >> (31 - hi)) & ((1 << (hi - lo + 1)) - 1)
}
fn is_mfspr_lr(instr: u32) -> Option<u32> {
// mfspr rD, LR → opcode 31, xo=339, spr=8
if op(instr) != 31 { return None; }
let xo = bits(instr, 30, 21);
if xo != 339 { return None; }
let spr = (bits(instr, 20, 16) << 5) | bits(instr, 15, 11);
if spr != 8 { return None; }
Some(bits(instr, 10, 6)) // return rD
}
#[allow(dead_code)]
fn is_mtspr_lr(instr: u32) -> bool {
// mtspr LR, rS → opcode 31, xo=467, spr=8
if op(instr) != 31 { return false; }
let xo = bits(instr, 30, 21);
if xo != 467 { return false; }
let spr = (bits(instr, 20, 16) << 5) | bits(instr, 15, 11);
spr == 8
}
fn is_stwu_r1(instr: u32) -> Option<i32> {
// stwu r1, d(r1) → opcode 37, rS=1, rA=1
if op(instr) != 37 { return None; }
let rs = bits(instr, 10, 6);
let ra = bits(instr, 15, 11);
if rs != 1 || ra != 1 { return None; }
let d = ((instr & 0xFFFF) as i16) as i32;
Some(d) // negative = frame allocation
}
fn is_blr(instr: u32) -> bool {
instr == 0x4E800020
}
fn is_bctr(instr: u32) -> bool {
instr == 0x4E800420
}
fn is_bl(instr: u32) -> Option<u32> {
// bl target → opcode 18, LK=1, AA=0
if op(instr) != 18 { return None; }
if instr & 1 == 0 { return None; } // must have LK bit
if instr & 2 != 0 { return None; } // not absolute
// Return the signed offset
let li = instr & 0x03FFFFFC;
Some(li)
}
fn is_b(instr: u32) -> Option<u32> {
// b target → opcode 18, LK=0, AA=0
if op(instr) != 18 { return None; }
if instr & 1 != 0 { return None; } // no LK bit
if instr & 2 != 0 { return None; } // not absolute
Some(instr & 0x03FFFFFC)
}
fn sign_ext26(val: u32) -> i32 {
((val << 6) as i32) >> 6
}
fn bl_target(instr: u32, addr: u32) -> Option<u32> {
is_bl(instr).map(|off| addr.wrapping_add(sign_ext26(off) as u32))
}
fn b_target(instr: u32, addr: u32) -> Option<u32> {
is_b(instr).map(|off| addr.wrapping_add(sign_ext26(off) as u32))
}
// ── Read instruction from PE ───────────────────────────────────────────────
fn read_instr(pe: &[u8], abs_addr: u32, image_base: u32) -> Option<u32> {
let off = abs_addr.wrapping_sub(image_base) as usize;
if off + 4 > pe.len() { return None; }
Some(u32::from_be_bytes([pe[off], pe[off+1], pe[off+2], pe[off+3]]))
}
// ── Detect the save/restore GPR helper stubs ───────────────────────────────
//
// These are a well-known pattern emitted by the Xbox 360 linker.
// Save block: a cascade of `std rN, offset(r1)` for r14..r31 + `stw r12, -8(r1)` + `blr`
// Restore: a cascade of `ld rN, offset(r1)` for r14..r31 + `lwz r12, -8(r1)` + `mtspr LR, r12` + `blr`
//
// We detect the save block by finding 18 consecutive `std rN, ...(r1)` instructions
// for r14 through r31.
fn find_saverestore_stubs(
pe: &[u8],
image_base: u32,
code_ranges: &[(u32, u32)], // (abs_start, abs_end)
) -> (Option<u32>, Option<u32>) {
let mut save_base = None;
let mut restore_base = None;
for &(start, end) in code_ranges {
let mut addr = start;
while addr + 4 * 18 < end {
// Check if this is `std r14, ...(r1)` — opcode 62 (std), rS=14, rA=1
let instr = match read_instr(pe, addr, image_base) { Some(i) => i, None => { addr += 4; continue; } };
if op(instr) == 62 && bits(instr, 10, 6) == 14 && bits(instr, 15, 11) == 1 && (instr & 3) == 0 {
// Verify it's a cascade: r14, r15, ..., r31
let mut ok = true;
for i in 0u32..18 {
let check = match read_instr(pe, addr + i * 4, image_base) { Some(c) => c, None => { ok = false; break; } };
if op(check) != 62 || bits(check, 10, 6) != 14 + i || bits(check, 15, 11) != 1 {
ok = false;
break;
}
}
if ok {
save_base = Some(addr);
// Restore block typically follows the save block
// After save: stw r12, -8(r1) + blr, then restore starts
let after_save = addr + 18 * 4 + 8; // skip stw r12 + blr
let check = read_instr(pe, after_save, image_base);
if let Some(c) = check {
// Should be `ld r14, ...(r1)` — opcode 58, rT=14, rA=1
if op(c) == 58 && bits(c, 10, 6) == 14 && bits(c, 15, 11) == 1 {
restore_base = Some(after_save);
}
}
break;
}
}
addr += 4;
}
if save_base.is_some() { break; }
}
(save_base, restore_base)
}
// ── Main analysis ──────────────────────────────────────────────────────────
pub fn analyze(
pe: &[u8],
image_base: u32,
entry_point: u32,
code_sections: &[(u32, u32, u32)], // (va_start, va_size, flags)
) -> FuncAnalysis {
let code_ranges: Vec<(u32, u32)> = code_sections.iter()
.map(|(va, sz, _)| (image_base + va, image_base + va + sz))
.collect();
// 1. Find save/restore stubs
let (save_base, restore_base) = find_saverestore_stubs(pe, image_base, &code_ranges);
if let Some(sb) = save_base {
eprintln!("[func] __savegprlr stub at 0x{sb:08X}");
}
if let Some(rb) = restore_base {
eprintln!("[func] __restgprlr stub at 0x{rb:08X}");
}
// Set of addresses in the save/restore region (to exclude from function detection)
let mut saverestore_addrs: HashSet<u32> = HashSet::new();
if let Some(sb) = save_base {
// Save block: 18 std + stw + blr = 20 instructions
for i in 0..20 { saverestore_addrs.insert(sb + i * 4); }
}
if let Some(rb) = restore_base {
// Restore block: 18 ld + lwz + mtspr + blr = 21 instructions
for i in 0..21 { saverestore_addrs.insert(rb + i * 4); }
}
// 2. Collect all bl targets as candidate function entries
let mut call_targets: HashSet<u32> = HashSet::new();
call_targets.insert(entry_point);
for &(start, end) in &code_ranges {
let mut addr = start;
while addr < end {
if let Some(instr) = read_instr(pe, addr, image_base) {
if let Some(target) = bl_target(instr, addr) {
// Don't count calls into save/restore stubs as function entries
if !saverestore_addrs.contains(&target) {
call_targets.insert(target);
}
}
}
addr += 4;
}
}
eprintln!("[func] {} bl targets (candidate functions)", call_targets.len());
// 3. For each candidate, detect prologue and walk to epilogue
let mut functions: BTreeMap<u32, FuncInfo> = BTreeMap::new();
for &func_addr in &call_targets {
if let Some(fi) = analyze_function(pe, image_base, func_addr, &code_ranges, save_base, restore_base) {
functions.insert(func_addr, fi);
}
}
// 4. Label save/restore stubs as special functions — one entry for the whole block
if let Some(sb) = save_base {
// The save block is one cascade: entry at each rN, falls through to blr
// Treat as a single function with the first entry point
functions.insert(sb, FuncInfo {
start: sb,
end: sb + 20 * 4, // 18 std + stw r12 + blr
frame_size: 0,
saved_gprs: 18,
is_leaf: true,
is_saverestore: true,
});
}
if let Some(rb) = restore_base {
functions.insert(rb, FuncInfo {
start: rb,
end: rb + 21 * 4, // 18 ld + lwz r12 + mtspr LR + blr
frame_size: 0,
saved_gprs: 18,
is_leaf: true,
is_saverestore: true,
});
}
eprintln!("[func] {} functions detected", functions.len());
FuncAnalysis {
functions,
save_gpr_base: save_base,
restore_gpr_base: restore_base,
}
}
/// Analyze a single function starting at `func_addr`.
fn analyze_function(
pe: &[u8],
image_base: u32,
func_addr: u32,
code_ranges: &[(u32, u32)],
save_base: Option<u32>,
restore_base: Option<u32>,
) -> Option<FuncInfo> {
// Verify the address is within a code section
let in_code = code_ranges.iter().any(|&(s, e)| func_addr >= s && func_addr < e);
if !in_code { return None; }
let instr0 = read_instr(pe, func_addr, image_base)?;
let mut frame_size: u32 = 0;
let mut saved_gprs: u32 = 0;
let mut is_leaf = false;
let mut prologue_len: u32 = 0;
// Pattern A: mfspr rN, LR [+ bl __savegprlr_NN] + stwu r1, -N(r1)
if let Some(_lr_reg) = is_mfspr_lr(instr0) {
prologue_len = 4;
let instr1 = read_instr(pe, func_addr + 4, image_base).unwrap_or(0);
// Check if next is bl to save stub
if let Some(target) = bl_target(instr1, func_addr + 4) {
if let Some(sb) = save_base {
if target >= sb && target < sb + 18 * 4 {
let idx = (target - sb) / 4;
saved_gprs = 18 - idx;
prologue_len = 8;
}
}
}
// Next should be stwu r1, -N(r1)
let stwu_instr = read_instr(pe, func_addr + prologue_len, image_base).unwrap_or(0);
if let Some(d) = is_stwu_r1(stwu_instr) {
frame_size = (-d) as u32;
prologue_len += 4;
}
}
// Pattern B: stwu r1, -N(r1) without mfspr (rare but possible for leaf-ish functions)
else if let Some(d) = is_stwu_r1(instr0) {
frame_size = (-d) as u32;
prologue_len = 4;
is_leaf = true; // no LR save = likely leaf (or uses CTR)
}
// Pattern C: no prologue — leaf function, just code until blr
else {
is_leaf = true;
}
// Walk forward to find the end of the function
let max_range = code_ranges.iter()
.find(|&&(s, e)| func_addr >= s && func_addr < e)
.map(|&(_, e)| e)
.unwrap_or(func_addr + 0x100000);
let mut end_addr = func_addr + 4;
let mut addr = func_addr + prologue_len;
let scan_limit = std::cmp::min(addr + 0x100000, max_range); // 1MB max function
while addr < scan_limit {
let instr = match read_instr(pe, addr, image_base) {
Some(i) => i,
None => break,
};
// Epilogue: blr
if is_blr(instr) {
end_addr = addr + 4;
// Check if the instruction after blr looks like padding or another function
// Sometimes there's trailing data after blr; we stop at the first blr
// that isn't inside a branch-over pattern
break;
}
// Epilogue: b __restgprlr_NN (tail branch into restore stub)
if let Some(target) = b_target(instr, addr) {
if let Some(rb) = restore_base {
if target >= rb && target < rb + 18 * 4 {
end_addr = addr + 4;
break;
}
}
}
// Epilogue: bctr (indirect tail call — end of function)
if is_bctr(instr) {
end_addr = addr + 4;
break;
}
addr += 4;
}
// If we didn't find any epilogue within a reasonable range, still emit
// the function but mark end at the scan point
if end_addr <= func_addr + 4 && prologue_len > 0 {
end_addr = addr;
}
// Don't emit zero-size "functions" for addresses that are just data
if end_addr <= func_addr + 4 && prologue_len == 0 {
return None;
}
Some(FuncInfo {
start: func_addr,
end: end_addr,
frame_size,
saved_gprs,
is_leaf,
is_saverestore: false,
})
}
// ── Label generation ───────────────────────────────────────────────────────
impl FuncAnalysis {
/// Generate labels for all detected functions.
/// Call targets with confirmed prologues get `sub_XXXXXXXX`.
/// Save/restore entries get `__savegprlr_NN` / `__restgprlr_NN`.
pub fn generate_labels(&self) -> HashMap<u32, String> {
let mut labels = HashMap::new();
for (&addr, fi) in &self.functions {
if fi.is_saverestore {
// Label the block start, plus individual register entry points
if let Some(sb) = self.save_gpr_base {
if addr == sb {
for i in 0u32..18 {
let reg = 14 + i;
labels.insert(sb + i * 4, format!("__savegprlr_{reg}"));
}
continue;
}
}
if let Some(rb) = self.restore_gpr_base {
if addr == rb {
for i in 0u32..18 {
let reg = 14 + i;
labels.insert(rb + i * 4, format!("__restgprlr_{reg}"));
}
continue;
}
}
}
labels.insert(addr, format!("sub_{addr:08X}"));
}
labels
}
/// Returns true if `addr` is the start of a detected function.
pub fn is_function_start(&self, addr: u32) -> bool {
self.functions.contains_key(&addr)
}
/// Get info for the function starting at `addr`.
pub fn get(&self, addr: u32) -> Option<&FuncInfo> {
self.functions.get(&addr)
}
}

View File

@@ -0,0 +1,10 @@
pub mod ppc;
pub mod func;
pub mod xref;
pub mod db;
pub mod formatter;
mod ordinals;
pub use ordinals::resolve_ordinal;
pub use xref::{XrefKind, Xref, XrefMap, resolve_source_label};
pub use db::{DbWriter, ExecTraceEntry, ImportCallEntry, BranchTraceEntry};

View File

@@ -0,0 +1 @@
include!(concat!(env!("OUT_DIR"), "/ordinals.rs"));

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,296 @@
//! Cross-reference analysis for Xbox 360 PE images.
use std::collections::HashMap;
use xenia_xex::pe::PeSection;
use crate::func::FuncAnalysis;
// ── Cross-reference types ────────────────────────────────────────────────
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum XrefKind {
Call, // bl
Jump, // b (unconditional)
Branch, // bc / bXX (conditional)
DataRead, // lwz, lbz, lhz, lha, lfs, lfd, etc. from resolved address
DataWrite, // stw, stb, sth, stfs, stfd, etc. to resolved address
DataRef, // address computed via lis+addi/ori but not directly loaded/stored
}
impl XrefKind {
pub fn tag(self) -> &'static str {
match self {
XrefKind::Call => "call",
XrefKind::Jump => "j",
XrefKind::Branch => "br",
XrefKind::DataRead => "read",
XrefKind::DataWrite => "write",
XrefKind::DataRef => "ref",
}
}
pub fn is_data(self) -> bool {
matches!(self, XrefKind::DataRead | XrefKind::DataWrite | XrefKind::DataRef)
}
pub fn db_tag(self) -> &'static str {
self.tag()
}
}
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub struct Xref {
pub source: u32,
pub kind: XrefKind,
}
pub type XrefMap = HashMap<u32, Vec<Xref>>;
/// Result of cross-reference analysis.
pub struct XrefResult {
pub labels: HashMap<u32, String>,
pub xrefs: XrefMap,
pub data_annotations: HashMap<u32, (u32, XrefKind)>,
}
/// Perform full cross-reference analysis on a PE image.
pub fn analyze_xrefs(
pe: &[u8],
image_base: u32,
entry_point: u32,
sections: &[PeSection],
func_analysis: &FuncAnalysis,
import_map: &HashMap<u32, String>,
) -> XrefResult {
let func_labels = func_analysis.generate_labels();
let mut labels: HashMap<u32, String> = func_labels;
labels.insert(entry_point, "entry_point".to_string());
// Add import thunks as labels
for (addr, name) in import_map {
labels.insert(*addr, format!("__imp_{}", name.replace("::", "_")));
}
// First pass: collect branch targets + cross-references from code sections
let mut xrefs: XrefMap = HashMap::new();
for section in sections {
if !section.is_code() { continue; }
let va_start = section.virtual_address;
let va_end = va_start + section.virtual_size;
let file_start = section.virtual_address as usize;
let mut addr = va_start;
while addr < va_end {
let abs_addr = image_base + addr;
let off = (addr - va_start) as usize + file_start;
if off + 4 > pe.len() { break; }
let instr = u32::from_be_bytes([
pe[off], pe[off+1], pe[off+2], pe[off+3]
]);
collect_branch_target(instr, abs_addr, &mut labels, &mut xrefs);
addr += 4;
}
}
// Second pass: resolve data references via lis+load/store pattern matching
let mut data_annotations: HashMap<u32, (u32, XrefKind)> = HashMap::new();
// Build set of valid data address ranges for filtering false positives
let data_ranges: Vec<(u32, u32)> = sections.iter()
.map(|s| (image_base + s.virtual_address,
image_base + s.virtual_address + s.virtual_size))
.collect();
for section in sections {
if !section.is_code() { continue; }
let va_start = section.virtual_address;
let va_end = va_start + section.virtual_size;
let file_start = section.virtual_address as usize;
// Register state: track lis results. reg_hi[r] = Some(high_16_bits << 16)
let mut reg_hi: [Option<u32>; 32] = [None; 32];
let mut addr = va_start;
while addr < va_end {
let abs_addr = image_base + addr;
let off = (addr - va_start) as usize + file_start;
if off + 4 > pe.len() { break; }
let instr = u32::from_be_bytes([
pe[off], pe[off+1], pe[off+2], pe[off+3]
]);
let opcode = (instr >> 26) & 0x3F;
let rd = ((instr >> 21) & 0x1F) as usize;
let ra = ((instr >> 16) & 0x1F) as usize;
let simm = ((instr & 0xFFFF) as i16) as i32;
let uimm = (instr & 0xFFFF) as u32;
// Reset tracking on function boundaries (prologue = mfspr rN, LR)
if opcode == 31 {
let xo = (instr >> 1) & 0x3FF;
if xo == 339 { // mfspr
let spr = (((instr >> 16) & 0x1F) << 5) | ((instr >> 11) & 0x1F);
if spr == 8 { // LR
reg_hi = [None; 32];
}
}
}
match opcode {
// lis rD, IMM (encoded as addis rD, r0, IMM)
15 if ra == 0 => {
reg_hi[rd] = Some(uimm << 16);
}
// addis rD, rA, IMM (rA != 0) — if rA has known lis, update
15 if ra != 0 => {
if let Some(base) = reg_hi[ra] {
reg_hi[rd] = Some(base.wrapping_add(uimm << 16));
} else {
reg_hi[rd] = None;
}
}
// addi rD, rA, IMM — compute full address if rA has known lis
14 if ra != 0 => {
if let Some(base) = reg_hi[ra] {
let data_addr = base.wrapping_add(simm as u32);
if is_in_ranges(data_addr, &data_ranges) {
data_annotations.insert(abs_addr, (data_addr, XrefKind::DataRef));
xrefs.entry(data_addr).or_default().push(Xref { source: abs_addr, kind: XrefKind::DataRef });
labels.entry(data_addr).or_insert_with(|| format!("dat_{data_addr:08X}"));
}
reg_hi[rd] = Some(data_addr); // propagate for chained access
} else {
reg_hi[rd] = None;
}
}
// ori rA, rS, UIMM — compute full address
24 => {
let rs = rd; // source is bits 21-25 for ori
if let Some(base) = reg_hi[rs] {
let data_addr = base | uimm;
if is_in_ranges(data_addr, &data_ranges) {
data_annotations.insert(abs_addr, (data_addr, XrefKind::DataRef));
xrefs.entry(data_addr).or_default().push(Xref { source: abs_addr, kind: XrefKind::DataRef });
labels.entry(data_addr).or_insert_with(|| format!("dat_{data_addr:08X}"));
}
reg_hi[ra] = Some(data_addr);
} else {
reg_hi[ra] = None;
}
}
// Load instructions: lwz, lbz, lhz, lha, lfs, lfd, lwzu, etc.
32 | 33 | 34 | 35 | 40 | 41 | 42 | 43 | 46 | 48 | 49 | 50 | 51 => {
if ra != 0 {
if let Some(base) = reg_hi[ra] {
let data_addr = base.wrapping_add(simm as u32);
if is_in_ranges(data_addr, &data_ranges) {
data_annotations.insert(abs_addr, (data_addr, XrefKind::DataRead));
xrefs.entry(data_addr).or_default().push(Xref { source: abs_addr, kind: XrefKind::DataRead });
labels.entry(data_addr).or_insert_with(|| format!("dat_{data_addr:08X}"));
}
}
}
// Load into rD may clobber the tracked value
reg_hi[rd] = None;
}
// Store instructions: stw, stb, sth, stfs, stfd, stwu, etc.
36 | 37 | 38 | 39 | 44 | 45 | 47 | 52 | 53 | 54 | 55 => {
if ra != 0 {
if let Some(base) = reg_hi[ra] {
let data_addr = base.wrapping_add(simm as u32);
if is_in_ranges(data_addr, &data_ranges) {
data_annotations.insert(abs_addr, (data_addr, XrefKind::DataWrite));
xrefs.entry(data_addr).or_default().push(Xref { source: abs_addr, kind: XrefKind::DataWrite });
labels.entry(data_addr).or_insert_with(|| format!("dat_{data_addr:08X}"));
}
}
}
}
// Any other instruction writing to rD: invalidate
_ => {
// Conservatively invalidate for instructions that modify rD
// (most ALU ops, loads, etc.)
if opcode != 18 && opcode != 16 && opcode != 17 { // skip branch/sc
reg_hi[rd] = None;
}
}
}
addr += 4;
}
}
XrefResult { labels, xrefs, data_annotations }
}
fn collect_branch_target(instr: u32, addr: u32, labels: &mut HashMap<u32, String>, xrefs: &mut XrefMap) {
let op = (instr >> 26) & 0x3F;
match op {
18 => {
// I-form: b/bl/ba/bla
let li = sign_ext26(instr & 0x03FFFFFC);
let aa = instr & 2 != 0;
let lk = instr & 1 != 0;
let target = if aa { li as u32 } else { addr.wrapping_add(li as u32) };
labels.entry(target).or_insert_with(|| format!("loc_{target:08X}"));
let kind = if lk { XrefKind::Call } else { XrefKind::Jump };
xrefs.entry(target).or_default().push(Xref { source: addr, kind });
}
16 => {
// B-form: bc/bcl
let bd = sign_ext16(instr & 0xFFFC);
let aa = instr & 2 != 0;
let target = if aa { bd as u32 } else { addr.wrapping_add(bd as u32) };
labels.entry(target).or_insert_with(|| format!("loc_{target:08X}"));
xrefs.entry(target).or_default().push(Xref { source: addr, kind: XrefKind::Branch });
}
_ => {}
}
}
fn sign_ext16(val: u32) -> i32 {
((val << 16) as i32) >> 16
}
fn sign_ext26(val: u32) -> i32 {
((val << 6) as i32) >> 6
}
fn is_in_ranges(addr: u32, ranges: &[(u32, u32)]) -> bool {
ranges.iter().any(|&(start, end)| addr >= start && addr < end)
}
/// Find which section a data address falls in.
pub fn section_for_addr<'a>(addr: u32, sections: &'a [PeSection], image_base: u32) -> Option<&'a str> {
for s in sections {
let start = image_base + s.virtual_address;
let end = start + s.virtual_size;
if addr >= start && addr < end {
return Some(&s.name);
}
}
None
}
/// Resolve a source address to "function_name+0xNN" or just "0xADDR".
pub fn resolve_source_label(
addr: u32,
func_analysis: &FuncAnalysis,
labels: &HashMap<u32, String>,
) -> String {
// Direct label hit?
if let Some(lbl) = labels.get(&addr) {
return lbl.clone();
}
// Find the containing function (largest start <= addr)
if let Some((&func_start, _fi)) = func_analysis.functions.range(..=addr).next_back() {
if let Some(func_label) = labels.get(&func_start) {
let offset = addr - func_start;
return format!("{func_label}+0x{offset:X}");
}
}
format!("0x{addr:08X}")
}

View 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 }

View 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, &sections, &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: &sections,
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: &sections,
};
// 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: &sections,
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, &sections, &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: &sections,
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(())
}

View File

@@ -0,0 +1,10 @@
[package]
name = "xenia-apu"
version.workspace = true
edition.workspace = true
license.workspace = true
[dependencies]
xenia-types = { workspace = true }
tracing = { workspace = true }
thiserror = { workspace = true }

View File

@@ -0,0 +1,16 @@
/// Audio processing unit stub. Logging only for now.
pub struct AudioSystem {
pub enabled: bool,
}
impl AudioSystem {
pub fn new() -> Self {
Self { enabled: false }
}
}
impl Default for AudioSystem {
fn default() -> Self {
Self::new()
}
}

View File

@@ -0,0 +1,12 @@
[package]
name = "xenia-cpu"
version.workspace = true
edition.workspace = true
license.workspace = true
[dependencies]
xenia-types = { workspace = true }
xenia-memory = { workspace = true }
tracing = { workspace = true }
bitflags = { workspace = true }
thiserror = { workspace = true }

View File

@@ -0,0 +1,191 @@
use xenia_types::Vec128;
/// Condition register field (one of CR0-CR7).
#[derive(Debug, Clone, Copy, Default)]
pub struct CrField {
pub lt: bool,
pub gt: bool,
pub eq: bool,
pub so: bool,
}
impl CrField {
pub fn as_u8(&self) -> u8 {
((self.lt as u8) << 3) | ((self.gt as u8) << 2) | ((self.eq as u8) << 1) | (self.so as u8)
}
pub fn from_u8(val: u8) -> Self {
Self {
lt: val & 8 != 0,
gt: val & 4 != 0,
eq: val & 2 != 0,
so: val & 1 != 0,
}
}
}
/// SPR (Special Purpose Register) numbers used by mfspr/mtspr.
pub mod spr {
pub const XER: u32 = 1;
pub const LR: u32 = 8;
pub const CTR: u32 = 9;
pub const TBL: u32 = 268;
pub const TBU: u32 = 269;
pub const SPRG0: u32 = 272;
pub const SPRG1: u32 = 273;
pub const SPRG2: u32 = 274;
pub const SPRG3: u32 = 275;
pub const PVR: u32 = 287;
pub const PIR: u32 = 1023;
}
/// PowerPC processor context. Holds all register state for one guest thread.
/// Mirrors PPCContext from ppc_context.h, minus JIT-specific fields.
#[repr(C, align(64))]
pub struct PpcContext {
// General purpose registers (R0-R31)
pub gpr: [u64; 32],
// Count register
pub ctr: u64,
// Link register
pub lr: u64,
// Machine state register
pub msr: u64,
// Floating-point registers (F0-F31)
pub fpr: [f64; 32],
// VMX128 vector registers (V0-V127, Xbox 360 extended set)
pub vr: [Vec128; 128],
// Condition register fields (CR0-CR7)
pub cr: [CrField; 8],
// Floating-point status and control register
pub fpscr: u32,
// XER register (split for easy individual updates)
pub xer_ca: u8,
pub xer_ov: u8,
pub xer_so: u8,
// Altivec VSCR saturation bit
pub vscr_sat: u8,
// Program counter
pub pc: u32,
// Reservation address/value for lwarx/stwcx
pub reserved_addr: u32,
pub reserved_val: u64,
pub has_reservation: bool,
// Thread ID (for kernel use)
pub thread_id: u32,
// Cycle counter for timing
pub cycle_count: u64,
// Time base (incremented each instruction for debugging)
pub timebase: u64,
}
impl PpcContext {
pub fn new() -> Self {
Self {
gpr: [0; 32],
ctr: 0,
lr: 0,
msr: 0,
fpr: [0.0; 32],
vr: [Vec128::ZERO; 128],
cr: [CrField::default(); 8],
fpscr: 0,
xer_ca: 0,
xer_ov: 0,
xer_so: 0,
vscr_sat: 0,
pc: 0,
reserved_addr: 0,
reserved_val: 0,
has_reservation: false,
thread_id: 0,
cycle_count: 0,
timebase: 0,
}
}
/// Get the full 32-bit condition register.
pub fn cr(&self) -> u32 {
let mut val = 0u32;
for (i, field) in self.cr.iter().enumerate() {
val |= (field.as_u8() as u32) << (28 - i * 4);
}
val
}
/// Set the full 32-bit condition register.
pub fn set_cr(&mut self, val: u32) {
for i in 0..8 {
self.cr[i] = CrField::from_u8(((val >> (28 - i * 4)) & 0xF) as u8);
}
}
/// Get a single CR bit by absolute bit number (0-31).
pub fn get_cr_bit(&self, bit: u32) -> bool {
let field = (bit / 4) as usize;
let sub = bit % 4;
match sub {
0 => self.cr[field].lt,
1 => self.cr[field].gt,
2 => self.cr[field].eq,
3 => self.cr[field].so,
_ => unreachable!(),
}
}
/// Set a single CR bit by absolute bit number (0-31).
pub fn set_cr_bit(&mut self, bit: u32, val: bool) {
let field = (bit / 4) as usize;
let sub = bit % 4;
match sub {
0 => self.cr[field].lt = val,
1 => self.cr[field].gt = val,
2 => self.cr[field].eq = val,
3 => self.cr[field].so = val,
_ => unreachable!(),
}
}
/// Update a condition register field based on a comparison result (signed).
pub fn update_cr_signed(&mut self, field: usize, val: i64) {
self.cr[field] = CrField {
lt: val < 0,
gt: val > 0,
eq: val == 0,
so: self.xer_so != 0,
};
}
/// Update a condition register field based on a comparison result (unsigned).
pub fn update_cr_unsigned(&mut self, field: usize, a: u64, b: u64) {
self.cr[field] = CrField {
lt: a < b,
gt: a > b,
eq: a == b,
so: self.xer_so != 0,
};
}
/// Get the full XER register value.
pub fn xer(&self) -> u32 {
((self.xer_so as u32) << 31) | ((self.xer_ov as u32) << 30) | ((self.xer_ca as u32) << 29)
}
/// Set XER from a full 32-bit value.
pub fn set_xer(&mut self, val: u32) {
self.xer_so = ((val >> 31) & 1) as u8;
self.xer_ov = ((val >> 30) & 1) as u8;
self.xer_ca = ((val >> 29) & 1) as u8;
}
}
impl Default for PpcContext {
fn default() -> Self {
Self::new()
}
}

View File

@@ -0,0 +1,819 @@
use crate::opcode::PpcOpcode;
/// Extract bits [a..=b] from a 32-bit value (PPC bit numbering: 0 = MSB).
#[inline(always)]
const fn extract_bits(v: u32, a: u32, b: u32) -> u32 {
(v >> (32 - 1 - b)) & ((1 << (b - a + 1)) - 1)
}
/// Decoded PPC instruction with extracted operand fields.
#[derive(Debug, Clone, Copy)]
pub struct DecodedInstr {
pub opcode: PpcOpcode,
pub raw: u32,
pub addr: u32,
}
impl DecodedInstr {
// Common field extractors (PPC bit numbering)
/// Primary opcode (bits 0-5)
#[inline] pub fn op(&self) -> u32 { extract_bits(self.raw, 0, 5) }
/// rD/rS/rT (bits 6-10) - destination/source register
#[inline] pub fn rd(&self) -> usize { extract_bits(self.raw, 6, 10) as usize }
#[inline] pub fn rs(&self) -> usize { self.rd() }
#[inline] pub fn rt(&self) -> usize { self.rd() }
/// rA (bits 11-15)
#[inline] pub fn ra(&self) -> usize { extract_bits(self.raw, 11, 15) as usize }
/// rB (bits 16-20)
#[inline] pub fn rb(&self) -> usize { extract_bits(self.raw, 16, 20) as usize }
/// rC (bits 21-25) - for 4-operand instructions
#[inline] pub fn rc(&self) -> usize { extract_bits(self.raw, 21, 25) as usize }
/// SIMM/UIMM (bits 16-31) - signed/unsigned immediate
#[inline] pub fn simm16(&self) -> i16 { (self.raw & 0xFFFF) as i16 }
#[inline] pub fn uimm16(&self) -> u16 { (self.raw & 0xFFFF) as u16 }
/// D-form displacement (signed, bits 16-31)
#[inline] pub fn d(&self) -> i32 { self.simm16() as i32 }
/// DS-form displacement (signed, bits 16-29, shifted left 2)
#[inline] pub fn ds(&self) -> i32 { (self.raw & 0xFFFC) as i16 as i32 }
/// LI field for branch (bits 6-29, sign-extended, shifted left 2)
#[inline] pub fn li(&self) -> i32 {
let li = extract_bits(self.raw, 6, 29);
// Sign-extend from 24 bits, then shift left 2
let sign_extended = ((li as i32) << 8) >> 8;
sign_extended << 2
}
/// BD field for conditional branch (bits 16-29, sign-extended, shifted left 2)
#[inline] pub fn bd(&self) -> i32 {
let bd = extract_bits(self.raw, 16, 29);
let sign_extended = ((bd as i32) << 18) >> 18;
sign_extended << 2
}
/// BO field (bits 6-10) - branch options
#[inline] pub fn bo(&self) -> u32 { extract_bits(self.raw, 6, 10) }
/// BI field (bits 11-15) - branch condition
#[inline] pub fn bi(&self) -> u32 { extract_bits(self.raw, 11, 15) }
/// AA bit (bit 30) - absolute address
#[inline] pub fn aa(&self) -> bool { (self.raw >> 1) & 1 != 0 }
/// LK bit (bit 31) - link (update LR)
#[inline] pub fn lk(&self) -> bool { self.raw & 1 != 0 }
/// Rc bit (bit 31) - record CR0
#[inline] pub fn rc_bit(&self) -> bool { self.raw & 1 != 0 }
/// OE bit (bit 21) - overflow enable
#[inline] pub fn oe(&self) -> bool { extract_bits(self.raw, 21, 21) != 0 }
/// MB, ME fields for rotate instructions
#[inline] pub fn mb(&self) -> u32 { extract_bits(self.raw, 21, 25) }
#[inline] pub fn me(&self) -> u32 { extract_bits(self.raw, 26, 30) }
/// SH field (bits 16-20) for shift instructions
#[inline] pub fn sh(&self) -> u32 { extract_bits(self.raw, 16, 20) }
/// SH field for 64-bit shifts (bits 16-20 + bit 30)
#[inline] pub fn sh64(&self) -> u32 {
(extract_bits(self.raw, 16, 20) << 1) | extract_bits(self.raw, 30, 30)
}
/// SPR field (bits 11-20, swapped halves)
#[inline] pub fn spr(&self) -> u32 {
let spr_raw = extract_bits(self.raw, 11, 20);
((spr_raw & 0x1F) << 5) | ((spr_raw >> 5) & 0x1F)
}
/// CRM field (bits 12-19) for mtcrf
#[inline] pub fn crm(&self) -> u32 { extract_bits(self.raw, 12, 19) }
/// crfD (bits 6-8) - condition register field destination
#[inline] pub fn crfd(&self) -> usize { extract_bits(self.raw, 6, 8) as usize }
/// crfS (bits 11-13)
#[inline] pub fn crfs(&self) -> usize { extract_bits(self.raw, 11, 13) as usize }
/// L bit (bit 10) - 64-bit compare
#[inline] pub fn l(&self) -> bool { extract_bits(self.raw, 10, 10) != 0 }
/// crbD (bits 6-10)
#[inline] pub fn crbd(&self) -> u32 { extract_bits(self.raw, 6, 10) }
/// crbA (bits 11-15)
#[inline] pub fn crba(&self) -> u32 { extract_bits(self.raw, 11, 15) }
/// crbB (bits 16-20)
#[inline] pub fn crbb(&self) -> u32 { extract_bits(self.raw, 16, 20) }
// VMX128 field extractors
/// VA128 (bits 6-10, plus bit from 29)
#[inline] pub fn va128(&self) -> usize {
(extract_bits(self.raw, 6, 10) | (extract_bits(self.raw, 29, 29) << 5)) as usize
}
/// VB128 (bits 16-20, plus bits from 28, 30)
#[inline] pub fn vb128(&self) -> usize {
(extract_bits(self.raw, 16, 20)
| (extract_bits(self.raw, 28, 28) << 5)
| (extract_bits(self.raw, 30, 30) << 6)) as usize
}
/// VD128 (bits 6-10, plus bits from 21, 22)
#[inline] pub fn vd128(&self) -> usize {
(extract_bits(self.raw, 6, 10)
| (extract_bits(self.raw, 21, 21) << 5)
| (extract_bits(self.raw, 22, 22) << 6)) as usize
}
/// VS128 - same encoding as VD128
#[inline] pub fn vs128(&self) -> usize { self.vd128() }
/// NB field (bits 16-20) for lswi/stswi
#[inline] pub fn nb(&self) -> u32 { extract_bits(self.raw, 16, 20) }
}
/// Decode a 32-bit PPC instruction into its opcode.
/// Direct translation of the C++ LookupOpcode from ppc_opcode_lookup_gen.cc.
pub fn decode(raw: u32, addr: u32) -> DecodedInstr {
let opcode = lookup_opcode(raw);
DecodedInstr { opcode, raw, addr }
}
fn lookup_opcode(code: u32) -> PpcOpcode {
match extract_bits(code, 0, 5) {
2 => PpcOpcode::tdi,
3 => PpcOpcode::twi,
4 => decode_op4(code),
5 => decode_op5(code),
6 => decode_op6(code),
7 => PpcOpcode::mulli,
8 => PpcOpcode::subficx,
10 => PpcOpcode::cmpli,
11 => PpcOpcode::cmpi,
12 => PpcOpcode::addic,
13 => PpcOpcode::addicx,
14 => PpcOpcode::addi,
15 => PpcOpcode::addis,
16 => PpcOpcode::bcx,
17 => PpcOpcode::sc,
18 => PpcOpcode::bx,
19 => decode_op19(code),
20 => PpcOpcode::rlwimix,
21 => PpcOpcode::rlwinmx,
23 => PpcOpcode::rlwnmx,
24 => PpcOpcode::ori,
25 => PpcOpcode::oris,
26 => PpcOpcode::xori,
27 => PpcOpcode::xoris,
28 => PpcOpcode::andix,
29 => PpcOpcode::andisx,
30 => decode_op30(code),
31 => decode_op31(code),
32 => PpcOpcode::lwz,
33 => PpcOpcode::lwzu,
34 => PpcOpcode::lbz,
35 => PpcOpcode::lbzu,
36 => PpcOpcode::stw,
37 => PpcOpcode::stwu,
38 => PpcOpcode::stb,
39 => PpcOpcode::stbu,
40 => PpcOpcode::lhz,
41 => PpcOpcode::lhzu,
42 => PpcOpcode::lha,
43 => PpcOpcode::lhau,
44 => PpcOpcode::sth,
45 => PpcOpcode::sthu,
46 => PpcOpcode::lmw,
47 => PpcOpcode::stmw,
48 => PpcOpcode::lfs,
49 => PpcOpcode::lfsu,
50 => PpcOpcode::lfd,
51 => PpcOpcode::lfdu,
52 => PpcOpcode::stfs,
53 => PpcOpcode::stfsu,
54 => PpcOpcode::stfd,
55 => PpcOpcode::stfdu,
58 => match extract_bits(code, 30, 31) {
0b00 => PpcOpcode::ld,
0b01 => PpcOpcode::ldu,
0b10 => PpcOpcode::lwa,
_ => PpcOpcode::Invalid,
},
59 => match extract_bits(code, 26, 30) {
0b10010 => PpcOpcode::fdivsx,
0b10100 => PpcOpcode::fsubsx,
0b10101 => PpcOpcode::faddsx,
0b10110 => PpcOpcode::fsqrtsx,
0b11000 => PpcOpcode::fresx,
0b11001 => PpcOpcode::fmulsx,
0b11100 => PpcOpcode::fmsubsx,
0b11101 => PpcOpcode::fmaddsx,
0b11110 => PpcOpcode::fnmsubsx,
0b11111 => PpcOpcode::fnmaddsx,
_ => PpcOpcode::Invalid,
},
62 => match extract_bits(code, 30, 31) {
0b00 => PpcOpcode::std,
0b01 => PpcOpcode::stdu,
_ => PpcOpcode::Invalid,
},
63 => decode_op63(code),
_ => PpcOpcode::Invalid,
}
}
fn decode_op4(code: u32) -> PpcOpcode {
// VMX128 load/store (op=4, bits 21-27 << 4 | bits 30-31)
let key1 = (extract_bits(code, 21, 27) << 4) | extract_bits(code, 30, 31);
match key1 {
0b00000000011 => return PpcOpcode::lvsl128,
0b00001000011 => return PpcOpcode::lvsr128,
0b00010000011 => return PpcOpcode::lvewx128,
0b00011000011 => return PpcOpcode::lvx128,
0b00110000011 => return PpcOpcode::stvewx128,
0b00111000011 => return PpcOpcode::stvx128,
0b01011000011 => return PpcOpcode::lvxl128,
0b01111000011 => return PpcOpcode::stvxl128,
0b10000000011 => return PpcOpcode::lvlx128,
0b10001000011 => return PpcOpcode::lvrx128,
0b10100000011 => return PpcOpcode::stvlx128,
0b10101000011 => return PpcOpcode::stvrx128,
0b11000000011 => return PpcOpcode::lvlxl128,
0b11001000011 => return PpcOpcode::lvrxl128,
0b11100000011 => return PpcOpcode::stvlxl128,
0b11101000011 => return PpcOpcode::stvrxl128,
_ => {}
}
// Standard VMX (op=4, bits 21-31)
let key2 = extract_bits(code, 21, 31);
match key2 {
0b00000000000 => return PpcOpcode::vaddubm,
0b00000000010 => return PpcOpcode::vmaxub,
0b00000000100 => return PpcOpcode::vrlb,
0b00000001000 => return PpcOpcode::vmuloub,
0b00000001010 => return PpcOpcode::vaddfp,
0b00000001100 => return PpcOpcode::vmrghb,
0b00000001110 => return PpcOpcode::vpkuhum,
0b00001000000 => return PpcOpcode::vadduhm,
0b00001000010 => return PpcOpcode::vmaxuh,
0b00001000100 => return PpcOpcode::vrlh,
0b00001001000 => return PpcOpcode::vmulouh,
0b00001001010 => return PpcOpcode::vsubfp,
0b00001001100 => return PpcOpcode::vmrghh,
0b00001001110 => return PpcOpcode::vpkuwum,
0b00010000000 => return PpcOpcode::vadduwm,
0b00010000010 => return PpcOpcode::vmaxuw,
0b00010000100 => return PpcOpcode::vrlw,
0b00010001100 => return PpcOpcode::vmrghw,
0b00010001110 => return PpcOpcode::vpkuhus,
0b00011001110 => return PpcOpcode::vpkuwus,
0b00100000010 => return PpcOpcode::vmaxsb,
0b00100000100 => return PpcOpcode::vslb,
0b00100001000 => return PpcOpcode::vmulosb,
0b00100001010 => return PpcOpcode::vrefp,
0b00100001100 => return PpcOpcode::vmrglb,
0b00100001110 => return PpcOpcode::vpkshus,
0b00101000010 => return PpcOpcode::vmaxsh,
0b00101000100 => return PpcOpcode::vslh,
0b00101001000 => return PpcOpcode::vmulosh,
0b00101001010 => return PpcOpcode::vrsqrtefp,
0b00101001100 => return PpcOpcode::vmrglh,
0b00101001110 => return PpcOpcode::vpkswus,
0b00110000000 => return PpcOpcode::vaddcuw,
0b00110000010 => return PpcOpcode::vmaxsw,
0b00110000100 => return PpcOpcode::vslw,
0b00110001010 => return PpcOpcode::vexptefp,
0b00110001100 => return PpcOpcode::vmrglw,
0b00110001110 => return PpcOpcode::vpkshss,
0b00111000100 => return PpcOpcode::vsl,
0b00111001010 => return PpcOpcode::vlogefp,
0b00111001110 => return PpcOpcode::vpkswss,
0b01000000000 => return PpcOpcode::vaddubs,
0b01000000010 => return PpcOpcode::vminub,
0b01000000100 => return PpcOpcode::vsrb,
0b01000001000 => return PpcOpcode::vmuleub,
0b01000001010 => return PpcOpcode::vrfin,
0b01000001100 => return PpcOpcode::vspltb,
0b01000001110 => return PpcOpcode::vupkhsb,
0b01001000000 => return PpcOpcode::vadduhs,
0b01001000010 => return PpcOpcode::vminuh,
0b01001000100 => return PpcOpcode::vsrh,
0b01001001000 => return PpcOpcode::vmuleuh,
0b01001001010 => return PpcOpcode::vrfiz,
0b01001001100 => return PpcOpcode::vsplth,
0b01001001110 => return PpcOpcode::vupkhsh,
0b01010000000 => return PpcOpcode::vadduws,
0b01010000010 => return PpcOpcode::vminuw,
0b01010000100 => return PpcOpcode::vsrw,
0b01010001010 => return PpcOpcode::vrfip,
0b01010001100 => return PpcOpcode::vspltw,
0b01010001110 => return PpcOpcode::vupklsb,
0b01011000100 => return PpcOpcode::vsr,
0b01011001010 => return PpcOpcode::vrfim,
0b01011001110 => return PpcOpcode::vupklsh,
0b01100000000 => return PpcOpcode::vaddsbs,
0b01100000010 => return PpcOpcode::vminsb,
0b01100000100 => return PpcOpcode::vsrab,
0b01100001000 => return PpcOpcode::vmulesb,
0b01100001010 => return PpcOpcode::vcfux,
0b01100001100 => return PpcOpcode::vspltisb,
0b01100001110 => return PpcOpcode::vpkpx,
0b01101000000 => return PpcOpcode::vaddshs,
0b01101000010 => return PpcOpcode::vminsh,
0b01101000100 => return PpcOpcode::vsrah,
0b01101001000 => return PpcOpcode::vmulesh,
0b01101001010 => return PpcOpcode::vcfsx,
0b01101001100 => return PpcOpcode::vspltish,
0b01101001110 => return PpcOpcode::vupkhpx,
0b01110000000 => return PpcOpcode::vaddsws,
0b01110000010 => return PpcOpcode::vminsw,
0b01110000100 => return PpcOpcode::vsraw,
0b01110001010 => return PpcOpcode::vctuxs,
0b01110001100 => return PpcOpcode::vspltisw,
0b01111001010 => return PpcOpcode::vctsxs,
0b01111001110 => return PpcOpcode::vupklpx,
0b10000000000 => return PpcOpcode::vsububm,
0b10000000010 => return PpcOpcode::vavgub,
0b10000000100 => return PpcOpcode::vand,
0b10000001010 => return PpcOpcode::vmaxfp,
0b10000001100 => return PpcOpcode::vslo,
0b10001000000 => return PpcOpcode::vsubuhm,
0b10001000010 => return PpcOpcode::vavguh,
0b10001000100 => return PpcOpcode::vandc,
0b10001001010 => return PpcOpcode::vminfp,
0b10001001100 => return PpcOpcode::vsro,
0b10010000000 => return PpcOpcode::vsubuwm,
0b10010000010 => return PpcOpcode::vavguw,
0b10010000100 => return PpcOpcode::vor,
0b10011000100 => return PpcOpcode::vxor,
0b10100000010 => return PpcOpcode::vavgsb,
0b10100000100 => return PpcOpcode::vnor,
0b10101000010 => return PpcOpcode::vavgsh,
0b10110000000 => return PpcOpcode::vsubcuw,
0b10110000010 => return PpcOpcode::vavgsw,
0b11000000000 => return PpcOpcode::vsububs,
0b11000000100 => return PpcOpcode::mfvscr,
0b11000001000 => return PpcOpcode::vsum4ubs,
0b11001000000 => return PpcOpcode::vsubuhs,
0b11001000100 => return PpcOpcode::mtvscr,
0b11001001000 => return PpcOpcode::vsum4shs,
0b11010000000 => return PpcOpcode::vsubuws,
0b11010001000 => return PpcOpcode::vsum2sws,
0b11100000000 => return PpcOpcode::vsubsbs,
0b11100001000 => return PpcOpcode::vsum4sbs,
0b11101000000 => return PpcOpcode::vsubshs,
0b11110000000 => return PpcOpcode::vsubsws,
0b11110001000 => return PpcOpcode::vsumsws,
_ => {}
}
// VMX compare (op=4, bits 22-31)
let key3 = extract_bits(code, 22, 31);
match key3 {
0b0000000110 => return PpcOpcode::vcmpequb,
0b0001000110 => return PpcOpcode::vcmpequh,
0b0010000110 => return PpcOpcode::vcmpequw,
0b0011000110 => return PpcOpcode::vcmpeqfp,
0b0111000110 => return PpcOpcode::vcmpgefp,
0b1000000110 => return PpcOpcode::vcmpgtub,
0b1001000110 => return PpcOpcode::vcmpgtuh,
0b1010000110 => return PpcOpcode::vcmpgtuw,
0b1011000110 => return PpcOpcode::vcmpgtfp,
0b1100000110 => return PpcOpcode::vcmpgtsb,
0b1101000110 => return PpcOpcode::vcmpgtsh,
0b1110000110 => return PpcOpcode::vcmpgtsw,
0b1111000110 => return PpcOpcode::vcmpbfp,
_ => {}
}
// VMX 4-operand (op=4, bits 26-31)
let key4 = extract_bits(code, 26, 31);
match key4 {
0b100000 => return PpcOpcode::vmhaddshs,
0b100001 => return PpcOpcode::vmhraddshs,
0b100010 => return PpcOpcode::vmladduhm,
0b100100 => return PpcOpcode::vmsumubm,
0b100101 => return PpcOpcode::vmsummbm,
0b100110 => return PpcOpcode::vmsumuhm,
0b100111 => return PpcOpcode::vmsumuhs,
0b101000 => return PpcOpcode::vmsumshm,
0b101001 => return PpcOpcode::vmsumshs,
0b101010 => return PpcOpcode::vsel,
0b101011 => return PpcOpcode::vperm,
0b101100 => return PpcOpcode::vsldoi,
0b101110 => return PpcOpcode::vmaddfp,
0b101111 => return PpcOpcode::vnmsubfp,
_ => {}
}
// vsldoi128 (op=4, bit 27)
if extract_bits(code, 27, 27) == 1 {
return PpcOpcode::vsldoi128;
}
PpcOpcode::Invalid
}
fn decode_op5(code: u32) -> PpcOpcode {
// vperm128 (op=5, bits 22,27)
let key1 = (extract_bits(code, 22, 22) << 5) | extract_bits(code, 27, 27);
if key1 == 0b000000 {
return PpcOpcode::vperm128;
}
let key2 = (extract_bits(code, 22, 25) << 2) | extract_bits(code, 27, 27);
match key2 {
0b000001 => PpcOpcode::vaddfp128,
0b000101 => PpcOpcode::vsubfp128,
0b001001 => PpcOpcode::vmulfp128,
0b001101 => PpcOpcode::vmaddfp128,
0b010001 => PpcOpcode::vmaddcfp128,
0b010101 => PpcOpcode::vnmsubfp128,
0b011001 => PpcOpcode::vmsum3fp128,
0b011101 => PpcOpcode::vmsum4fp128,
0b100000 => PpcOpcode::vpkshss128,
0b100001 => PpcOpcode::vand128,
0b100100 => PpcOpcode::vpkshus128,
0b100101 => PpcOpcode::vandc128,
0b101000 => PpcOpcode::vpkswss128,
0b101001 => PpcOpcode::vnor128,
0b101100 => PpcOpcode::vpkswus128,
0b101101 => PpcOpcode::vor128,
0b110000 => PpcOpcode::vpkuhum128,
0b110001 => PpcOpcode::vxor128,
0b110100 => PpcOpcode::vpkuhus128,
0b110101 => PpcOpcode::vsel128,
0b111000 => PpcOpcode::vpkuwum128,
0b111001 => PpcOpcode::vslo128,
0b111100 => PpcOpcode::vpkuwus128,
0b111101 => PpcOpcode::vsro128,
_ => PpcOpcode::Invalid,
}
}
fn decode_op6(code: u32) -> PpcOpcode {
// vpermwi128
let key1 = (extract_bits(code, 21, 22) << 5) | extract_bits(code, 26, 27);
if key1 == 0b0100001 {
return PpcOpcode::vpermwi128;
}
// vpkd3d128, vrlimi128
let key2 = (extract_bits(code, 21, 23) << 4) | extract_bits(code, 26, 27);
match key2 {
0b1100001 => return PpcOpcode::vpkd3d128,
0b1110001 => return PpcOpcode::vrlimi128,
_ => {}
}
// Unary VMX128 ops
let key3 = extract_bits(code, 21, 27);
match key3 {
0b0100011 => return PpcOpcode::vcfpsxws128,
0b0100111 => return PpcOpcode::vcfpuxws128,
0b0101011 => return PpcOpcode::vcsxwfp128,
0b0101111 => return PpcOpcode::vcuxwfp128,
0b0110011 => return PpcOpcode::vrfim128,
0b0110111 => return PpcOpcode::vrfin128,
0b0111011 => return PpcOpcode::vrfip128,
0b0111111 => return PpcOpcode::vrfiz128,
0b1100011 => return PpcOpcode::vrefp128,
0b1100111 => return PpcOpcode::vrsqrtefp128,
0b1101011 => return PpcOpcode::vexptefp128,
0b1101111 => return PpcOpcode::vlogefp128,
0b1110011 => return PpcOpcode::vspltw128,
0b1110111 => return PpcOpcode::vspltisw128,
0b1111111 => return PpcOpcode::vupkd3d128,
_ => {}
}
// VMX128 compare
let key4 = (extract_bits(code, 22, 24) << 3) | extract_bits(code, 27, 27);
match key4 {
0b000000 => return PpcOpcode::vcmpeqfp128,
0b001000 => return PpcOpcode::vcmpgefp128,
0b010000 => return PpcOpcode::vcmpgtfp128,
0b011000 => return PpcOpcode::vcmpbfp128,
0b100000 => return PpcOpcode::vcmpequw128,
_ => {}
}
// VMX128 shift/merge
let key5 = (extract_bits(code, 22, 25) << 2) | extract_bits(code, 27, 27);
match key5 {
0b000101 => return PpcOpcode::vrlw128,
0b001101 => return PpcOpcode::vslw128,
0b010101 => return PpcOpcode::vsraw128,
0b011101 => return PpcOpcode::vsrw128,
0b101000 => return PpcOpcode::vmaxfp128,
0b101100 => return PpcOpcode::vminfp128,
0b110000 => return PpcOpcode::vmrghw128,
0b110100 => return PpcOpcode::vmrglw128,
0b111000 => return PpcOpcode::vupkhsb128,
0b111100 => return PpcOpcode::vupklsb128,
_ => {}
}
PpcOpcode::Invalid
}
fn decode_op19(code: u32) -> PpcOpcode {
match extract_bits(code, 21, 30) {
0b0000000000 => PpcOpcode::mcrf,
0b0000010000 => PpcOpcode::bclrx,
0b0000100001 => PpcOpcode::crnor,
0b0010000001 => PpcOpcode::crandc,
0b0010010110 => PpcOpcode::isync,
0b0011000001 => PpcOpcode::crxor,
0b0011100001 => PpcOpcode::crnand,
0b0100000001 => PpcOpcode::crand,
0b0100100001 => PpcOpcode::creqv,
0b0110100001 => PpcOpcode::crorc,
0b0111000001 => PpcOpcode::cror,
0b1000010000 => PpcOpcode::bcctrx,
_ => PpcOpcode::Invalid,
}
}
fn decode_op30(code: u32) -> PpcOpcode {
match extract_bits(code, 27, 29) {
0b000 => PpcOpcode::rldiclx,
0b001 => PpcOpcode::rldicrx,
0b010 => PpcOpcode::rldicx,
0b011 => PpcOpcode::rldimix,
_ => match extract_bits(code, 27, 30) {
0b1000 => PpcOpcode::rldclx,
0b1001 => PpcOpcode::rldcrx,
_ => PpcOpcode::Invalid,
},
}
}
fn decode_op31(code: u32) -> PpcOpcode {
// sradix has a unique 10-bit key (bits 21-29)
if extract_bits(code, 21, 29) == 0b110011101 {
return PpcOpcode::sradix;
}
// Main op31 table (bits 21-30)
let key = extract_bits(code, 21, 30);
match key {
0b0000000000 => return PpcOpcode::cmp,
0b0000000100 => return PpcOpcode::tw,
0b0000000110 => return PpcOpcode::lvsl,
0b0000000111 => return PpcOpcode::lvebx,
0b0000010011 => return PpcOpcode::mfcr,
0b0000010100 => return PpcOpcode::lwarx,
0b0000010101 => return PpcOpcode::ldx,
0b0000010111 => return PpcOpcode::lwzx,
0b0000011000 => return PpcOpcode::slwx,
0b0000011010 => return PpcOpcode::cntlzwx,
0b0000011011 => return PpcOpcode::sldx,
0b0000011100 => return PpcOpcode::andx,
0b0000100000 => return PpcOpcode::cmpl,
0b0000100110 => return PpcOpcode::lvsr,
0b0000100111 => return PpcOpcode::lvehx,
0b0000110101 => return PpcOpcode::ldux,
0b0000110110 => return PpcOpcode::dcbst,
0b0000110111 => return PpcOpcode::lwzux,
0b0000111010 => return PpcOpcode::cntlzdx,
0b0000111100 => return PpcOpcode::andcx,
0b0001000100 => return PpcOpcode::td,
0b0001000111 => return PpcOpcode::lvewx,
0b0001010011 => return PpcOpcode::mfmsr,
0b0001010100 => return PpcOpcode::ldarx,
0b0001010110 => return PpcOpcode::dcbf,
0b0001010111 => return PpcOpcode::lbzx,
0b0001100111 => return PpcOpcode::lvx,
0b0001110111 => return PpcOpcode::lbzux,
0b0001111100 => return PpcOpcode::norx,
0b0010000111 => return PpcOpcode::stvebx,
0b0010010000 => return PpcOpcode::mtcrf,
0b0010010010 => return PpcOpcode::mtmsr,
0b0010010101 => return PpcOpcode::stdx,
0b0010010110 => return PpcOpcode::stwcx,
0b0010010111 => return PpcOpcode::stwx,
0b0010100111 => return PpcOpcode::stvehx,
0b0010110010 => return PpcOpcode::mtmsrd,
0b0010110101 => return PpcOpcode::stdux,
0b0010110111 => return PpcOpcode::stwux,
0b0011000111 => return PpcOpcode::stvewx,
0b0011010110 => return PpcOpcode::stdcx,
0b0011010111 => return PpcOpcode::stbx,
0b0011100111 => return PpcOpcode::stvx,
0b0011110110 => return PpcOpcode::dcbtst,
0b0011110111 => return PpcOpcode::stbux,
0b0100010110 => return PpcOpcode::dcbt,
0b0100010111 => return PpcOpcode::lhzx,
0b0100011100 => return PpcOpcode::eqvx,
0b0100110111 => return PpcOpcode::lhzux,
0b0100111100 => return PpcOpcode::xorx,
0b0101010011 => return PpcOpcode::mfspr,
0b0101010101 => return PpcOpcode::lwax,
0b0101010111 => return PpcOpcode::lhax,
0b0101100111 => return PpcOpcode::lvxl,
0b0101110011 => return PpcOpcode::mftb,
0b0101110101 => return PpcOpcode::lwaux,
0b0101110111 => return PpcOpcode::lhaux,
0b0110010111 => return PpcOpcode::sthx,
0b0110011100 => return PpcOpcode::orcx,
0b0110110111 => return PpcOpcode::sthux,
0b0110111100 => return PpcOpcode::orx,
0b0111010011 => return PpcOpcode::mtspr,
0b0111010110 => return PpcOpcode::dcbi,
0b0111011100 => return PpcOpcode::nandx,
0b0111100111 => return PpcOpcode::stvxl,
0b1000000000 => return PpcOpcode::mcrxr,
0b1000000111 => return PpcOpcode::lvlx,
0b1000010100 => return PpcOpcode::ldbrx,
0b1000010101 => return PpcOpcode::lswx,
0b1000010110 => return PpcOpcode::lwbrx,
0b1000010111 => return PpcOpcode::lfsx,
0b1000011000 => return PpcOpcode::srwx,
0b1000011011 => return PpcOpcode::srdx,
0b1000100111 => return PpcOpcode::lvrx,
0b1000110111 => return PpcOpcode::lfsux,
0b1001010101 => return PpcOpcode::lswi,
0b1001010110 => return PpcOpcode::sync,
0b1001010111 => return PpcOpcode::lfdx,
0b1001110111 => return PpcOpcode::lfdux,
0b1010000111 => return PpcOpcode::stvlx,
0b1010010100 => return PpcOpcode::stdbrx,
0b1010010101 => return PpcOpcode::stswx,
0b1010010110 => return PpcOpcode::stwbrx,
0b1010010111 => return PpcOpcode::stfsx,
0b1010100111 => return PpcOpcode::stvrx,
0b1010110111 => return PpcOpcode::stfsux,
0b1011010101 => return PpcOpcode::stswi,
0b1011010111 => return PpcOpcode::stfdx,
0b1011110111 => return PpcOpcode::stfdux,
0b1100000111 => return PpcOpcode::lvlxl,
0b1100010110 => return PpcOpcode::lhbrx,
0b1100011000 => return PpcOpcode::srawx,
0b1100011010 => return PpcOpcode::sradx,
0b1100100111 => return PpcOpcode::lvrxl,
0b1100111000 => return PpcOpcode::srawix,
0b1101010110 => return PpcOpcode::eieio,
0b1110000111 => return PpcOpcode::stvlxl,
0b1110010110 => return PpcOpcode::sthbrx,
0b1110011010 => return PpcOpcode::extshx,
0b1110100111 => return PpcOpcode::stvrxl,
0b1110111010 => return PpcOpcode::extsbx,
0b1111010110 => return PpcOpcode::icbi,
0b1111010111 => return PpcOpcode::stfiwx,
0b1111011010 => return PpcOpcode::extswx,
_ => {}
}
// Arithmetic op31 (bits 22-30)
let key2 = extract_bits(code, 22, 30);
match key2 {
0b000001000 => return PpcOpcode::subfcx,
0b000001001 => return PpcOpcode::mulhdux,
0b000001010 => return PpcOpcode::addcx,
0b000001011 => return PpcOpcode::mulhwux,
0b000101000 => return PpcOpcode::subfx,
0b001001001 => return PpcOpcode::mulhdx,
0b001001011 => return PpcOpcode::mulhwx,
0b001101000 => return PpcOpcode::negx,
0b010001000 => return PpcOpcode::subfex,
0b010001010 => return PpcOpcode::addex,
0b011001000 => return PpcOpcode::subfzex,
0b011001010 => return PpcOpcode::addzex,
0b011101000 => return PpcOpcode::subfmex,
0b011101001 => return PpcOpcode::mulldx,
0b011101010 => return PpcOpcode::addmex,
0b011101011 => return PpcOpcode::mullwx,
0b100001010 => return PpcOpcode::addx,
0b111001001 => return PpcOpcode::divdux,
0b111001011 => return PpcOpcode::divwux,
0b111101001 => return PpcOpcode::divdx,
0b111101011 => return PpcOpcode::divwx,
_ => {}
}
// dcbz/dcbz128 special case
let key3 = (extract_bits(code, 6, 10) << 20) | (extract_bits(code, 21, 30));
match key3 {
0b0000000000000001111110110 => return PpcOpcode::dcbz,
0b0000100000000001111110110 => return PpcOpcode::dcbz128,
_ => {}
}
PpcOpcode::Invalid
}
fn decode_op63(code: u32) -> PpcOpcode {
// Primary op63 table (bits 21-30)
match extract_bits(code, 21, 30) {
0b0000000000 => return PpcOpcode::fcmpu,
0b0000001100 => return PpcOpcode::frspx,
0b0000001110 => return PpcOpcode::fctiwx,
0b0000001111 => return PpcOpcode::fctiwzx,
0b0000100000 => return PpcOpcode::fcmpo,
0b0000100110 => return PpcOpcode::mtfsb1x,
0b0000101000 => return PpcOpcode::fnegx,
0b0001000000 => return PpcOpcode::mcrfs,
0b0001000110 => return PpcOpcode::mtfsb0x,
0b0001001000 => return PpcOpcode::fmrx,
0b0010000110 => return PpcOpcode::mtfsfix,
0b0010001000 => return PpcOpcode::fnabsx,
0b0100001000 => return PpcOpcode::fabsx,
0b1001000111 => return PpcOpcode::mffsx,
0b1011000111 => return PpcOpcode::mtfsfx,
0b1100101110 => return PpcOpcode::fctidx,
0b1100101111 => return PpcOpcode::fctidzx,
0b1101001110 => return PpcOpcode::fcfidx,
_ => {}
}
// FPU arithmetic (bits 26-30)
match extract_bits(code, 26, 30) {
0b10010 => PpcOpcode::fdivx,
0b10100 => PpcOpcode::fsubx,
0b10101 => PpcOpcode::faddx,
0b10110 => PpcOpcode::fsqrtx,
0b10111 => PpcOpcode::fselx,
0b11001 => PpcOpcode::fmulx,
0b11010 => PpcOpcode::frsqrtex,
0b11100 => PpcOpcode::fmsubx,
0b11101 => PpcOpcode::fmaddx,
0b11110 => PpcOpcode::fnmsubx,
0b11111 => PpcOpcode::fnmaddx,
_ => PpcOpcode::Invalid,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_decode_addi() {
// addi r3, r1, 0x10 => opcode 14, rD=3, rA=1, SIMM=0x10
let raw: u32 = (14 << 26) | (3 << 21) | (1 << 16) | 0x10;
let instr = decode(raw, 0);
assert_eq!(instr.opcode, PpcOpcode::addi);
assert_eq!(instr.rd(), 3);
assert_eq!(instr.ra(), 1);
assert_eq!(instr.simm16(), 0x10);
}
#[test]
fn test_decode_lwz() {
// lwz r5, 0x20(r1) => opcode 32
let raw: u32 = (32 << 26) | (5 << 21) | (1 << 16) | 0x20;
let instr = decode(raw, 0);
assert_eq!(instr.opcode, PpcOpcode::lwz);
assert_eq!(instr.rd(), 5);
assert_eq!(instr.ra(), 1);
assert_eq!(instr.d(), 0x20);
}
#[test]
fn test_decode_branch() {
// b +0x100 => opcode 18, LI=0x40 (shifted left 2 = 0x100), AA=0, LK=0
let raw: u32 = (18 << 26) | (0x40 << 2);
let instr = decode(raw, 0);
assert_eq!(instr.opcode, PpcOpcode::bx);
assert_eq!(instr.li(), 0x100);
assert!(!instr.aa());
assert!(!instr.lk());
}
#[test]
fn test_decode_stw() {
// stw r7, 0x8(r2)
let raw: u32 = (36 << 26) | (7 << 21) | (2 << 16) | 0x8;
let instr = decode(raw, 0);
assert_eq!(instr.opcode, PpcOpcode::stw);
assert_eq!(instr.rs(), 7);
assert_eq!(instr.ra(), 2);
}
#[test]
fn test_decode_ori_nop() {
// ori r0, r0, 0 = NOP
let raw: u32 = 24 << 26;
let instr = decode(raw, 0);
assert_eq!(instr.opcode, PpcOpcode::ori);
}
#[test]
fn test_extract_bits() {
assert_eq!(extract_bits(0xFFFF_FFFF, 0, 5), 0x3F);
assert_eq!(extract_bits(0x8000_0000, 0, 0), 1);
assert_eq!(extract_bits(0x0000_0001, 31, 31), 1);
}
}

View File

@@ -0,0 +1,276 @@
use crate::decoder::DecodedInstr;
use crate::opcode::PpcOpcode;
use std::fmt::Write;
/// Disassemble a decoded instruction into PPC assembly text.
pub fn disassemble(instr: &DecodedInstr) -> String {
let mut out = String::new();
match instr.opcode {
// Branch instructions
PpcOpcode::bx => {
let target = if instr.aa() {
instr.li() as u32
} else {
instr.addr.wrapping_add(instr.li() as u32)
};
let mnemonic = if instr.lk() { "bl" } else { "b" };
write!(out, "{} 0x{:08X}", mnemonic, target).unwrap();
}
PpcOpcode::bcx => {
let bo = instr.bo();
let bi = instr.bi();
let target = if instr.aa() {
instr.bd() as u32
} else {
instr.addr.wrapping_add(instr.bd() as u32)
};
let mnemonic = if instr.lk() { "bcl" } else { "bc" };
write!(out, "{} {},{},0x{:08X}", mnemonic, bo, bi, target).unwrap();
}
PpcOpcode::bclrx => {
let mnemonic = if instr.lk() { "bclrl" } else { "bclr" };
write!(out, "{} {},{}", mnemonic, instr.bo(), instr.bi()).unwrap();
}
PpcOpcode::bcctrx => {
let mnemonic = if instr.lk() { "bcctrl" } else { "bcctr" };
write!(out, "{} {},{}", mnemonic, instr.bo(), instr.bi()).unwrap();
}
// System call
PpcOpcode::sc => {
write!(out, "sc").unwrap();
}
// D-form load/store
PpcOpcode::lwz | PpcOpcode::lwzu | PpcOpcode::lbz | PpcOpcode::lbzu |
PpcOpcode::lhz | PpcOpcode::lhzu | PpcOpcode::lha | PpcOpcode::lhau |
PpcOpcode::lfs | PpcOpcode::lfsu | PpcOpcode::lfd | PpcOpcode::lfdu => {
write!(out, "{:?} r{},{}(r{})", instr.opcode, instr.rd(), instr.d(), instr.ra()).unwrap();
}
PpcOpcode::stw | PpcOpcode::stwu | PpcOpcode::stb | PpcOpcode::stbu |
PpcOpcode::sth | PpcOpcode::sthu |
PpcOpcode::stfs | PpcOpcode::stfsu | PpcOpcode::stfd | PpcOpcode::stfdu => {
write!(out, "{:?} r{},{}(r{})", instr.opcode, instr.rs(), instr.d(), instr.ra()).unwrap();
}
// D-form immediate ALU
PpcOpcode::addi | PpcOpcode::addis | PpcOpcode::addic | PpcOpcode::addicx |
PpcOpcode::subficx | PpcOpcode::mulli => {
write!(out, "{:?} r{},r{},{}", instr.opcode, instr.rd(), instr.ra(), instr.simm16()).unwrap();
}
// D-form immediate logical
PpcOpcode::ori | PpcOpcode::oris | PpcOpcode::xori | PpcOpcode::xoris |
PpcOpcode::andix | PpcOpcode::andisx => {
write!(out, "{:?} r{},r{},0x{:04X}", instr.opcode, instr.ra(), instr.rs(), instr.uimm16()).unwrap();
}
// Compare
PpcOpcode::cmpi => {
write!(out, "cmp{}i cr{},r{},{}", if instr.l() { "d" } else { "w" },
instr.crfd(), instr.ra(), instr.simm16()).unwrap();
}
PpcOpcode::cmpli => {
write!(out, "cmpl{}i cr{},r{},0x{:04X}", if instr.l() { "d" } else { "w" },
instr.crfd(), instr.ra(), instr.uimm16()).unwrap();
}
PpcOpcode::cmp => {
write!(out, "cmp{} cr{},r{},r{}", if instr.l() { "d" } else { "w" },
instr.crfd(), instr.ra(), instr.rb()).unwrap();
}
PpcOpcode::cmpl => {
write!(out, "cmpl{} cr{},r{},r{}", if instr.l() { "d" } else { "w" },
instr.crfd(), instr.ra(), instr.rb()).unwrap();
}
// X-form ALU (3-register)
PpcOpcode::addx | PpcOpcode::addcx | PpcOpcode::addex | PpcOpcode::addzex |
PpcOpcode::addmex | PpcOpcode::subfx | PpcOpcode::subfcx | PpcOpcode::subfex |
PpcOpcode::subfzex | PpcOpcode::subfmex | PpcOpcode::negx |
PpcOpcode::mullwx | PpcOpcode::mulhwx | PpcOpcode::mulhwux |
PpcOpcode::divwx | PpcOpcode::divwux |
PpcOpcode::mulldx | PpcOpcode::mulhdx | PpcOpcode::mulhdux |
PpcOpcode::divdx | PpcOpcode::divdux => {
write!(out, "{:?} r{},r{},r{}", instr.opcode, instr.rd(), instr.ra(), instr.rb()).unwrap();
}
// X-form logical
PpcOpcode::andx | PpcOpcode::andcx | PpcOpcode::orx | PpcOpcode::orcx |
PpcOpcode::xorx | PpcOpcode::norx | PpcOpcode::nandx | PpcOpcode::eqvx => {
write!(out, "{:?} r{},r{},r{}", instr.opcode, instr.ra(), instr.rs(), instr.rb()).unwrap();
}
// Shift/rotate
PpcOpcode::slwx | PpcOpcode::srwx | PpcOpcode::srawx | PpcOpcode::sldx |
PpcOpcode::srdx | PpcOpcode::sradx => {
write!(out, "{:?} r{},r{},r{}", instr.opcode, instr.ra(), instr.rs(), instr.rb()).unwrap();
}
PpcOpcode::srawix => {
write!(out, "srawi r{},r{},{}", instr.ra(), instr.rs(), instr.sh()).unwrap();
}
PpcOpcode::sradix => {
write!(out, "sradi r{},r{},{}", instr.ra(), instr.rs(), instr.sh64()).unwrap();
}
// Rotate
PpcOpcode::rlwinmx => {
write!(out, "rlwinm r{},r{},{},{},{}", instr.ra(), instr.rs(), instr.sh(), instr.mb(), instr.me()).unwrap();
}
PpcOpcode::rlwimix => {
write!(out, "rlwimi r{},r{},{},{},{}", instr.ra(), instr.rs(), instr.sh(), instr.mb(), instr.me()).unwrap();
}
PpcOpcode::rlwnmx => {
write!(out, "rlwnm r{},r{},r{},{},{}", instr.ra(), instr.rs(), instr.rb(), instr.mb(), instr.me()).unwrap();
}
// Special register moves
PpcOpcode::mfspr => {
let spr_name = match instr.spr() {
1 => "xer",
8 => "lr",
9 => "ctr",
268 => "tbl",
269 => "tbu",
_ => "",
};
if spr_name.is_empty() {
write!(out, "mfspr r{},{}", instr.rd(), instr.spr()).unwrap();
} else {
write!(out, "mf{} r{}", spr_name, instr.rd()).unwrap();
}
}
PpcOpcode::mtspr => {
let spr_name = match instr.spr() {
1 => "xer",
8 => "lr",
9 => "ctr",
_ => "",
};
if spr_name.is_empty() {
write!(out, "mtspr {},r{}", instr.spr(), instr.rs()).unwrap();
} else {
write!(out, "mt{} r{}", spr_name, instr.rs()).unwrap();
}
}
PpcOpcode::mfcr => {
write!(out, "mfcr r{}", instr.rd()).unwrap();
}
PpcOpcode::mtcrf => {
write!(out, "mtcrf 0x{:02X},r{}", instr.crm(), instr.rs()).unwrap();
}
// Extend
PpcOpcode::extsbx => write!(out, "extsb r{},r{}", instr.ra(), instr.rs()).unwrap(),
PpcOpcode::extshx => write!(out, "extsh r{},r{}", instr.ra(), instr.rs()).unwrap(),
PpcOpcode::extswx => write!(out, "extsw r{},r{}", instr.ra(), instr.rs()).unwrap(),
PpcOpcode::cntlzwx => write!(out, "cntlzw r{},r{}", instr.ra(), instr.rs()).unwrap(),
PpcOpcode::cntlzdx => write!(out, "cntlzd r{},r{}", instr.ra(), instr.rs()).unwrap(),
// X-form load/store
PpcOpcode::lwzx | PpcOpcode::lwzux | PpcOpcode::lbzx | PpcOpcode::lbzux |
PpcOpcode::lhzx | PpcOpcode::lhzux | PpcOpcode::lhax | PpcOpcode::lhaux |
PpcOpcode::lwax | PpcOpcode::lwaux | PpcOpcode::ldx | PpcOpcode::ldux |
PpcOpcode::lfsx | PpcOpcode::lfsux | PpcOpcode::lfdx | PpcOpcode::lfdux |
PpcOpcode::lwbrx | PpcOpcode::lhbrx | PpcOpcode::ldbrx |
PpcOpcode::lwarx | PpcOpcode::ldarx => {
write!(out, "{:?} r{},r{},r{}", instr.opcode, instr.rd(), instr.ra(), instr.rb()).unwrap();
}
PpcOpcode::stwx | PpcOpcode::stwux | PpcOpcode::stbx | PpcOpcode::stbux |
PpcOpcode::sthx | PpcOpcode::sthux | PpcOpcode::stdx | PpcOpcode::stdux |
PpcOpcode::stfsx | PpcOpcode::stfsux | PpcOpcode::stfdx | PpcOpcode::stfdux |
PpcOpcode::stwbrx | PpcOpcode::sthbrx | PpcOpcode::stdbrx |
PpcOpcode::stwcx | PpcOpcode::stdcx | PpcOpcode::stfiwx => {
write!(out, "{:?} r{},r{},r{}", instr.opcode, instr.rs(), instr.ra(), instr.rb()).unwrap();
}
// Cache/sync ops (no-ops for interpreter)
PpcOpcode::dcbf | PpcOpcode::dcbi | PpcOpcode::dcbst |
PpcOpcode::dcbt | PpcOpcode::dcbtst | PpcOpcode::icbi => {
write!(out, "{:?} r{},r{}", instr.opcode, instr.ra(), instr.rb()).unwrap();
}
PpcOpcode::dcbz | PpcOpcode::dcbz128 => {
write!(out, "{:?} r{},r{}", instr.opcode, instr.ra(), instr.rb()).unwrap();
}
PpcOpcode::sync | PpcOpcode::eieio | PpcOpcode::isync => {
write!(out, "{:?}", instr.opcode).unwrap();
}
// Load/store multiple
PpcOpcode::lmw => write!(out, "lmw r{},{}(r{})", instr.rd(), instr.d(), instr.ra()).unwrap(),
PpcOpcode::stmw => write!(out, "stmw r{},{}(r{})", instr.rs(), instr.d(), instr.ra()).unwrap(),
// DS-form loads/stores
PpcOpcode::ld | PpcOpcode::ldu | PpcOpcode::lwa => {
write!(out, "{:?} r{},{}(r{})", instr.opcode, instr.rd(), instr.ds(), instr.ra()).unwrap();
}
PpcOpcode::std | PpcOpcode::stdu => {
write!(out, "{:?} r{},{}(r{})", instr.opcode, instr.rs(), instr.ds(), instr.ra()).unwrap();
}
// CR logical ops
PpcOpcode::crand | PpcOpcode::crandc | PpcOpcode::creqv | PpcOpcode::crnand |
PpcOpcode::crnor | PpcOpcode::cror | PpcOpcode::crorc | PpcOpcode::crxor => {
write!(out, "{:?} {},{},{}", instr.opcode, instr.crbd(), instr.crba(), instr.crbb()).unwrap();
}
PpcOpcode::mcrf => {
write!(out, "mcrf cr{},cr{}", instr.crfd(), instr.crfs()).unwrap();
}
// Trap
PpcOpcode::tdi => write!(out, "tdi {},r{},{}", instr.rd(), instr.ra(), instr.simm16()).unwrap(),
PpcOpcode::twi => write!(out, "twi {},r{},{}", instr.rd(), instr.ra(), instr.simm16()).unwrap(),
PpcOpcode::td => write!(out, "td {},r{},r{}", instr.rd(), instr.ra(), instr.rb()).unwrap(),
PpcOpcode::tw => write!(out, "tw {},r{},r{}", instr.rd(), instr.ra(), instr.rb()).unwrap(),
// Default: just print opcode and raw hex
_ => {
write!(out, "{:?} [{:08X}]", instr.opcode, instr.raw).unwrap();
}
}
out
}
/// Disassemble a range of instructions from a byte slice.
pub fn disassemble_block(data: &[u8], base_addr: u32, count: usize) -> Vec<(u32, String)> {
let mut result = Vec::new();
for i in 0..count {
let offset = i * 4;
if offset + 4 > data.len() {
break;
}
let raw = u32::from_be_bytes([
data[offset],
data[offset + 1],
data[offset + 2],
data[offset + 3],
]);
let addr = base_addr + offset as u32;
let instr = crate::decode(raw, addr);
let text = disassemble(&instr);
result.push((addr, text));
}
result
}
#[cfg(test)]
mod tests {
use super::*;
use crate::decoder::decode;
#[test]
fn test_disasm_nop() {
// ori r0, r0, 0 = NOP
let instr = decode(0x60000000, 0);
let text = disassemble(&instr);
assert!(text.contains("ori"), "Expected 'ori', got: {}", text);
}
#[test]
fn test_disasm_addi() {
let raw = (14u32 << 26) | (3 << 21) | (1 << 16) | 16;
let instr = decode(raw, 0);
let text = disassemble(&instr);
assert!(text.contains("addi"), "Got: {}", text);
assert!(text.contains("r3"), "Got: {}", text);
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,9 @@
pub mod context;
pub mod decoder;
pub mod disasm;
pub mod interpreter;
pub mod opcode;
pub use context::PpcContext;
pub use decoder::decode;
pub use opcode::PpcOpcode;

View File

@@ -0,0 +1,196 @@
/// All PPC opcodes supported by the Xbox 360, including VMX128 extensions.
/// Directly mirrors the C++ PPCOpcode enum from ppc_opcode.h.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[repr(u32)]
#[allow(non_camel_case_types)]
pub enum PpcOpcode {
// ALU
addcx, addex, addi, addic, addicx, addis, addmex, addx, addzex,
andcx, andisx, andix, andx,
// Branch
bcctrx, bclrx, bcx, bx,
// Compare
cmp, cmpi, cmpl, cmpli,
// Count leading zeros
cntlzdx, cntlzwx,
// Condition register
crand, crandc, creqv, crnand, crnor, cror, crorc, crxor,
// Data cache
dcbf, dcbi, dcbst, dcbt, dcbtst, dcbz, dcbz128,
// Division
divdux, divdx, divwux, divwx,
// Sync/barrier
eieio,
// Logical
eqvx, extsbx, extshx, extswx,
// FPU
fabsx, faddsx, faddx, fcfidx, fcmpo, fcmpu, fctidx, fctidzx, fctiwx, fctiwzx,
fdivsx, fdivx, fmaddsx, fmaddx, fmrx, fmsubsx, fmsubx, fmulsx, fmulx,
fnabsx, fnegx, fnmaddsx, fnmaddx, fnmsubsx, fnmsubx, fresx, frspx, frsqrtex,
fselx, fsqrtsx, fsqrtx, fsubsx, fsubx,
// Instruction cache
icbi, isync,
// Load byte
lbz, lbzu, lbzux, lbzx,
// Load doubleword
ld, ldarx, ldbrx, ldu, ldux, ldx,
// Load float
lfd, lfdu, lfdux, lfdx, lfs, lfsu, lfsux, lfsx,
// Load halfword
lha, lhau, lhaux, lhax, lhbrx, lhz, lhzu, lhzux, lhzx,
// Load multiple/string
lmw, lswi, lswx,
// Load vector
lvebx, lvehx, lvewx, lvewx128, lvlx, lvlx128, lvlxl, lvlxl128,
lvrx, lvrx128, lvrxl, lvrxl128,
lvsl, lvsl128, lvsr, lvsr128,
lvx, lvx128, lvxl, lvxl128,
// Load word
lwa, lwarx, lwaux, lwax, lwbrx, lwz, lwzu, lwzux, lwzx,
// Move CR
mcrf, mcrfs, mcrxr,
// Move from special
mfcr, mffsx, mfmsr, mfspr, mftb, mfvscr,
// Move to special
mtcrf, mtfsb0x, mtfsb1x, mtfsfix, mtfsfx, mtmsr, mtmsrd, mtspr, mtvscr,
// Multiply
mulhdux, mulhdx, mulhwux, mulhwx, mulldx, mulli, mullwx,
// Logical
nandx, negx, norx, orcx, ori, oris, orx,
// Rotate
rldclx, rldcrx, rldiclx, rldicrx, rldicx, rldimix, rlwimix, rlwinmx, rlwnmx,
// System call
sc,
// Shift
sldx, slwx, sradix, sradx, srawix, srawx, srdx, srwx,
// Store byte
stb, stbu, stbux, stbx,
// Store doubleword
std, stdbrx, stdcx, stdu, stdux, stdx,
// Store float
stfd, stfdu, stfdux, stfdx, stfiwx, stfs, stfsu, stfsux, stfsx,
// Store halfword
sth, sthbrx, sthu, sthux, sthx,
// Store multiple/string
stmw, stswi, stswx,
// Store vector
stvebx, stvehx, stvewx, stvewx128, stvlx, stvlx128, stvlxl, stvlxl128,
stvrx, stvrx128, stvrxl, stvrxl128,
stvx, stvx128, stvxl, stvxl128,
// Store word
stw, stwbrx, stwcx, stwu, stwux, stwx,
// Subtract
subfcx, subfex, subficx, subfmex, subfx, subfzex,
// Sync
sync,
// Trap
td, tdi, tw, twi,
// VMX integer
vaddcuw, vaddfp, vaddfp128, vaddsbs, vaddshs, vaddsws,
vaddubm, vaddubs, vadduhm, vadduhs, vadduwm, vadduws,
vand, vand128, vandc, vandc128,
vavgsb, vavgsh, vavgsw, vavgub, vavguh, vavguw,
vcfpsxws128, vcfpuxws128, vcfsx, vcfux,
vcmpbfp, vcmpbfp128, vcmpeqfp, vcmpeqfp128,
vcmpequb, vcmpequh, vcmpequw, vcmpequw128,
vcmpgefp, vcmpgefp128, vcmpgtfp, vcmpgtfp128,
vcmpgtsb, vcmpgtsh, vcmpgtsw, vcmpgtub, vcmpgtuh, vcmpgtuw,
vcsxwfp128, vctsxs, vctuxs, vcuxwfp128,
vexptefp, vexptefp128, vlogefp, vlogefp128,
vmaddcfp128, vmaddfp, vmaddfp128,
vmaxfp, vmaxfp128, vmaxsb, vmaxsh, vmaxsw, vmaxub, vmaxuh, vmaxuw,
vmhaddshs, vmhraddshs,
vminfp, vminfp128, vminsb, vminsh, vminsw, vminub, vminuh, vminuw,
vmladduhm,
vmrghb, vmrghh, vmrghw, vmrghw128, vmrglb, vmrglh, vmrglw, vmrglw128,
vmsum3fp128, vmsum4fp128,
vmsummbm, vmsumshm, vmsumshs, vmsumubm, vmsumuhm, vmsumuhs,
vmulesb, vmulesh, vmuleub, vmuleuh, vmulfp128,
vmulosb, vmulosh, vmuloub, vmulouh,
vnmsubfp, vnmsubfp128, vnor, vnor128,
vor, vor128,
vperm, vperm128, vpermwi128, vpkd3d128,
vpkpx, vpkshss, vpkshss128, vpkshus, vpkshus128,
vpkswss, vpkswss128, vpkswus, vpkswus128,
vpkuhum, vpkuhum128, vpkuhus, vpkuhus128,
vpkuwum, vpkuwum128, vpkuwus, vpkuwus128,
vrefp, vrefp128,
vrfim, vrfim128, vrfin, vrfin128, vrfip, vrfip128, vrfiz, vrfiz128,
vrlb, vrlh, vrlimi128, vrlw, vrlw128,
vrsqrtefp, vrsqrtefp128,
vsel, vsel128,
vsl, vslb, vsldoi, vsldoi128, vslh, vslo, vslo128, vslw, vslw128,
vspltb, vsplth, vspltisb, vspltish, vspltisw, vspltisw128, vspltw, vspltw128,
vsr, vsrab, vsrah, vsraw, vsraw128, vsrb, vsrh, vsro, vsro128, vsrw, vsrw128,
vsubcuw, vsubfp, vsubfp128, vsubsbs, vsubshs, vsubsws,
vsububm, vsububs, vsubuhm, vsubuhs, vsubuwm, vsubuws,
vsum2sws, vsum4sbs, vsum4shs, vsum4ubs, vsumsws,
vupkd3d128, vupkhpx, vupkhsb, vupkhsb128, vupkhsh,
vupklpx, vupklsb, vupklsb128, vupklsh,
vxor, vxor128,
// XOR immediate
xori, xoris, xorx,
// Invalid
Invalid,
}
impl PpcOpcode {
/// Returns true if this opcode is a branch instruction.
pub fn is_branch(&self) -> bool {
matches!(self, Self::bx | Self::bcx | Self::bclrx | Self::bcctrx)
}
/// Returns true if this opcode is a system call.
pub fn is_syscall(&self) -> bool {
matches!(self, Self::sc)
}
/// Returns true if this is a load instruction.
pub fn is_load(&self) -> bool {
matches!(self,
Self::lbz | Self::lbzu | Self::lbzux | Self::lbzx |
Self::lhz | Self::lhzu | Self::lhzux | Self::lhzx |
Self::lha | Self::lhau | Self::lhaux | Self::lhax |
Self::lwz | Self::lwzu | Self::lwzux | Self::lwzx |
Self::lwa | Self::lwax | Self::lwaux |
Self::ld | Self::ldu | Self::ldux | Self::ldx |
Self::lfs | Self::lfsu | Self::lfsux | Self::lfsx |
Self::lfd | Self::lfdu | Self::lfdux | Self::lfdx |
Self::lhbrx | Self::lwbrx | Self::ldbrx |
Self::lmw | Self::lswi | Self::lswx |
Self::lwarx | Self::ldarx
)
}
/// Returns true if this is a store instruction.
pub fn is_store(&self) -> bool {
matches!(self,
Self::stb | Self::stbu | Self::stbux | Self::stbx |
Self::sth | Self::sthu | Self::sthux | Self::sthx |
Self::stw | Self::stwu | Self::stwux | Self::stwx |
Self::std | Self::stdu | Self::stdux | Self::stdx |
Self::stfs | Self::stfsu | Self::stfsux | Self::stfsx |
Self::stfd | Self::stfdu | Self::stfdux | Self::stfdx |
Self::sthbrx | Self::stwbrx | Self::stdbrx |
Self::stmw | Self::stswi | Self::stswx |
Self::stwcx | Self::stdcx | Self::stfiwx
)
}
pub fn name(&self) -> &'static str {
match self {
Self::Invalid => "invalid",
_ => {
// Use debug formatting to get the variant name
// This is a placeholder - in practice we'd have a lookup table
"?"
}
}
}
}
impl std::fmt::Display for PpcOpcode {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
std::fmt::Debug::fmt(self, f)
}
}

View File

@@ -0,0 +1,13 @@
[package]
name = "xenia-debugger"
version.workspace = true
edition.workspace = true
license.workspace = true
[dependencies]
xenia-types = { workspace = true }
xenia-memory = { workspace = true }
xenia-cpu = { workspace = true }
tracing = { workspace = true }
thiserror = { workspace = true }
anyhow = { workspace = true }

View File

@@ -0,0 +1,7 @@
/// A code breakpoint at a specific guest address.
#[derive(Debug, Clone)]
pub struct Breakpoint {
pub addr: u32,
pub enabled: bool,
pub condition: Option<String>,
}

View File

@@ -0,0 +1,125 @@
pub mod breakpoint;
pub mod trace;
use std::collections::HashMap;
use xenia_cpu::context::PpcContext;
use xenia_memory::MemoryAccess;
pub use breakpoint::Breakpoint;
pub use trace::TraceEntry;
/// The debugger. Hooks into every instruction step for observation.
pub struct Debugger {
pub breakpoints: HashMap<u32, Breakpoint>,
pub trace_log: Vec<TraceEntry>,
pub trace_enabled: bool,
pub max_trace_entries: usize,
pub paused: bool,
pub step_mode: StepMode,
break_pending: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum StepMode {
/// Run freely until breakpoint or pause
Run,
/// Execute one instruction then pause
StepInto,
/// Run but break after current function returns (when LR changes)
StepOver { return_addr: u32 },
}
impl Debugger {
pub fn new() -> Self {
Self {
breakpoints: HashMap::new(),
trace_log: Vec::new(),
trace_enabled: true,
max_trace_entries: 100_000,
paused: true, // Start paused for debugging
step_mode: StepMode::StepInto,
break_pending: false,
}
}
/// Called before each instruction executes.
pub fn pre_step(&mut self, ctx: &PpcContext, _mem: &dyn MemoryAccess) {
// Check breakpoints
if let Some(bp) = self.breakpoints.get(&ctx.pc) {
if bp.enabled {
self.break_pending = true;
tracing::info!("Breakpoint hit at {:#010x}", ctx.pc);
}
}
}
/// Called after each instruction executes.
pub fn post_step(&mut self, ctx: &PpcContext, _mem: &dyn MemoryAccess) {
// Log to trace
if self.trace_enabled {
if self.trace_log.len() >= self.max_trace_entries {
self.trace_log.remove(0);
}
self.trace_log.push(TraceEntry {
pc: ctx.pc,
cycle: ctx.cycle_count,
gpr_snapshot: [ctx.gpr[0], ctx.gpr[1], ctx.gpr[3], ctx.gpr[4]],
lr: ctx.lr,
});
}
// Handle step mode
match self.step_mode {
StepMode::StepInto => {
self.break_pending = true;
}
StepMode::StepOver { return_addr } => {
if ctx.pc == return_addr {
self.break_pending = true;
}
}
StepMode::Run => {}
}
}
/// Should we break execution?
pub fn should_break(&self) -> bool {
self.break_pending || self.paused
}
/// Add a breakpoint at the given address.
pub fn add_breakpoint(&mut self, addr: u32) {
self.breakpoints.insert(addr, Breakpoint { addr, enabled: true, condition: None });
}
/// Remove a breakpoint.
pub fn remove_breakpoint(&mut self, addr: u32) {
self.breakpoints.remove(&addr);
}
/// Continue execution.
pub fn continue_execution(&mut self) {
self.paused = false;
self.break_pending = false;
self.step_mode = StepMode::Run;
}
/// Step one instruction.
pub fn step_into(&mut self) {
self.paused = false;
self.break_pending = false;
self.step_mode = StepMode::StepInto;
}
/// Clear break state after handling.
pub fn acknowledge_break(&mut self) {
self.break_pending = false;
self.paused = true;
}
}
impl Default for Debugger {
fn default() -> Self {
Self::new()
}
}

View File

@@ -0,0 +1,9 @@
/// A single entry in the instruction trace log.
#[derive(Debug, Clone)]
pub struct TraceEntry {
pub pc: u32,
pub cycle: u64,
/// Snapshot of key GPRs: [r0, r1(sp), r3(arg0/retval), r4(arg1)]
pub gpr_snapshot: [u64; 4],
pub lr: u64,
}

View File

@@ -0,0 +1,13 @@
[package]
name = "xenia-gpu"
version.workspace = true
edition.workspace = true
license.workspace = true
[dependencies]
xenia-types = { workspace = true }
xenia-memory = { workspace = true }
tracing = { workspace = true }
thiserror = { workspace = true }
anyhow = { workspace = true }
byteorder = { workspace = true }

View File

@@ -0,0 +1,17 @@
/// PM4 command processor stub.
/// Will parse the GPU command ring buffer and dispatch to render operations.
pub struct CommandProcessor {
pub enabled: bool,
}
impl CommandProcessor {
pub fn new() -> Self {
Self { enabled: false }
}
}
impl Default for CommandProcessor {
fn default() -> Self {
Self::new()
}
}

View File

@@ -0,0 +1,21 @@
pub mod command_processor;
pub mod register_file;
/// Stub GPU system for initial implementation.
pub struct GpuSystem {
pub register_file: register_file::RegisterFile,
}
impl GpuSystem {
pub fn new() -> Self {
Self {
register_file: register_file::RegisterFile::new(),
}
}
}
impl Default for GpuSystem {
fn default() -> Self {
Self::new()
}
}

View File

@@ -0,0 +1,28 @@
/// Xenos GPU register file. 0x6000 32-bit registers.
pub struct RegisterFile {
pub regs: Vec<u32>,
}
impl RegisterFile {
pub fn new() -> Self {
Self {
regs: vec![0u32; 0x6000],
}
}
pub fn read(&self, index: u32) -> u32 {
self.regs.get(index as usize).copied().unwrap_or(0)
}
pub fn write(&mut self, index: u32, value: u32) {
if let Some(r) = self.regs.get_mut(index as usize) {
*r = value;
}
}
}
impl Default for RegisterFile {
fn default() -> Self {
Self::new()
}
}

View File

@@ -0,0 +1,10 @@
[package]
name = "xenia-hid"
version.workspace = true
edition.workspace = true
license.workspace = true
[dependencies]
xenia-types = { workspace = true }
tracing = { workspace = true }
thiserror = { workspace = true }

View File

@@ -0,0 +1,47 @@
/// Human input device system stub.
pub struct InputSystem {
pub gamepad: GamepadState,
}
#[derive(Default, Clone, Copy)]
pub struct GamepadState {
pub buttons: u16,
pub left_trigger: u8,
pub right_trigger: u8,
pub left_stick_x: i16,
pub left_stick_y: i16,
pub right_stick_x: i16,
pub right_stick_y: i16,
}
/// Xbox 360 button flags
pub mod buttons {
pub const DPAD_UP: u16 = 0x0001;
pub const DPAD_DOWN: u16 = 0x0002;
pub const DPAD_LEFT: u16 = 0x0004;
pub const DPAD_RIGHT: u16 = 0x0008;
pub const START: u16 = 0x0010;
pub const BACK: u16 = 0x0020;
pub const LEFT_THUMB: u16 = 0x0040;
pub const RIGHT_THUMB: u16 = 0x0080;
pub const LEFT_SHOULDER: u16 = 0x0100;
pub const RIGHT_SHOULDER: u16 = 0x0200;
pub const A: u16 = 0x1000;
pub const B: u16 = 0x2000;
pub const X: u16 = 0x4000;
pub const Y: u16 = 0x8000;
}
impl InputSystem {
pub fn new() -> Self {
Self {
gamepad: GamepadState::default(),
}
}
}
impl Default for InputSystem {
fn default() -> Self {
Self::new()
}
}

View File

@@ -0,0 +1,13 @@
[package]
name = "xenia-kernel"
version.workspace = true
edition.workspace = true
license.workspace = true
[dependencies]
xenia-types = { workspace = true }
xenia-memory = { workspace = true }
xenia-cpu = { workspace = true }
tracing = { workspace = true }
thiserror = { workspace = true }
anyhow = { workspace = true }

View File

@@ -0,0 +1,763 @@
//! HLE kernel export implementations (xboxkrnl.exe).
//! Each export mirrors a function from xboxkrnl_table.inc.
use crate::objects::KernelObject;
use crate::state::{KernelState, ModuleId};
use xenia_cpu::PpcContext;
use xenia_memory::{GuestMemory, MemoryAccess};
pub fn register_exports(state: &mut KernelState) {
use ModuleId::Xboxkrnl;
// Debug
state.register_export(Xboxkrnl, 0x01, "DbgBreakPoint", dbg_break_point);
state.register_export(Xboxkrnl, 0x03, "DbgPrint", dbg_print);
// ExCreateThread and friends
state.register_export(Xboxkrnl, 0x0D, "ExCreateThread", ex_create_thread);
state.register_export(Xboxkrnl, 0x10, "ExGetXConfigSetting", ex_get_xconfig_setting);
state.register_export(Xboxkrnl, 0x15, "ExRegisterTitleTerminateNotification", stub_success);
state.register_export(Xboxkrnl, 0x19, "ExTerminateThread", ex_terminate_thread);
// Hal
state.register_export(Xboxkrnl, 0x28, "HalReturnToFirmware", hal_return_to_firmware);
// I/O
state.register_export(Xboxkrnl, 0x3C, "IoDismountVolumeByFileHandle", stub_success);
// Ke* Threading/Sync
state.register_export(Xboxkrnl, 0x4D, "KeAcquireSpinLockAtRaisedIrql", stub_return_zero);
state.register_export(Xboxkrnl, 0x52, "KeBugCheck", ke_bug_check);
state.register_export(Xboxkrnl, 0x53, "KeBugCheckEx", ke_bug_check_ex);
state.register_export(Xboxkrnl, 0x5A, "KeDelayExecutionThread", stub_success);
state.register_export(Xboxkrnl, 0x5D, "KeEnableFpuExceptions", stub_success);
state.register_export(Xboxkrnl, 0x5F, "KeEnterCriticalRegion", stub_success);
state.register_export(Xboxkrnl, 0x66, "KeGetCurrentProcessType", ke_get_current_process_type);
state.register_export(Xboxkrnl, 0x6B, "KeLockL2", stub_success);
state.register_export(Xboxkrnl, 0x6C, "KeUnlockL2", stub_success);
state.register_export(Xboxkrnl, 0x74, "KeInitializeSemaphore", ke_initialize_semaphore);
state.register_export(Xboxkrnl, 0x7D, "KeLeaveCriticalRegion", stub_success);
state.register_export(Xboxkrnl, 0x81, "KeQueryBasePriorityThread", stub_return_zero);
state.register_export(Xboxkrnl, 0x83, "KeQueryPerformanceFrequency", ke_query_performance_frequency);
state.register_export(Xboxkrnl, 0x84, "KeQuerySystemTime", ke_query_system_time);
state.register_export(Xboxkrnl, 0x85, "KeRaiseIrqlToDpcLevel", stub_return_zero);
state.register_export(Xboxkrnl, 0x88, "KeReleaseSemaphore", stub_return_zero);
state.register_export(Xboxkrnl, 0x89, "KeReleaseSpinLockFromRaisedIrql", stub_success);
state.register_export(Xboxkrnl, 0x8F, "KeResetEvent", stub_return_zero);
state.register_export(Xboxkrnl, 0x92, "KeResumeThread", stub_return_zero);
state.register_export(Xboxkrnl, 0x97, "KeSetAffinityThread", stub_return_zero);
state.register_export(Xboxkrnl, 0x99, "KeSetBasePriorityThread", stub_return_zero);
state.register_export(Xboxkrnl, 0x9B, "KeSetCurrentStackPointers", stub_success);
state.register_export(Xboxkrnl, 0x9D, "KeSetEvent", stub_return_zero);
state.register_export(Xboxkrnl, 0xAE, "KeTryToAcquireSpinLockAtRaisedIrql", ke_try_acquire_spinlock);
state.register_export(Xboxkrnl, 0xAF, "KeWaitForMultipleObjects", stub_success);
state.register_export(Xboxkrnl, 0xB0, "KeWaitForSingleObject", stub_success);
state.register_export(Xboxkrnl, 0xB1, "KfAcquireSpinLock", stub_return_zero);
state.register_export(Xboxkrnl, 0xB3, "KfLowerIrql", stub_success);
state.register_export(Xboxkrnl, 0xB4, "KfReleaseSpinLock", stub_success);
state.register_export(Xboxkrnl, 0x0152, "KeTlsAlloc", ke_tls_alloc);
state.register_export(Xboxkrnl, 0x0153, "KeTlsFree", stub_success);
state.register_export(Xboxkrnl, 0x0154, "KeTlsGetValue", ke_tls_get_value);
state.register_export(Xboxkrnl, 0x0155, "KeTlsSetValue", ke_tls_set_value);
state.register_export(Xboxkrnl, 0x01DF, "KiApcNormalRoutineNop", stub_success);
// Memory
state.register_export(Xboxkrnl, 0xBA, "MmAllocatePhysicalMemoryEx", mm_allocate_physical_memory_ex);
state.register_export(Xboxkrnl, 0xBB, "MmCreateKernelStack", mm_create_kernel_stack);
state.register_export(Xboxkrnl, 0xBC, "MmDeleteKernelStack", stub_success);
state.register_export(Xboxkrnl, 0xBD, "MmFreePhysicalMemory", stub_success);
state.register_export(Xboxkrnl, 0xBE, "MmGetPhysicalAddress", mm_get_physical_address);
state.register_export(Xboxkrnl, 0xC4, "MmQueryAddressProtect", mm_query_address_protect);
state.register_export(Xboxkrnl, 0xC6, "MmQueryStatistics", mm_query_statistics);
// Nt*
state.register_export(Xboxkrnl, 0xCC, "NtAllocateVirtualMemory", nt_allocate_virtual_memory);
state.register_export(Xboxkrnl, 0xCD, "NtCancelTimer", stub_success);
state.register_export(Xboxkrnl, 0xCE, "NtClearEvent", stub_success);
state.register_export(Xboxkrnl, 0xCF, "NtClose", nt_close);
state.register_export(Xboxkrnl, 0xD1, "NtCreateEvent", nt_create_event);
state.register_export(Xboxkrnl, 0xD2, "NtCreateFile", nt_create_file);
state.register_export(Xboxkrnl, 0xD5, "NtCreateSemaphore", nt_create_semaphore);
state.register_export(Xboxkrnl, 0xD7, "NtCreateTimer", nt_create_timer);
state.register_export(Xboxkrnl, 0xD9, "NtDeviceIoControlFile", stub_success);
state.register_export(Xboxkrnl, 0xDA, "NtDuplicateObject", stub_success);
state.register_export(Xboxkrnl, 0xDB, "NtFlushBuffersFile", stub_success);
state.register_export(Xboxkrnl, 0xDC, "NtFreeVirtualMemory", stub_success);
state.register_export(Xboxkrnl, 0xDF, "NtOpenFile", nt_open_file);
state.register_export(Xboxkrnl, 0xE4, "NtQueryDirectoryFile", nt_query_directory_file);
state.register_export(Xboxkrnl, 0xE7, "NtQueryFullAttributesFile", nt_query_full_attributes_file);
state.register_export(Xboxkrnl, 0xE8, "NtQueryInformationFile", stub_success);
state.register_export(Xboxkrnl, 0xEE, "NtQueryVirtualMemory", stub_success);
state.register_export(Xboxkrnl, 0xEF, "NtQueryVolumeInformationFile", stub_success);
state.register_export(Xboxkrnl, 0xF0, "NtReadFile", nt_read_file);
state.register_export(Xboxkrnl, 0xF3, "NtReleaseSemaphore", stub_return_zero);
state.register_export(Xboxkrnl, 0xF5, "NtResumeThread", stub_return_zero);
state.register_export(Xboxkrnl, 0xF6, "NtSetEvent", stub_success);
state.register_export(Xboxkrnl, 0xF7, "NtSetInformationFile", stub_success);
state.register_export(Xboxkrnl, 0xFA, "NtSetTimerEx", stub_success);
state.register_export(Xboxkrnl, 0xFC, "NtSuspendThread", stub_return_zero);
state.register_export(Xboxkrnl, 0xFD, "NtWaitForSingleObjectEx", stub_success);
state.register_export(Xboxkrnl, 0xFE, "NtWaitForMultipleObjectsEx", stub_success);
state.register_export(Xboxkrnl, 0xFF, "NtWriteFile", nt_write_file);
state.register_export(Xboxkrnl, 0x0101, "NtYieldExecution", stub_success);
// Object
state.register_export(Xboxkrnl, 0x0103, "ObCreateSymbolicLink", stub_success);
state.register_export(Xboxkrnl, 0x0104, "ObDeleteSymbolicLink", stub_success);
state.register_export(Xboxkrnl, 0x0105, "ObDereferenceObject", stub_success);
state.register_export(Xboxkrnl, 0x010B, "ObLookupThreadByThreadId", stub_success);
state.register_export(Xboxkrnl, 0x010E, "ObOpenObjectByPointer", stub_success);
state.register_export(Xboxkrnl, 0x0110, "ObReferenceObjectByHandle", stub_success);
// RTL
state.register_export(Xboxkrnl, 0x0119, "RtlCaptureContext", rtl_capture_context);
state.register_export(Xboxkrnl, 0x011B, "RtlCompareMemoryUlong", rtl_compare_memory_ulong);
state.register_export(Xboxkrnl, 0x0125, "RtlEnterCriticalSection", rtl_enter_critical_section);
state.register_export(Xboxkrnl, 0x0126, "RtlFillMemoryUlong", rtl_fill_memory_ulong);
state.register_export(Xboxkrnl, 0x0127, "RtlFreeAnsiString", stub_success);
state.register_export(Xboxkrnl, 0x012B, "RtlImageXexHeaderField", rtl_image_xex_header_field);
state.register_export(Xboxkrnl, 0x012C, "RtlInitAnsiString", rtl_init_ansi_string);
state.register_export(Xboxkrnl, 0x012D, "RtlInitUnicodeString", rtl_init_unicode_string);
state.register_export(Xboxkrnl, 0x012E, "RtlInitializeCriticalSection", rtl_initialize_critical_section);
state.register_export(Xboxkrnl, 0x012F, "RtlInitializeCriticalSectionAndSpinCount", rtl_initialize_critical_section);
state.register_export(Xboxkrnl, 0x0130, "RtlLeaveCriticalSection", rtl_leave_critical_section);
state.register_export(Xboxkrnl, 0x0133, "RtlMultiByteToUnicodeN", rtl_multi_byte_to_unicode_n);
state.register_export(Xboxkrnl, 0x0135, "RtlNtStatusToDosError", rtl_nt_status_to_dos_error);
state.register_export(Xboxkrnl, 0x0136, "RtlRaiseException", rtl_raise_exception);
state.register_export(Xboxkrnl, 0x013B, "sprintf", stub_sprintf);
state.register_export(Xboxkrnl, 0x013F, "RtlTimeFieldsToTime", stub_success);
state.register_export(Xboxkrnl, 0x0140, "RtlTimeToTimeFields", stub_success);
state.register_export(Xboxkrnl, 0x0141, "RtlTryEnterCriticalSection", rtl_try_enter_critical_section);
state.register_export(Xboxkrnl, 0x0142, "RtlUnicodeStringToAnsiString", stub_success);
state.register_export(Xboxkrnl, 0x0143, "RtlUnicodeToMultiByteN", stub_success);
state.register_export(Xboxkrnl, 0x0147, "RtlUnwind", rtl_unwind);
state.register_export(Xboxkrnl, 0x014D, "_vsnprintf", stub_vsnprintf);
// Stfs
state.register_export(Xboxkrnl, 0x0259, "StfsCreateDevice", stub_success);
state.register_export(Xboxkrnl, 0x025A, "StfsControlDevice", stub_success);
// Video
state.register_export(Xboxkrnl, 0x01B1, "VdCallGraphicsNotificationRoutines", stub_success);
state.register_export(Xboxkrnl, 0x01B4, "VdEnableDisableClockGating", stub_success);
state.register_export(Xboxkrnl, 0x01B6, "VdEnableRingBufferRPtrWriteBack", stub_success);
state.register_export(Xboxkrnl, 0x01B9, "VdGetCurrentDisplayGamma", stub_return_zero);
state.register_export(Xboxkrnl, 0x01BA, "VdGetCurrentDisplayInformation", stub_success);
state.register_export(Xboxkrnl, 0x01BD, "VdGetSystemCommandBuffer", vd_get_system_command_buffer);
state.register_export(Xboxkrnl, 0x01C2, "VdInitializeEngines", stub_success);
state.register_export(Xboxkrnl, 0x01C3, "VdInitializeRingBuffer", stub_success);
state.register_export(Xboxkrnl, 0x01C5, "VdInitializeScalerCommandBuffer", stub_success);
state.register_export(Xboxkrnl, 0x01C6, "VdIsHSIOTrainingSucceeded", vd_is_hsio_training_succeeded);
state.register_export(Xboxkrnl, 0x01C7, "VdPersistDisplay", stub_success);
state.register_export(Xboxkrnl, 0x01C9, "VdQueryVideoFlags", stub_return_zero);
state.register_export(Xboxkrnl, 0x01CA, "VdQueryVideoMode", vd_query_video_mode);
state.register_export(Xboxkrnl, 0x0269, "VdRetrainEDRAM", stub_success);
state.register_export(Xboxkrnl, 0x026A, "VdRetrainEDRAMWorker", stub_success);
state.register_export(Xboxkrnl, 0x01D3, "VdSetDisplayMode", stub_success);
state.register_export(Xboxkrnl, 0x01D5, "VdSetGraphicsInterruptCallback", stub_success);
state.register_export(Xboxkrnl, 0x01D9, "VdSetSystemCommandBufferGpuIdentifierAddress", stub_success);
state.register_export(Xboxkrnl, 0x01DC, "VdShutdownEngines", stub_success);
state.register_export(Xboxkrnl, 0x025B, "VdSwap", vd_swap);
// Audio
state.register_export(Xboxkrnl, 0x01F3, "XAudioRegisterRenderDriverClient", xaudio_register_render_driver);
state.register_export(Xboxkrnl, 0x01F4, "XAudioUnregisterRenderDriverClient", stub_success);
state.register_export(Xboxkrnl, 0x01F5, "XAudioSubmitRenderDriverFrame", stub_success);
state.register_export(Xboxkrnl, 0x01F7, "XAudioGetVoiceCategoryVolumeChangeMask", stub_return_zero);
state.register_export(Xboxkrnl, 0x01F8, "XAudioGetVoiceCategoryVolume", stub_success);
state.register_export(Xboxkrnl, 0x0224, "XMACreateContext", xma_create_context);
state.register_export(Xboxkrnl, 0x0226, "XMAReleaseContext", stub_success);
// Crypto
state.register_export(Xboxkrnl, 0x0192, "XeCryptSha", stub_success);
state.register_export(Xboxkrnl, 0x0256, "XeKeysConsolePrivateKeySign", stub_success);
state.register_export(Xboxkrnl, 0x0257, "XeKeysConsoleSignatureVerification", stub_success);
// Xex module
state.register_export(Xboxkrnl, 0x0194, "XexCheckExecutablePrivilege", stub_return_zero);
state.register_export(Xboxkrnl, 0x0195, "XexGetModuleHandle", stub_return_zero);
state.register_export(Xboxkrnl, 0x0197, "XexGetProcedureAddress", xex_get_procedure_address);
// Exception handling
state.register_export(Xboxkrnl, 0x01A5, "__C_specific_handler", c_specific_handler);
}
// ===== Generic stubs =====
fn stub_success(ctx: &mut PpcContext, _mem: &mut GuestMemory, _state: &mut KernelState) {
ctx.gpr[3] = 0; // STATUS_SUCCESS
}
fn stub_return_zero(ctx: &mut PpcContext, _mem: &mut GuestMemory, _state: &mut KernelState) {
ctx.gpr[3] = 0;
}
// ===== Debug =====
fn dbg_break_point(_ctx: &mut PpcContext, _mem: &mut GuestMemory, _state: &mut KernelState) {
tracing::warn!("DbgBreakPoint hit");
}
fn dbg_print(ctx: &mut PpcContext, mem: &mut GuestMemory, _state: &mut KernelState) {
let str_ptr = ctx.gpr[3] as u32;
if str_ptr != 0 {
let s = read_cstring(mem, str_ptr);
tracing::info!("DbgPrint: {}", s);
}
ctx.gpr[3] = 0;
}
// ===== Threading =====
fn ex_create_thread(ctx: &mut PpcContext, mem: &mut GuestMemory, state: &mut KernelState) {
// r3 = handle_ptr, r4 = stack_size, r5 = thread_id_ptr, r6 = xapi_startup
// r7 = start_address, r8 = start_context, r9 = creation_flags
let handle_ptr = ctx.gpr[3] as u32;
let thread_id_ptr = ctx.gpr[5] as u32;
let tid = state.next_thread_id;
state.next_thread_id += 1;
let handle = state.alloc_handle_for(KernelObject::Thread { id: tid });
if handle_ptr != 0 {
mem.write_u32(handle_ptr, handle);
}
if thread_id_ptr != 0 {
mem.write_u32(thread_id_ptr, tid);
}
tracing::info!("ExCreateThread: handle={:#x} tid={}", handle, tid);
ctx.gpr[3] = 0; // STATUS_SUCCESS
}
fn ex_terminate_thread(ctx: &mut PpcContext, _mem: &mut GuestMemory, _state: &mut KernelState) {
tracing::info!("ExTerminateThread: exit_status={:#x}", ctx.gpr[3]);
ctx.gpr[3] = 0;
}
fn hal_return_to_firmware(ctx: &mut PpcContext, _mem: &mut GuestMemory, _state: &mut KernelState) {
tracing::warn!("HalReturnToFirmware: reason={:#x}", ctx.gpr[3]);
ctx.gpr[3] = 0;
}
// ===== Ke* =====
fn ke_bug_check(ctx: &mut PpcContext, _mem: &mut GuestMemory, _state: &mut KernelState) {
tracing::error!("KeBugCheck: code={:#x}", ctx.gpr[3]);
ctx.gpr[3] = 0;
}
fn ke_bug_check_ex(ctx: &mut PpcContext, _mem: &mut GuestMemory, _state: &mut KernelState) {
tracing::error!("KeBugCheckEx: code={:#x} p1={:#x} p2={:#x} p3={:#x}",
ctx.gpr[3], ctx.gpr[4], ctx.gpr[5], ctx.gpr[6]);
ctx.gpr[3] = 0;
}
fn ke_get_current_process_type(ctx: &mut PpcContext, _mem: &mut GuestMemory, _state: &mut KernelState) {
ctx.gpr[3] = 1; // PROC_USER
}
fn ke_query_performance_frequency(ctx: &mut PpcContext, _mem: &mut GuestMemory, _state: &mut KernelState) {
ctx.gpr[3] = 50_000_000; // 50 MHz
}
fn ke_query_system_time(ctx: &mut PpcContext, mem: &mut GuestMemory, _state: &mut KernelState) {
let time_ptr = ctx.gpr[3] as u32;
if time_ptr != 0 {
let fake_time: u64 = 132_500_000_000_000_000; // ~2021 FILETIME
mem.write_u32(time_ptr, (fake_time >> 32) as u32);
mem.write_u32(time_ptr + 4, fake_time as u32);
}
}
fn ke_initialize_semaphore(ctx: &mut PpcContext, mem: &mut GuestMemory, _state: &mut KernelState) {
// r3 = semaphore_ptr, r4 = count, r5 = limit
let sem_ptr = ctx.gpr[3] as u32;
if sem_ptr != 0 {
// Zero-init the KSEMAPHORE structure (0x14 bytes)
for i in (0..0x14).step_by(4) {
mem.write_u32(sem_ptr + i, 0);
}
}
}
fn ke_try_acquire_spinlock(ctx: &mut PpcContext, _mem: &mut GuestMemory, _state: &mut KernelState) {
ctx.gpr[3] = 1; // TRUE (acquired successfully in single-threaded mode)
}
fn ke_tls_alloc(ctx: &mut PpcContext, _mem: &mut GuestMemory, state: &mut KernelState) {
ctx.gpr[3] = state.tls_alloc() as u64;
}
fn ke_tls_get_value(ctx: &mut PpcContext, _mem: &mut GuestMemory, state: &mut KernelState) {
let index = ctx.gpr[3] as u32;
ctx.gpr[3] = state.tls_get(index);
}
fn ke_tls_set_value(ctx: &mut PpcContext, _mem: &mut GuestMemory, state: &mut KernelState) {
let index = ctx.gpr[3] as u32;
let value = ctx.gpr[4];
state.tls_set(index, value);
ctx.gpr[3] = 1; // TRUE
}
fn ex_get_xconfig_setting(ctx: &mut PpcContext, _mem: &mut GuestMemory, _state: &mut KernelState) {
ctx.gpr[3] = 0; // STATUS_SUCCESS (writes nothing)
}
// ===== Memory =====
fn nt_allocate_virtual_memory(ctx: &mut PpcContext, mem: &mut GuestMemory, state: &mut KernelState) {
// r3 = base_addr_ptr (in/out), r4 = region_size_ptr (in/out)
// r5 = alloc_type, r6 = protect
let base_ptr = ctx.gpr[3] as u32;
let size_ptr = ctx.gpr[4] as u32;
let requested_base = mem.read_u32(base_ptr);
let requested_size = mem.read_u32(size_ptr);
let aligned_size = (requested_size + 0xFFF) & !0xFFF;
if aligned_size == 0 {
ctx.gpr[3] = 0xC000_0010; // STATUS_INVALID_PARAMETER
return;
}
let base = if requested_base != 0 {
// Try to allocate at the requested address
let protect = xenia_memory::page_table::MemoryProtect::READ
| xenia_memory::page_table::MemoryProtect::WRITE;
if mem.alloc(requested_base, aligned_size, protect).is_ok() {
requested_base
} else {
// Already allocated? Treat as success (common for re-commit)
requested_base
}
} else {
// Allocate from heap
match state.heap_alloc(aligned_size, mem) {
Some(addr) => addr,
None => {
tracing::warn!("NtAllocateVirtualMemory: heap exhausted (size={:#x})", aligned_size);
ctx.gpr[3] = 0xC000_0017; // STATUS_NO_MEMORY
return;
}
}
};
mem.write_u32(base_ptr, base);
mem.write_u32(size_ptr, aligned_size);
tracing::info!("NtAllocateVirtualMemory: base={:#010x} size={:#x}", base, aligned_size);
ctx.gpr[3] = 0; // STATUS_SUCCESS
}
fn mm_allocate_physical_memory_ex(ctx: &mut PpcContext, mem: &mut GuestMemory, state: &mut KernelState) {
// r3 = size, r4 = protect, r5 = min_addr, r6 = max_addr, r7 = alignment
let size = ctx.gpr[3] as u32;
match state.heap_alloc(size, mem) {
Some(addr) => ctx.gpr[3] = addr as u64,
None => ctx.gpr[3] = 0,
}
}
fn mm_create_kernel_stack(ctx: &mut PpcContext, mem: &mut GuestMemory, state: &mut KernelState) {
// r3 = stack_size, r4 = reserved
let size = std::cmp::max(ctx.gpr[3] as u32, 0x4000); // Min 16KB
match state.stack_alloc(size, mem) {
Some(top) => {
tracing::info!("MmCreateKernelStack: top={:#010x} size={:#x}", top, size);
ctx.gpr[3] = top as u64;
}
None => ctx.gpr[3] = 0,
}
}
fn mm_get_physical_address(ctx: &mut PpcContext, _mem: &mut GuestMemory, _state: &mut KernelState) {
// r3 = virtual address -> return physical address
ctx.gpr[3] = ctx.gpr[3] & 0x1FFF_FFFF; // Mask to 512MB physical
}
fn mm_query_address_protect(ctx: &mut PpcContext, _mem: &mut GuestMemory, _state: &mut KernelState) {
// Return PAGE_READWRITE (0x04)
ctx.gpr[3] = 0x04;
}
fn mm_query_statistics(ctx: &mut PpcContext, mem: &mut GuestMemory, _state: &mut KernelState) {
// r3 = stats_ptr — write fake memory statistics
let ptr = ctx.gpr[3] as u32;
if ptr != 0 {
// Total physical = 512MB
mem.write_u32(ptr + 0x04, 512 * 1024 * 1024); // TotalPhysicalPages (in bytes)
mem.write_u32(ptr + 0x10, 256 * 1024 * 1024); // AvailablePages
}
ctx.gpr[3] = 0;
}
// ===== File I/O =====
fn nt_create_file(ctx: &mut PpcContext, _mem: &mut GuestMemory, state: &mut KernelState) {
let handle = state.alloc_handle_for(KernelObject::File { path: String::new() });
tracing::info!("NtCreateFile: handle={:#x}", handle);
ctx.gpr[3] = 0;
}
fn nt_open_file(ctx: &mut PpcContext, _mem: &mut GuestMemory, state: &mut KernelState) {
let handle = state.alloc_handle_for(KernelObject::File { path: String::new() });
tracing::info!("NtOpenFile: handle={:#x}", handle);
ctx.gpr[3] = 0;
}
fn nt_read_file(ctx: &mut PpcContext, _mem: &mut GuestMemory, _state: &mut KernelState) {
ctx.gpr[3] = 0xC000_0011; // STATUS_END_OF_FILE
}
fn nt_write_file(ctx: &mut PpcContext, _mem: &mut GuestMemory, _state: &mut KernelState) {
ctx.gpr[3] = 0; // STATUS_SUCCESS (discard data)
}
fn nt_query_full_attributes_file(ctx: &mut PpcContext, _mem: &mut GuestMemory, _state: &mut KernelState) {
ctx.gpr[3] = 0xC000_0034; // STATUS_OBJECT_NAME_NOT_FOUND
}
fn nt_query_directory_file(ctx: &mut PpcContext, _mem: &mut GuestMemory, _state: &mut KernelState) {
ctx.gpr[3] = 0xC000_0034; // STATUS_OBJECT_NAME_NOT_FOUND
}
fn nt_close(ctx: &mut PpcContext, _mem: &mut GuestMemory, state: &mut KernelState) {
let handle = ctx.gpr[3] as u32;
state.objects.remove(&handle);
ctx.gpr[3] = 0;
}
fn nt_create_event(ctx: &mut PpcContext, mem: &mut GuestMemory, state: &mut KernelState) {
// r3 = handle_ptr, r4 = obj_attrs, r5 = event_type, r6 = initial_state
let handle_ptr = ctx.gpr[3] as u32;
let manual_reset = ctx.gpr[5] != 0;
let signaled = ctx.gpr[6] != 0;
let handle = state.alloc_handle_for(KernelObject::Event { manual_reset, signaled });
if handle_ptr != 0 {
mem.write_u32(handle_ptr, handle);
}
ctx.gpr[3] = 0;
}
fn nt_create_semaphore(ctx: &mut PpcContext, mem: &mut GuestMemory, state: &mut KernelState) {
// r3 = handle_ptr, r4 = obj_attrs, r5 = initial_count, r6 = max_count
let handle_ptr = ctx.gpr[3] as u32;
let count = ctx.gpr[5] as i32;
let max = ctx.gpr[6] as i32;
let handle = state.alloc_handle_for(KernelObject::Semaphore { count, max });
if handle_ptr != 0 {
mem.write_u32(handle_ptr, handle);
}
ctx.gpr[3] = 0;
}
fn nt_create_timer(ctx: &mut PpcContext, mem: &mut GuestMemory, state: &mut KernelState) {
let handle_ptr = ctx.gpr[3] as u32;
let handle = state.alloc_handle_for(KernelObject::Timer);
if handle_ptr != 0 {
mem.write_u32(handle_ptr, handle);
}
ctx.gpr[3] = 0;
}
// ===== RTL =====
fn rtl_initialize_critical_section(ctx: &mut PpcContext, mem: &mut GuestMemory, _state: &mut KernelState) {
// r3 = critical_section_ptr (28 bytes on Xbox 360)
let cs_ptr = ctx.gpr[3] as u32;
if cs_ptr != 0 {
for i in (0..28).step_by(4) {
mem.write_u32(cs_ptr + i, 0);
}
// Set recursion count to -1 (unlocked)
mem.write_u32(cs_ptr + 8, 0xFFFF_FFFF_u32);
}
ctx.gpr[3] = 0;
}
fn rtl_enter_critical_section(ctx: &mut PpcContext, mem: &mut GuestMemory, _state: &mut KernelState) {
// r3 = critical_section_ptr
// For single-threaded: increment lock count, always succeed
let cs_ptr = ctx.gpr[3] as u32;
if cs_ptr != 0 {
let lock_count = mem.read_u32(cs_ptr + 4) as i32;
mem.write_u32(cs_ptr + 4, (lock_count + 1) as u32);
let recursion = mem.read_u32(cs_ptr + 8) as i32;
mem.write_u32(cs_ptr + 8, (recursion + 1) as u32);
}
ctx.gpr[3] = 0;
}
fn rtl_leave_critical_section(ctx: &mut PpcContext, mem: &mut GuestMemory, _state: &mut KernelState) {
let cs_ptr = ctx.gpr[3] as u32;
if cs_ptr != 0 {
let lock_count = mem.read_u32(cs_ptr + 4) as i32;
mem.write_u32(cs_ptr + 4, (lock_count - 1) as u32);
let recursion = mem.read_u32(cs_ptr + 8) as i32;
mem.write_u32(cs_ptr + 8, (recursion - 1) as u32);
}
ctx.gpr[3] = 0;
}
fn rtl_try_enter_critical_section(ctx: &mut PpcContext, mem: &mut GuestMemory, _state: &mut KernelState) {
// Always succeed in single-threaded mode
let cs_ptr = ctx.gpr[3] as u32;
if cs_ptr != 0 {
let lock_count = mem.read_u32(cs_ptr + 4) as i32;
mem.write_u32(cs_ptr + 4, (lock_count + 1) as u32);
let recursion = mem.read_u32(cs_ptr + 8) as i32;
mem.write_u32(cs_ptr + 8, (recursion + 1) as u32);
}
ctx.gpr[3] = 1; // TRUE
}
fn rtl_init_ansi_string(ctx: &mut PpcContext, mem: &mut GuestMemory, _state: &mut KernelState) {
let dest_ptr = ctx.gpr[3] as u32;
let src_ptr = ctx.gpr[4] as u32;
if src_ptr != 0 {
let mut len: u16 = 0;
let mut addr = src_ptr;
while mem.read_u8(addr) != 0 {
len += 1;
addr += 1;
}
mem.write_u16(dest_ptr, len);
mem.write_u16(dest_ptr + 2, len + 1);
mem.write_u32(dest_ptr + 4, src_ptr);
} else {
mem.write_u16(dest_ptr, 0);
mem.write_u16(dest_ptr + 2, 0);
mem.write_u32(dest_ptr + 4, 0);
}
}
fn rtl_init_unicode_string(ctx: &mut PpcContext, mem: &mut GuestMemory, _state: &mut KernelState) {
let dest_ptr = ctx.gpr[3] as u32;
let src_ptr = ctx.gpr[4] as u32;
if src_ptr != 0 {
let mut len: u16 = 0;
let mut addr = src_ptr;
while mem.read_u16(addr) != 0 {
len += 2;
addr += 2;
}
mem.write_u16(dest_ptr, len);
mem.write_u16(dest_ptr + 2, len + 2);
mem.write_u32(dest_ptr + 4, src_ptr);
} else {
mem.write_u16(dest_ptr, 0);
mem.write_u16(dest_ptr + 2, 0);
mem.write_u32(dest_ptr + 4, 0);
}
}
fn rtl_capture_context(ctx: &mut PpcContext, mem: &mut GuestMemory, _state: &mut KernelState) {
// r3 = context_ptr — write CPU registers to CONTEXT structure
let ptr = ctx.gpr[3] as u32;
if ptr != 0 {
// Write GPRs at offset 0 (simplified)
for i in 0..32 {
mem.write_u64(ptr + (i * 8) as u32, ctx.gpr[i]);
}
}
}
fn rtl_compare_memory_ulong(ctx: &mut PpcContext, mem: &mut GuestMemory, _state: &mut KernelState) {
// r3 = source, r4 = length, r5 = pattern
let source = ctx.gpr[3] as u32;
let length = ctx.gpr[4] as u32;
let pattern = ctx.gpr[5] as u32;
let mut matched: u32 = 0;
let count = length / 4;
for i in 0..count {
let val = mem.read_u32(source + i * 4);
if val != pattern {
break;
}
matched += 4;
}
ctx.gpr[3] = matched as u64;
}
fn rtl_fill_memory_ulong(ctx: &mut PpcContext, mem: &mut GuestMemory, _state: &mut KernelState) {
// r3 = destination, r4 = length, r5 = pattern
let dest = ctx.gpr[3] as u32;
let length = ctx.gpr[4] as u32;
let pattern = ctx.gpr[5] as u32;
let count = length / 4;
for i in 0..count {
mem.write_u32(dest + i * 4, pattern);
}
}
fn rtl_image_xex_header_field(ctx: &mut PpcContext, _mem: &mut GuestMemory, _state: &mut KernelState) {
// r3 = xex_header_ptr, r4 = field_id
// Return 0 for all fields
ctx.gpr[3] = 0;
}
fn rtl_multi_byte_to_unicode_n(ctx: &mut PpcContext, mem: &mut GuestMemory, _state: &mut KernelState) {
// r3 = unicode_str, r4 = max_bytes_out, r5 = bytes_written_ptr
// r6 = multi_byte_str, r7 = multi_byte_len
let uni_ptr = ctx.gpr[3] as u32;
let max_bytes = ctx.gpr[4] as u32;
let written_ptr = ctx.gpr[5] as u32;
let mb_ptr = ctx.gpr[6] as u32;
let mb_len = ctx.gpr[7] as u32;
let max_chars = max_bytes / 2;
let count = std::cmp::min(mb_len, max_chars);
for i in 0..count {
let byte = mem.read_u8(mb_ptr + i);
mem.write_u16(uni_ptr + i * 2, byte as u16);
}
if written_ptr != 0 {
mem.write_u32(written_ptr, count * 2);
}
ctx.gpr[3] = 0;
}
fn rtl_nt_status_to_dos_error(ctx: &mut PpcContext, _mem: &mut GuestMemory, _state: &mut KernelState) {
// Simple mapping for common cases
let status = ctx.gpr[3] as u32;
ctx.gpr[3] = match status {
0 => 0, // ERROR_SUCCESS
0xC000_0034 => 2, // ERROR_FILE_NOT_FOUND
0xC000_0011 => 38, // ERROR_HANDLE_EOF
_ => status as u64, // Pass through
};
}
fn rtl_raise_exception(ctx: &mut PpcContext, _mem: &mut GuestMemory, _state: &mut KernelState) {
tracing::warn!("RtlRaiseException: record_ptr={:#010x}", ctx.gpr[3]);
// Don't halt — just log and return
}
fn rtl_unwind(ctx: &mut PpcContext, _mem: &mut GuestMemory, _state: &mut KernelState) {
tracing::warn!("RtlUnwind: target_frame={:#010x}", ctx.gpr[3]);
// Stub — in a real implementation this would walk the stack
}
fn stub_sprintf(ctx: &mut PpcContext, mem: &mut GuestMemory, _state: &mut KernelState) {
let dest = ctx.gpr[3] as u32;
let fmt = ctx.gpr[4] as u32;
if fmt != 0 && dest != 0 {
let mut addr = fmt;
let mut daddr = dest;
loop {
let c = mem.read_u8(addr);
mem.write_u8(daddr, c);
if c == 0 { break; }
addr += 1;
daddr += 1;
}
}
ctx.gpr[3] = 0;
}
fn stub_vsnprintf(ctx: &mut PpcContext, mem: &mut GuestMemory, _state: &mut KernelState) {
// r3 = buffer, r4 = count, r5 = format, r6 = va_list
let dest = ctx.gpr[3] as u32;
let fmt = ctx.gpr[5] as u32;
if fmt != 0 && dest != 0 {
let mut addr = fmt;
let mut daddr = dest;
loop {
let c = mem.read_u8(addr);
mem.write_u8(daddr, c);
if c == 0 { break; }
addr += 1;
daddr += 1;
}
}
ctx.gpr[3] = 0;
}
// ===== Video =====
fn vd_query_video_mode(ctx: &mut PpcContext, mem: &mut GuestMemory, _state: &mut KernelState) {
let mode_ptr = ctx.gpr[3] as u32;
if mode_ptr != 0 {
mem.write_u32(mode_ptr, 1280);
mem.write_u32(mode_ptr + 4, 720);
mem.write_u32(mode_ptr + 8, 0); // is_interlaced
mem.write_u32(mode_ptr + 12, 1); // is_widescreen
mem.write_u32(mode_ptr + 16, 60); // refresh_rate
}
ctx.gpr[3] = 0;
}
fn vd_get_system_command_buffer(ctx: &mut PpcContext, mem: &mut GuestMemory, state: &mut KernelState) {
// r3 = cmd_buffer_ptr_ptr, r4 = cmd_buffer_size_ptr
let buf_ptr_ptr = ctx.gpr[3] as u32;
let buf_size_ptr = ctx.gpr[4] as u32;
if state.gpu_command_buffer == 0 {
// Allocate a 64KB command buffer
if let Some(addr) = state.heap_alloc(0x10000, mem) {
state.gpu_command_buffer = addr;
}
}
if buf_ptr_ptr != 0 {
mem.write_u32(buf_ptr_ptr, state.gpu_command_buffer);
}
if buf_size_ptr != 0 {
mem.write_u32(buf_size_ptr, 0x10000);
}
ctx.gpr[3] = 0;
}
fn vd_is_hsio_training_succeeded(ctx: &mut PpcContext, _mem: &mut GuestMemory, _state: &mut KernelState) {
ctx.gpr[3] = 1; // TRUE
}
fn vd_swap(ctx: &mut PpcContext, _mem: &mut GuestMemory, _state: &mut KernelState) {
tracing::info!("VdSwap (frame boundary)");
ctx.gpr[3] = 0;
}
// ===== Audio =====
fn xaudio_register_render_driver(ctx: &mut PpcContext, _mem: &mut GuestMemory, state: &mut KernelState) {
let handle = state.alloc_handle();
tracing::info!("XAudioRegisterRenderDriverClient: handle={:#x}", handle);
// r3 = callback_ptr, r4 = driver_ptr -> write handle
ctx.gpr[3] = 0;
}
fn xma_create_context(ctx: &mut PpcContext, _mem: &mut GuestMemory, state: &mut KernelState) {
let handle = state.alloc_handle();
tracing::info!("XMACreateContext: handle={:#x}", handle);
ctx.gpr[3] = handle as u64;
}
// ===== Xex =====
fn xex_get_procedure_address(ctx: &mut PpcContext, _mem: &mut GuestMemory, _state: &mut KernelState) {
let ordinal = ctx.gpr[4] as u32;
tracing::warn!("XexGetProcedureAddress: ordinal={:#x} not found", ordinal);
ctx.gpr[3] = 0xC000_0034; // STATUS_OBJECT_NAME_NOT_FOUND
}
// ===== Exception handling =====
fn c_specific_handler(ctx: &mut PpcContext, _mem: &mut GuestMemory, _state: &mut KernelState) {
tracing::warn!("__C_specific_handler called (exception handling stub)");
ctx.gpr[3] = 1; // ExceptionContinueSearch
}
// ===== Helpers =====
fn read_cstring(mem: &GuestMemory, addr: u32) -> String {
let mut s = String::new();
let mut a = addr;
loop {
let c = mem.read_u8(a);
if c == 0 { break; }
s.push(c as char);
a += 1;
if s.len() > 512 { break; } // Safety limit
}
s
}

View File

@@ -0,0 +1,6 @@
pub mod exports;
pub mod objects;
pub mod state;
pub mod xam;
pub use state::{KernelState, ModuleId};

View File

@@ -0,0 +1,12 @@
//! Kernel object tracking for HLE.
/// Kernel object types tracked by handle.
#[derive(Debug)]
pub enum KernelObject {
Event { manual_reset: bool, signaled: bool },
Semaphore { count: i32, max: i32 },
File { path: String },
Thread { id: u32 },
Timer,
Mutex,
}

View File

@@ -0,0 +1,159 @@
use std::collections::HashMap;
use xenia_cpu::PpcContext;
use xenia_memory::GuestMemory;
use crate::objects::KernelObject;
/// Function signature for HLE kernel exports.
pub type KernelExportFn = fn(&mut PpcContext, &mut GuestMemory, &mut KernelState);
/// Module identifier for kernel exports.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ModuleId {
Xboxkrnl,
Xam,
Xbdm,
}
/// Central kernel state tracking all guest OS state.
pub struct KernelState {
exports: HashMap<(ModuleId, u32), (&'static str, KernelExportFn)>,
next_handle: u32,
pub tls_slots: HashMap<u32, u64>,
next_tls_index: u32,
/// Kernel object table: handle → object
pub objects: HashMap<u32, KernelObject>,
/// Bump allocator for guest heap (NtAllocateVirtualMemory etc.)
pub heap_cursor: u32,
/// Stack allocator cursor for MmCreateKernelStack
pub stack_cursor: u32,
/// GPU command buffer address (set by VdGetSystemCommandBuffer)
pub gpu_command_buffer: u32,
/// Image base of the loaded XEX (for XexExecutableModuleHandle etc.)
pub image_base: u32,
/// Next thread ID
pub next_thread_id: u32,
}
impl KernelState {
pub fn new() -> Self {
let mut state = Self {
exports: HashMap::new(),
next_handle: 0x1000,
tls_slots: HashMap::new(),
next_tls_index: 0,
objects: HashMap::new(),
heap_cursor: 0x4000_0000, // Start of user heap region
stack_cursor: 0x7100_0000, // Above main stack
gpu_command_buffer: 0,
image_base: 0,
next_thread_id: 1,
};
crate::exports::register_exports(&mut state);
crate::xam::register_exports(&mut state);
state
}
pub fn register_export(
&mut self,
module: ModuleId,
ordinal: u32,
name: &'static str,
func: KernelExportFn,
) {
self.exports.insert((module, ordinal), (name, func));
}
pub fn call_export(
&mut self,
module: ModuleId,
ordinal: u32,
ctx: &mut PpcContext,
mem: &mut GuestMemory,
) -> bool {
if let Some(&(name, func)) = self.exports.get(&(module, ordinal)) {
tracing::info!(
"Kernel call: {:?}:{:#x} ({}) args=[{:#x}, {:#x}, {:#x}, {:#x}]",
module, ordinal, name,
ctx.gpr[3], ctx.gpr[4], ctx.gpr[5], ctx.gpr[6]
);
func(ctx, mem, self);
tracing::info!(" -> returned {:#x}", ctx.gpr[3]);
true
} else {
tracing::warn!(
"Unimplemented kernel export: {:?}:{:#x}",
module, ordinal
);
// Return 0 (STATUS_SUCCESS) by default for unimplemented calls
ctx.gpr[3] = 0;
false
}
}
pub fn export_name(&self, module: ModuleId, ordinal: u32) -> Option<&'static str> {
self.exports.get(&(module, ordinal)).map(|&(name, _)| name)
}
pub fn alloc_handle(&mut self) -> u32 {
let h = self.next_handle;
self.next_handle += 4;
h
}
pub fn alloc_handle_for(&mut self, obj: KernelObject) -> u32 {
let h = self.alloc_handle();
self.objects.insert(h, obj);
h
}
pub fn tls_get(&self, index: u32) -> u64 {
self.tls_slots.get(&index).copied().unwrap_or(0)
}
pub fn tls_set(&mut self, index: u32, value: u64) {
self.tls_slots.insert(index, value);
}
pub fn tls_alloc(&mut self) -> u32 {
let idx = self.next_tls_index;
self.next_tls_index += 1;
idx
}
/// Allocate guest memory from the heap bump allocator.
/// Returns the base address of the allocated region.
pub fn heap_alloc(&mut self, size: u32, mem: &mut GuestMemory) -> Option<u32> {
let aligned_size = (size + 0xFFF) & !0xFFF; // Page-align
let base = self.heap_cursor;
if base.checked_add(aligned_size).is_none() || base + aligned_size > 0x6FFF_FFFF {
return None;
}
let protect = xenia_memory::page_table::MemoryProtect::READ
| xenia_memory::page_table::MemoryProtect::WRITE;
if mem.alloc(base, aligned_size, protect).is_err() {
return None;
}
self.heap_cursor += aligned_size;
Some(base)
}
/// Allocate a kernel stack.
pub fn stack_alloc(&mut self, size: u32, mem: &mut GuestMemory) -> Option<u32> {
let aligned_size = (size + 0xFFF) & !0xFFF;
let base = self.stack_cursor;
let protect = xenia_memory::page_table::MemoryProtect::READ
| xenia_memory::page_table::MemoryProtect::WRITE;
if mem.alloc(base, aligned_size, protect).is_err() {
return None;
}
self.stack_cursor += aligned_size;
Some(base + aligned_size) // Return top of stack
}
}
impl Default for KernelState {
fn default() -> Self {
Self::new()
}
}

View File

@@ -0,0 +1,253 @@
//! HLE kernel export implementations (xam.xex).
use crate::state::{KernelState, ModuleId};
use xenia_cpu::PpcContext;
use xenia_memory::{GuestMemory, MemoryAccess};
pub fn register_exports(state: &mut KernelState) {
use ModuleId::Xam;
// Net
state.register_export(Xam, 0x01, "NetDll_WSAStartup", stub_success);
state.register_export(Xam, 0x02, "NetDll_WSACleanup", stub_success);
// Input
state.register_export(Xam, 0x0190, "XamInputGetCapabilities", xam_input_not_connected);
state.register_export(Xam, 0x0191, "XamInputGetState", xam_input_not_connected);
state.register_export(Xam, 0x0192, "XamInputSetState", xam_input_not_connected);
state.register_export(Xam, 0x0198, "XamInputGetKeystrokeEx", xam_input_not_connected);
// Inactivity
state.register_export(Xam, 0x01A0, "XamEnableInactivityProcessing", stub_success);
state.register_export(Xam, 0x01A1, "XamResetInactivity", stub_success);
// Loader
state.register_export(Xam, 0x01A4, "XamLoaderLaunchTitle", xam_loader_launch_title);
state.register_export(Xam, 0x01A9, "XamLoaderTerminateTitle", xam_loader_terminate_title);
// Task
state.register_export(Xam, 0x01AF, "XamTaskSchedule", xam_task_schedule);
state.register_export(Xam, 0x01B1, "XamTaskCloseHandle", stub_success);
state.register_export(Xam, 0x01B3, "XamTaskShouldExit", stub_return_zero);
// Alloc
state.register_export(Xam, 0x01EA, "XamAlloc", xam_alloc);
state.register_export(Xam, 0x01EC, "XamFree", stub_success);
// Msg
state.register_export(Xam, 0x01F4, "XMsgInProcessCall", stub_success);
state.register_export(Xam, 0x01F7, "XMsgStartIORequest", stub_success);
state.register_export(Xam, 0x01FC, "XMsgStartIORequestEx", stub_success);
// User
state.register_export(Xam, 0x020A, "XamUserGetXUID", xam_user_get_xuid);
state.register_export(Xam, 0x020E, "XamUserGetName", xam_user_get_name);
state.register_export(Xam, 0x0210, "XamUserGetSigninState", stub_return_zero);
state.register_export(Xam, 0x0219, "XamUserReadProfileSettings", xam_user_read_profile_settings);
state.register_export(Xam, 0x021A, "XamUserWriteProfileSettings", stub_success);
// Enum
state.register_export(Xam, 0x0250, "XamEnumerate", stub_error_no_more_files);
// Content
state.register_export(Xam, 0x0258, "XamContentCreate", stub_success);
state.register_export(Xam, 0x025A, "XamContentClose", stub_success);
state.register_export(Xam, 0x025B, "XamContentDelete", stub_success);
state.register_export(Xam, 0x025C, "XamContentCreateEnumerator", stub_success);
state.register_export(Xam, 0x025E, "XamContentGetDeviceData", stub_success);
state.register_export(Xam, 0x025F, "XamContentGetDeviceName", stub_success);
state.register_export(Xam, 0x0260, "XamContentSetThumbnail", stub_success);
state.register_export(Xam, 0x0262, "XamContentGetCreator", stub_success);
state.register_export(Xam, 0x0265, "XamContentGetDeviceState", stub_success);
// System
state.register_export(Xam, 0x0280, "XamGetExecutionId", xam_get_execution_id);
state.register_export(Xam, 0x0282, "XamGetSystemVersion", xam_get_system_version);
// Notify
state.register_export(Xam, 0x028A, "XamNotifyCreateListener", xam_notify_create_listener);
state.register_export(Xam, 0x028B, "XNotifyGetNext", xnotify_get_next);
state.register_export(Xam, 0x028C, "XNotifyPositionUI", stub_success);
// Achievements/Stats
state.register_export(Xam, 0x02EE, "XamUserCreateAchievementEnumerator", stub_success);
state.register_export(Xam, 0x02F7, "XamUserCreateStatsEnumerator", stub_success);
// UI
state.register_export(Xam, 0x02BC, "XamShowSigninUI", stub_success);
state.register_export(Xam, 0x02C1, "XamShowKeyboardUI", stub_success);
state.register_export(Xam, 0x02CB, "XamShowDeviceSelectorUI", stub_success);
state.register_export(Xam, 0x02D5, "XamShowGamerCardUIForXUID", stub_success);
state.register_export(Xam, 0x02D9, "XamShowDirtyDiscErrorUI", stub_success);
state.register_export(Xam, 0x02DC, "XamShowMessageBoxUIEx", stub_success);
// Session
state.register_export(Xam, 0x0316, "XamSessionCreateHandle", xam_session_create_handle);
state.register_export(Xam, 0x0317, "XamSessionRefObjByHandle", stub_success);
// Locale
state.register_export(Xam, 0x03CB, "XGetAVPack", xget_avpack);
state.register_export(Xam, 0x03CC, "XGetGameRegion", xget_game_region);
state.register_export(Xam, 0x03CD, "XGetLanguage", xget_language);
state.register_export(Xam, 0x03D1, "XGetVideoMode", xget_video_mode);
}
// ===== Generic stubs =====
fn stub_success(ctx: &mut PpcContext, _mem: &mut GuestMemory, _state: &mut KernelState) {
ctx.gpr[3] = 0;
}
fn stub_return_zero(ctx: &mut PpcContext, _mem: &mut GuestMemory, _state: &mut KernelState) {
ctx.gpr[3] = 0;
}
fn stub_error_no_more_files(ctx: &mut PpcContext, _mem: &mut GuestMemory, _state: &mut KernelState) {
ctx.gpr[3] = 0x12; // ERROR_NO_MORE_FILES
}
// ===== Input =====
fn xam_input_not_connected(ctx: &mut PpcContext, _mem: &mut GuestMemory, _state: &mut KernelState) {
ctx.gpr[3] = 0x48F; // ERROR_DEVICE_NOT_CONNECTED
}
// ===== Loader =====
fn xam_loader_launch_title(ctx: &mut PpcContext, _mem: &mut GuestMemory, _state: &mut KernelState) {
tracing::warn!("XamLoaderLaunchTitle called");
ctx.gpr[3] = 0;
}
fn xam_loader_terminate_title(ctx: &mut PpcContext, _mem: &mut GuestMemory, _state: &mut KernelState) {
tracing::warn!("XamLoaderTerminateTitle called");
ctx.gpr[3] = 0;
}
// ===== Task =====
fn xam_task_schedule(ctx: &mut PpcContext, _mem: &mut GuestMemory, state: &mut KernelState) {
let handle = state.alloc_handle();
tracing::info!("XamTaskSchedule: handle={:#x}", handle);
ctx.gpr[3] = 0;
}
// ===== Alloc =====
fn xam_alloc(ctx: &mut PpcContext, mem: &mut GuestMemory, state: &mut KernelState) {
// r3 = flags, r4 = size, r5 = out_ptr_ptr
let size = ctx.gpr[4] as u32;
let out_ptr = ctx.gpr[5] as u32;
match state.heap_alloc(size, mem) {
Some(addr) => {
if out_ptr != 0 {
mem.write_u32(out_ptr, addr);
}
ctx.gpr[3] = 0; // SUCCESS
}
None => {
ctx.gpr[3] = 0x8007_000E; // E_OUTOFMEMORY
}
}
}
// ===== User =====
fn xam_user_get_xuid(ctx: &mut PpcContext, mem: &mut GuestMemory, _state: &mut KernelState) {
// r3 = user_index, r4 = xuid_ptr
let xuid_ptr = ctx.gpr[4] as u32;
if xuid_ptr != 0 {
mem.write_u64(xuid_ptr, 0); // No XUID
}
ctx.gpr[3] = 0;
}
fn xam_user_get_name(ctx: &mut PpcContext, mem: &mut GuestMemory, _state: &mut KernelState) {
// r3 = user_index, r4 = buffer, r5 = buffer_size
let buffer = ctx.gpr[4] as u32;
if buffer != 0 {
mem.write_u8(buffer, 0); // Empty string
}
ctx.gpr[3] = 0;
}
fn xam_user_read_profile_settings(ctx: &mut PpcContext, _mem: &mut GuestMemory, _state: &mut KernelState) {
// Return error — no profile
ctx.gpr[3] = 0x0000_048B; // ERROR_NOT_FOUND
}
// ===== System =====
fn xam_get_execution_id(ctx: &mut PpcContext, mem: &mut GuestMemory, state: &mut KernelState) {
// r3 = execution_id_ptr_ptr — write pointer to execution info
let ptr_ptr = ctx.gpr[3] as u32;
if ptr_ptr != 0 {
// Allocate and fill a fake XEX_EXECUTION_ID structure
if let Some(exec_id_addr) = state.heap_alloc(0x18, mem) {
mem.write_u32(exec_id_addr, 0x535107D4); // title_id (Project Sylpheed)
mem.write_u32(exec_id_addr + 4, 0x2D2E2EEB); // media_id
mem.write_u16(exec_id_addr + 8, 0); // version
mem.write_u16(exec_id_addr + 10, 0); // base_version
mem.write_u16(exec_id_addr + 12, 1); // disc_number
mem.write_u16(exec_id_addr + 14, 1); // disc_count
mem.write_u32(ptr_ptr, exec_id_addr);
}
}
ctx.gpr[3] = 0;
}
fn xam_get_system_version(ctx: &mut PpcContext, _mem: &mut GuestMemory, _state: &mut KernelState) {
ctx.gpr[3] = 0x2000_0000; // System version
}
// ===== Notify =====
fn xam_notify_create_listener(ctx: &mut PpcContext, _mem: &mut GuestMemory, state: &mut KernelState) {
let handle = state.alloc_handle();
ctx.gpr[3] = handle as u64;
}
fn xnotify_get_next(ctx: &mut PpcContext, _mem: &mut GuestMemory, _state: &mut KernelState) {
// r3 = handle, r4 = id_ptr, r5 = param_ptr
ctx.gpr[3] = 0; // FALSE (no notifications)
}
// ===== Session =====
fn xam_session_create_handle(ctx: &mut PpcContext, mem: &mut GuestMemory, state: &mut KernelState) {
// r3 = handle_ptr
let handle_ptr = ctx.gpr[3] as u32;
let handle = state.alloc_handle();
if handle_ptr != 0 {
mem.write_u32(handle_ptr, handle);
}
ctx.gpr[3] = 0;
}
// ===== Locale =====
fn xget_avpack(ctx: &mut PpcContext, _mem: &mut GuestMemory, _state: &mut KernelState) {
ctx.gpr[3] = 0x16; // HDMI
}
fn xget_game_region(ctx: &mut PpcContext, _mem: &mut GuestMemory, _state: &mut KernelState) {
ctx.gpr[3] = 0xFF; // All regions
}
fn xget_language(ctx: &mut PpcContext, _mem: &mut GuestMemory, _state: &mut KernelState) {
ctx.gpr[3] = 1; // English
}
fn xget_video_mode(ctx: &mut PpcContext, mem: &mut GuestMemory, _state: &mut KernelState) {
// r3 = video_mode_ptr
let ptr = ctx.gpr[3] as u32;
if ptr != 0 {
mem.write_u32(ptr, 1280); // width
mem.write_u32(ptr + 4, 720); // height
mem.write_u32(ptr + 8, 0); // is_interlaced
mem.write_u32(ptr + 12, 1); // is_widescreen
mem.write_u32(ptr + 16, 60); // refresh_rate
}
ctx.gpr[3] = 0;
}

View File

@@ -0,0 +1,17 @@
[package]
name = "xenia-memory"
version.workspace = true
edition.workspace = true
license.workspace = true
[dependencies]
xenia-types = { workspace = true }
tracing = { workspace = true }
bitflags = { workspace = true }
thiserror = { workspace = true }
[target.'cfg(unix)'.dependencies]
libc = "0.2"
[target.'cfg(windows)'.dependencies]
windows-sys = { version = "0.59", features = ["Win32_System_Memory", "Win32_Foundation"] }

View File

@@ -0,0 +1,47 @@
/// Trait for all guest memory access. Every load/store goes through this,
/// enabling MMIO checking and debugger observation on every access.
/// This is the key abstraction that eliminates the need for MMIO exception handlers.
pub trait MemoryAccess {
fn read_u8(&self, addr: u32) -> u8;
fn read_u16(&self, addr: u32) -> u16;
fn read_u32(&self, addr: u32) -> u32;
fn read_u64(&self, addr: u32) -> u64;
fn read_f32(&self, addr: u32) -> f32 {
f32::from_bits(self.read_u32(addr))
}
fn read_f64(&self, addr: u32) -> f64 {
f64::from_bits(self.read_u64(addr))
}
fn write_u8(&mut self, addr: u32, val: u8);
fn write_u16(&mut self, addr: u32, val: u16);
fn write_u32(&mut self, addr: u32, val: u32);
fn write_u64(&mut self, addr: u32, val: u64);
fn write_f32(&mut self, addr: u32, val: f32) {
self.write_u32(addr, val.to_bits());
}
fn write_f64(&mut self, addr: u32, val: f64) {
self.write_u64(addr, val.to_bits());
}
/// Read a block of bytes from guest memory.
fn read_bytes(&self, addr: u32, buf: &mut [u8]) {
for (i, byte) in buf.iter_mut().enumerate() {
*byte = self.read_u8(addr.wrapping_add(i as u32));
}
}
/// Write a block of bytes to guest memory.
fn write_bytes(&mut self, addr: u32, buf: &[u8]) {
for (i, &byte) in buf.iter().enumerate() {
self.write_u8(addr.wrapping_add(i as u32), byte);
}
}
/// Get a direct host pointer for the given guest address.
/// Returns None if the address is invalid or in an MMIO region.
fn translate(&self, addr: u32) -> Option<*const u8>;
/// Get a mutable direct host pointer for the given guest address.
fn translate_mut(&mut self, addr: u32) -> Option<*mut u8>;
}

View File

@@ -0,0 +1,265 @@
use crate::access::MemoryAccess;
use crate::mmio::MmioRegion;
use crate::page_table::{AllocationState, MemoryProtect, PageEntry};
use crate::MemoryError;
const PAGE_SIZE: u32 = 4096;
/// Total guest address space: 4GB.
const GUEST_ADDRESS_SPACE: usize = 0x1_0000_0000;
/// Number of 4K pages in the 4GB address space.
const PAGE_COUNT: usize = GUEST_ADDRESS_SPACE / PAGE_SIZE as usize;
/// Physical memory mask (512MB physical address space).
const PHYSICAL_ADDR_MASK: u32 = 0x1FFF_FFFF;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum HeapType {
GuestVirtual,
GuestXex,
GuestPhysical,
}
/// The core guest memory system. Manages a 4GB virtual address space
/// via mmap/VirtualAlloc, with page-level tracking and MMIO dispatch.
pub struct GuestMemory {
/// Host pointer to the base of the 4GB guest address space.
membase: *mut u8,
/// Page table tracking allocation state for each 4K page.
page_table: Vec<PageEntry>,
/// Registered MMIO regions (sorted by base address for binary search).
mmio_regions: Vec<MmioRegion>,
/// Whether the memory mapping is owned (should be unmapped on drop).
owned: bool,
}
unsafe impl Send for GuestMemory {}
unsafe impl Sync for GuestMemory {}
impl GuestMemory {
/// Create a new guest memory space by reserving a 4GB virtual address region.
pub fn new() -> Result<Self, MemoryError> {
let membase = crate::platform::reserve_address_space(GUEST_ADDRESS_SPACE)?;
Ok(Self {
membase,
page_table: vec![PageEntry::default(); PAGE_COUNT],
mmio_regions: Vec::new(),
owned: true,
})
}
/// Get the host base pointer for the guest address space.
pub fn membase(&self) -> *const u8 {
self.membase
}
/// Get a mutable host base pointer.
pub fn membase_mut(&mut self) -> *mut u8 {
self.membase
}
/// Translate a guest virtual address to a host pointer.
pub fn translate_virtual(&self, guest_addr: u32) -> *const u8 {
unsafe { self.membase.add(guest_addr as usize) }
}
/// Translate a guest virtual address to a mutable host pointer.
pub fn translate_virtual_mut(&mut self, guest_addr: u32) -> *mut u8 {
unsafe { self.membase.add(guest_addr as usize) }
}
/// Translate a guest physical address to a host pointer.
pub fn translate_physical(&self, guest_addr: u32) -> *const u8 {
let phys = guest_addr & PHYSICAL_ADDR_MASK;
unsafe { self.membase.add(phys as usize) }
}
/// Register an MMIO region.
pub fn add_mmio_region(&mut self, region: MmioRegion) {
let base = region.base_address;
let idx = self
.mmio_regions
.binary_search_by_key(&base, |r| r.base_address)
.unwrap_or_else(|i| i);
self.mmio_regions.insert(idx, region);
}
/// Check if an address is in a registered MMIO region.
fn find_mmio(&self, addr: u32) -> Option<&MmioRegion> {
self.mmio_regions.iter().find(|r| r.contains(addr))
}
/// Allocate a region in the guest address space.
pub fn alloc(
&mut self,
base: u32,
size: u32,
protect: MemoryProtect,
) -> Result<u32, MemoryError> {
let page_start = (base / PAGE_SIZE) as usize;
let page_count = ((size + PAGE_SIZE - 1) / PAGE_SIZE) as usize;
// Commit pages via platform
let host_ptr = unsafe { self.membase.add(base as usize) };
crate::platform::commit_memory(host_ptr, (page_count * PAGE_SIZE as usize) as usize)?;
// Update page table
for i in 0..page_count {
let idx = page_start + i;
if idx < self.page_table.len() {
let entry = &mut self.page_table[idx];
entry.set_base_address(page_start as u32);
entry.set_region_page_count(page_count as u32);
entry.set_allocation_protect(protect);
entry.set_current_protect(protect);
entry.set_state(AllocationState::RESERVE | AllocationState::COMMIT);
}
}
Ok(base)
}
/// Read a slice of bytes from guest memory (bypassing MMIO for bulk reads).
pub fn read_bulk(&self, addr: u32, buf: &mut [u8]) {
let ptr = self.translate_virtual(addr);
unsafe {
std::ptr::copy_nonoverlapping(ptr, buf.as_mut_ptr(), buf.len());
}
}
/// Write a slice of bytes to guest memory (bypassing MMIO for bulk writes).
pub fn write_bulk(&mut self, addr: u32, buf: &[u8]) {
let ptr = self.translate_virtual_mut(addr);
unsafe {
std::ptr::copy_nonoverlapping(buf.as_ptr(), ptr, buf.len());
}
}
/// Check if a guest address has been allocated/committed.
pub fn is_mapped(&self, addr: u32) -> bool {
let page = (addr / PAGE_SIZE) as usize;
if page >= self.page_table.len() {
return false;
}
self.page_table[page].state().contains(AllocationState::COMMIT)
}
/// Get a page table entry for a given address.
pub fn page_entry(&self, addr: u32) -> &PageEntry {
let page = (addr / PAGE_SIZE) as usize;
&self.page_table[page]
}
}
impl MemoryAccess for GuestMemory {
fn read_u8(&self, addr: u32) -> u8 {
if !self.is_mapped(addr) { return 0; }
let ptr = self.translate_virtual(addr);
unsafe { *ptr }
}
fn read_u16(&self, addr: u32) -> u16 {
if let Some(mmio) = self.find_mmio(addr) {
(mmio.read_callback)(addr) as u16
} else if !self.is_mapped(addr) {
0
} else {
let ptr = self.translate_virtual(addr) as *const [u8; 2];
u16::from_be_bytes(unsafe { *ptr })
}
}
fn read_u32(&self, addr: u32) -> u32 {
if let Some(mmio) = self.find_mmio(addr) {
(mmio.read_callback)(addr)
} else if !self.is_mapped(addr) {
0
} else {
let ptr = self.translate_virtual(addr) as *const [u8; 4];
u32::from_be_bytes(unsafe { *ptr })
}
}
fn read_u64(&self, addr: u32) -> u64 {
if let Some(mmio) = self.find_mmio(addr) {
let hi = (mmio.read_callback)(addr) as u64;
let lo = (mmio.read_callback)(addr.wrapping_add(4)) as u64;
(hi << 32) | lo
} else if !self.is_mapped(addr) {
0
} else {
let ptr = self.translate_virtual(addr) as *const [u8; 8];
u64::from_be_bytes(unsafe { *ptr })
}
}
fn write_u8(&mut self, addr: u32, val: u8) {
if !self.is_mapped(addr) { return; }
let ptr = self.translate_virtual_mut(addr);
unsafe { *ptr = val };
}
fn write_u16(&mut self, addr: u32, val: u16) {
if let Some(mmio) = self.find_mmio(addr) {
(mmio.write_callback)(addr, val as u32);
} else if !self.is_mapped(addr) {
return;
} else {
let ptr = self.translate_virtual_mut(addr);
unsafe {
std::ptr::copy_nonoverlapping(val.to_be_bytes().as_ptr(), ptr, 2);
}
}
}
fn write_u32(&mut self, addr: u32, val: u32) {
if let Some(mmio) = self.find_mmio(addr) {
(mmio.write_callback)(addr, val);
} else if !self.is_mapped(addr) {
return;
} else {
let ptr = self.translate_virtual_mut(addr);
unsafe {
std::ptr::copy_nonoverlapping(val.to_be_bytes().as_ptr(), ptr, 4);
}
}
}
fn write_u64(&mut self, addr: u32, val: u64) {
if let Some(mmio) = self.find_mmio(addr) {
(mmio.write_callback)(addr, (val >> 32) as u32);
(mmio.write_callback)(addr.wrapping_add(4), val as u32);
} else if !self.is_mapped(addr) {
return;
} else {
let ptr = self.translate_virtual_mut(addr);
unsafe {
std::ptr::copy_nonoverlapping(val.to_be_bytes().as_ptr(), ptr, 8);
}
}
}
fn translate(&self, addr: u32) -> Option<*const u8> {
if self.find_mmio(addr).is_some() || !self.is_mapped(addr) {
None
} else {
Some(self.translate_virtual(addr))
}
}
fn translate_mut(&mut self, addr: u32) -> Option<*mut u8> {
if self.find_mmio(addr).is_some() {
None
} else {
Some(self.translate_virtual_mut(addr))
}
}
}
impl Drop for GuestMemory {
fn drop(&mut self) {
if self.owned && !self.membase.is_null() {
unsafe {
crate::platform::release_address_space(self.membase, GUEST_ADDRESS_SPACE);
}
}
}
}

View File

@@ -0,0 +1,31 @@
pub mod access;
pub mod heap;
pub mod mmio;
pub mod page_table;
mod platform;
use thiserror::Error;
pub use access::MemoryAccess;
pub use heap::{GuestMemory, HeapType};
pub use mmio::MmioRegion;
pub use page_table::PageEntry;
#[derive(Debug, Error)]
pub enum MemoryError {
#[error("Failed to allocate guest address space: {0}")]
AllocationFailed(String),
#[error("Invalid guest address: {0:#010x}")]
InvalidAddress(u32),
#[error("MMIO access at {0:#010x}")]
MmioAccess(u32),
#[error("Protection violation at {0:#010x}")]
ProtectionViolation(u32),
#[error("Out of memory in heap {0:?}")]
OutOfMemory(HeapType),
}

View File

@@ -0,0 +1,27 @@
/// Represents a mapped MMIO region with read/write callbacks.
/// Instead of trapping access violations (as the C++ JIT does), the interpreter
/// explicitly checks each memory access against registered MMIO regions.
pub struct MmioRegion {
pub base_address: u32,
pub mask: u32,
pub size: u32,
pub read_callback: Box<dyn Fn(u32) -> u32 + Send + Sync>,
pub write_callback: Box<dyn Fn(u32, u32) + Send + Sync>,
}
impl MmioRegion {
pub fn contains(&self, addr: u32) -> bool {
let masked = addr & self.mask;
masked >= self.base_address && masked < self.base_address + self.size
}
}
impl std::fmt::Debug for MmioRegion {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("MmioRegion")
.field("base_address", &format_args!("{:#010x}", self.base_address))
.field("mask", &format_args!("{:#010x}", self.mask))
.field("size", &format_args!("{:#x}", self.size))
.finish()
}
}

View File

@@ -0,0 +1,122 @@
use bitflags::bitflags;
/// Describes a single page in the page table.
/// Mirrors the C++ `PageEntry` union from memory.h:82-99.
#[derive(Clone, Copy, Default)]
pub struct PageEntry(u64);
impl PageEntry {
/// Base address of the allocated region in 4K pages (20 bits).
pub fn base_address(&self) -> u32 {
(self.0 & 0xFFFFF) as u32
}
pub fn set_base_address(&mut self, val: u32) {
self.0 = (self.0 & !0xFFFFF) | (val as u64 & 0xFFFFF);
}
/// Total number of pages in the allocated region (20 bits).
pub fn region_page_count(&self) -> u32 {
((self.0 >> 20) & 0xFFFFF) as u32
}
pub fn set_region_page_count(&mut self, val: u32) {
self.0 = (self.0 & !(0xFFFFF << 20)) | ((val as u64 & 0xFFFFF) << 20);
}
/// Protection bits specified during region allocation (4 bits).
pub fn allocation_protect(&self) -> MemoryProtect {
MemoryProtect::from_bits_truncate(((self.0 >> 40) & 0xF) as u32)
}
pub fn set_allocation_protect(&mut self, val: MemoryProtect) {
self.0 = (self.0 & !(0xF << 40)) | ((val.bits() as u64 & 0xF) << 40);
}
/// Current protection bits (4 bits).
pub fn current_protect(&self) -> MemoryProtect {
MemoryProtect::from_bits_truncate(((self.0 >> 44) & 0xF) as u32)
}
pub fn set_current_protect(&mut self, val: MemoryProtect) {
self.0 = (self.0 & !(0xF << 44)) | ((val.bits() as u64 & 0xF) << 44);
}
/// Allocation state (2 bits).
pub fn state(&self) -> AllocationState {
AllocationState::from_bits_truncate(((self.0 >> 48) & 0x3) as u32)
}
pub fn set_state(&mut self, val: AllocationState) {
self.0 = (self.0 & !(0x3 << 48)) | ((val.bits() as u64 & 0x3) << 48);
}
pub fn is_committed(&self) -> bool {
self.state().contains(AllocationState::COMMIT)
}
pub fn is_reserved(&self) -> bool {
self.state().contains(AllocationState::RESERVE)
}
pub fn is_free(&self) -> bool {
self.state().is_empty()
}
}
bitflags! {
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct MemoryProtect: u32 {
const READ = 1 << 0;
const WRITE = 1 << 1;
const NO_CACHE = 1 << 2;
const WRITE_COMBINE = 1 << 3;
}
}
bitflags! {
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct AllocationState: u32 {
const RESERVE = 1 << 0;
const COMMIT = 1 << 1;
}
}
impl std::fmt::Debug for PageEntry {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("PageEntry")
.field("base_address", &format_args!("{:#x}", self.base_address()))
.field("region_page_count", &self.region_page_count())
.field("allocation_protect", &self.allocation_protect())
.field("current_protect", &self.current_protect())
.field("state", &self.state())
.finish()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_page_entry_bitfields() {
let mut entry = PageEntry::default();
assert!(entry.is_free());
entry.set_base_address(0x100);
entry.set_region_page_count(0x10);
entry.set_allocation_protect(MemoryProtect::READ | MemoryProtect::WRITE);
entry.set_current_protect(MemoryProtect::READ);
entry.set_state(AllocationState::RESERVE | AllocationState::COMMIT);
assert_eq!(entry.base_address(), 0x100);
assert_eq!(entry.region_page_count(), 0x10);
assert_eq!(
entry.allocation_protect(),
MemoryProtect::READ | MemoryProtect::WRITE
);
assert_eq!(entry.current_protect(), MemoryProtect::READ);
assert!(entry.is_committed());
assert!(entry.is_reserved());
}
}

View File

@@ -0,0 +1,98 @@
use crate::MemoryError;
/// Reserve a contiguous virtual address region without committing physical pages.
#[cfg(unix)]
pub fn reserve_address_space(size: usize) -> Result<*mut u8, MemoryError> {
unsafe {
let ptr = libc::mmap(
std::ptr::null_mut(),
size,
libc::PROT_NONE,
libc::MAP_PRIVATE | libc::MAP_ANONYMOUS | libc::MAP_NORESERVE,
-1,
0,
);
if ptr == libc::MAP_FAILED {
Err(MemoryError::AllocationFailed(format!(
"mmap failed for {} bytes: {}",
size,
std::io::Error::last_os_error()
)))
} else {
Ok(ptr as *mut u8)
}
}
}
/// Commit (make accessible) a region within a previously reserved address space.
#[cfg(unix)]
pub fn commit_memory(ptr: *mut u8, size: usize) -> Result<(), MemoryError> {
unsafe {
let result = libc::mprotect(ptr as *mut libc::c_void, size, libc::PROT_READ | libc::PROT_WRITE);
if result != 0 {
Err(MemoryError::AllocationFailed(format!(
"mprotect failed for {} bytes: {}",
size,
std::io::Error::last_os_error()
)))
} else {
Ok(())
}
}
}
/// Release a previously reserved address space.
#[cfg(unix)]
pub unsafe fn release_address_space(ptr: *mut u8, size: usize) {
unsafe { libc::munmap(ptr as *mut libc::c_void, size); }
}
#[cfg(windows)]
pub fn reserve_address_space(size: usize) -> Result<*mut u8, MemoryError> {
unsafe {
let ptr = windows_sys::Win32::System::Memory::VirtualAlloc(
std::ptr::null_mut(),
size,
windows_sys::Win32::System::Memory::MEM_RESERVE,
windows_sys::Win32::System::Memory::PAGE_NOACCESS,
);
if ptr.is_null() {
Err(MemoryError::AllocationFailed(format!(
"VirtualAlloc reserve failed for {} bytes",
size,
)))
} else {
Ok(ptr as *mut u8)
}
}
}
#[cfg(windows)]
pub fn commit_memory(ptr: *mut u8, size: usize) -> Result<(), MemoryError> {
unsafe {
let result = windows_sys::Win32::System::Memory::VirtualAlloc(
ptr as *mut _,
size,
windows_sys::Win32::System::Memory::MEM_COMMIT,
windows_sys::Win32::System::Memory::PAGE_READWRITE,
);
if result.is_null() {
Err(MemoryError::AllocationFailed(format!(
"VirtualAlloc commit failed for {} bytes",
size,
)))
} else {
Ok(())
}
}
}
#[cfg(windows)]
pub unsafe fn release_address_space(ptr: *mut u8, size: usize) {
let _ = size;
windows_sys::Win32::System::Memory::VirtualFree(
ptr as *mut _,
0,
windows_sys::Win32::System::Memory::MEM_RELEASE,
);
}

View File

@@ -0,0 +1,11 @@
[package]
name = "xenia-types"
version.workspace = true
edition.workspace = true
license.workspace = true
[dependencies]
bitflags = { workspace = true }
byteorder = { workspace = true }
thiserror = { workspace = true }
serde = { workspace = true }

View File

@@ -0,0 +1,122 @@
use serde::{Deserialize, Serialize};
use std::fmt;
use std::marker::PhantomData;
/// Big-endian value wrapper matching `xe::be<T>` from the C++ codebase.
/// Stores the value in big-endian byte order and transparently converts
/// on access. Used for guest memory structures that are natively big-endian.
#[derive(Clone, Copy, Serialize, Deserialize)]
#[repr(transparent)]
pub struct Be<T: BeSwap>(T::Bytes, PhantomData<T>);
impl<T: BeSwap> Be<T> {
pub fn new(val: T) -> Self {
Self(val.to_be_bytes(), PhantomData)
}
pub fn get(self) -> T {
T::from_be_bytes(self.0)
}
pub fn set(&mut self, val: T) {
self.0 = val.to_be_bytes();
}
pub fn raw_bytes(&self) -> &T::Bytes {
&self.0
}
}
impl<T: BeSwap + Default> Default for Be<T> {
fn default() -> Self {
Self::new(T::default())
}
}
impl<T: BeSwap + fmt::Debug> fmt::Debug for Be<T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.get().fmt(f)
}
}
impl<T: BeSwap + fmt::Display> fmt::Display for Be<T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.get().fmt(f)
}
}
impl<T: BeSwap + PartialEq> PartialEq for Be<T> {
fn eq(&self, other: &Self) -> bool {
// Compare raw bytes for efficiency (same byte order)
self.0.as_ref() == other.0.as_ref()
}
}
impl<T: BeSwap + Eq> Eq for Be<T> {}
/// Trait for types that can be converted to/from big-endian byte representations.
pub trait BeSwap: Copy {
type Bytes: Copy + AsRef<[u8]> + serde::Serialize + for<'de> serde::Deserialize<'de>;
fn to_be_bytes(self) -> Self::Bytes;
fn from_be_bytes(bytes: Self::Bytes) -> Self;
}
macro_rules! impl_be_swap {
($t:ty) => {
impl BeSwap for $t {
type Bytes = [u8; std::mem::size_of::<$t>()];
fn to_be_bytes(self) -> Self::Bytes {
<$t>::to_be_bytes(self)
}
fn from_be_bytes(bytes: Self::Bytes) -> Self {
<$t>::from_be_bytes(bytes)
}
}
};
}
impl_be_swap!(u8);
impl_be_swap!(u16);
impl_be_swap!(u32);
impl_be_swap!(u64);
impl_be_swap!(i8);
impl_be_swap!(i16);
impl_be_swap!(i32);
impl_be_swap!(i64);
impl_be_swap!(f32);
impl_be_swap!(f64);
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_be_u32() {
let v = Be::<u32>::new(0x12345678);
assert_eq!(v.get(), 0x12345678);
assert_eq!(v.raw_bytes(), &[0x12, 0x34, 0x56, 0x78]);
}
#[test]
fn test_be_u16() {
let v = Be::<u16>::new(0xABCD);
assert_eq!(v.get(), 0xABCD);
assert_eq!(v.raw_bytes(), &[0xAB, 0xCD]);
}
#[test]
fn test_be_mutate() {
let mut v = Be::<u32>::new(1);
assert_eq!(v.get(), 1);
v.set(42);
assert_eq!(v.get(), 42);
}
#[test]
fn test_be_f32() {
let v = Be::<f32>::new(1.0);
assert_eq!(v.get(), 1.0);
// IEEE 754: 1.0f = 0x3F800000 => bytes [0x3F, 0x80, 0x00, 0x00]
assert_eq!(v.raw_bytes(), &[0x3F, 0x80, 0x00, 0x00]);
}
}

View File

@@ -0,0 +1,27 @@
use thiserror::Error;
#[derive(Debug, Error)]
pub enum XeniaError {
#[error("Invalid XEX2 file: {0}")]
InvalidXex(String),
#[error("Invalid XISO file: {0}")]
InvalidXiso(String),
#[error("Memory error: {0}")]
Memory(String),
#[error("Unimplemented opcode: {0}")]
UnimplementedOpcode(String),
#[error("Unimplemented kernel export: module={module} ordinal={ordinal:#x}")]
UnimplementedExport { module: String, ordinal: u32 },
#[error("Invalid guest address: {0:#010x}")]
InvalidAddress(u32),
#[error("I/O error: {0}")]
Io(#[from] std::io::Error),
}
pub type XeniaResult<T> = Result<T, XeniaError>;

View File

@@ -0,0 +1,6 @@
pub mod endian;
pub mod error;
pub mod vec128;
pub use endian::Be;
pub use vec128::Vec128;

View File

@@ -0,0 +1,206 @@
use serde::{Deserialize, Serialize};
use std::fmt;
/// 128-bit vector register type matching the Xbox 360's VMX128 registers.
/// Stored in big-endian byte order (matching guest memory layout).
#[derive(Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[repr(C, align(16))]
pub struct Vec128 {
pub bytes: [u8; 16],
}
impl Vec128 {
pub const ZERO: Self = Self { bytes: [0; 16] };
pub fn from_u32x4(a: u32, b: u32, c: u32, d: u32) -> Self {
let mut bytes = [0u8; 16];
bytes[0..4].copy_from_slice(&a.to_be_bytes());
bytes[4..8].copy_from_slice(&b.to_be_bytes());
bytes[8..12].copy_from_slice(&c.to_be_bytes());
bytes[12..16].copy_from_slice(&d.to_be_bytes());
Self { bytes }
}
pub fn from_f32x4(a: f32, b: f32, c: f32, d: f32) -> Self {
Self::from_u32x4(a.to_bits(), b.to_bits(), c.to_bits(), d.to_bits())
}
/// Read the i-th u32 element (big-endian, 0-indexed).
pub fn u32x4(&self, i: usize) -> u32 {
let off = i * 4;
u32::from_be_bytes([
self.bytes[off],
self.bytes[off + 1],
self.bytes[off + 2],
self.bytes[off + 3],
])
}
/// Write the i-th u32 element (big-endian, 0-indexed).
pub fn set_u32x4(&mut self, i: usize, val: u32) {
let off = i * 4;
self.bytes[off..off + 4].copy_from_slice(&val.to_be_bytes());
}
/// Read the i-th f32 element (big-endian, 0-indexed).
pub fn f32x4(&self, i: usize) -> f32 {
f32::from_bits(self.u32x4(i))
}
/// Write the i-th f32 element (big-endian, 0-indexed).
pub fn set_f32x4(&mut self, i: usize, val: f32) {
self.set_u32x4(i, val.to_bits());
}
/// Read the i-th u16 element (big-endian, 0-indexed).
pub fn u16x8(&self, i: usize) -> u16 {
let off = i * 2;
u16::from_be_bytes([self.bytes[off], self.bytes[off + 1]])
}
/// Write the i-th u16 element (big-endian, 0-indexed).
pub fn set_u16x8(&mut self, i: usize, val: u16) {
let off = i * 2;
self.bytes[off..off + 2].copy_from_slice(&val.to_be_bytes());
}
/// Read the i-th u8 element (0-indexed).
pub fn u8x16(&self, i: usize) -> u8 {
self.bytes[i]
}
/// Write the i-th u8 element (0-indexed).
pub fn set_u8x16(&mut self, i: usize, val: u8) {
self.bytes[i] = val;
}
/// Read as two u64 values (big-endian).
pub fn u64x2(&self, i: usize) -> u64 {
let off = i * 8;
u64::from_be_bytes([
self.bytes[off],
self.bytes[off + 1],
self.bytes[off + 2],
self.bytes[off + 3],
self.bytes[off + 4],
self.bytes[off + 5],
self.bytes[off + 6],
self.bytes[off + 7],
])
}
pub fn set_u64x2(&mut self, i: usize, val: u64) {
let off = i * 8;
self.bytes[off..off + 8].copy_from_slice(&val.to_be_bytes());
}
/// Get all 4 u32 elements as an array.
pub fn as_u32x4(&self) -> [u32; 4] {
[self.u32x4(0), self.u32x4(1), self.u32x4(2), self.u32x4(3)]
}
/// Get all 4 f32 elements as an array.
pub fn as_f32x4(&self) -> [f32; 4] {
[self.f32x4(0), self.f32x4(1), self.f32x4(2), self.f32x4(3)]
}
/// Get all 8 u16 elements as an array.
pub fn as_u16x8(&self) -> [u16; 8] {
[
self.u16x8(0), self.u16x8(1), self.u16x8(2), self.u16x8(3),
self.u16x8(4), self.u16x8(5), self.u16x8(6), self.u16x8(7),
]
}
/// Get all 16 bytes as an array.
pub fn as_bytes(&self) -> [u8; 16] {
self.bytes
}
/// Create from a byte array.
pub fn from_bytes(bytes: [u8; 16]) -> Self {
Self { bytes }
}
/// Create from a u32 array (big-endian elements).
pub fn from_u32x4_array(arr: [u32; 4]) -> Self {
Self::from_u32x4(arr[0], arr[1], arr[2], arr[3])
}
/// Create from an f32 array (big-endian elements).
pub fn from_f32x4_array(arr: [f32; 4]) -> Self {
Self::from_f32x4(arr[0], arr[1], arr[2], arr[3])
}
/// Create from a u16 array (big-endian elements).
pub fn from_u16x8_array(arr: [u16; 8]) -> Self {
let mut v = Self::ZERO;
for i in 0..8 { v.set_u16x8(i, arr[i]); }
v
}
}
impl Default for Vec128 {
fn default() -> Self {
Self::ZERO
}
}
impl fmt::Debug for Vec128 {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"Vec128({:08X}_{:08X}_{:08X}_{:08X})",
self.u32x4(0),
self.u32x4(1),
self.u32x4(2),
self.u32x4(3),
)
}
}
impl fmt::Display for Vec128 {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt::Debug::fmt(self, f)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_u32x4_roundtrip() {
let v = Vec128::from_u32x4(0xDEADBEEF, 0xCAFEBABE, 0x12345678, 0x9ABCDEF0);
assert_eq!(v.u32x4(0), 0xDEADBEEF);
assert_eq!(v.u32x4(1), 0xCAFEBABE);
assert_eq!(v.u32x4(2), 0x12345678);
assert_eq!(v.u32x4(3), 0x9ABCDEF0);
}
#[test]
fn test_f32x4_roundtrip() {
let v = Vec128::from_f32x4(1.0, -2.5, 3.14, 0.0);
assert_eq!(v.f32x4(0), 1.0);
assert_eq!(v.f32x4(1), -2.5);
assert!((v.f32x4(2) - 3.14).abs() < f32::EPSILON);
assert_eq!(v.f32x4(3), 0.0);
}
#[test]
fn test_u16x8() {
let v = Vec128::from_u32x4(0x00010002, 0x00030004, 0x00050006, 0x00070008);
assert_eq!(v.u16x8(0), 0x0001);
assert_eq!(v.u16x8(1), 0x0002);
assert_eq!(v.u16x8(6), 0x0007);
assert_eq!(v.u16x8(7), 0x0008);
}
#[test]
fn test_zero() {
let v = Vec128::ZERO;
for i in 0..4 {
assert_eq!(v.u32x4(i), 0);
}
}
}

View File

@@ -0,0 +1,12 @@
[package]
name = "xenia-vfs"
version.workspace = true
edition.workspace = true
license.workspace = true
[dependencies]
xenia-types = { workspace = true }
tracing = { workspace = true }
byteorder = { workspace = true }
thiserror = { workspace = true }
anyhow = { workspace = true }

View File

@@ -0,0 +1,54 @@
use crate::{VfsDevice, VfsEntry, VfsError};
use std::path::{Path, PathBuf};
/// Host filesystem pass-through device.
pub struct HostPathDevice {
name: String,
root: PathBuf,
}
impl HostPathDevice {
pub fn new(name: impl Into<String>, root: impl AsRef<Path>) -> Self {
Self {
name: name.into(),
root: root.as_ref().to_path_buf(),
}
}
}
impl VfsDevice for HostPathDevice {
fn name(&self) -> &str {
&self.name
}
fn list_root(&self) -> Result<Vec<VfsEntry>, VfsError> {
let mut entries = Vec::new();
for entry in std::fs::read_dir(&self.root)? {
let entry = entry?;
let metadata = entry.metadata()?;
entries.push(VfsEntry {
name: entry.file_name().to_string_lossy().into_owned(),
is_directory: metadata.is_dir(),
size: metadata.len(),
offset: 0,
});
}
Ok(entries)
}
fn read_file(&self, path: &str) -> Result<Vec<u8>, VfsError> {
let full_path = self.root.join(path);
std::fs::read(&full_path).map_err(VfsError::from)
}
fn stat(&self, path: &str) -> Result<VfsEntry, VfsError> {
let full_path = self.root.join(path);
let metadata = std::fs::metadata(&full_path)?;
Ok(VfsEntry {
name: path.to_string(),
is_directory: metadata.is_dir(),
size: metadata.len(),
offset: 0,
})
}
}

View File

@@ -0,0 +1,185 @@
use crate::{VfsDevice, VfsEntry, VfsError};
use std::io::{Read, Seek, SeekFrom};
/// XISO disc image device. Parses Xbox 360 disc images (GDFX/XISO format).
pub struct DiscImageDevice {
name: String,
path: std::path::PathBuf,
game_offset: u64,
/// Cached root directory buffer (typically small, a few KB).
root_buffer: Vec<u8>,
}
/// XISO sector size
pub const SECTOR_SIZE: u64 = 0x800;
/// GDFX magic string
const GDFX_MAGIC: &[u8; 20] = b"MICROSOFT*XBOX*MEDIA";
/// File attribute: directory
const FILE_ATTRIBUTE_DIRECTORY: u8 = 0x10;
/// Known game partition offsets to try
const LIKELY_OFFSETS: &[u64] = &[
0x0000_0000,
0x0000_FB20,
0x0002_0600,
0x0208_0000,
0x0FD9_0000,
];
impl DiscImageDevice {
pub fn open(name: impl Into<String>, path: &std::path::Path) -> Result<Self, VfsError> {
let mut file = std::fs::File::open(path)?;
// Find the game partition by locating the GDFX magic at sector 32
let mut game_offset = 0u64;
let mut magic_found = false;
let mut magic_buf = [0u8; 20];
for &offset in LIKELY_OFFSETS {
let magic_pos = offset + 32 * SECTOR_SIZE;
if file.seek(SeekFrom::Start(magic_pos)).is_ok()
&& file.read_exact(&mut magic_buf).is_ok()
&& magic_buf == *GDFX_MAGIC
{
game_offset = offset;
magic_found = true;
break;
}
}
if !magic_found {
return Err(VfsError::InvalidFormat(
"GDFX magic not found - not a valid XISO disc image".into(),
));
}
// Read root directory info from sector 32 header
let fs_ptr = game_offset + 32 * SECTOR_SIZE;
file.seek(SeekFrom::Start(fs_ptr + 20))?;
let mut buf4 = [0u8; 4];
file.read_exact(&mut buf4)?;
let root_sector = u32::from_le_bytes(buf4) as u64;
file.read_exact(&mut buf4)?;
let root_size = u32::from_le_bytes(buf4) as u64;
let root_byte_offset = game_offset + root_sector * SECTOR_SIZE;
// Read the root directory buffer into memory (typically small)
file.seek(SeekFrom::Start(root_byte_offset))?;
let mut root_buffer = vec![0u8; root_size as usize];
file.read_exact(&mut root_buffer)?;
Ok(Self {
name: name.into(),
path: path.to_path_buf(),
game_offset,
root_buffer,
})
}
/// Read all directory entries from the root directory tree.
fn read_entries(&self) -> Vec<VfsEntry> {
let mut entries = Vec::new();
self.read_entry(&self.root_buffer, 0, &mut entries);
entries
}
/// Recursively read a directory entry from the binary tree structure.
fn read_entry(&self, buffer: &[u8], ordinal: u16, entries: &mut Vec<VfsEntry>) {
let p = ordinal as usize * 4;
if p + 14 > buffer.len() {
return;
}
let node_l = u16::from_le_bytes([buffer[p], buffer[p + 1]]);
let node_r = u16::from_le_bytes([buffer[p + 2], buffer[p + 3]]);
let sector = u32::from_le_bytes([buffer[p + 4], buffer[p + 5], buffer[p + 6], buffer[p + 7]]) as u64;
let length = u32::from_le_bytes([buffer[p + 8], buffer[p + 9], buffer[p + 10], buffer[p + 11]]) as u64;
let attributes = buffer[p + 12];
let name_length = buffer[p + 13] as usize;
if p + 14 + name_length > buffer.len() {
return;
}
// Traverse left subtree first (smaller names)
if node_l != 0 && node_l != 0xFFFF {
self.read_entry(buffer, node_l, entries);
}
// Read this entry's name
let name = String::from_utf8_lossy(&buffer[p + 14..p + 14 + name_length]).to_string();
let is_directory = (attributes & FILE_ATTRIBUTE_DIRECTORY) != 0;
let file_offset = self.game_offset + sector * SECTOR_SIZE;
entries.push(VfsEntry {
name,
is_directory,
size: length,
offset: file_offset,
});
// Traverse right subtree (larger names)
if node_r != 0 && node_r != 0xFFFF {
self.read_entry(buffer, node_r, entries);
}
}
}
impl VfsDevice for DiscImageDevice {
fn name(&self) -> &str {
&self.name
}
fn list_root(&self) -> Result<Vec<VfsEntry>, VfsError> {
Ok(self.read_entries())
}
fn read_file(&self, path: &str) -> Result<Vec<u8>, VfsError> {
let entries = self.read_entries();
let entry = entries.iter()
.find(|e| e.name.eq_ignore_ascii_case(path) && !e.is_directory)
.ok_or_else(|| VfsError::NotFound(path.to_string()))?;
let offset = entry.offset;
let size = entry.size as usize;
// Read from file using seek
let mut file = std::fs::File::open(&self.path)?;
let file_len = file.seek(SeekFrom::End(0))?;
if offset + size as u64 > file_len {
return Err(VfsError::NotFound(format!(
"File data extends past end of image: {} (offset={:#x}, size={:#x}, image_len={:#x})",
path, offset, size, file_len
)));
}
file.seek(SeekFrom::Start(offset))?;
let mut buf = vec![0u8; size];
let bytes_read = file.read(&mut buf)?;
if bytes_read < size {
// Try reading the rest
let mut total = bytes_read;
while total < size {
let n = file.read(&mut buf[total..])?;
if n == 0 {
return Err(VfsError::NotFound(format!(
"Short read: got {} of {} bytes for {}",
total, size, path
)));
}
total += n;
}
}
Ok(buf)
}
fn stat(&self, path: &str) -> Result<VfsEntry, VfsError> {
let entries = self.read_entries();
entries.into_iter()
.find(|e| e.name.eq_ignore_ascii_case(path))
.ok_or_else(|| VfsError::NotFound(path.to_string()))
}
}

View File

@@ -0,0 +1,33 @@
pub mod device;
pub mod disc_image;
use thiserror::Error;
#[derive(Debug, Error)]
pub enum VfsError {
#[error("I/O error: {0}")]
Io(#[from] std::io::Error),
#[error("Invalid format: {0}")]
InvalidFormat(String),
#[error("File not found: {0}")]
NotFound(String),
}
/// A virtual filesystem entry (file or directory).
#[derive(Debug)]
pub struct VfsEntry {
pub name: String,
pub is_directory: bool,
pub size: u64,
pub offset: u64,
}
/// Trait for VFS device implementations (XISO, STFS, host path, etc.)
pub trait VfsDevice: Send + Sync {
fn name(&self) -> &str;
fn list_root(&self) -> Result<Vec<VfsEntry>, VfsError>;
fn read_file(&self, path: &str) -> Result<Vec<u8>, VfsError>;
fn stat(&self, path: &str) -> Result<VfsEntry, VfsError>;
}

View File

@@ -0,0 +1,17 @@
[package]
name = "xenia-xex"
version.workspace = true
edition.workspace = true
license.workspace = true
[dependencies]
xenia-types = { workspace = true }
xenia-memory = { workspace = true }
tracing = { workspace = true }
byteorder = { workspace = true }
thiserror = { workspace = true }
anyhow = { workspace = true }
aes = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }

View File

@@ -0,0 +1 @@
fn main() {}

View File

@@ -0,0 +1,128 @@
use serde::Serialize;
/// XEX2 file header. Parsed from the beginning of an Xbox 360 executable.
#[derive(Debug, Serialize)]
pub struct Xex2Header {
pub magic: u32,
pub module_flags: u32,
pub header_size: u32,
pub security_offset: u32,
pub header_count: u32,
pub optional_headers: Vec<Xex2OptionalHeader>,
pub security_info: Option<Xex2SecurityInfo>,
/// Parsed file format info (if present).
pub file_format_info: Option<FileFormatInfo>,
/// Parsed import libraries (addresses only until resolve_imports is called).
pub import_libraries: Vec<ImportLibrary>,
/// Execution info (title ID, media ID, etc.).
pub execution_info: Option<ExecutionInfo>,
/// Original PE name from the XEX header.
pub original_pe_name: Option<String>,
}
#[derive(Debug, Serialize)]
pub struct Xex2OptionalHeader {
pub key: u32,
pub value: u32,
}
#[derive(Debug, Serialize)]
pub struct Xex2SecurityInfo {
pub image_size: u32,
pub load_address: u32,
pub export_table_address: u32,
pub image_flags: u32,
/// Encrypted session key (decrypted with retail/devkit key to get actual session key).
pub aes_key: [u8; 16],
pub page_descriptors: Vec<Xex2PageDescriptor>,
}
#[derive(Debug, Clone, Copy, Serialize)]
pub struct Xex2PageDescriptor {
pub size_and_info: u32,
}
impl Xex2PageDescriptor {
pub fn page_count(&self) -> u32 {
self.size_and_info >> 4
}
pub fn info(&self) -> u32 {
self.size_and_info & 0xF
}
}
/// File format info (compression and encryption types).
#[derive(Debug, Clone, Serialize)]
pub struct FileFormatInfo {
pub info_size: u32,
pub encryption_type: u16,
pub compression_type: u16,
/// For basic compression: list of (data_size, zero_size) block pairs.
pub basic_blocks: Vec<BasicCompressionBlock>,
/// For normal (LZX) compression: window size.
pub normal_window_size: u32,
/// For normal (LZX) compression: first block size (from header).
pub normal_first_block_size: u32,
/// For normal (LZX) compression: first block hash (from header).
pub normal_first_block_hash: [u8; 20],
}
#[derive(Debug, Clone, Copy, Serialize)]
pub struct BasicCompressionBlock {
pub data_size: u32,
pub zero_size: u32,
}
/// An imported library with its resolved imports.
#[derive(Debug, Clone, Serialize)]
pub struct ImportLibrary {
pub name: String,
pub id: u32,
pub version_min: u32,
pub version_cur: u32,
/// Import entries. Before `resolve_imports`, these contain addresses but no ordinals.
/// After `resolve_imports`, ordinals and record types are filled in from the PE image.
pub imports: Vec<ImportEntry>,
}
/// A single import entry within an import library.
#[derive(Debug, Clone, Serialize)]
pub struct ImportEntry {
pub ordinal: u16,
pub record_type: u8, // 0 = variable, 1 = thunk
pub address: u32,
}
/// Execution info parsed from the XEX header.
#[derive(Debug, Clone, Serialize)]
pub struct ExecutionInfo {
pub media_id: u32,
pub title_id: u32,
pub disc_number: u8,
pub disc_count: u8,
}
/// XEX2 magic: "XEX2"
pub const XEX2_MAGIC: u32 = 0x58455832;
/// Compression types
pub const COMPRESSION_NONE: u16 = 0;
pub const COMPRESSION_BASIC: u16 = 1;
pub const COMPRESSION_NORMAL: u16 = 2;
/// Encryption types
pub const ENCRYPTION_NONE: u16 = 0;
pub const ENCRYPTION_NORMAL: u16 = 1;
/// Optional header keys
pub mod header_keys {
pub const ENTRY_POINT: u32 = 0x00010100;
pub const IMAGE_BASE_ADDRESS: u32 = 0x00010201;
pub const IMPORT_LIBRARIES: u32 = 0x000103FF;
pub const TLS_INFO: u32 = 0x00020200;
pub const EXECUTION_INFO: u32 = 0x00040006;
pub const DEFAULT_STACK_SIZE: u32 = 0x00020104;
pub const ORIGINAL_PE_NAME: u32 = 0x000183FF;
pub const FILE_FORMAT_INFO: u32 = 0x000003FF;
}

View File

@@ -0,0 +1,6 @@
pub mod header;
pub mod loader;
pub mod lzx;
pub mod pe;
pub use header::Xex2Header;

View File

@@ -0,0 +1,571 @@
use crate::header::*;
use aes::cipher::{BlockDecrypt, KeyInit};
use aes::Aes128;
use byteorder::{BigEndian, ReadBytesExt};
use std::io::{self, Cursor, Read, Seek, SeekFrom};
/// Parse a XEX2 header from raw file data.
pub fn parse_xex2_header(data: &[u8]) -> io::Result<Xex2Header> {
let mut cursor = Cursor::new(data);
let magic = cursor.read_u32::<BigEndian>()?;
if magic != XEX2_MAGIC {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
format!("Invalid XEX2 magic: {:#010x} (expected {:#010x})", magic, XEX2_MAGIC),
));
}
let module_flags = cursor.read_u32::<BigEndian>()?;
let header_size = cursor.read_u32::<BigEndian>()?;
let _reserved = cursor.read_u32::<BigEndian>()?;
let security_offset = cursor.read_u32::<BigEndian>()?;
let header_count = cursor.read_u32::<BigEndian>()?;
let mut optional_headers = Vec::new();
for _ in 0..header_count {
let key = cursor.read_u32::<BigEndian>()?;
let value = cursor.read_u32::<BigEndian>()?;
optional_headers.push(Xex2OptionalHeader { key, value });
}
// Parse security info
let security_info = if (security_offset as usize) < data.len() {
cursor.seek(SeekFrom::Start(security_offset as u64))?;
Some(parse_security_info(&mut cursor)?)
} else {
None
};
// Parse file format info
let file_format_info = parse_file_format_info(data, &optional_headers);
// Parse import libraries (addresses only; call resolve_imports after decompression)
let import_libraries = parse_import_libraries(data, &optional_headers);
// Parse execution info
let execution_info = parse_execution_info(data, &optional_headers);
// Parse original PE name
let original_pe_name = parse_original_pe_name(data, &optional_headers);
Ok(Xex2Header {
magic,
module_flags,
header_size,
security_offset,
header_count,
optional_headers,
security_info,
file_format_info,
import_libraries,
execution_info,
original_pe_name,
})
}
fn parse_security_info(cursor: &mut Cursor<&[u8]>) -> io::Result<Xex2SecurityInfo> {
// xex2_security_info layout (from xex2_info.h):
// 0x000: header_size (u32)
// 0x004: image_size (u32)
// 0x008: rsa_signature (0x100 bytes)
// 0x108: unk_108 (u32)
// 0x10C: image_flags (u32)
// 0x110: load_address (u32)
// 0x114: section_digest (0x14 bytes)
// 0x128: import_table_count (u32)
// 0x12C: import_table_digest (0x14 bytes)
// 0x140: xgd2_media_id (0x10 bytes)
// 0x150: aes_key (0x10 bytes)
// 0x160: export_table (u32)
// 0x164: header_digest (0x14 bytes)
// 0x178: region (u32)
// 0x17C: allowed_media_types (u32)
// 0x180: page_descriptor_count (u32)
// 0x184: page_descriptors[] (each is 0x18 bytes: u32 value + 0x14 digest)
let _header_size = cursor.read_u32::<BigEndian>()?; // 0x000
let image_size = cursor.read_u32::<BigEndian>()?; // 0x004
// Skip RSA signature (0x100 bytes)
let mut rsa_sig = [0u8; 0x100];
cursor.read_exact(&mut rsa_sig)?; // 0x008
let _unk_108 = cursor.read_u32::<BigEndian>()?; // 0x108
let image_flags = cursor.read_u32::<BigEndian>()?; // 0x10C
let load_address = cursor.read_u32::<BigEndian>()?; // 0x110
// Skip section_digest (0x14 bytes)
let mut digest = [0u8; 0x14];
cursor.read_exact(&mut digest)?; // 0x114
let _import_table_count = cursor.read_u32::<BigEndian>()?; // 0x128
// Skip import_table_digest (0x14 bytes)
cursor.read_exact(&mut digest)?; // 0x12C
// Skip xgd2_media_id (0x10 bytes)
let mut media_id = [0u8; 0x10];
cursor.read_exact(&mut media_id)?; // 0x140
// Read aes_key (0x10 bytes)
let mut aes_key = [0u8; 0x10];
cursor.read_exact(&mut aes_key)?; // 0x150
let export_table_address = cursor.read_u32::<BigEndian>()?; // 0x160
// Skip header_digest (0x14 bytes)
cursor.read_exact(&mut digest)?; // 0x164
let _region = cursor.read_u32::<BigEndian>()?; // 0x178
let _allowed_media = cursor.read_u32::<BigEndian>()?; // 0x17C
let page_descriptor_count = cursor.read_u32::<BigEndian>()?; // 0x180
let mut page_descriptors = Vec::new();
for _ in 0..page_descriptor_count {
let size_and_info = cursor.read_u32::<BigEndian>()?;
// Skip data_digest (0x14 bytes per descriptor)
cursor.read_exact(&mut digest)?;
page_descriptors.push(Xex2PageDescriptor { size_and_info });
}
Ok(Xex2SecurityInfo {
image_size,
load_address,
export_table_address,
image_flags,
aes_key,
page_descriptors,
})
}
/// Parse file format info from the optional header data.
fn parse_file_format_info(data: &[u8], headers: &[Xex2OptionalHeader]) -> Option<FileFormatInfo> {
// The key format: low 8 bits indicate the data size category
// 0xFF = data offset is a pointer to variable-size data in the header area
let header = headers.iter().find(|h| h.key == header_keys::FILE_FORMAT_INFO)?;
let offset = header.value as usize;
if offset + 8 > data.len() {
return None;
}
let mut cursor = Cursor::new(data);
cursor.seek(SeekFrom::Start(offset as u64)).ok()?;
let info_size = cursor.read_u32::<BigEndian>().ok()?;
let encryption_type = cursor.read_u16::<BigEndian>().ok()?;
let compression_type = cursor.read_u16::<BigEndian>().ok()?;
let mut basic_blocks = Vec::new();
let mut normal_window_size = 0u32;
let mut normal_first_block_size = 0u32;
let mut normal_first_block_hash = [0u8; 20];
match compression_type {
COMPRESSION_BASIC => {
// Basic compression blocks: (data_size, zero_size) pairs
// Number of blocks = (info_size - 8) / 8
let block_count = if info_size > 8 { (info_size - 8) / 8 } else { 0 };
for _ in 0..block_count {
let data_size = cursor.read_u32::<BigEndian>().ok()?;
let zero_size = cursor.read_u32::<BigEndian>().ok()?;
basic_blocks.push(BasicCompressionBlock { data_size, zero_size });
}
}
COMPRESSION_NORMAL => {
normal_window_size = cursor.read_u32::<BigEndian>().ok()?;
// Read first_block: block_size (4) + block_hash (20)
normal_first_block_size = cursor.read_u32::<BigEndian>().ok()?;
cursor.read_exact(&mut normal_first_block_hash).ok()?;
}
_ => {}
}
Some(FileFormatInfo {
info_size,
encryption_type,
compression_type,
basic_blocks,
normal_window_size,
normal_first_block_size,
normal_first_block_hash,
})
}
/// Parse import libraries from the optional header data.
/// At this stage, only record addresses are read; ordinals and record types
/// are resolved later by `resolve_imports` once the PE image is decompressed.
fn parse_import_libraries(data: &[u8], headers: &[Xex2OptionalHeader]) -> Vec<ImportLibrary> {
let header = match headers.iter().find(|h| h.key == header_keys::IMPORT_LIBRARIES) {
Some(h) => h,
None => return Vec::new(),
};
let offset = header.value as usize;
if offset + 12 > data.len() {
return Vec::new();
}
fn be_u32(data: &[u8], off: usize) -> u32 {
u32::from_be_bytes([data[off], data[off+1], data[off+2], data[off+3]])
}
fn be_u16(data: &[u8], off: usize) -> u16 {
u16::from_be_bytes([data[off], data[off+1]])
}
let total_size = be_u32(data, offset) as usize;
let string_table_size = be_u32(data, offset + 4) as usize;
let string_count = be_u32(data, offset + 8) as usize;
// Parse string table (null-terminated, 4-byte aligned)
let string_data_start = offset + 12;
let mut strings = Vec::new();
let mut spos = 0usize;
for _ in 0..string_count {
let start = string_data_start + spos;
let mut end = start;
while end < data.len() && data[end] != 0 { end += 1; }
let name = std::str::from_utf8(&data[start..end]).unwrap_or("???").to_string();
spos += name.len() + 1;
// 4-byte alignment
if spos % 4 != 0 { spos += 4 - (spos % 4); }
strings.push(name);
}
// Parse libraries
let mut libs = Vec::new();
let mut lib_off = offset + 12 + string_table_size;
while lib_off + 0x28 <= data.len() && lib_off < offset + total_size {
let lib_size = be_u32(data, lib_off) as usize;
if lib_size == 0 { break; }
let id = be_u32(data, lib_off + 0x18);
let version_cur = be_u32(data, lib_off + 0x1C);
let version_min = be_u32(data, lib_off + 0x20);
let name_index = (be_u16(data, lib_off + 0x24) & 0xFF) as usize;
let count = be_u16(data, lib_off + 0x26) as usize;
let lib_name = strings.get(name_index).cloned().unwrap_or_else(|| format!("lib_{name_index}"));
let mut imports = Vec::new();
for i in 0..count {
let record_addr = be_u32(data, lib_off + 0x28 + i * 4);
imports.push(ImportEntry {
ordinal: 0,
record_type: 0xFF,
address: record_addr,
});
}
libs.push(ImportLibrary {
name: lib_name,
id,
version_min,
version_cur,
imports,
});
lib_off += lib_size;
}
libs
}
/// Resolve import ordinals and record types from the decompressed PE image.
/// Must be called after `load_image` provides the PE data.
pub fn resolve_imports(header: &mut Xex2Header, pe_image: &[u8]) {
let image_base = get_image_base(header).unwrap_or(0);
for lib in &mut header.import_libraries {
for imp in &mut lib.imports {
let pe_off = imp.address.wrapping_sub(image_base) as usize;
if pe_off + 4 <= pe_image.len() {
// PE image values are big-endian (Xbox 360 native)
let val = u32::from_be_bytes([
pe_image[pe_off], pe_image[pe_off+1],
pe_image[pe_off+2], pe_image[pe_off+3],
]);
imp.record_type = ((val >> 24) & 0xFF) as u8;
imp.ordinal = (val & 0xFFFF) as u16;
}
}
}
}
/// Parse execution info from optional header data.
fn parse_execution_info(data: &[u8], headers: &[Xex2OptionalHeader]) -> Option<ExecutionInfo> {
// EXECUTION_INFO key is 0x00040006 — the low byte 0x06 means the value
// is an inline struct of 6 u32 words (24 bytes total).
// Layout: media_id(4), version(4), base_version(4), title_id(4),
// platform(1), exec_type(1), disc_number(1), disc_count(1)
let header = headers.iter().find(|h| h.key == header_keys::EXECUTION_INFO)?;
let off = header.value as usize;
if off + 20 > data.len() {
return None;
}
let media_id = u32::from_be_bytes([data[off], data[off+1], data[off+2], data[off+3]]);
let title_id = u32::from_be_bytes([data[off+12], data[off+13], data[off+14], data[off+15]]);
let disc_number = data[off + 18];
let disc_count = data[off + 19];
Some(ExecutionInfo {
media_id,
title_id,
disc_number,
disc_count,
})
}
/// Parse original PE name from optional header data.
fn parse_original_pe_name(data: &[u8], headers: &[Xex2OptionalHeader]) -> Option<String> {
let header = headers.iter().find(|h| h.key == header_keys::ORIGINAL_PE_NAME)?;
let off = header.value as usize;
if off + 4 > data.len() {
return None;
}
let size = u32::from_be_bytes([data[off], data[off+1], data[off+2], data[off+3]]) as usize;
if off + size > data.len() || size <= 4 {
return None;
}
let name_bytes = &data[off + 4..off + size];
Some(String::from_utf8_lossy(name_bytes).trim_end_matches('\0').to_string())
}
/// Get an optional header value by key.
pub fn get_opt_header(header: &Xex2Header, key: u32) -> Option<u32> {
header.optional_headers.iter()
.find(|h| h.key == key)
.map(|h| h.value)
}
/// Get the entry point address from the XEX2 header.
pub fn get_entry_point(header: &Xex2Header) -> Option<u32> {
get_opt_header(header, header_keys::ENTRY_POINT)
}
/// Get the image base address.
pub fn get_image_base(header: &Xex2Header) -> Option<u32> {
get_opt_header(header, header_keys::IMAGE_BASE_ADDRESS)
}
/// Get the default stack size.
pub fn get_stack_size(header: &Xex2Header) -> u32 {
get_opt_header(header, header_keys::DEFAULT_STACK_SIZE).unwrap_or(0x10_0000) // Default 1MB
}
/// Load the XEX image data into a flat buffer (decompressing if needed).
/// Returns the decompressed image bytes ready to map into guest memory.
pub fn load_image(data: &[u8], header: &Xex2Header) -> io::Result<Vec<u8>> {
let source = &data[header.header_size as usize..];
match &header.file_format_info {
Some(info) if info.compression_type == COMPRESSION_BASIC => {
load_basic_compressed(source, info)
}
Some(info) if info.compression_type == COMPRESSION_NORMAL => {
load_normal_compressed(source, info, header)
}
_ => {
// Uncompressed (or no format info = treat as uncompressed)
Ok(source.to_vec())
}
}
}
/// Load basic compressed image data.
fn load_basic_compressed(source: &[u8], info: &FileFormatInfo) -> io::Result<Vec<u8>> {
// Calculate total uncompressed size
let total_size: u64 = info.basic_blocks.iter()
.map(|b| b.data_size as u64 + b.zero_size as u64)
.sum();
let mut output = vec![0u8; total_size as usize];
let mut src_offset = 0usize;
let mut dst_offset = 0usize;
for block in &info.basic_blocks {
let data_size = block.data_size as usize;
let zero_size = block.zero_size as usize;
if src_offset + data_size > source.len() {
return Err(io::Error::new(
io::ErrorKind::UnexpectedEof,
format!("Basic compression block data extends past end of file (src_offset={:#x}, data_size={:#x}, source_len={:#x})",
src_offset, data_size, source.len()),
));
}
// Copy data block
if dst_offset + data_size <= output.len() {
output[dst_offset..dst_offset + data_size]
.copy_from_slice(&source[src_offset..src_offset + data_size]);
}
src_offset += data_size;
dst_offset += data_size;
// Zero-filled gap (already zeroed from vec initialization)
dst_offset += zero_size;
}
Ok(output)
}
/// Xbox 360 retail AES key for XEX2 session key decryption.
const XEX2_RETAIL_KEY: [u8; 16] = [
0x20, 0xB1, 0x85, 0xA5, 0x9D, 0x28, 0xFD, 0xC3,
0x40, 0x58, 0x3F, 0xBB, 0x08, 0x96, 0xBF, 0x91,
];
/// Xbox 360 devkit AES key (all zeros).
#[allow(dead_code)]
const XEX2_DEVKIT_KEY: [u8; 16] = [0u8; 16];
/// AES-128-CBC decryption with zero IV (matching Xbox 360 XEX decryption).
fn aes_decrypt_cbc(key: &[u8; 16], input: &[u8]) -> Vec<u8> {
let cipher = Aes128::new(key.into());
let mut output = vec![0u8; input.len()];
let mut iv = [0u8; 16];
for (i, chunk) in input.chunks(16).enumerate() {
if chunk.len() < 16 {
// Partial block at end - copy as-is
output[i * 16..i * 16 + chunk.len()].copy_from_slice(chunk);
break;
}
let mut block = aes::Block::clone_from_slice(chunk);
cipher.decrypt_block(&mut block);
// XOR with IV (previous ciphertext block)
for j in 0..16 {
block[j] ^= iv[j];
}
iv.copy_from_slice(chunk);
output[i * 16..(i + 1) * 16].copy_from_slice(&block);
}
output
}
/// Derive the session key by decrypting the XEX's aes_key field with the retail key.
/// Falls back to devkit key if retail produces invalid results.
fn derive_session_key(header: &Xex2Header) -> [u8; 16] {
let sec = match &header.security_info {
Some(s) => s,
None => return [0u8; 16],
};
let decrypted = aes_decrypt_cbc(&XEX2_RETAIL_KEY, &sec.aes_key);
let mut session_key = [0u8; 16];
session_key.copy_from_slice(&decrypted[..16]);
session_key
}
/// De-block compressed data: strip block headers and extract chunk payloads.
///
/// The first block's size comes from the file format header (first_block_size).
/// Each block in the data starts with a block_info struct for the NEXT block:
/// - block_size: u32 BE (size of the next block)
/// - block_hash: [u8; 20] (SHA1 of the next block)
/// Followed by chunks: { chunk_size: u16 BE, data: [u8; chunk_size] }, terminated by chunk_size=0
fn deblock(input: &[u8], first_block_size: u32) -> io::Result<Vec<u8>> {
let mut output = Vec::new();
let mut pos = 0usize;
let mut cur_block_size = first_block_size as usize;
while cur_block_size > 0 && pos < input.len() {
let next_block_pos = pos + cur_block_size;
// Read next block's info from start of current block data
let next_block_size = if pos + 4 <= input.len() {
u32::from_be_bytes([
input[pos], input[pos + 1], input[pos + 2], input[pos + 3],
]) as usize
} else {
0
};
// Skip block_info header (4 bytes size + 20 bytes hash)
let mut p = pos + 4 + 20;
// Read chunks within this block
loop {
if p + 2 > input.len() {
break;
}
let chunk_size = ((input[p] as usize) << 8) | (input[p + 1] as usize);
p += 2;
if chunk_size == 0 {
break;
}
if p + chunk_size > input.len() {
return Err(io::Error::new(
io::ErrorKind::UnexpectedEof,
format!("De-block chunk extends past input (pos={:#x}, chunk_size={:#x}, input_len={:#x})",
p, chunk_size, input.len()),
));
}
output.extend_from_slice(&input[p..p + chunk_size]);
p += chunk_size;
}
if next_block_pos <= pos {
break; // Prevent infinite loop
}
pos = next_block_pos;
cur_block_size = next_block_size;
}
Ok(output)
}
/// Load normal (LZX) compressed image data.
/// Pipeline: decrypt → de-block → LZX decompress (pure Rust)
fn load_normal_compressed(source: &[u8], info: &FileFormatInfo, header: &Xex2Header) -> io::Result<Vec<u8>> {
let uncompressed_size = header.security_info.as_ref()
.map(|s| s.image_size as usize)
.unwrap_or(0);
if uncompressed_size == 0 {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
"Cannot decompress: image_size is 0",
));
}
// Step 1: Decrypt if needed
let decrypted;
let input = if info.encryption_type == ENCRYPTION_NORMAL {
let session_key = derive_session_key(header);
decrypted = aes_decrypt_cbc(&session_key, source);
&decrypted
} else {
source
};
// Step 2: De-block (strip block headers, extract chunk payloads)
let deblocked = deblock(input, info.normal_first_block_size)?;
if deblocked.is_empty() {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
"De-blocking produced no data",
));
}
// Step 3: LZX decompress using pure Rust decoder
let window_bits = match info.normal_window_size {
s if s == 0 => 15, // default
s => (s as f64).log2() as u32,
};
let mut decoder = crate::lzx::LzxDecoder::new(window_bits);
let output = decoder.decompress(&deblocked, uncompressed_size)
.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, format!("LZX decompression failed: {e}")))?;
tracing::info!("LZX decompressed: {} -> {} bytes", deblocked.len(), uncompressed_size);
Ok(output)
}

692
crates/xenia-xex/src/lzx.rs Normal file
View File

@@ -0,0 +1,692 @@
//! LZX decompressor for Xbox 360 XEX2 "normal compression".
//! Ported from libmspack lzxd.c (C) 2003-2013 Stuart Caie, LGPL 2.1.
use std::fmt;
// ── LZX constants ───────────────────────────────────────────────────────────
const LZX_MIN_MATCH: usize = 2;
const LZX_NUM_CHARS: usize = 256;
const LZX_BLOCKTYPE_VERBATIM: u8 = 1;
const LZX_BLOCKTYPE_ALIGNED: u8 = 2;
const LZX_BLOCKTYPE_UNCOMPRESSED: u8 = 3;
const LZX_NUM_PRIMARY_LENGTHS: usize = 7;
const LZX_NUM_SECONDARY_LENGTHS: usize = 249;
const LZX_FRAME_SIZE: usize = 32768;
const HUFF_MAXBITS: usize = 16;
const PRETREE_MAXSYMS: usize = 20;
const PRETREE_TABLEBITS: usize = 6;
const MAINTREE_MAXSYMS: usize = LZX_NUM_CHARS + 290 * 8; // 2576
const MAINTREE_TABLEBITS: usize = 12;
const LENGTH_MAXSYMS: usize = LZX_NUM_SECONDARY_LENGTHS + 1; // 250
const LENGTH_TABLEBITS: usize = 12;
const ALIGNED_MAXSYMS: usize = 8;
const ALIGNED_TABLEBITS: usize = 7;
const LENTABLE_SAFETY: usize = 64;
const BITBUF_WIDTH: u32 = 32;
// ── Static tables ───────────────────────────────────────────────────────────
static POSITION_SLOTS: [u32; 11] = [30, 32, 34, 36, 38, 42, 50, 66, 98, 162, 290];
static EXTRA_BITS: [u8; 36] = [
0, 0, 0, 0, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6,
7, 7, 8, 8, 9, 9, 10, 10, 11, 11, 12, 12, 13, 13, 14, 14,
15, 15, 16, 16,
];
#[rustfmt::skip]
static POSITION_BASE: [u32; 290] = [
0, 1, 2, 3, 4, 6, 8, 12, 16, 24, 32, 48, 64, 96, 128, 192, 256, 384, 512,
768, 1024, 1536, 2048, 3072, 4096, 6144, 8192, 12288, 16384, 24576, 32768,
49152, 65536, 98304, 131072, 196608, 262144, 393216, 524288, 655360,
786432, 917504, 1048576, 1179648, 1310720, 1441792, 1572864, 1703936,
1835008, 1966080, 2097152, 2228224, 2359296, 2490368, 2621440, 2752512,
2883584, 3014656, 3145728, 3276800, 3407872, 3538944, 3670016, 3801088,
3932160, 4063232, 4194304, 4325376, 4456448, 4587520, 4718592, 4849664,
4980736, 5111808, 5242880, 5373952, 5505024, 5636096, 5767168, 5898240,
6029312, 6160384, 6291456, 6422528, 6553600, 6684672, 6815744, 6946816,
7077888, 7208960, 7340032, 7471104, 7602176, 7733248, 7864320, 7995392,
8126464, 8257536, 8388608, 8519680, 8650752, 8781824, 8912896, 9043968,
9175040, 9306112, 9437184, 9568256, 9699328, 9830400, 9961472, 10092544,
10223616, 10354688, 10485760, 10616832, 10747904, 10878976, 11010048,
11141120, 11272192, 11403264, 11534336, 11665408, 11796480, 11927552,
12058624, 12189696, 12320768, 12451840, 12582912, 12713984, 12845056,
12976128, 13107200, 13238272, 13369344, 13500416, 13631488, 13762560,
13893632, 14024704, 14155776, 14286848, 14417920, 14548992, 14680064,
14811136, 14942208, 15073280, 15204352, 15335424, 15466496, 15597568,
15728640, 15859712, 15990784, 16121856, 16252928, 16384000, 16515072,
16646144, 16777216, 16908288, 17039360, 17170432, 17301504, 17432576,
17563648, 17694720, 17825792, 17956864, 18087936, 18219008, 18350080,
18481152, 18612224, 18743296, 18874368, 19005440, 19136512, 19267584,
19398656, 19529728, 19660800, 19791872, 19922944, 20054016, 20185088,
20316160, 20447232, 20578304, 20709376, 20840448, 20971520, 21102592,
21233664, 21364736, 21495808, 21626880, 21757952, 21889024, 22020096,
22151168, 22282240, 22413312, 22544384, 22675456, 22806528, 22937600,
23068672, 23199744, 23330816, 23461888, 23592960, 23724032, 23855104,
23986176, 24117248, 24248320, 24379392, 24510464, 24641536, 24772608,
24903680, 25034752, 25165824, 25296896, 25427968, 25559040, 25690112,
25821184, 25952256, 26083328, 26214400, 26345472, 26476544, 26607616,
26738688, 26869760, 27000832, 27131904, 27262976, 27394048, 27525120,
27656192, 27787264, 27918336, 28049408, 28180480, 28311552, 28442624,
28573696, 28704768, 28835840, 28966912, 29097984, 29229056, 29360128,
29491200, 29622272, 29753344, 29884416, 30015488, 30146560, 30277632,
30408704, 30539776, 30670848, 30801920, 30932992, 31064064, 31195136,
31326208, 31457280, 31588352, 31719424, 31850496, 31981568, 32112640,
32243712, 32374784, 32505856, 32636928, 32768000, 32899072, 33030144,
33161216, 33292288, 33423360,
];
// ── Error type ──────────────────────────────────────────────────────────────
#[derive(Debug)]
pub enum LzxError {
BadHuffmanTable,
Decrunch(String),
}
impl fmt::Display for LzxError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::BadHuffmanTable => write!(f, "failed to build Huffman table"),
Self::Decrunch(msg) => write!(f, "LZX decrunch error: {msg}"),
}
}
}
impl std::error::Error for LzxError {}
// ── Bit reader (MSB order, 16-bit LE pairs) ────────────────────────────────
struct BitReader<'a> {
data: &'a [u8],
pos: usize,
buf: u32,
left: i32,
}
impl<'a> BitReader<'a> {
fn new(data: &'a [u8]) -> Self {
Self { data, pos: 0, buf: 0, left: 0 }
}
/// Inject one 16-bit little-endian pair into MSB bit buffer.
fn fill(&mut self) {
let b0 = if self.pos < self.data.len() {
let b = self.data[self.pos]; self.pos += 1; b as u32
} else { 0 };
let b1 = if self.pos < self.data.len() {
let b = self.data[self.pos]; self.pos += 1; b as u32
} else { 0 };
let word = (b1 << 8) | b0;
self.buf |= word << (16 - self.left as u32);
self.left += 16;
}
#[inline]
fn ensure(&mut self, n: i32) {
while self.left < n { self.fill(); }
}
#[inline]
fn peek(&self, n: u32) -> u32 {
self.buf >> (BITBUF_WIDTH - n)
}
#[inline]
fn remove(&mut self, n: u32) {
self.buf <<= n;
self.left -= n as i32;
}
#[inline]
fn read(&mut self, n: u32) -> u32 {
self.ensure(n as i32);
let v = self.peek(n);
self.remove(n);
v
}
/// Read a raw byte directly (for UNCOMPRESSED blocks).
fn raw_byte(&mut self) -> u8 {
if self.pos < self.data.len() {
let b = self.data[self.pos]; self.pos += 1; b
} else { 0 }
}
/// Re-align the bitstream at a frame boundary.
fn align_frame(&mut self) {
if self.left > 0 { self.ensure(16); }
let r = self.left & 15;
if r != 0 { self.remove(r as u32); }
}
}
// ── Huffman table builder (MSB order) ───────────────────────────────────────
fn make_decode_table(
nsyms: usize,
nbits: usize,
length: &[u8],
table: &mut [u16],
) -> bool {
let mut pos: usize = 0;
let table_mask = 1usize << nbits;
let mut bit_mask = table_mask >> 1;
// Short codes: direct mapping
for bit_num in 1..=nbits {
for sym in 0..nsyms {
if length[sym] as usize != bit_num { continue; }
let leaf = pos;
pos += bit_mask;
if pos > table_mask { return true; }
for i in leaf..leaf + bit_mask {
table[i] = sym as u16;
}
}
bit_mask >>= 1;
}
if pos == table_mask { return false; }
// Mark remaining entries as unused
for i in pos..table_mask {
table[i] = 0xFFFF;
}
let mut next_symbol = if (table_mask >> 1) < nsyms { nsyms } else { table_mask >> 1 };
let mut pos32 = (pos as u32) << 16;
let table_mask32 = (table_mask as u32) << 16;
let mut bit_mask32: u32 = 1 << 15;
// Long codes: tree traversal
for bit_num in (nbits + 1)..=HUFF_MAXBITS {
for sym in 0..nsyms {
if length[sym] as usize != bit_num { continue; }
if pos32 >= table_mask32 { return true; }
let mut leaf = (pos32 >> 16) as usize;
for fill in 0..(bit_num - nbits) {
if table[leaf] == 0xFFFF {
table[next_symbol << 1] = 0xFFFF;
table[(next_symbol << 1) + 1] = 0xFFFF;
table[leaf] = next_symbol as u16;
next_symbol += 1;
}
leaf = (table[leaf] as usize) << 1;
if (pos32 >> (15 - fill as u32)) & 1 != 0 {
leaf += 1;
}
}
table[leaf] = sym as u16;
pos32 += bit_mask32;
}
bit_mask32 >>= 1;
}
pos32 != table_mask32
}
// ── Huffman symbol decoder ──────────────────────────────────────────────────
fn read_huffsym(
br: &mut BitReader,
table: &[u16],
lens: &[u8],
tablebits: usize,
maxsyms: usize,
) -> Result<usize, LzxError> {
br.ensure(HUFF_MAXBITS as i32);
let mut sym = table[br.peek(tablebits as u32) as usize] as usize;
if sym >= maxsyms {
let mut i: u32 = 1 << (BITBUF_WIDTH - tablebits as u32);
loop {
i >>= 1;
if i == 0 { return Err(LzxError::BadHuffmanTable); }
sym = table[(sym << 1) | if br.buf & i != 0 { 1 } else { 0 }] as usize;
if sym < maxsyms { break; }
}
}
br.remove(lens[sym] as u32);
Ok(sym)
}
// ── LZX decoder state ───────────────────────────────────────────────────────
pub struct LzxDecoder {
window: Vec<u8>,
window_size: usize,
window_posn: usize,
frame_posn: usize,
frame: usize,
num_offsets: usize,
r0: u32,
r1: u32,
r2: u32,
block_type: u8,
block_length: usize,
block_remaining: usize,
header_read: bool,
intel_filesize: i32,
intel_curpos: i32,
intel_started: bool,
// Huffman code lengths
pretree_len: Vec<u8>,
maintree_len: Vec<u8>,
length_len: Vec<u8>,
aligned_len: Vec<u8>,
// Huffman decode tables
pretree_table: Vec<u16>,
maintree_table: Vec<u16>,
length_table: Vec<u16>,
aligned_table: Vec<u16>,
length_empty: bool,
}
impl LzxDecoder {
pub fn new(window_bits: u32) -> Self {
assert!((15..=21).contains(&window_bits));
let window_size = 1usize << window_bits;
let num_offsets = (POSITION_SLOTS[(window_bits - 15) as usize] as usize) << 3;
Self {
window: vec![0u8; window_size],
window_size,
window_posn: 0,
frame_posn: 0,
frame: 0,
num_offsets,
r0: 1, r1: 1, r2: 1,
block_type: 0,
block_length: 0,
block_remaining: 0,
header_read: false,
intel_filesize: 0,
intel_curpos: 0,
intel_started: false,
pretree_len: vec![0u8; PRETREE_MAXSYMS + LENTABLE_SAFETY],
maintree_len: vec![0u8; MAINTREE_MAXSYMS + LENTABLE_SAFETY],
length_len: vec![0u8; LENGTH_MAXSYMS + LENTABLE_SAFETY],
aligned_len: vec![0u8; ALIGNED_MAXSYMS + LENTABLE_SAFETY],
pretree_table: vec![0u16; (1 << PRETREE_TABLEBITS) + PRETREE_MAXSYMS * 2],
maintree_table: vec![0u16; (1 << MAINTREE_TABLEBITS) + MAINTREE_MAXSYMS * 2],
length_table: vec![0u16; (1 << LENGTH_TABLEBITS) + LENGTH_MAXSYMS * 2],
aligned_table: vec![0u16; (1 << ALIGNED_TABLEBITS) + ALIGNED_MAXSYMS * 2],
length_empty: false,
}
}
fn build_table(
lens: &[u8], table: &mut [u16], maxsyms: usize, tablebits: usize,
) -> Result<(), LzxError> {
if make_decode_table(maxsyms, tablebits, lens, table) {
Err(LzxError::BadHuffmanTable)
} else {
Ok(())
}
}
fn build_table_maybe_empty(
lens: &[u8], table: &mut [u16], maxsyms: usize, tablebits: usize,
) -> Result<bool, LzxError> {
if make_decode_table(maxsyms, tablebits, lens, table) {
// Check if table is simply empty (all lengths zero)
for i in 0..maxsyms {
if lens[i] > 0 {
return Err(LzxError::BadHuffmanTable);
}
}
Ok(true) // empty
} else {
Ok(false) // not empty
}
}
/// Read Huffman code lengths using the pretree (lzxd_read_lens).
fn read_lens(
br: &mut BitReader,
lens: &mut [u8],
pretree_len: &mut [u8],
pretree_table: &mut [u16],
first: usize,
last: usize,
) -> Result<(), LzxError> {
// Build pretree: 20 symbols, 4 bits each
for i in 0..20 {
pretree_len[i] = br.read(4) as u8;
}
Self::build_table(pretree_len, pretree_table, PRETREE_MAXSYMS, PRETREE_TABLEBITS)?;
let mut x = first;
while x < last {
let z = read_huffsym(br, pretree_table, pretree_len, PRETREE_TABLEBITS, PRETREE_MAXSYMS)?;
if z == 17 {
// Run of zeros: [read 4 bits] + 4
let mut y = br.read(4) as usize + 4;
while y > 0 && x < last { lens[x] = 0; x += 1; y -= 1; }
} else if z == 18 {
// Run of zeros: [read 5 bits] + 20
let mut y = br.read(5) as usize + 20;
while y > 0 && x < last { lens[x] = 0; x += 1; y -= 1; }
} else if z == 19 {
// Run of same: [read 1 bit] + 4, then read symbol
let mut y = br.read(1) as usize + 4;
let z2 = read_huffsym(br, pretree_table, pretree_len, PRETREE_TABLEBITS, PRETREE_MAXSYMS)?;
let mut val = lens[x] as i32 - z2 as i32;
if val < 0 { val += 17; }
while y > 0 && x < last { lens[x] = val as u8; x += 1; y -= 1; }
} else {
// Delta: code 0..16
let mut val = lens[x] as i32 - z as i32;
if val < 0 { val += 17; }
lens[x] = val as u8;
x += 1;
}
}
Ok(())
}
/// Decompress the full LZX stream into the output buffer.
pub fn decompress(&mut self, input: &[u8], output_len: usize) -> Result<Vec<u8>, LzxError> {
let mut br = BitReader::new(input);
let mut output = Vec::with_capacity(output_len);
let mut offset: usize = 0;
let end_frame = (output_len / LZX_FRAME_SIZE) + 1;
while self.frame < end_frame {
// Read header once
if !self.header_read {
let i_bit = br.read(1);
let (hi, lo) = if i_bit != 0 {
(br.read(16), br.read(16))
} else {
(0, 0)
};
self.intel_filesize = ((hi << 16) | lo) as i32;
self.header_read = true;
}
// Frame size
let frame_size = if output_len > 0 && (output_len - offset) < LZX_FRAME_SIZE {
output_len - offset
} else {
LZX_FRAME_SIZE
};
let mut bytes_todo = (self.frame_posn + frame_size).wrapping_sub(self.window_posn) as i32;
while bytes_todo > 0 {
// New block?
if self.block_remaining == 0 {
// Realign after odd UNCOMPRESSED block
if self.block_type == LZX_BLOCKTYPE_UNCOMPRESSED && (self.block_length & 1) != 0 {
br.raw_byte();
}
// Read block type (3 bits) and length (24 bits)
self.block_type = br.read(3) as u8;
let hi = br.read(16) as usize;
let lo = br.read(8) as usize;
self.block_length = (hi << 8) | lo;
self.block_remaining = self.block_length;
match self.block_type {
LZX_BLOCKTYPE_ALIGNED => {
for i in 0..8 { self.aligned_len[i] = br.read(3) as u8; }
Self::build_table(&self.aligned_len, &mut self.aligned_table, ALIGNED_MAXSYMS, ALIGNED_TABLEBITS)?;
// Fall through to verbatim tree reading
Self::read_lens(&mut br, &mut self.maintree_len, &mut self.pretree_len, &mut self.pretree_table, 0, 256)?;
Self::read_lens(&mut br, &mut self.maintree_len, &mut self.pretree_len, &mut self.pretree_table, 256, LZX_NUM_CHARS + self.num_offsets)?;
Self::build_table(&self.maintree_len, &mut self.maintree_table, MAINTREE_MAXSYMS, MAINTREE_TABLEBITS)?;
if self.maintree_len[0xE8] != 0 { self.intel_started = true; }
Self::read_lens(&mut br, &mut self.length_len, &mut self.pretree_len, &mut self.pretree_table, 0, LZX_NUM_SECONDARY_LENGTHS)?;
self.length_empty = Self::build_table_maybe_empty(&self.length_len, &mut self.length_table, LENGTH_MAXSYMS, LENGTH_TABLEBITS)?;
}
LZX_BLOCKTYPE_VERBATIM => {
Self::read_lens(&mut br, &mut self.maintree_len, &mut self.pretree_len, &mut self.pretree_table, 0, 256)?;
Self::read_lens(&mut br, &mut self.maintree_len, &mut self.pretree_len, &mut self.pretree_table, 256, LZX_NUM_CHARS + self.num_offsets)?;
Self::build_table(&self.maintree_len, &mut self.maintree_table, MAINTREE_MAXSYMS, MAINTREE_TABLEBITS)?;
if self.maintree_len[0xE8] != 0 { self.intel_started = true; }
Self::read_lens(&mut br, &mut self.length_len, &mut self.pretree_len, &mut self.pretree_table, 0, LZX_NUM_SECONDARY_LENGTHS)?;
self.length_empty = Self::build_table_maybe_empty(&self.length_len, &mut self.length_table, LENGTH_MAXSYMS, LENGTH_TABLEBITS)?;
}
LZX_BLOCKTYPE_UNCOMPRESSED => {
self.intel_started = true;
// Align to byte boundary
if br.left == 0 { br.ensure(16); }
br.left = 0;
br.buf = 0;
// Read R0, R1, R2 (12 bytes, little-endian u32s)
let mut buf = [0u8; 12];
for b in &mut buf { *b = br.raw_byte(); }
self.r0 = u32::from_le_bytes([buf[0], buf[1], buf[2], buf[3]]);
self.r1 = u32::from_le_bytes([buf[4], buf[5], buf[6], buf[7]]);
self.r2 = u32::from_le_bytes([buf[8], buf[9], buf[10], buf[11]]);
}
_ => return Err(LzxError::Decrunch("bad block type".into())),
}
}
let mut this_run = self.block_remaining as i32;
if this_run > bytes_todo { this_run = bytes_todo; }
bytes_todo -= this_run;
self.block_remaining -= this_run as usize;
let window_size = self.window_size;
match self.block_type {
LZX_BLOCKTYPE_VERBATIM => {
while this_run > 0 {
let main_element = read_huffsym(&mut br, &self.maintree_table, &self.maintree_len, MAINTREE_TABLEBITS, MAINTREE_MAXSYMS)?;
if main_element < LZX_NUM_CHARS {
self.window[self.window_posn] = main_element as u8;
self.window_posn += 1;
this_run -= 1;
} else {
let me = main_element - LZX_NUM_CHARS;
let mut match_length = me & LZX_NUM_PRIMARY_LENGTHS;
if match_length == LZX_NUM_PRIMARY_LENGTHS {
if self.length_empty { return Err(LzxError::Decrunch("LENGTH tree empty".into())); }
let footer = read_huffsym(&mut br, &self.length_table, &self.length_len, LENGTH_TABLEBITS, LENGTH_MAXSYMS)?;
match_length += footer;
}
match_length += LZX_MIN_MATCH;
let mut match_offset = (me >> 3) as u32;
match match_offset {
0 => match_offset = self.r0,
1 => { match_offset = self.r1; self.r1 = self.r0; self.r0 = match_offset; }
2 => { match_offset = self.r2; self.r2 = self.r0; self.r0 = match_offset; }
3 => { match_offset = 1; self.r2 = self.r1; self.r1 = self.r0; self.r0 = match_offset; }
_ => {
let extra = if match_offset >= 36 { 17 } else { EXTRA_BITS[match_offset as usize] as u32 };
let verbatim_bits = br.read(extra);
match_offset = POSITION_BASE[match_offset as usize] - 2 + verbatim_bits;
self.r2 = self.r1; self.r1 = self.r0; self.r0 = match_offset;
}
}
if self.window_posn + match_length > window_size {
return Err(LzxError::Decrunch("match overrun".into()));
}
self.copy_match(match_offset as usize, match_length);
this_run -= match_length as i32;
}
}
}
LZX_BLOCKTYPE_ALIGNED => {
while this_run > 0 {
let main_element = read_huffsym(&mut br, &self.maintree_table, &self.maintree_len, MAINTREE_TABLEBITS, MAINTREE_MAXSYMS)?;
if main_element < LZX_NUM_CHARS {
self.window[self.window_posn] = main_element as u8;
self.window_posn += 1;
this_run -= 1;
} else {
let me = main_element - LZX_NUM_CHARS;
let mut match_length = me & LZX_NUM_PRIMARY_LENGTHS;
if match_length == LZX_NUM_PRIMARY_LENGTHS {
if self.length_empty { return Err(LzxError::Decrunch("LENGTH tree empty".into())); }
let footer = read_huffsym(&mut br, &self.length_table, &self.length_len, LENGTH_TABLEBITS, LENGTH_MAXSYMS)?;
match_length += footer;
}
match_length += LZX_MIN_MATCH;
let mut match_offset = (me >> 3) as u32;
match match_offset {
0 => match_offset = self.r0,
1 => { match_offset = self.r1; self.r1 = self.r0; self.r0 = match_offset; }
2 => { match_offset = self.r2; self.r2 = self.r0; self.r0 = match_offset; }
_ => {
let extra = if match_offset >= 36 { 17 } else { EXTRA_BITS[match_offset as usize] as u32 };
match_offset = POSITION_BASE[match_offset as usize] - 2;
if extra > 3 {
let verbatim_bits = br.read(extra - 3);
match_offset += verbatim_bits << 3;
let aligned = read_huffsym(&mut br, &self.aligned_table, &self.aligned_len, ALIGNED_TABLEBITS, ALIGNED_MAXSYMS)?;
match_offset += aligned as u32;
} else if extra == 3 {
let aligned = read_huffsym(&mut br, &self.aligned_table, &self.aligned_len, ALIGNED_TABLEBITS, ALIGNED_MAXSYMS)?;
match_offset += aligned as u32;
} else if extra > 0 {
let verbatim_bits = br.read(extra);
match_offset += verbatim_bits;
} else {
match_offset = 1;
}
self.r2 = self.r1; self.r1 = self.r0; self.r0 = match_offset;
}
}
if self.window_posn + match_length > window_size {
return Err(LzxError::Decrunch("match overrun".into()));
}
self.copy_match(match_offset as usize, match_length);
this_run -= match_length as i32;
}
}
}
LZX_BLOCKTYPE_UNCOMPRESSED => {
let run = this_run as usize;
for _ in 0..run {
self.window[self.window_posn] = br.raw_byte();
self.window_posn += 1;
}
}
_ => return Err(LzxError::Decrunch("bad block type in decode".into())),
}
// Overrun accounting
if this_run < 0 {
let overrun = (-this_run) as usize;
if overrun > self.block_remaining {
return Err(LzxError::Decrunch("overrun past block end".into()));
}
self.block_remaining -= overrun;
}
}
// Frame boundary check
if (self.window_posn.wrapping_sub(self.frame_posn)) != frame_size {
return Err(LzxError::Decrunch(format!(
"decode beyond frame: {} != {}", self.window_posn - self.frame_posn, frame_size
)));
}
// Re-align bitstream
br.align_frame();
// Intel E8 postprocessing
if self.intel_started && self.intel_filesize != 0
&& self.frame <= 32768 && frame_size > 10
{
let mut e8_buf = vec![0u8; frame_size];
e8_buf.copy_from_slice(&self.window[self.frame_posn..self.frame_posn + frame_size]);
let mut i = 0usize;
let limit = frame_size - 10;
let mut curpos = self.intel_curpos;
let filesize = self.intel_filesize;
while i < limit {
if e8_buf[i] != 0xE8 { i += 1; curpos += 1; continue; }
let abs_off = e8_buf[i+1] as i32
| (e8_buf[i+2] as i32) << 8
| (e8_buf[i+3] as i32) << 16
| (e8_buf[i+4] as i32) << 24;
if abs_off >= -curpos && abs_off < filesize {
let rel_off = if abs_off >= 0 { abs_off - curpos } else { abs_off + filesize };
e8_buf[i+1] = rel_off as u8;
e8_buf[i+2] = (rel_off >> 8) as u8;
e8_buf[i+3] = (rel_off >> 16) as u8;
e8_buf[i+4] = (rel_off >> 24) as u8;
}
i += 5;
curpos += 5;
}
self.intel_curpos += frame_size as i32;
let to_write = frame_size.min(output_len - offset);
output.extend_from_slice(&e8_buf[..to_write]);
offset += to_write;
} else {
if self.intel_filesize != 0 { self.intel_curpos += frame_size as i32; }
let to_write = frame_size.min(output_len - offset);
output.extend_from_slice(&self.window[self.frame_posn..self.frame_posn + to_write]);
offset += to_write;
}
// Advance frame
self.frame_posn += frame_size;
self.frame += 1;
if self.window_posn == self.window_size { self.window_posn = 0; }
if self.frame_posn == self.window_size { self.frame_posn = 0; }
}
Ok(output)
}
/// Copy a match from the window (handles wrap-around).
fn copy_match(&mut self, match_offset: usize, match_length: usize) {
let window_size = self.window_size;
let mut remaining = match_length;
if match_offset > self.window_posn {
// Source wraps around window end
let j = match_offset - self.window_posn;
let mut src = window_size - j;
if j < remaining {
remaining -= j;
for _ in 0..j {
self.window[self.window_posn] = self.window[src];
self.window_posn += 1;
src += 1;
}
src = 0; // wrap to start
}
for _ in 0..remaining {
self.window[self.window_posn] = self.window[src];
self.window_posn += 1;
src += 1;
}
} else {
let mut src = self.window_posn - match_offset;
for _ in 0..remaining {
self.window[self.window_posn] = self.window[src];
self.window_posn += 1;
src += 1;
}
}
}
}

View File

@@ -0,0 +1,68 @@
//! Minimal PE parser for Xbox 360 executables.
//! PE headers are little-endian even on the big-endian Xbox 360.
use serde::Serialize;
#[derive(Serialize, Debug, Clone)]
pub struct PeSection {
pub name: String,
pub virtual_address: u32,
pub virtual_size: u32,
pub raw_offset: u32,
pub raw_size: u32,
pub flags: u32,
}
impl PeSection {
pub fn is_code(&self) -> bool {
self.flags & 0x20000000 != 0 // IMAGE_SCN_MEM_EXECUTE
}
}
fn le_u16(data: &[u8], off: usize) -> u16 {
u16::from_le_bytes([data[off], data[off + 1]])
}
fn le_u32(data: &[u8], off: usize) -> u32 {
u32::from_le_bytes([data[off], data[off + 1], data[off + 2], data[off + 3]])
}
pub fn parse_sections(pe: &[u8]) -> anyhow::Result<Vec<PeSection>> {
anyhow::ensure!(pe.len() >= 64, "PE too small");
anyhow::ensure!(pe[0] == b'M' && pe[1] == b'Z', "not a PE (bad MZ)");
let e_lfanew = le_u32(pe, 0x3C) as usize;
anyhow::ensure!(e_lfanew + 4 <= pe.len(), "e_lfanew out of bounds");
let nt_sig = le_u32(pe, e_lfanew);
anyhow::ensure!(nt_sig == 0x00004550, "bad PE signature: 0x{nt_sig:08X}");
let file_header_off = e_lfanew + 4;
let num_sections = le_u16(pe, file_header_off + 2) as usize;
let opt_header_size = le_u16(pe, file_header_off + 16) as usize;
let section_table_off = file_header_off + 20 + opt_header_size;
let mut sections = Vec::new();
for i in 0..num_sections {
let s = section_table_off + i * 40;
if s + 40 > pe.len() { break; }
let name_bytes = &pe[s..s + 8];
let name = std::str::from_utf8(name_bytes)
.unwrap_or("???")
.trim_end_matches('\0')
.to_string();
sections.push(PeSection {
name,
virtual_size: le_u32(pe, s + 8),
virtual_address: le_u32(pe, s + 12),
raw_size: le_u32(pe, s + 16),
raw_offset: le_u32(pe, s + 20),
flags: le_u32(pe, s + 36),
});
}
Ok(sections)
}