[Rust] Implement FPU/VMX128 opcodes, XEX LZX decompression, XISO browsing, and memory safety
Some checks failed
Orchestrator / Commit Message Validation (push) Has been skipped
Orchestrator / Lint (push) Successful in 2m5s
Orchestrator / Windows (x86-64) (push) Failing after 6m13s
Orchestrator / Linux (x86-64) (push) Failing after 20m59s
Orchestrator / Create Release (push) Has been skipped
Some checks failed
Orchestrator / Commit Message Validation (push) Has been skipped
Orchestrator / Lint (push) Successful in 2m5s
Orchestrator / Windows (x86-64) (push) Failing after 6m13s
Orchestrator / Linux (x86-64) (push) Failing after 20m59s
Orchestrator / Create Release (push) Has been skipped
Major additions to the xenia-rs Rust port: - CPU: ~170 new PPC opcode implementations (FPU, VMX128, 64-bit ALU, load/store variants) - XEX: Full LZX (normal) decompression pipeline with AES-128-CBC decryption via mspack FFI - XEX: Parse file format info, import libraries, and security info AES key from headers - VFS: Rewrite XISO disc image to use seek-based I/O (handles 7GB+ images without loading into memory) - App: Auto-detect ISO files and extract default.xex for all CLI commands - App: Add `info` and `browse` CLI subcommands - Kernel: Expand HLE exports from 14 to 40 stubs (memory, threading, TLS, I/O, video) - Memory: Add bounds checking on all guest memory accesses to prevent segfaults - Types: Add Vec128 array-based accessors (from_u32x4_array, from_f32x4_array, etc.) Tested against Project Sylpheed (USA) disc image - all four CLI commands (browse, info, disasm, exec) work correctly. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -55,8 +55,23 @@ fn main() -> Result<()> {
|
||||
}
|
||||
}
|
||||
|
||||
/// 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 = std::fs::read(path)?;
|
||||
let data = load_xex_data(path)?;
|
||||
let header = xenia_xex::loader::parse_xex2_header(&data)?;
|
||||
|
||||
println!("=== XEX2 Header ===");
|
||||
@@ -85,11 +100,34 @@ fn cmd_info(path: &str) -> Result<()> {
|
||||
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 !header.import_libraries.is_empty() {
|
||||
println!("\n=== Import Libraries ===");
|
||||
for lib in &header.import_libraries {
|
||||
println!(" {} (v{:#010x}, {} ordinals)", lib.name, lib.version_cur, lib.ordinals.len());
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn cmd_disasm(path: &str, count: usize) -> Result<()> {
|
||||
let data = std::fs::read(path)?;
|
||||
let data = load_xex_data(path)?;
|
||||
let header = xenia_xex::loader::parse_xex2_header(&data)?;
|
||||
|
||||
let entry = xenia_xex::loader::get_entry_point(&header)
|
||||
@@ -98,24 +136,27 @@ fn cmd_disasm(path: &str, count: usize) -> Result<()> {
|
||||
.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);
|
||||
|
||||
// For now, disassemble from the raw file data at the entry offset
|
||||
let entry_offset = (entry - base) as usize + header.header_size as usize;
|
||||
if entry_offset + count * 4 <= data.len() {
|
||||
let block = xenia_cpu::disasm::disassemble_block(&data[entry_offset..], entry, 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 file bounds)", entry_offset);
|
||||
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: u64) -> Result<()> {
|
||||
let data = std::fs::read(path)?;
|
||||
let data = load_xex_data(path)?;
|
||||
let header = xenia_xex::loader::parse_xex2_header(&data)?;
|
||||
|
||||
let entry = xenia_xex::loader::get_entry_point(&header)
|
||||
@@ -123,21 +164,38 @@ fn cmd_exec(path: &str, max_instructions: u64) -> Result<()> {
|
||||
let base = xenia_xex::loader::get_image_base(&header)
|
||||
.ok_or_else(|| anyhow::anyhow!("No image base found"))?;
|
||||
|
||||
// Print compression info
|
||||
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!(" {} ({} ordinals)", lib.name, lib.ordinals.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))?;
|
||||
|
||||
// Map the XEX image into guest memory
|
||||
let image_data = &data[header.header_size as usize..];
|
||||
// Load and decompress the XEX image
|
||||
let image_data = xenia_xex::loader::load_image(&data, &header)?;
|
||||
let alloc_size = ((image_data.len() + 4095) & !4095) as u32;
|
||||
mem.alloc(
|
||||
base,
|
||||
alloc_size,
|
||||
xenia_memory::page_table::MemoryProtect::READ | xenia_memory::page_table::MemoryProtect::WRITE,
|
||||
).map_err(|e| anyhow::anyhow!("Failed to allocate guest memory region: {}", e))?;
|
||||
mem.write_bulk(base, image_data);
|
||||
mem.write_bulk(base, &image_data);
|
||||
|
||||
// Allocate stack (1MB at 0x70000000)
|
||||
let stack_base = 0x7000_0000u32;
|
||||
@@ -174,6 +232,12 @@ fn cmd_exec(path: &str, max_instructions: u64) -> Result<()> {
|
||||
break;
|
||||
}
|
||||
|
||||
// Check if PC is in mapped memory before trying to execute
|
||||
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);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user