// Copyright (C) 2026 SharpEmu Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later using SharpEmu.Core; using SharpEmu.Core.Cpu; using SharpEmu.Core.Cpu.Disasm; using SharpEmu.Core.Loader; using SharpEmu.Core.Memory; using SharpEmu.HLE; using SharpEmu.Libs.VideoOut; using SharpEmu.Libs.Kernel; using SharpEmu.Libs.AppContent; using SharpEmu.Libs.SaveData; using SharpEmu.Logging; using System.Buffers.Binary; using System.Collections.Generic; using System.Linq; using System.Text; using System.IO; namespace SharpEmu.Core.Runtime; public sealed class SharpEmuRuntime : ISharpEmuRuntime { private static readonly SharpEmuLogger Log = SharpEmuLog.For("Runtime"); private readonly record struct LoadedModuleImage(string Path, SelfImage Image); private static readonly HashSet PreloadSkipModules = new(StringComparer.OrdinalIgnoreCase) { "libkernel.prx", "libkernel_sys.prx", }; private readonly ISelfLoader _selfLoader; private readonly IVirtualMemory _virtualMemory; private readonly ICpuDispatcher _cpuDispatcher; private readonly IModuleManager _moduleManager; private readonly ISymbolCatalog _symbolCatalog; private readonly CpuExecutionOptions _cpuExecutionOptions; private readonly IFileSystem _fileSystem; private bool _disposed; public string? LastExecutionDiagnostics { get; private set; } public string? LastExecutionTrace { get; private set; } public string? LastSessionSummary { get; private set; } public string? LastBasicBlockTrace { get; private set; } public string? LastMilestoneLog { get; private set; } public SharpEmuRuntime( ISelfLoader selfLoader, IVirtualMemory virtualMemory, ICpuDispatcher cpuDispatcher, IModuleManager moduleManager, ISymbolCatalog? symbolCatalog = null, CpuExecutionOptions cpuExecutionOptions = default, IFileSystem? fileSystem = null) { _selfLoader = selfLoader ?? throw new ArgumentNullException(nameof(selfLoader)); _virtualMemory = virtualMemory ?? throw new ArgumentNullException(nameof(virtualMemory)); _cpuDispatcher = cpuDispatcher ?? throw new ArgumentNullException(nameof(cpuDispatcher)); _moduleManager = moduleManager ?? throw new ArgumentNullException(nameof(moduleManager)); _symbolCatalog = symbolCatalog ?? Aerolib.Empty; _cpuExecutionOptions = new CpuExecutionOptions { CpuEngine = cpuExecutionOptions.CpuEngine, StrictDynlibResolution = cpuExecutionOptions.StrictDynlibResolution, ImportTraceLimit = Math.Max(0, cpuExecutionOptions.ImportTraceLimit), }; _fileSystem = fileSystem ?? new PhysicalFileSystem(); } public static ISharpEmuRuntime CreateDefault(SharpEmuRuntimeOptions options = default) { var cpuExecutionOptions = new CpuExecutionOptions { CpuEngine = options.CpuEngine, StrictDynlibResolution = options.StrictDynlibResolution, ImportTraceLimit = Math.Max(0, options.ImportTraceLimit), }; var moduleManager = new ModuleManager(); moduleManager.RegisterFromAssembly(typeof(VideoOutExports).Assembly, Generation.Gen4 | Generation.Gen5, Aerolib.Instance); moduleManager.RegisterFromAssembly(typeof(KernelExports).Assembly, Generation.Gen4 | Generation.Gen5, Aerolib.Instance); moduleManager.Freeze(); var virtualMemory = new PhysicalVirtualMemory(); var fileSystem = new PhysicalFileSystem(); return new SharpEmuRuntime( new SelfLoader(), virtualMemory, new CpuDispatcher(virtualMemory, moduleManager), moduleManager, Aerolib.Instance, cpuExecutionOptions, fileSystem); } public SelfImage LoadImage(string ebootPath) { ArgumentException.ThrowIfNullOrWhiteSpace(ebootPath); var fullPath = Path.GetFullPath(ebootPath); if (!File.Exists(fullPath)) { throw new FileNotFoundException("Executable file was not found.", fullPath); } var fileInfo = new FileInfo(fullPath); if (fileInfo.Length > int.MaxValue) { throw new NotSupportedException("Images larger than 2 GB are not currently supported."); } var bytes = GC.AllocateUninitializedArray((int)fileInfo.Length); using (var stream = File.OpenRead(fullPath)) { stream.ReadExactly(bytes); } var mountRoot = Path.GetDirectoryName(fullPath); return _selfLoader.Load(bytes.AsSpan(), _virtualMemory, _moduleManager, _fileSystem, mountRoot); } public OrbisGen2Result Run(string ebootPath) { var normalizedEbootPath = Path.GetFullPath(ebootPath); using var app0Binding = BindApp0Root(normalizedEbootPath); Log.Info($"Loading: {ebootPath}"); LastExecutionDiagnostics = null; LastExecutionTrace = null; LastSessionSummary = null; LastBasicBlockTrace = null; LastMilestoneLog = null; KernelModuleRegistry.Reset(); var image = LoadImage(normalizedEbootPath); VideoOutExports.ConfigureApplicationInfo(image.Title, image.TitleId, image.Version); SaveDataExports.ConfigureApplicationInfo(image.TitleId); LogAppBundleInfo(normalizedEbootPath, image); RegisterLoadedModule(normalizedEbootPath, image, isMain: true, isSystemModule: false); KernelRuntimeCompatExports.ConfigureProcessProcParamAddress(image.ProcParamAddress); Log.Info($"Entry: 0x{image.EntryPoint:X16}"); var generation = image.ElfHeader.AbiVersion == 2 ? Generation.Gen5 : Generation.Gen4; var activeImportStubs = new Dictionary(image.ImportStubs); var activeRuntimeSymbols = new Dictionary(image.RuntimeSymbols, StringComparer.Ordinal); var processImageName = Path.GetFileName(ebootPath); if (string.IsNullOrWhiteSpace(processImageName)) { processImageName = "eboot.bin"; } HleDataSymbols.ConfigureProcessImageName(processImageName); MergeKnownHleDataSymbols(activeRuntimeSymbols); var loadedModuleImages = LoadAdjacentSceModules(ebootPath, activeImportStubs, activeRuntimeSymbols); RebindImportedDataSymbols(image, loadedModuleImages, activeRuntimeSymbols); var initializerResult = RunAllInitializers( image, loadedModuleImages, generation, activeImportStubs, activeRuntimeSymbols, processImageName); if (initializerResult is { } failedInitializerResult) { Log.Error($"Initializer dispatch failed: {failedInitializerResult}"); LastExecutionTrace = _cpuDispatcher.LastImportResolutionTrace; LastMilestoneLog = _cpuDispatcher.LastMilestoneLog; LastSessionSummary = BuildSessionSummary(_cpuDispatcher.LastSessionSummary); LastBasicBlockTrace = _cpuDispatcher.LastBasicBlockTrace; return failedInitializerResult; } Log.Info($"Dispatching, gen: {generation}"); Log.Debug($"About to call DispatchEntry with entryPoint=0x{image.EntryPoint:X16}"); var result = _cpuDispatcher.DispatchEntry( image.EntryPoint, generation, activeImportStubs, activeRuntimeSymbols, processImageName, _cpuExecutionOptions); Log.Info($"DispatchEntry returned: {result}"); Log.Info($"Dispatch result: {result}"); LastExecutionTrace = _cpuDispatcher.LastImportResolutionTrace; LastMilestoneLog = _cpuDispatcher.LastMilestoneLog; LastSessionSummary = BuildSessionSummary(_cpuDispatcher.LastSessionSummary); LastBasicBlockTrace = _cpuDispatcher.LastBasicBlockTrace; if (result == OrbisGen2Result.ORBIS_GEN2_ERROR_CPU_TRAP && _cpuDispatcher.LastTrapInfo is { } trapInfo) { var opcodeBytes = ReadOpcodePreview(trapInfo.InstructionPointer, 8); var decodedTrapText = string.Empty; var ud2Hint = string.Empty; if (_cpuExecutionOptions.EnableDisasmDiagnostics && TryDecodeInstructionAt(trapInfo.InstructionPointer, out var trapInstruction)) { decodedTrapText = BuildDecodedInstructionFields(in trapInstruction); if (string.Equals(trapInstruction.Mnemonic, "Ud2", StringComparison.OrdinalIgnoreCase)) { ud2Hint = ", trap=ud2"; } } else if (opcodeBytes.StartsWith("0F 0B", StringComparison.Ordinal)) { ud2Hint = ", trap=ud2"; } var longModeHint = IsInvalidLongModeOpcode(trapInfo.Opcode) ? ", hint=invalid opcode for x64 long mode; likely wrong jump target or decode desync" : string.Empty; var hint = string.Empty; if (image.IsSelf && activeImportStubs.Count == 0 && trapInfo.InstructionPointer == 0 && trapInfo.Opcode == 0xCC) { hint = ", hint=SELF appears encrypted or unresolved; use a decrypted ELF/FSELF image"; } var transferText = string.Empty; if (_cpuDispatcher.LastControlTransferInfo is { } transferInfo) { var transferMode = transferInfo.IsIndirect ? "indirect" : "direct"; var transferBytes = ReadOpcodePreview(transferInfo.SourceInstructionPointer, 8); var transferDecodedText = TryDecodeInstructionAt(transferInfo.SourceInstructionPointer, out var transferInstruction) ? BuildDecodedInstructionFields(in transferInstruction, fieldPrefix: "src_inst") : string.Empty; transferText = $", last_transfer={transferInfo.Kind}:{transferMode} src=0x{transferInfo.SourceInstructionPointer:X16} op=0x{transferInfo.Opcode:X2} bytes={transferBytes} dst=0x{transferInfo.TargetInstructionPointer:X16}{transferDecodedText}"; } var ripStubText = activeImportStubs.TryGetValue(trapInfo.InstructionPointer, out var trapStubNid) ? $", rip_stub={trapStubNid}" : string.Empty; var diagnosticsBuilder = new StringBuilder(1024); diagnosticsBuilder.Append( $"CPU trap at RIP=0x{trapInfo.InstructionPointer:X16}, opcode=0x{trapInfo.Opcode:X2}, bytes={opcodeBytes}{decodedTrapText}, import_stubs={activeImportStubs.Count}{ud2Hint}{longModeHint}{hint}{ripStubText}{transferText}"); if (!string.IsNullOrWhiteSpace(_cpuDispatcher.LastRecentControlTransferTrace)) { diagnosticsBuilder.AppendLine(); diagnosticsBuilder.Append("Recent transfers:"); diagnosticsBuilder.AppendLine(); diagnosticsBuilder.Append(_cpuDispatcher.LastRecentControlTransferTrace); } if (!string.IsNullOrWhiteSpace(_cpuDispatcher.LastRecentInstructionWindow)) { diagnosticsBuilder.AppendLine(); diagnosticsBuilder.Append("Recent instructions:"); diagnosticsBuilder.AppendLine(); diagnosticsBuilder.Append(_cpuDispatcher.LastRecentInstructionWindow); } LastExecutionDiagnostics = diagnosticsBuilder.ToString(); } else if (result == OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT && _cpuDispatcher.LastMemoryFaultInfo is { } faultInfo) { var opcodeText = faultInfo.Opcode.HasValue ? $"0x{faultInfo.Opcode.Value:X2}" : "??"; var decodedFaultText = string.Empty; if (_cpuExecutionOptions.EnableDisasmDiagnostics && TryDecodeInstructionAt(faultInfo.InstructionPointer, out var faultInstruction)) { decodedFaultText = BuildDecodedInstructionFields(in faultInstruction); if (!faultInfo.Opcode.HasValue && faultInstruction.Bytes.Length > 0) { opcodeText = $"0x{faultInstruction.Bytes[0]:X2}"; } } var accessType = faultInfo.Access.IsWrite ? "write" : "read"; var transferText = string.Empty; if (_cpuDispatcher.LastControlTransferInfo is { } transferInfo) { var transferMode = transferInfo.IsIndirect ? "indirect" : "direct"; var transferBytes = ReadOpcodePreview(transferInfo.SourceInstructionPointer, 8); var transferDecodedText = TryDecodeInstructionAt(transferInfo.SourceInstructionPointer, out var transferInstruction) ? BuildDecodedInstructionFields(in transferInstruction, fieldPrefix: "src_inst") : string.Empty; var rbxTarget = TryReadUInt64At(transferInfo.Rbx, out var rbxDeref) ? $"*rbx=0x{rbxDeref:X16}" : "*rbx=??"; transferText = $", last_transfer={transferInfo.Kind}:{transferMode} src=0x{transferInfo.SourceInstructionPointer:X16} op=0x{transferInfo.Opcode:X2} bytes={transferBytes} dst=0x{transferInfo.TargetInstructionPointer:X16}{transferDecodedText} rax=0x{transferInfo.Rax:X16} rbx=0x{transferInfo.Rbx:X16} {rbxTarget} rsp=0x{transferInfo.Rsp:X16} rbp=0x{transferInfo.Rbp:X16}"; } var ripStubText = activeImportStubs.TryGetValue(faultInfo.InstructionPointer, out var faultStubNid) ? $", rip_stub={faultStubNid}" : string.Empty; LastExecutionDiagnostics = $"Memory fault at RIP=0x{faultInfo.InstructionPointer:X16}, opcode={opcodeText}{decodedFaultText}, {accessType}@0x{faultInfo.Access.Address:X16} size={faultInfo.Access.Size}, import_stubs={activeImportStubs.Count}{ripStubText}{transferText}"; } else if (result == OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_IMPLEMENTED && _cpuDispatcher.LastNotImplementedInfo is { } notImplementedInfo) { var inferredNid = notImplementedInfo.Nid; var decodedNotImplementedText = TryDecodeInstructionAt(notImplementedInfo.InstructionPointer, out var notImplementedInstruction) ? BuildDecodedInstructionFields(in notImplementedInstruction) : string.Empty; var ripStubText = string.Empty; if (activeImportStubs.TryGetValue(notImplementedInfo.InstructionPointer, out var ripStubNid)) { ripStubText = $", rip_stub={ripStubNid}"; if (string.IsNullOrWhiteSpace(inferredNid)) { inferredNid = ripStubNid; } } var inferredExportName = notImplementedInfo.ExportName; var inferredLibraryName = notImplementedInfo.LibraryName; if (!string.IsNullOrWhiteSpace(inferredNid) && _moduleManager.TryGetExport(inferredNid, out var export)) { inferredExportName = string.IsNullOrWhiteSpace(inferredExportName) ? export.Name : inferredExportName; inferredLibraryName = string.IsNullOrWhiteSpace(inferredLibraryName) ? export.LibraryName : inferredLibraryName; } var nidText = string.IsNullOrWhiteSpace(inferredNid) ? "?" : inferredNid; var exportText = string.IsNullOrWhiteSpace(inferredExportName) ? "?" : inferredExportName; var libraryText = string.IsNullOrWhiteSpace(inferredLibraryName) ? "?" : inferredLibraryName; var detailText = string.IsNullOrWhiteSpace(notImplementedInfo.Detail) ? string.Empty : $", detail={notImplementedInfo.Detail}"; var transferText = string.Empty; if (_cpuDispatcher.LastControlTransferInfo is { } transferInfo) { var transferMode = transferInfo.IsIndirect ? "indirect" : "direct"; var transferBytes = ReadOpcodePreview(transferInfo.SourceInstructionPointer, 8); var transferDecodedText = TryDecodeInstructionAt(transferInfo.SourceInstructionPointer, out var transferInstruction) ? BuildDecodedInstructionFields(in transferInstruction, fieldPrefix: "src_inst") : string.Empty; var transferStubText = activeImportStubs.TryGetValue(transferInfo.TargetInstructionPointer, out var transferStubNid) ? $" stub={transferStubNid}" : string.Empty; transferText = $", last_transfer={transferInfo.Kind}:{transferMode} src=0x{transferInfo.SourceInstructionPointer:X16} op=0x{transferInfo.Opcode:X2} bytes={transferBytes} dst=0x{transferInfo.TargetInstructionPointer:X16}{transferDecodedText}{transferStubText}"; } var aerolibText = string.Empty; if (!string.IsNullOrWhiteSpace(inferredExportName) && _symbolCatalog.TryGetByExportName(inferredExportName, out var symbol)) { aerolibText = $", aerolib_nid={symbol.Nid}"; } else if (!string.IsNullOrWhiteSpace(inferredNid) && _symbolCatalog.TryGetByNid(inferredNid, out var symbolByNid)) { aerolibText = $", aerolib_export={symbolByNid.ExportName}"; } LastExecutionDiagnostics = $"Not implemented: source={notImplementedInfo.Source}, rip=0x{notImplementedInfo.InstructionPointer:X16}{decodedNotImplementedText}, nid={nidText}, export={exportText}, library={libraryText}, import_stubs={activeImportStubs.Count}{ripStubText}{aerolibText}{detailText}{transferText}"; } return result; } private static void LogAppBundleInfo(string ebootPath, SelfImage image) { var executableName = Path.GetFileName(ebootPath); if (string.IsNullOrWhiteSpace(executableName)) { executableName = "eboot.bin"; } var displayName = string.IsNullOrWhiteSpace(image.Title) ? "(unknown)" : image.Title!.Trim(); var titleId = string.IsNullOrWhiteSpace(image.TitleId) ? "(unknown)" : image.TitleId!.Trim(); var version = string.IsNullOrWhiteSpace(image.Version) ? "(unknown)" : image.Version!.Trim(); var contentId = ResolveContentId(Path.GetDirectoryName(ebootPath)) ?? "(unknown)"; var builder = new StringBuilder(); builder.AppendLine("App bundle info:"); builder.AppendLine($"- Display name: {displayName}"); builder.AppendLine($"- Version: {version}"); builder.AppendLine($"- Title ID: {titleId}"); builder.AppendLine($"- Content ID: {contentId}"); builder.AppendLine($"- Executable: {executableName}"); builder.Append("- Platform: PlayStation 5"); Log.Info(builder.ToString()); } private static string? ResolveContentId(string? bundleRoot) { if (string.IsNullOrEmpty(bundleRoot)) { return null; } foreach (var candidate in new[] { Path.Combine(bundleRoot, "sce_sys", "param.json"), Path.Combine(bundleRoot, "param.json"), }) { if (!File.Exists(candidate)) { continue; } try { using var doc = System.Text.Json.JsonDocument.Parse(File.ReadAllBytes(candidate)); if (doc.RootElement.TryGetProperty("contentId", out var contentId)) { return contentId.GetString(); } } catch (Exception ex) when (ex is IOException or System.Text.Json.JsonException) { } break; } return null; } private static App0BindingScope? BindApp0Root(string normalizedEbootPath) { const string app0VariableName = "SHARPEMU_APP0_DIR"; if (!string.IsNullOrWhiteSpace(Environment.GetEnvironmentVariable(app0VariableName))) { return null; } var app0Root = Path.GetDirectoryName(normalizedEbootPath); if (string.IsNullOrWhiteSpace(app0Root)) { return null; } Environment.SetEnvironmentVariable(app0VariableName, app0Root); return new App0BindingScope(app0VariableName); } private sealed class App0BindingScope(string variableName) : IDisposable { private readonly string _variableName = variableName; private bool _disposed; public void Dispose() { if (_disposed) { return; } Environment.SetEnvironmentVariable(_variableName, null); _disposed = true; } } private OrbisGen2Result? RunAllInitializers( SelfImage mainImage, IReadOnlyList loadedModuleImages, Generation generation, IReadOnlyDictionary activeImportStubs, IReadOnlyDictionary activeRuntimeSymbols, string processImageName) { var moduleStartResult = RunPreloadedModuleInitializers( loadedModuleImages, generation, activeImportStubs, activeRuntimeSymbols); if (moduleStartResult is not null) { return moduleStartResult; } // On current PS5 dumps DT_INIT commonly resolves to imageBase+0x10, which is inside // the mapped ELF header rather than a callable guest routine. Startup must remain // guest-driven until the PS5 init/module ABI is identified precisely. return null; } private OrbisGen2Result? RunPreloadedModuleInitializers( IReadOnlyList loadedModuleImages, Generation generation, IReadOnlyDictionary activeImportStubs, IReadOnlyDictionary activeRuntimeSymbols) { for (var i = 0; i < loadedModuleImages.Count; i++) { var loadedModule = loadedModuleImages[i]; var initEntryPoint = loadedModule.Image.InitFunctionEntryPoint; if (initEntryPoint < 0x10000) { continue; } var moduleName = Path.GetFileName(loadedModule.Path); if (string.IsNullOrWhiteSpace(moduleName)) { moduleName = $"module#{i}"; } Log.Info( $"Starting module {moduleName}: dt_init=0x{initEntryPoint:X16}"); var result = _cpuDispatcher.DispatchModuleInitializer( initEntryPoint, generation, activeImportStubs, activeRuntimeSymbols, moduleName, _cpuExecutionOptions); if (result != OrbisGen2Result.ORBIS_GEN2_OK) { Log.Error( $"Module start failed: {moduleName} -> {result}"); return result; } } return null; } private OrbisGen2Result? RunImageInitializers( string label, SelfImage image, Generation generation, IReadOnlyDictionary activeImportStubs, IReadOnlyDictionary activeRuntimeSymbols, string processImageName) { if (image.PreInitializerFunctions.Count == 0 && image.InitializerFunctions.Count == 0) { return null; } Log.Info( $"Running initializers for {label}: preinit={image.PreInitializerFunctions.Count}, init={image.InitializerFunctions.Count}"); var result = RunInitializerList( $"{label}:preinit", image.PreInitializerFunctions, generation, activeImportStubs, activeRuntimeSymbols, processImageName); if (result is not null) { return result; } return RunInitializerList( $"{label}:init", image.InitializerFunctions, generation, activeImportStubs, activeRuntimeSymbols, processImageName); } private OrbisGen2Result? RunInitializerList( string label, IReadOnlyList initializerFunctions, Generation generation, IReadOnlyDictionary activeImportStubs, IReadOnlyDictionary activeRuntimeSymbols, string processImageName) { for (var i = 0; i < initializerFunctions.Count; i++) { var initializerAddress = initializerFunctions[i]; if (initializerAddress < 0x10000) { continue; } Log.Debug( $" Initializer {label}[{i}] -> 0x{initializerAddress:X16}"); var result = _cpuDispatcher.DispatchEntry( initializerAddress, generation, activeImportStubs, activeRuntimeSymbols, processImageName, _cpuExecutionOptions); if (result != OrbisGen2Result.ORBIS_GEN2_OK) { return result; } } return null; } private List LoadAdjacentSceModules( string ebootPath, IDictionary importStubs, IDictionary runtimeSymbols) { var loadedImages = new List(); var ebootDirectory = Path.GetDirectoryName(ebootPath); if (string.IsNullOrWhiteSpace(ebootDirectory)) { return loadedImages; } var moduleDirectories = new[] { Path.Combine(ebootDirectory, "sce_module"), Path.Combine(ebootDirectory, "sce_modules"), Path.Combine(ebootDirectory, "Media", "Modules"), } .Distinct(StringComparer.OrdinalIgnoreCase) .Where(Directory.Exists) .ToArray(); if (moduleDirectories.Length == 0) { return loadedImages; } var allModulePaths = moduleDirectories .SelectMany(directory => Directory .EnumerateFiles(directory) .OrderBy(path => path, StringComparer.OrdinalIgnoreCase)) .Where(path => { var extension = Path.GetExtension(path); return string.Equals(extension, ".prx", StringComparison.OrdinalIgnoreCase) || string.Equals(extension, ".sprx", StringComparison.OrdinalIgnoreCase); }) .Distinct(StringComparer.OrdinalIgnoreCase) .ToArray(); var modulePaths = allModulePaths .Where(ShouldPreloadModule) .ToArray(); var skippedModules = allModulePaths .Where(path => !ShouldPreloadModule(path)) .Select(Path.GetFileName) .Where(name => !string.IsNullOrWhiteSpace(name)) .ToArray(); if (skippedModules.Length > 0) { Log.Info($"Skipping {skippedModules.Length} core module(s): {string.Join(", ", skippedModules)}"); } if (modulePaths.Length == 0) { return loadedImages; } Log.Debug($"Module search directories: {string.Join(", ", moduleDirectories)}"); Log.Info($"Loading {modulePaths.Length} module(s)..."); var loadedModules = 0; var failedModules = 0; var mergedImportCount = 0; var mergedSymbolCount = 0; foreach (var modulePath in modulePaths) { try { var fileInfo = new FileInfo(modulePath); if (!fileInfo.Exists || fileInfo.Length <= 0 || fileInfo.Length > int.MaxValue) { failedModules++; continue; } var moduleBytes = GC.AllocateUninitializedArray((int)fileInfo.Length); using (var stream = File.OpenRead(modulePath)) { stream.ReadExactly(moduleBytes); } var moduleImage = _selfLoader.LoadAdditional( moduleBytes.AsSpan(), _virtualMemory, _moduleManager, _fileSystem, Path.GetDirectoryName(modulePath)); mergedImportCount += MergeImportStubs(importStubs, moduleImage.ImportStubs, modulePath); mergedSymbolCount += MergeRuntimeSymbols(runtimeSymbols, moduleImage.RuntimeSymbols); RegisterLoadedModule(modulePath, moduleImage, isMain: false, isSystemModule: false); loadedImages.Add(new LoadedModuleImage(modulePath, moduleImage)); loadedModules++; Log.Info( $"Loaded module {Path.GetFileName(modulePath)}: entry=0x{moduleImage.EntryPoint:X16}, imports={moduleImage.ImportStubs.Count}, symbols={moduleImage.RuntimeSymbols.Count}"); } catch (Exception ex) { failedModules++; Log.Error($"Module load failed: {modulePath} ({ex.GetType().Name}: {ex.Message})", ex); } } Log.Info( $"Module preload summary: loaded={loadedModules}, failed={failedModules}, merged_imports={mergedImportCount}, merged_symbols={mergedSymbolCount}"); return loadedImages; } private void RebindImportedDataSymbols( SelfImage mainImage, IReadOnlyList loadedModuleImages, IReadOnlyDictionary runtimeSymbols) { var rebound = 0; var unresolved = 0; rebound += RebindImportedDataSymbols(mainImage, runtimeSymbols, ref unresolved); for (var i = 0; i < loadedModuleImages.Count; i++) { rebound += RebindImportedDataSymbols(loadedModuleImages[i].Image, runtimeSymbols, ref unresolved); } if (rebound != 0 || unresolved != 0) { Log.Info( $"Imported data rebind: rebound={rebound}, unresolved={unresolved}"); } } private int RebindImportedDataSymbols( SelfImage image, IReadOnlyDictionary runtimeSymbols, ref int unresolved) { if (image.ImportedRelocations.Count == 0) { return 0; } var rebound = 0; var logRebind = string.Equals( Environment.GetEnvironmentVariable("SHARPEMU_LOG_DATA_REBIND"), "1", StringComparison.Ordinal); for (var i = 0; i < image.ImportedRelocations.Count; i++) { var relocation = image.ImportedRelocations[i]; if (!relocation.IsData) { continue; } if (!runtimeSymbols.TryGetValue(relocation.Nid, out var symbolAddress) || !IsUsableRuntimeSymbolAddress(symbolAddress)) { if (logRebind) { Log.Warning( $"Imported data unresolved: nid={relocation.Nid} target=0x{relocation.TargetAddress:X16} addend=0x{unchecked((ulong)relocation.Addend):X16}"); } unresolved++; continue; } var reboundValue = AddSigned(symbolAddress, relocation.Addend); if (!TryWriteUInt64(_virtualMemory, relocation.TargetAddress, reboundValue)) { if (logRebind) { Log.Error( $"Imported data write-failed: nid={relocation.Nid} target=0x{relocation.TargetAddress:X16} value=0x{reboundValue:X16}"); } unresolved++; continue; } if (logRebind) { Log.Debug( $"Imported data rebound: nid={relocation.Nid} target=0x{relocation.TargetAddress:X16} value=0x{reboundValue:X16}"); } rebound++; } return rebound; } private static void MergeKnownHleDataSymbols(IDictionary runtimeSymbols) { foreach (var nid in HleDataSymbols.EnumerateKnownNids()) { if (runtimeSymbols.ContainsKey(nid) || !HleDataSymbols.TryGetAddress(nid, out var symbolAddress) || !IsUsableRuntimeSymbolAddress(symbolAddress)) { continue; } runtimeSymbols[nid] = symbolAddress; } } private static int MergeImportStubs( IDictionary destination, IReadOnlyDictionary source, string modulePath) { var added = 0; foreach (var (address, nid) in source) { if (destination.TryGetValue(address, out var existingNid)) { if (!string.Equals(existingNid, nid, StringComparison.Ordinal)) { Log.Warning( $"Import stub conflict at 0x{address:X16}: keep={existingNid}, skip={nid} ({Path.GetFileName(modulePath)})"); } continue; } destination[address] = nid; added++; } return added; } private static int MergeRuntimeSymbols( IDictionary destination, IReadOnlyDictionary source) { var added = 0; foreach (var (name, address) in source) { if (string.IsNullOrWhiteSpace(name) || !IsUsableRuntimeSymbolAddress(address)) { continue; } if (destination.TryGetValue(name, out var existingAddress)) { if (IsPreferredRuntimeSymbolAddress(existingAddress, address)) { destination[name] = address; added++; } continue; } destination[name] = address; added++; } return added; } private static bool IsPreferredRuntimeSymbolAddress(ulong existingAddress, ulong candidateAddress) { return !IsUsableRuntimeSymbolAddress(existingAddress) && IsUsableRuntimeSymbolAddress(candidateAddress); } private static bool IsUsableRuntimeSymbolAddress(ulong address) { return address >= 0x10000 && !IsUnresolvedRuntimeSentinel(address); } private static bool TryWriteUInt64(IVirtualMemory virtualMemory, ulong address, ulong value) { Span bytes = stackalloc byte[sizeof(ulong)]; BinaryPrimitives.WriteUInt64LittleEndian(bytes, value); return virtualMemory.TryWrite(address, bytes); } private static ulong AddSigned(ulong value, long addend) { if (addend >= 0) { return unchecked(value + (ulong)addend); } var magnitude = unchecked((ulong)(-(addend + 1))) + 1; return unchecked(value - magnitude); } private static bool IsUnresolvedRuntimeSentinel(ulong value) { return value == 0xFFFEUL || value == 0xFFFFFFFEUL || value == 0xFFFFFFFFFFFFFFFEUL; } private static bool ShouldPreloadModule(string modulePath) { if (string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_PRELOAD_ALL_SCE_MODULES"), "1", StringComparison.Ordinal)) { return true; } var fileName = Path.GetFileName(modulePath); if (string.IsNullOrWhiteSpace(fileName)) { return false; } return !PreloadSkipModules.Contains(fileName); } private static void RegisterLoadedModule(string modulePath, SelfImage image, bool isMain, bool isSystemModule) { if (!TryComputeImageRange(image, out var baseAddress, out var size)) { baseAddress = 0; size = 0; } var handle = KernelModuleRegistry.RegisterModule( modulePath, baseAddress, size, image.EntryPoint, isMain, isSystemModule); Log.Info( $"Registered module handle={handle} name={Path.GetFileName(modulePath)} base=0x{baseAddress:X16} size=0x{size:X16}"); } private static bool TryComputeImageRange(SelfImage image, out ulong baseAddress, out ulong size) { baseAddress = 0; size = 0; if (image.ProgramHeaders.Count == 0) { return false; } var imageBase = image.EntryPoint >= image.ElfHeader.EntryPoint ? image.EntryPoint - image.ElfHeader.EntryPoint : 0UL; var found = false; ulong minAddress = ulong.MaxValue; ulong maxAddress = 0; for (var i = 0; i < image.ProgramHeaders.Count; i++) { var header = image.ProgramHeaders[i]; if (header.HeaderType != ProgramHeaderType.Load || header.MemorySize == 0) { continue; } var segmentStart = unchecked(imageBase + header.VirtualAddress); var segmentEnd = unchecked(segmentStart + header.MemorySize); if (!found || segmentStart < minAddress) { minAddress = segmentStart; } if (!found || segmentEnd > maxAddress) { maxAddress = segmentEnd; } found = true; } if (!found || maxAddress <= minAddress) { return false; } baseAddress = minAddress; size = maxAddress - minAddress; return true; } private static string BuildSessionSummary(CpuSessionSummary summary) { var resultText = summary.Result == OrbisGen2Result.ORBIS_GEN2_OK ? "OK" : summary.Result.ToString(); var exitText = summary.ExitCode.HasValue ? summary.ExitCode.Value.ToString() : "?"; return $"Summary: result={resultText} reason={summary.Reason} exit={exitText} last_guest_rip=0x{summary.LastGuestRip:X16} last_stub_rip=0x{summary.LastStubRip:X16} instr={summary.TotalInstructions} imports={summary.ImportsHit} unique_nids={summary.UniqueNidsHit}"; } private string ReadOpcodePreview(ulong instructionPointer, int maxBytes) { if (maxBytes <= 0) { return "??"; } Span oneByte = stackalloc byte[1]; var parts = new string[maxBytes]; var count = 0; for (var i = 0; i < maxBytes; i++) { if (!_virtualMemory.TryRead(instructionPointer + (ulong)i, oneByte)) { break; } parts[count] = oneByte[0].ToString("X2"); count++; } return count == 0 ? "??" : string.Join(' ', parts, 0, count); } private bool TryReadUInt64At(ulong address, out ulong value) { Span buffer = stackalloc byte[sizeof(ulong)]; if (!_virtualMemory.TryRead(address, buffer)) { value = 0; return false; } value = BinaryPrimitives.ReadUInt64LittleEndian(buffer); return true; } private bool TryDecodeInstructionAt(ulong instructionPointer, out DecodedInst decodedInstruction) { if (!IcedDecoder.TryReadGuestBytes(_virtualMemory, instructionPointer, maxLen: 15, out var instructionBytes)) { decodedInstruction = default; return false; } return IcedDecoder.TryDecode(instructionPointer, instructionBytes, out decodedInstruction); } private static bool IsInvalidLongModeOpcode(byte opcode) { return opcode is 0x06 or // PUSH ES 0x07 or // POP ES 0x0E or // PUSH CS 0x16 or // PUSH SS 0x17 or // POP SS 0x1E or // PUSH DS 0x1F or // POP DS 0x27 or // DAA 0x2F or // DAS 0x37 or // AAA 0x3F or // AAS 0x60 or // PUSHA/PUSHAD 0x61 or // POPA/POPAD 0xD4 or // AAM 0xD5; // AAD } private static string BuildDecodedInstructionFields(in DecodedInst instruction, string fieldPrefix = "inst") { var text = $", {fieldPrefix}={instruction.Text}, {fieldPrefix}_len={instruction.Length}, {fieldPrefix}_mnemonic={instruction.Mnemonic}, {fieldPrefix}_flow={instruction.FlowControl}"; if (instruction.NearBranchTarget is { } target) { text += $", {fieldPrefix}_target=0x{target:X16}"; } if (instruction.MemoryAddress is { } memoryAddress) { text += $", {fieldPrefix}_mem=0x{memoryAddress:X16}"; } if (instruction.Bytes.Length > 0) { text += $", {fieldPrefix}_bytes={IcedDecoder.FormatBytes(instruction.Bytes)}"; } return text; } public OrbisGen2Result DispatchHleCall(string nid, CpuContext context) { return _moduleManager.Dispatch(nid, context); } public void Dispose() { if (_disposed) { return; } _disposed = true; if (_cpuDispatcher is IDisposable disposableDispatcher) { disposableDispatcher.Dispose(); } if (_virtualMemory is IDisposable disposableMemory) { disposableMemory.Dispose(); } } }