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,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, "")
}
}