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:
763
crates/xenia-kernel/src/exports.rs
Normal file
763
crates/xenia-kernel/src/exports.rs
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user