mirror of
https://github.com/par274/sharpemu.git
synced 2026-07-26 04:39:17 +08:00
f23161be9a
* [Host] Introduce host platform abstraction with IHostMemory Add SharpEmu.HLE/Host with IHostPlatform/IHostMemory interfaces, neutral page-protection/region enums, and a HostPlatform.Current factory that resolves the Windows backend (or throws PlatformNotSupportedException on other OSes, matching today's de-facto behavior). WindowsHostMemory wraps the exact VirtualAlloc/VirtualFree/VirtualProtect/VirtualQuery calls used across the engine today, with identical MEM_*/PAGE_* constants. Migrate StubManager as the first consumer: its private kernel32 P/Invokes and enums are replaced by IHostMemory calls that issue the same two native operations (RWX commit+reserve of the PLT arena, release on Dispose). No behavior change. This is the first step toward supporting non-Windows hosts; subsequent commits move the remaining direct P/Invokes in Core and Libs behind the same seam. * [Host] Route PhysicalVirtualMemory through IHostMemory Replace the class's private VirtualAlloc/VirtualFree/VirtualProtect/ VirtualQuery P/Invokes with IHostMemory calls. Every site maps 1:1 onto the exact native call it issued before: MEM_COMMIT|MEM_RESERVE -> Allocate, MEM_RESERVE -> Reserve, fault-path commits -> Commit, and MEM_RELEASE -> Free, with identical protection values produced by the Windows backend. IHostMemory gains ProtectRaw so the save/restore protection sequences in TryWriteExclusive and TryTemporarilyProtectForRead round-trip the raw OS protection word (including modifier bits the neutral enum cannot represent) exactly as before. Raw PAGE_* constants remain only for the internal region-classification helpers, which only ever see values this class itself assigned. The exact-address free-on-mismatch, lazy reserve-only threshold, prime loop, and all trace strings are unchanged. * [Host] Add IGuestAddressSpace and retire the reflection-based allocator lookup Introduce IGuestAddressSpace in SharpEmu.HLE (fixed-address AllocateAt / TryAllocateAtOrAbove and guest mprotect via TryProtect) with signatures copied from PhysicalVirtualMemory, which now implements it. TryProtect reproduces the read/write/execute decomposition that KernelMemoryCompatExports.ResolveHostProtection performs, yielding the same PAGE_* values through the Windows backend. KernelVirtualRangeAllocator previously located AllocateAt via cached MethodInfo reflection (because SharpEmu.Libs cannot see Core types) and walked wrapper memories through an untyped 'Inner' property. Both are now typed: ICpuMemoryWrapper exposes the decorated memory (implemented by TrackedCpuMemory, whose Inner property already existed) and the allocator type-tests for IGuestAddressSpace with the same bounded unwrap depth. Failure paths keep the exact [LOADER][TRACE] strings. * [Host] Move Kernel HLE memory exports off direct kernel32 P/Invokes KernelMemoryCompatExports loses its private VirtualQuery/VirtualProtect/ VirtualAlloc/VirtualFree declarations and MemoryBasicInformation struct: - Guest mprotect (sceKernelMprotect/sceKernelMtypeprotect) now routes through IGuestAddressSpace.TryProtect resolved from ctx.Memory. The orbis read/write/execute decomposition moves into a GuestPageProtection conversion whose mapping is value-identical to the removed ResolveHostProtection. - The guarded libc heap and host-page accessibility checks go through IHostMemory (same commit+reserve/protect/free sequence; guard-page and protection-mask checks compare HostRegionInfo.RawProtection against the same PAGE_* literals as before). - HostMemory is exposed as a property so merely loading the type never resolves the platform backend on non-Windows hosts. KernelRuntimeCompatExports' RDTSC stub allocates its 16-byte RWX page via IHostMemory.Allocate; the OperatingSystem.IsWindows() gate returning null is unchanged. * [Host] Abstract thread, TLS, and symbol primitives in the execution backend Add IHostThreading (native TLS slots, current-thread id, affinity, raw thread create/join, diagnostic register capture) and IHostSymbolResolver (enum-keyed host function addresses baked into emitted stubs), with Windows implementations wrapping the exact kernel32 calls the backend made directly before. DirectExecutionBackend takes an optional IHostPlatform (defaulting to HostPlatform.Current) and routes every TlsAlloc/TlsFree/TlsSet/GetValue, GetCurrentThreadId, SetThreadAffinityMask, GetModuleHandle/GetProcAddress and the suspend+GetThreadContext diagnostic snapshot through it. The snapshot moves wholesale into WindowsHostThreading (including the Win64 CONTEXT size/flags/offsets, which are Windows-specific by nature) and returns a neutral HostCapturedRegisters. NativeGuestExecutor resolves WaitForSingleObject/SetEvent/ExitThread via the symbol resolver — the same addresses end up in the emitted run loop, so stub bytes are unchanged — and creates/joins its raw worker thread through IHostThreading with the same stack-reservation semantics. The run-loop emitter itself does not move. Marshal.GetLastWin32Error() in the affinity-failure log still observes SetThreadAffinityMask's error because the wrapper makes no intervening SetLastError call. * [Host] Move fault handling and remaining backend memory ops behind the seam Add IHostFaultHandling (handler-thunk creation, first-chance handler install/remove, unhandled-filter set) with WindowsFaultHandling in a new Cpu/Native/Windows/ folder. The exception-handler trampoline emitter moves there whole — same pre-filtered NTSTATUS codes, same TEB gs:[8]/ gs:[0x10] stack-limit reads, same host-RSP TLS switch — parameterized only by (managed callback, TLS slot, TlsGetValue address), which is exactly what SetupExceptionHandler passed it before. Handler installation order, the AddVectoredExceptionHandler(first=1) flag, the SHARPEMU_DISABLE_RAW_HANDLER gate, and all install/teardown log strings are unchanged. Every remaining VirtualAlloc/VirtualProtect/VirtualFree/VirtualQuery/ FlushInstructionCache in the backend partials routes through IHostMemory with 1:1 call mapping (RWX emit -> RX downgrade -> flush for stub emission, reserve/commit for the PRT aperture and lazy-commit fault path, raw-protection round-trips via ProtectRaw). HostRegionInfo gains RawState/RawAllocationProtection so the lazy-commit trace lines and protection-mask checks keep printing and comparing the exact native values. Windows semantics leaked as bare literals become named constants with identical values: NTSTATUS codes (WindowsFaultCodes) and Win64 CONTEXT byte offsets (Win64ContextOffsets, with the existing CTX_* constants aliased to it and handler-local numeric offsets replaced by the names). * [Host] Resolve the host platform explicitly at the composition root SharpEmuRuntime.CreateDefault() now resolves HostPlatform.Current once and passes it explicitly to PhysicalVirtualMemory and (via a new optional CpuDispatcher parameter) to DirectExecutionBackend, replacing the implicit default-argument fallbacks. On unsupported OSes boot now fails at the root with PlatformNotSupportedException and a clear message instead of on the first native call. A future Linux/macOS backend plugs in by returning a different IHostPlatform here. * [Host] Convert the platform backends to source-generated P/Invokes Replace [DllImport] with [LibraryImport] in the four Windows backend files added by this branch (WindowsHostMemory, WindowsHostThreading, WindowsHostSymbolResolver, WindowsFaultHandling). Marshalling stubs are now generated at compile time instead of JIT-emitted at runtime, which fits the pre-JIT-everything boot model and keeps the backends NativeAOT/trimming ready. Interop stays zero-copy: all signatures are blittable, GetModuleHandleW now pins the managed string via Utf16 marshalling instead of copying, and GetProcAddress names marshal through a stack-allocated Utf8 buffer. Implicit contracts become explicit where LibraryImport requires it: TlsFree/TlsSetValue gain [MarshalAs(UnmanagedType.Bool)] (the 4-byte Win32 BOOL DllImport assumed silently), and GetModuleHandle targets the W entry point directly since LibraryImport never probes suffixes. The CONTEXT snapshot buffer stays a NativeMemory allocation rather than stackalloc: CONTEXT requires 16-byte alignment, now documented at the call site. Native call sequences are unchanged. * [Host] Address Copilot review: harden failure paths, honor injected platform - Free the handler thunk page when the RX protection downgrade fails (the leak predates this branch, but the failure path is boot-fatal so releasing the page is unobservable). - TraceThreadMode and the static diagnostics helpers now resolve host primitives through the backend bound to the current thread, falling back to HostPlatform.Current only when no run is active (identical on supported configs, honors injection everywhere a backend exists). - HostPlatform.Create additionally requires an x64 process so native Windows ARM64 fails with the promised PlatformNotSupportedException instead of emitting x86-64 stubs into an ARM64 process.
1172 lines
36 KiB
C#
1172 lines
36 KiB
C#
// Copyright (C) 2026 SharpEmu Emulator Project
|
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
|
|
using System;
|
|
using System.Buffers.Binary;
|
|
using System.Collections.Generic;
|
|
using System.Globalization;
|
|
using System.Reflection;
|
|
using System.Runtime.InteropServices;
|
|
using System.Threading;
|
|
using SharpEmu.Core.Cpu.Disasm;
|
|
using SharpEmu.Core.Cpu.Native.Windows;
|
|
using SharpEmu.HLE;
|
|
using SharpEmu.HLE.Host;
|
|
|
|
namespace SharpEmu.Core.Cpu.Native;
|
|
|
|
public sealed partial class DirectExecutionBackend
|
|
{
|
|
private const ulong LazyCommitWindowBytes = 0x0200_0000UL;
|
|
private static int _lazyCommitTraceCount;
|
|
|
|
private unsafe void SetupExceptionHandler()
|
|
{
|
|
if (!string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_DISABLE_RAW_HANDLER"), "1", StringComparison.Ordinal))
|
|
{
|
|
_rawExceptionHandlerStub = _faultHandling.CreateHandlerThunk(RawVectoredHandlerPtrManaged, _hostRspSlotTlsIndex, _tlsGetValueAddress);
|
|
if (_rawExceptionHandlerStub == 0)
|
|
{
|
|
throw new InvalidOperationException("Failed to create raw exception handler trampoline");
|
|
}
|
|
_rawExceptionHandler = _faultHandling.AddFirstChanceHandler(_rawExceptionHandlerStub);
|
|
Console.Error.WriteLine($"[LOADER][INFO] Raw exception handler installed: 0x{_rawExceptionHandler:X16}");
|
|
}
|
|
else
|
|
{
|
|
Console.Error.WriteLine("[LOADER][INFO] Raw exception handler disabled by SHARPEMU_DISABLE_RAW_HANDLER=1");
|
|
}
|
|
|
|
_handlerDelegate = VectoredHandler;
|
|
_handlerHandle = GCHandle.Alloc(_handlerDelegate);
|
|
_exceptionHandlerStub = _faultHandling.CreateHandlerThunk(Marshal.GetFunctionPointerForDelegate(_handlerDelegate), _hostRspSlotTlsIndex, _tlsGetValueAddress);
|
|
if (_exceptionHandlerStub == 0)
|
|
{
|
|
throw new InvalidOperationException("Failed to create exception handler trampoline");
|
|
}
|
|
_exceptionHandler = _faultHandling.AddFirstChanceHandler(_exceptionHandlerStub);
|
|
Console.Error.WriteLine($"[LOADER][INFO] Exception handler installed: 0x{_exceptionHandler:X16}");
|
|
|
|
_unhandledFilterDelegate = UnhandledExceptionFilter;
|
|
_unhandledFilterHandle = GCHandle.Alloc(_unhandledFilterDelegate);
|
|
_unhandledFilterStub = _faultHandling.CreateHandlerThunk(Marshal.GetFunctionPointerForDelegate(_unhandledFilterDelegate), _hostRspSlotTlsIndex, _tlsGetValueAddress);
|
|
if (_unhandledFilterStub == 0)
|
|
{
|
|
throw new InvalidOperationException("Failed to create unhandled exception filter trampoline");
|
|
}
|
|
_faultHandling.SetUnhandledFilter(_unhandledFilterStub);
|
|
}
|
|
|
|
private unsafe int UnhandledExceptionFilter(void* exceptionInfo)
|
|
{
|
|
try
|
|
{
|
|
EXCEPTION_RECORD* exceptionRecord = ((EXCEPTION_POINTERS*)exceptionInfo)->ExceptionRecord;
|
|
ulong rip = ReadCtxU64(((EXCEPTION_POINTERS*)exceptionInfo)->ContextRecord, CTX_RIP);
|
|
ulong rsp = ReadCtxU64(((EXCEPTION_POINTERS*)exceptionInfo)->ContextRecord, CTX_RSP);
|
|
Console.Error.WriteLine("[LOADER][FATAL] Unhandled exception filter fired.");
|
|
Console.Error.WriteLine($"[LOADER][FATAL] Code: 0x{exceptionRecord->ExceptionCode:X8}");
|
|
Console.Error.WriteLine($"[LOADER][FATAL] Exception Address: 0x{(ulong)(nint)exceptionRecord->ExceptionAddress:X16}");
|
|
Console.Error.WriteLine($"[LOADER][FATAL] RIP: 0x{rip:X16}");
|
|
Console.Error.WriteLine($"[LOADER][FATAL] RSP: 0x{rsp:X16}");
|
|
Console.Error.Flush();
|
|
}
|
|
catch
|
|
{
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
private unsafe int VectoredHandler(void* exceptionInfo)
|
|
{
|
|
if (_vectoredHandlerDepth > 0)
|
|
{
|
|
LogNestedVectoredException(exceptionInfo);
|
|
Console.Error.Flush();
|
|
return 0;
|
|
}
|
|
|
|
_vectoredHandlerDepth++;
|
|
try
|
|
{
|
|
EXCEPTION_RECORD* exceptionRecord = ((EXCEPTION_POINTERS*)exceptionInfo)->ExceptionRecord;
|
|
uint exceptionCode = exceptionRecord->ExceptionCode;
|
|
uint exceptionFlags = exceptionRecord->ExceptionFlags;
|
|
ulong exceptionAddress = (ulong)exceptionRecord->ExceptionAddress;
|
|
void* contextRecord = ((EXCEPTION_POINTERS*)exceptionInfo)->ContextRecord;
|
|
if (contextRecord == null)
|
|
{
|
|
Console.Error.WriteLine("[LOADER][FATAL] ContextRecord is null!");
|
|
Console.Error.Flush();
|
|
return 0;
|
|
}
|
|
|
|
ulong rip = ReadCtxU64(contextRecord, CTX_RIP);
|
|
ulong rsp = ReadCtxU64(contextRecord, CTX_RSP);
|
|
|
|
// Thread-mode probe: a hardware exception raised while this thread is inside
|
|
// the managed import gateway means the VEH->managed reentry happened from
|
|
// cooperative GC mode — a ReversePInvokeBadTransition candidate.
|
|
if (LogThreadMode && _threadModeGatewayDepth > 0)
|
|
{
|
|
TraceThreadMode(
|
|
$"veh_in_gateway code=0x{exceptionCode:X8} rip=0x{rip:X16} gateway_depth={_threadModeGatewayDepth}");
|
|
}
|
|
|
|
if (exceptionCode == WindowsFaultCodes.AccessViolation && TryHandleLazyCommittedPage(exceptionRecord, rip, rsp))
|
|
{
|
|
return -1;
|
|
}
|
|
if (IsBenignHostDebugException(exceptionCode))
|
|
{
|
|
return -1;
|
|
}
|
|
if (exceptionCode == MSVC_CPP_EXCEPTION)
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
switch (exceptionCode)
|
|
{
|
|
case WindowsFaultCodes.AccessViolation:
|
|
LogAccessViolationTrace(exceptionAddress, exceptionRecord);
|
|
break;
|
|
case WindowsFaultCodes.FastFail:
|
|
{
|
|
ulong p0 = exceptionRecord->NumberParameters >= 1 ? (*exceptionRecord->ExceptionInformation) : 0;
|
|
ulong p1 = exceptionRecord->NumberParameters >= 2 ? exceptionRecord->ExceptionInformation[1] : 0;
|
|
Console.Error.WriteLine($"[LOADER][TRACE] VEH_FASTFAIL code=0x{exceptionCode:X8} ex=0x{exceptionAddress:X16} rip=0x{rip:X16} rsp=0x{rsp:X16} p0=0x{p0:X16} p1=0x{p1:X16}");
|
|
Console.Error.Flush();
|
|
break;
|
|
}
|
|
}
|
|
|
|
ulong rax = ReadCtxU64(contextRecord, CTX_RAX);
|
|
ulong rbx = ReadCtxU64(contextRecord, CTX_RBX);
|
|
ulong rcx = ReadCtxU64(contextRecord, CTX_RCX);
|
|
ulong rdx = ReadCtxU64(contextRecord, CTX_RDX);
|
|
ulong rsi = ReadCtxU64(contextRecord, CTX_RSI);
|
|
ulong rdi = ReadCtxU64(contextRecord, CTX_RDI);
|
|
ulong rbp = ReadCtxU64(contextRecord, CTX_RBP);
|
|
ulong r8 = ReadCtxU64(contextRecord, CTX_R8);
|
|
ulong r9 = ReadCtxU64(contextRecord, CTX_R9);
|
|
ulong r10 = ReadCtxU64(contextRecord, CTX_R10);
|
|
ulong r11 = ReadCtxU64(contextRecord, CTX_R11);
|
|
ulong r12 = ReadCtxU64(contextRecord, CTX_R12);
|
|
ulong r13 = ReadCtxU64(contextRecord, CTX_R13);
|
|
ulong r14 = ReadCtxU64(contextRecord, CTX_R14);
|
|
ulong r15 = ReadCtxU64(contextRecord, CTX_R15);
|
|
|
|
Console.Error.WriteLine("[LOADER][INFO] =========================================");
|
|
Console.Error.WriteLine("[LOADER][INFO] NATIVE EXCEPTION CAUGHT!");
|
|
Console.Error.WriteLine($"[LOADER][INFO] Code: 0x{exceptionCode:X8}");
|
|
Console.Error.WriteLine($"[LOADER][INFO] Exception Address: 0x{exceptionAddress:X16}");
|
|
Console.Error.WriteLine($"[LOADER][INFO] RIP: 0x{rip:X16}");
|
|
if (TryFormatNearestRuntimeSymbol(rip, out string symbol))
|
|
{
|
|
Console.Error.WriteLine("[LOADER][INFO] RIP symbol: " + symbol);
|
|
}
|
|
Console.Error.WriteLine($"[LOADER][INFO] RAX: 0x{rax:X16}");
|
|
Console.Error.WriteLine($"[LOADER][INFO] RBX: 0x{rbx:X16}");
|
|
Console.Error.WriteLine($"[LOADER][INFO] RCX: 0x{rcx:X16}");
|
|
Console.Error.WriteLine($"[LOADER][INFO] RDX: 0x{rdx:X16}");
|
|
Console.Error.WriteLine($"[LOADER][INFO] RSI: 0x{rsi:X16}");
|
|
Console.Error.WriteLine($"[LOADER][INFO] RDI: 0x{rdi:X16}");
|
|
Console.Error.WriteLine($"[LOADER][INFO] RBP: 0x{rbp:X16}");
|
|
Console.Error.WriteLine($"[LOADER][INFO] RSP: 0x{rsp:X16}");
|
|
Console.Error.WriteLine($"[LOADER][INFO] R8 : 0x{r8:X16}");
|
|
Console.Error.WriteLine($"[LOADER][INFO] R9 : 0x{r9:X16}");
|
|
Console.Error.WriteLine($"[LOADER][INFO] R10: 0x{r10:X16}");
|
|
Console.Error.WriteLine($"[LOADER][INFO] R11: 0x{r11:X16}");
|
|
Console.Error.WriteLine($"[LOADER][INFO] R12: 0x{r12:X16}");
|
|
Console.Error.WriteLine($"[LOADER][INFO] R13: 0x{r13:X16}");
|
|
Console.Error.WriteLine($"[LOADER][INFO] R14: 0x{r14:X16}");
|
|
Console.Error.WriteLine($"[LOADER][INFO] R15: 0x{r15:X16}");
|
|
Console.Error.WriteLine($"[LOADER][INFO] Flags: 0x{exceptionFlags:X8}");
|
|
|
|
ulong accessType = 0;
|
|
ulong target = 0;
|
|
if (exceptionCode == WindowsFaultCodes.AccessViolation && exceptionRecord->NumberParameters >= 2)
|
|
{
|
|
accessType = *exceptionRecord->ExceptionInformation;
|
|
target = exceptionRecord->ExceptionInformation[1];
|
|
string accessText = accessType switch
|
|
{
|
|
0uL => "read",
|
|
1uL => "write",
|
|
8uL => "execute",
|
|
_ => $"unknown({accessType})"
|
|
};
|
|
Console.Error.WriteLine("[LOADER][INFO] AV access: " + accessText);
|
|
Console.Error.WriteLine($"[LOADER][INFO] AV target: 0x{target:X16}");
|
|
if (_hostMemory.Query(target, out var mbi))
|
|
{
|
|
Console.Error.WriteLine($"[LOADER][INFO] AV target region: base=0x{mbi.BaseAddress:X16} size=0x{mbi.RegionSize:X16} state=0x{mbi.RawState:X08} protect=0x{mbi.RawProtection:X08}");
|
|
}
|
|
|
|
}
|
|
|
|
try
|
|
{
|
|
Console.Error.WriteLine("[LOADER][INFO] Stack qwords (RSP..):");
|
|
for (int i = 0; i < 16; i++)
|
|
{
|
|
ulong stackAddr = rsp + (ulong)(i * 8);
|
|
ulong value = (ulong)Marshal.ReadInt64((nint)stackAddr);
|
|
Console.Error.WriteLine($"[LOADER][INFO] [rsp+0x{i * 8:X2}] @0x{stackAddr:X16} = 0x{value:X16}");
|
|
}
|
|
}
|
|
catch
|
|
{
|
|
Console.Error.WriteLine("[LOADER][WARNING] Could not read stack qwords.");
|
|
}
|
|
|
|
try
|
|
{
|
|
Console.Error.WriteLine("[LOADER][INFO] Frame chain (RBP walk):");
|
|
ulong frame = rbp;
|
|
for (int i = 0; i < 12; i++)
|
|
{
|
|
if (frame < 140733193388032L || frame > 140737488355327L)
|
|
{
|
|
break;
|
|
}
|
|
ulong next = (ulong)Marshal.ReadInt64((nint)frame);
|
|
ulong ret = (ulong)Marshal.ReadInt64((nint)(frame + 8));
|
|
string extra = TryFormatNearestRuntimeSymbol(ret, out string retSym) ? $" [{retSym}]" : string.Empty;
|
|
Console.Error.WriteLine($"[LOADER][INFO] frame#{i}: rbp=0x{frame:X16} ret=0x{ret:X16}{extra} next=0x{next:X16}");
|
|
if (next <= frame)
|
|
{
|
|
break;
|
|
}
|
|
frame = next;
|
|
}
|
|
}
|
|
catch
|
|
{
|
|
Console.Error.WriteLine("[LOADER][WARNING] Could not walk RBP frame chain.");
|
|
}
|
|
|
|
switch (exceptionCode)
|
|
{
|
|
case WindowsFaultCodes.AccessViolation:
|
|
Console.Error.WriteLine("[LOADER][ERROR] Type: Access Violation");
|
|
Console.Error.WriteLine("[LOADER][ERROR] This usually means:");
|
|
Console.Error.WriteLine("[LOADER][ERROR] - Guest code called an unmapped import");
|
|
Console.Error.WriteLine("[LOADER][ERROR] - Guest code accessed unmapped memory");
|
|
Console.Error.WriteLine("[LOADER][ERROR] - Need to implement HLE for this NID");
|
|
try
|
|
{
|
|
byte[] code = new byte[16];
|
|
Marshal.Copy((nint)rip, code, 0, code.Length);
|
|
Console.Error.WriteLine("[LOADER][INFO] Code at RIP: " + BitConverter.ToString(code).Replace("-", " "));
|
|
if (code[0] == 100)
|
|
{
|
|
Console.Error.WriteLine("[LOADER][ERROR] Detected FS segment prefix - TLS access not patched!");
|
|
}
|
|
else if (code[0] == 101)
|
|
{
|
|
Console.Error.WriteLine("[LOADER][ERROR] Detected GS segment prefix - TLS access not patched!");
|
|
}
|
|
else if (code[0] == 197 || code[0] == 196)
|
|
{
|
|
Console.Error.WriteLine("[LOADER][INFO] Detected AVX instruction - check CPU support!");
|
|
Console.Error.WriteLine($"[LOADER][INFO] RBP: 0x{rbp:X16} (mod 16 = {rbp % 16})");
|
|
Console.Error.WriteLine($"[LOADER][INFO] RSP: 0x{rsp:X16} (mod 16 = {rsp % 16})");
|
|
}
|
|
if (rip > 16)
|
|
{
|
|
byte[] before = new byte[16];
|
|
Marshal.Copy((nint)(rip - 16), before, 0, before.Length);
|
|
Console.Error.WriteLine("[LOADER][INFO] Code before RIP: " + BitConverter.ToString(before).Replace("-", " "));
|
|
}
|
|
if (rip > 32)
|
|
{
|
|
byte[] window = new byte[64];
|
|
Marshal.Copy((nint)(rip - 32), window, 0, window.Length);
|
|
Console.Error.WriteLine("[LOADER][INFO] Code window [RIP-0x20..]: " + BitConverter.ToString(window).Replace("-", " "));
|
|
}
|
|
}
|
|
catch
|
|
{
|
|
Console.Error.WriteLine("[LOADER][ERROR] Could not read code at RIP");
|
|
}
|
|
DumpRecentImportTrace();
|
|
DumpGuestDisasmDiagnostics(rip, rbp);
|
|
DumpGuestReferenceDiagnostics();
|
|
DumpGuestPointerWindowDiagnostics();
|
|
break;
|
|
case WindowsFaultCodes.Breakpoint:
|
|
Console.Error.WriteLine("[LOADER][WARNING] Type: Breakpoint (int3)");
|
|
Console.Error.WriteLine("[LOADER][WARNING] Unexpected breakpoint in direct-bridge mode");
|
|
break;
|
|
case WindowsFaultCodes.IllegalInstruction:
|
|
Console.Error.WriteLine("[LOADER][INFO] Type: Illegal Instruction");
|
|
break;
|
|
}
|
|
|
|
Console.Error.WriteLine("[LOADER][INFO] =========================================");
|
|
Console.Error.Flush();
|
|
return 0;
|
|
}
|
|
finally
|
|
{
|
|
_vectoredHandlerDepth--;
|
|
}
|
|
}
|
|
|
|
private static bool IsBenignHostDebugException(uint exceptionCode)
|
|
{
|
|
return exceptionCode is DBG_PRINTEXCEPTION_C or DBG_PRINTEXCEPTION_WIDE_C or MS_VC_THREADNAME_EXCEPTION;
|
|
}
|
|
|
|
private unsafe static void LogNestedVectoredException(void* exceptionInfo)
|
|
{
|
|
int count = Interlocked.Increment(ref _nestedVehTraceCount);
|
|
if (count > 16 && count % 128 != 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
try
|
|
{
|
|
EXCEPTION_POINTERS* pointers = (EXCEPTION_POINTERS*)exceptionInfo;
|
|
EXCEPTION_RECORD* record = pointers->ExceptionRecord;
|
|
void* contextRecord = pointers->ContextRecord;
|
|
ulong rip = contextRecord != null ? ReadCtxU64(contextRecord, CTX_RIP) : 0;
|
|
ulong rsp = contextRecord != null ? ReadCtxU64(contextRecord, CTX_RSP) : 0;
|
|
ulong accessType = record->NumberParameters >= 1 ? *record->ExceptionInformation : 0;
|
|
ulong target = record->NumberParameters >= 2 ? record->ExceptionInformation[1] : 0;
|
|
Console.Error.WriteLine(
|
|
$"[LOADER][TRACE] Nested VEH exception#{count}: code=0x{record->ExceptionCode:X8} ex=0x{(ulong)record->ExceptionAddress:X16} rip=0x{rip:X16} rsp=0x{rsp:X16} type={accessType} target=0x{target:X16}; passing through.");
|
|
}
|
|
catch
|
|
{
|
|
Console.Error.WriteLine($"[LOADER][TRACE] Nested VEH exception#{count}; passing through.");
|
|
}
|
|
}
|
|
|
|
private unsafe void LogAccessViolationTrace(ulong exceptionAddress, EXCEPTION_RECORD* exceptionRecord)
|
|
{
|
|
ulong accessType = exceptionRecord->NumberParameters >= 1 ? (*exceptionRecord->ExceptionInformation) : 0;
|
|
ulong target = exceptionRecord->NumberParameters >= 2 ? exceptionRecord->ExceptionInformation[1] : 0;
|
|
if (_lastAvTraceRip == exceptionAddress && _lastAvTraceType == accessType && _lastAvTraceTarget == target)
|
|
{
|
|
_lastAvTraceRepeatCount++;
|
|
if (_lastAvTraceRepeatCount > 4 && _lastAvTraceRepeatCount % 128 != 0)
|
|
{
|
|
return;
|
|
}
|
|
Console.Error.WriteLine($"[LOADER][TRACE] VEH_AV repeat#{_lastAvTraceRepeatCount} at 0x{exceptionAddress:X16} type={accessType} target=0x{target:X16}");
|
|
Console.Error.Flush();
|
|
return;
|
|
}
|
|
|
|
_lastAvTraceRip = exceptionAddress;
|
|
_lastAvTraceType = accessType;
|
|
_lastAvTraceTarget = target;
|
|
_lastAvTraceRepeatCount = 1;
|
|
Console.Error.WriteLine($"[LOADER][TRACE] VEH_AV first-chance at 0x{exceptionAddress:X16} type={accessType} target=0x{target:X16}");
|
|
Console.Error.Flush();
|
|
}
|
|
|
|
private void DumpGuestInstructionStream(string name, ulong startRip, int maxInstructions)
|
|
{
|
|
if (_cpuContext == null || startRip < 0x10000 || maxInstructions <= 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
Console.Error.WriteLine($"[LOADER][INFO] {name} disasm @0x{startRip:X16}:");
|
|
ulong rip = startRip;
|
|
for (int i = 0; i < maxInstructions; i++)
|
|
{
|
|
if (!IcedDecoder.TryReadGuestBytes(_cpuContext.Memory, rip, maxLen: 15, out var bytes) ||
|
|
!IcedDecoder.TryDecode(rip, bytes, out var instruction))
|
|
{
|
|
Console.Error.WriteLine($"[LOADER][INFO] 0x{rip:X16}: <decode-failed>");
|
|
break;
|
|
}
|
|
|
|
Console.Error.WriteLine(
|
|
$"[LOADER][INFO] 0x{instruction.Rip:X16}: {instruction.Text} bytes={IcedDecoder.FormatBytes(instruction.Bytes)}");
|
|
rip += (ulong)instruction.Length;
|
|
}
|
|
}
|
|
|
|
private void DumpGuestDisasmDiagnostics(ulong rip, ulong rbp)
|
|
{
|
|
if (!string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_DISASM"), "1", StringComparison.Ordinal))
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (rip >= 0x20)
|
|
{
|
|
DumpGuestInstructionStream("fault-prelude", rip - 0x20, 24);
|
|
}
|
|
|
|
try
|
|
{
|
|
ulong frame = rbp;
|
|
for (int i = 0; i < 3; i++)
|
|
{
|
|
if (frame < 140733193388032L || frame > 140737488355327L)
|
|
{
|
|
break;
|
|
}
|
|
|
|
ulong ret = (ulong)Marshal.ReadInt64((nint)(frame + 8));
|
|
if (ret >= 0x40)
|
|
{
|
|
DumpGuestInstructionStream($"frame#{i}-ret-prelude", ret - 0x40, 24);
|
|
}
|
|
|
|
ulong next = (ulong)Marshal.ReadInt64((nint)frame);
|
|
if (next <= frame)
|
|
{
|
|
break;
|
|
}
|
|
|
|
frame = next;
|
|
}
|
|
}
|
|
catch
|
|
{
|
|
Console.Error.WriteLine("[LOADER][WARNING] Could not dump disasm diagnostics.");
|
|
}
|
|
|
|
var extraAddresses = Environment.GetEnvironmentVariable("SHARPEMU_LOG_DISASM_ADDRS");
|
|
if (string.IsNullOrWhiteSpace(extraAddresses))
|
|
{
|
|
return;
|
|
}
|
|
|
|
foreach (var token in extraAddresses.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries))
|
|
{
|
|
var normalized = token.StartsWith("0x", StringComparison.OrdinalIgnoreCase)
|
|
? token[2..]
|
|
: token;
|
|
if (!ulong.TryParse(normalized, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var address) || address < 0x20)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
DumpGuestInstructionStream($"extra-0x{address:X16}", address, 48);
|
|
}
|
|
}
|
|
|
|
private unsafe void DumpGuestReferenceDiagnostics()
|
|
{
|
|
var targetList = ParseDiagnosticAddresses(Environment.GetEnvironmentVariable("SHARPEMU_LOG_REFSCAN_ADDRS"));
|
|
if (targetList.Count == 0 || _cpuContext == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
const ulong scanBase = 0x0000000800000000UL;
|
|
const ulong scanEnd = 0x0000000810000000UL;
|
|
const int maxHitsPerTarget = 24;
|
|
|
|
Console.Error.WriteLine(
|
|
$"[LOADER][INFO] Ref scan targets: {string.Join(", ", targetList.ConvertAll(static addr => $"0x{addr:X16}"))}");
|
|
|
|
var hitCounts = new Dictionary<ulong, int>(targetList.Count);
|
|
for (var i = 0; i < targetList.Count; i++)
|
|
{
|
|
hitCounts[targetList[i]] = 0;
|
|
}
|
|
|
|
ulong address = scanBase;
|
|
while (address < scanEnd)
|
|
{
|
|
if (!_hostMemory.Query(address, out var mbi))
|
|
{
|
|
break;
|
|
}
|
|
|
|
ulong regionBase = mbi.BaseAddress;
|
|
ulong regionEnd = regionBase + mbi.RegionSize;
|
|
if (regionEnd <= address)
|
|
{
|
|
break;
|
|
}
|
|
|
|
if (mbi.State == HostRegionState.Committed &&
|
|
IsReadableProtection(mbi.RawProtection) &&
|
|
IsExecutableProtection(mbi.RawProtection))
|
|
{
|
|
ScanExecutableRegionForTargetReferences(regionBase, regionEnd, targetList, hitCounts, maxHitsPerTarget);
|
|
}
|
|
|
|
var allTargetsSatisfied = true;
|
|
for (var i = 0; i < targetList.Count; i++)
|
|
{
|
|
if (hitCounts[targetList[i]] < maxHitsPerTarget)
|
|
{
|
|
allTargetsSatisfied = false;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (allTargetsSatisfied)
|
|
{
|
|
break;
|
|
}
|
|
|
|
address = regionEnd;
|
|
}
|
|
|
|
for (var i = 0; i < targetList.Count; i++)
|
|
{
|
|
var target = targetList[i];
|
|
if (!hitCounts.TryGetValue(target, out var count) || count == 0)
|
|
{
|
|
Console.Error.WriteLine($"[LOADER][INFO] Ref scan 0x{target:X16}: none");
|
|
}
|
|
}
|
|
}
|
|
|
|
private void DumpGuestPointerWindowDiagnostics()
|
|
{
|
|
var targetList = ParseDiagnosticAddresses(Environment.GetEnvironmentVariable("SHARPEMU_LOG_POINTER_WINDOWS"));
|
|
if (targetList.Count == 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var windowSize = 0x80;
|
|
var rawWindowSize = Environment.GetEnvironmentVariable("SHARPEMU_LOG_POINTER_WINDOW_SIZE");
|
|
if (!string.IsNullOrWhiteSpace(rawWindowSize))
|
|
{
|
|
var normalized = rawWindowSize.StartsWith("0x", StringComparison.OrdinalIgnoreCase)
|
|
? rawWindowSize[2..]
|
|
: rawWindowSize;
|
|
if (int.TryParse(normalized, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var parsedWindowSize) &&
|
|
parsedWindowSize > 0)
|
|
{
|
|
windowSize = parsedWindowSize;
|
|
}
|
|
}
|
|
|
|
foreach (var target in targetList)
|
|
{
|
|
DumpPointerWindow($"ptrwin-0x{target:X16}", target, windowSize);
|
|
}
|
|
}
|
|
|
|
private void ScanExecutableRegionForTargetReferences(
|
|
ulong regionBase,
|
|
ulong regionEnd,
|
|
IReadOnlyList<ulong> targets,
|
|
IDictionary<ulong, int> hitCounts,
|
|
int maxHitsPerTarget)
|
|
{
|
|
if (_cpuContext == null || regionEnd <= regionBase)
|
|
{
|
|
return;
|
|
}
|
|
|
|
ulong rip = regionBase;
|
|
while (rip < regionEnd)
|
|
{
|
|
if (!IcedDecoder.TryReadGuestBytes(_cpuContext.Memory, rip, maxLen: 15, out var bytes) ||
|
|
!IcedDecoder.TryDecode(rip, bytes, out var instruction) ||
|
|
instruction.Length <= 0)
|
|
{
|
|
rip++;
|
|
continue;
|
|
}
|
|
|
|
if (instruction.MemoryAddress is { } memoryAddress)
|
|
{
|
|
for (var i = 0; i < targets.Count; i++)
|
|
{
|
|
var target = targets[i];
|
|
if (memoryAddress != target ||
|
|
!hitCounts.TryGetValue(target, out var count) ||
|
|
count >= maxHitsPerTarget)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
hitCounts[target] = count + 1;
|
|
Console.Error.WriteLine(
|
|
$"[LOADER][INFO] Ref scan hit target=0x{target:X16} rip=0x{instruction.Rip:X16} text={instruction.Text} bytes={IcedDecoder.FormatBytes(instruction.Bytes)}");
|
|
}
|
|
}
|
|
|
|
rip += (ulong)instruction.Length;
|
|
}
|
|
}
|
|
|
|
private static List<ulong> ParseDiagnosticAddresses(string? rawValue)
|
|
{
|
|
var result = new List<ulong>();
|
|
if (string.IsNullOrWhiteSpace(rawValue))
|
|
{
|
|
return result;
|
|
}
|
|
|
|
foreach (var token in rawValue.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries))
|
|
{
|
|
var normalized = token.StartsWith("0x", StringComparison.OrdinalIgnoreCase)
|
|
? token[2..]
|
|
: token;
|
|
if (!ulong.TryParse(normalized, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var address))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (!result.Contains(address))
|
|
{
|
|
result.Add(address);
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
private void DumpUnresolvedSentinelWindow(string name, ulong baseAddress, int size)
|
|
{
|
|
if (baseAddress < 0x10000 || size <= 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
ulong scanStart = baseAddress;
|
|
ulong scanEnd = baseAddress + (ulong)size;
|
|
List<ulong> hits = ScanSuspiciousResolverPointers(scanStart, scanEnd);
|
|
if (hits.Count == 0)
|
|
{
|
|
Console.Error.WriteLine($"[LOADER][INFO] {name} unresolved scan: none");
|
|
return;
|
|
}
|
|
|
|
Console.Error.WriteLine($"[LOADER][INFO] {name} unresolved scan hits: {hits.Count}");
|
|
for (int i = 0; i < hits.Count && i < 8; i++)
|
|
{
|
|
ulong slotAddress = hits[i];
|
|
if (TryReadQword(slotAddress, out var value))
|
|
{
|
|
Console.Error.WriteLine($"[LOADER][INFO] hit#{i}: slot=0x{slotAddress:X16} value=0x{value:X16}");
|
|
}
|
|
}
|
|
}
|
|
|
|
private void DumpSentinelPatternWindow(string name, ulong baseAddress, int size)
|
|
{
|
|
if (_cpuContext == null || baseAddress < 0x10000 || size <= 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
byte[] buffer = new byte[size];
|
|
if (!_cpuContext.Memory.TryRead(baseAddress, buffer))
|
|
{
|
|
Console.Error.WriteLine($"[LOADER][INFO] {name} sentinel-pattern scan: unreadable");
|
|
return;
|
|
}
|
|
|
|
List<string> hits = new();
|
|
for (int offset = 0; offset + 2 <= buffer.Length; offset++)
|
|
{
|
|
if (BinaryPrimitives.ReadUInt16LittleEndian(buffer.AsSpan(offset, 2)) == 0xFFFE)
|
|
{
|
|
hits.Add($"+0x{offset:X}:u16");
|
|
}
|
|
|
|
if (offset + 4 <= buffer.Length &&
|
|
BinaryPrimitives.ReadUInt32LittleEndian(buffer.AsSpan(offset, 4)) == 0xFFFFFFFEu)
|
|
{
|
|
hits.Add($"+0x{offset:X}:u32");
|
|
}
|
|
|
|
if (offset + 8 <= buffer.Length &&
|
|
BinaryPrimitives.ReadUInt64LittleEndian(buffer.AsSpan(offset, 8)) == 0xFFFFFFFFFFFFFFFEuL)
|
|
{
|
|
hits.Add($"+0x{offset:X}:u64");
|
|
}
|
|
}
|
|
|
|
if (hits.Count == 0)
|
|
{
|
|
Console.Error.WriteLine($"[LOADER][INFO] {name} sentinel-pattern scan: none");
|
|
return;
|
|
}
|
|
|
|
Console.Error.WriteLine($"[LOADER][INFO] {name} sentinel-pattern hits: {string.Join(", ", hits.GetRange(0, Math.Min(hits.Count, 12)))}");
|
|
}
|
|
|
|
private void DumpReturnTargetCandidates(ulong rsp)
|
|
{
|
|
if (rsp < 0x10000)
|
|
{
|
|
return;
|
|
}
|
|
|
|
ulong start = rsp >= 0x10 ? rsp - 0x10 : rsp;
|
|
Console.Error.WriteLine($"[LOADER][INFO] Return-target candidates near RSP=0x{rsp:X16}:");
|
|
for (int offset = 0; offset <= 0x20; offset++)
|
|
{
|
|
ulong address = start + (ulong)offset;
|
|
try
|
|
{
|
|
ulong value = (ulong)Marshal.ReadInt64((nint)address);
|
|
Console.Error.WriteLine($"[LOADER][INFO] [0x{address:X16}] -> 0x{value:X16}");
|
|
}
|
|
catch
|
|
{
|
|
Console.Error.WriteLine($"[LOADER][INFO] [0x{address:X16}] -> <unreadable>");
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
private void DumpObjectFieldTargets(string name, ulong objectAddress, int[] offsets, int windowSize)
|
|
{
|
|
if (objectAddress < 0x10000 || offsets.Length == 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
foreach (int offset in offsets)
|
|
{
|
|
ulong slotAddress = objectAddress + (ulong)offset;
|
|
if (!TryReadQword(slotAddress, out var target) || target < 0x10000)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
Console.Error.WriteLine($"[LOADER][INFO] {name}+0x{offset:X2} target = {FormatPointerWithNearestSymbol(target)}");
|
|
DumpPointerWindow($"{name}+0x{offset:X2}", target, windowSize);
|
|
DumpUnresolvedSentinelWindow($"{name}+0x{offset:X2}", target, 0x80);
|
|
}
|
|
}
|
|
|
|
private void DumpSuspiciousGlobalSlots()
|
|
{
|
|
DumpAbsoluteSlot("callback_slot[0x80293BD08]", 0x000000080293BD08uL);
|
|
DumpAbsoluteSlot("callback_arg[0x8030FBBE8]", 0x00000008030FBBE8uL);
|
|
DumpAbsoluteSlot("tsc_freq_global[0x8030FD590]", 0x00000008030FD590uL);
|
|
DumpAbsoluteSlot("tsc_base_global[0x8030FD598]", 0x00000008030FD598uL);
|
|
DumpAbsoluteSlot("plt_got[0x8028F6100]", 0x00000008028F6100uL);
|
|
DumpAbsoluteSlot("plt_got[0x8028F6158]", 0x00000008028F6158uL);
|
|
DumpAbsoluteSlot("plt_got[0x8028F6160]", 0x00000008028F6160uL);
|
|
DumpAbsoluteSlot("plt_got[0x8028F64C0]", 0x00000008028F64C0uL);
|
|
DumpAbsoluteSlot("plt_got[0x8028F64C8]", 0x00000008028F64C8uL);
|
|
DumpAbsoluteSlot("plt_got[0x8028F6590]", 0x00000008028F6590uL);
|
|
DumpAbsoluteSlot("plt_got[0x8028F6708]", 0x00000008028F6708uL);
|
|
DumpUnresolvedSentinelWindow("PLT-GOT", 0x00000008028F6100uL, 0x700);
|
|
}
|
|
|
|
private void DumpAbsoluteSlot(string name, ulong slotAddress)
|
|
{
|
|
if (!TryReadQword(slotAddress, out var value))
|
|
{
|
|
Console.Error.WriteLine($"[LOADER][INFO] {name} @0x{slotAddress:X16} = <unreadable>");
|
|
return;
|
|
}
|
|
|
|
Console.Error.WriteLine($"[LOADER][INFO] {name} @0x{slotAddress:X16} = {FormatPointerWithNearestSymbol(value)}");
|
|
}
|
|
private void DumpPointerWindow(string name, ulong baseAddress, int size)
|
|
{
|
|
if (baseAddress < 0x10000 || size <= 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
Console.Error.WriteLine($"[LOADER][INFO] {name} window @0x{baseAddress:X16}:");
|
|
for (int offset = 0; offset < size; offset += 8)
|
|
{
|
|
ulong slotAddress = baseAddress + (ulong)offset;
|
|
if (!TryReadQword(slotAddress, out var value))
|
|
{
|
|
Console.Error.WriteLine($"[LOADER][INFO] +0x{offset:X2}: <unreadable>");
|
|
break;
|
|
}
|
|
|
|
Console.Error.WriteLine($"[LOADER][INFO] +0x{offset:X2}: {FormatPointerWithNearestSymbol(value)}");
|
|
}
|
|
}
|
|
|
|
private unsafe bool TryReadQword(ulong address, out ulong value)
|
|
{
|
|
value = 0;
|
|
if (address < 0x10000)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (!_hostMemory.Query(address, out var mbi))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
ulong regionEnd = mbi.BaseAddress + mbi.RegionSize;
|
|
if (mbi.State != HostRegionState.Committed || !IsReadableProtection(mbi.RawProtection) || regionEnd <= address || address > regionEnd - 8)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
try
|
|
{
|
|
value = (ulong)Marshal.ReadInt64((nint)address);
|
|
return true;
|
|
}
|
|
catch
|
|
{
|
|
value = 0;
|
|
return false;
|
|
}
|
|
}
|
|
|
|
private string FormatPointerWithNearestSymbol(ulong value)
|
|
{
|
|
string text = $"0x{value:X16}";
|
|
if (TryFormatNearestRuntimeSymbol(value, out string symbol))
|
|
{
|
|
text += $" [{symbol}]";
|
|
}
|
|
|
|
return text;
|
|
}
|
|
|
|
private void InitializeRuntimeSymbolIndex(IReadOnlyDictionary<string, ulong> runtimeSymbols)
|
|
{
|
|
_runtimeSymbolsByName.Clear();
|
|
if (runtimeSymbols.Count == 0)
|
|
{
|
|
_runtimeSymbolsByAddress = Array.Empty<KeyValuePair<string, ulong>>();
|
|
return;
|
|
}
|
|
|
|
List<KeyValuePair<string, ulong>> list = new(runtimeSymbols.Count);
|
|
foreach (KeyValuePair<string, ulong> runtimeSymbol in runtimeSymbols)
|
|
{
|
|
if (runtimeSymbol.Value != 0L && !string.IsNullOrWhiteSpace(runtimeSymbol.Key))
|
|
{
|
|
list.Add(runtimeSymbol);
|
|
_runtimeSymbolsByName[runtimeSymbol.Key] = runtimeSymbol.Value;
|
|
}
|
|
}
|
|
|
|
list.Sort((a, b) => a.Value.CompareTo(b.Value));
|
|
_runtimeSymbolsByAddress = list.ToArray();
|
|
}
|
|
|
|
private bool TryFormatNearestRuntimeSymbol(ulong address, out string text)
|
|
{
|
|
text = string.Empty;
|
|
KeyValuePair<string, ulong>[] runtimeSymbolsByAddress = _runtimeSymbolsByAddress;
|
|
if (runtimeSymbolsByAddress.Length == 0)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
int low = 0;
|
|
int high = runtimeSymbolsByAddress.Length - 1;
|
|
int best = -1;
|
|
while (low <= high)
|
|
{
|
|
int mid = low + ((high - low) >> 1);
|
|
ulong value = runtimeSymbolsByAddress[mid].Value;
|
|
if (value <= address)
|
|
{
|
|
best = mid;
|
|
low = mid + 1;
|
|
}
|
|
else
|
|
{
|
|
high = mid - 1;
|
|
}
|
|
}
|
|
|
|
if (best < 0)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
KeyValuePair<string, ulong> symbol = runtimeSymbolsByAddress[best];
|
|
ulong delta = address - symbol.Value;
|
|
text = delta == 0
|
|
? $"{symbol.Key} (0x{symbol.Value:X16})"
|
|
: $"{symbol.Key}+0x{delta:X} (0x{symbol.Value:X16})";
|
|
return true;
|
|
}
|
|
|
|
private unsafe bool TryHandleLazyCommittedPage(EXCEPTION_RECORD* exceptionRecord, ulong rip, ulong rsp)
|
|
{
|
|
if (exceptionRecord->NumberParameters < 2)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
ulong accessType = *exceptionRecord->ExceptionInformation;
|
|
ulong faultAddress = exceptionRecord->ExceptionInformation[1];
|
|
if (accessType == 8 && faultAddress < 4294967296L)
|
|
{
|
|
return false;
|
|
}
|
|
if (faultAddress < 65536 || faultAddress >= 140737488355328L)
|
|
{
|
|
return false;
|
|
}
|
|
if (!IsGuestOwnedLazyCommitAddress(faultAddress, out var owner))
|
|
{
|
|
return false;
|
|
}
|
|
if (!_hostMemory.Query(faultAddress, out var mbi))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
ulong pageBase = faultAddress & 0xFFFFFFFFFFFFF000uL;
|
|
uint commitProtect = ResolveLazyCommitProtection(accessType, mbi.RawAllocationProtection);
|
|
int traceIndex = Interlocked.Increment(ref _lazyCommitTraceCount);
|
|
bool traceLazyCommit = ShouldTraceLazyCommit(traceIndex);
|
|
if (traceLazyCommit)
|
|
{
|
|
Console.Error.WriteLine($"[LOADER][TRACE] lazy-query#{traceIndex}: fault=0x{faultAddress:X16} owner={owner} rip=0x{rip:X16} rsp=0x{rsp:X16} state=0x{mbi.RawState:X08} base=0x{mbi.BaseAddress:X16} size=0x{mbi.RegionSize:X16} alloc=0x{mbi.RawAllocationProtection:X08} prot=0x{mbi.RawProtection:X08}");
|
|
}
|
|
|
|
if (mbi.State == HostRegionState.Committed && IsAccessCompatible(accessType, mbi.RawProtection))
|
|
{
|
|
if (traceLazyCommit)
|
|
{
|
|
Console.Error.WriteLine($"[LOADER][TRACE] lazy-commit-race#{traceIndex}: fault=0x{faultAddress:X16} protect=0x{mbi.RawProtection:X08}");
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool committed = false;
|
|
ulong committedBase = 0;
|
|
ulong committedSize = 0;
|
|
|
|
if (mbi.State == HostRegionState.Free)
|
|
{
|
|
if (TryGetLazyCommitWindow(faultAddress, mbi.BaseAddress, mbi.RegionSize, out var windowBase, out var windowSize) &&
|
|
TryReserveThenCommit(_hostMemory, windowBase, windowSize, windowBase, windowSize, commitProtect))
|
|
{
|
|
committed = true;
|
|
committedBase = windowBase;
|
|
committedSize = windowSize;
|
|
}
|
|
else
|
|
{
|
|
ulong largeBase = faultAddress & 0xFFFFFFFFFFE00000uL;
|
|
if (TryReserveThenCommit(_hostMemory, largeBase, 2097152uL, largeBase, 2097152uL, commitProtect))
|
|
{
|
|
committed = true;
|
|
committedBase = largeBase;
|
|
committedSize = 2097152uL;
|
|
}
|
|
}
|
|
|
|
if (!committed)
|
|
{
|
|
ulong region64kBase = faultAddress & 0xFFFFFFFFFFFF0000uL;
|
|
if (TryReserveThenCommit(_hostMemory, region64kBase, 65536uL, region64kBase, 65536uL, commitProtect))
|
|
{
|
|
committed = true;
|
|
committedBase = region64kBase;
|
|
committedSize = 65536uL;
|
|
}
|
|
else if (TryReserveThenCommit(_hostMemory, pageBase, 4096uL, pageBase, 4096uL, commitProtect))
|
|
{
|
|
committed = true;
|
|
committedBase = pageBase;
|
|
committedSize = 4096uL;
|
|
}
|
|
}
|
|
|
|
if (!committed)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
TryCommitRange(_hostMemory, pageBase + 4096, 4096uL, commitProtect);
|
|
if (traceLazyCommit)
|
|
{
|
|
Console.Error.WriteLine($"[LOADER][TRACE] lazy-reserve-commit#{traceIndex}: addr=0x{committedBase:X16} size=0x{committedSize:X16} access={accessType} protect=0x{commitProtect:X8}");
|
|
}
|
|
return true;
|
|
}
|
|
|
|
if (mbi.State != HostRegionState.Reserved)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (TryGetLazyCommitWindow(faultAddress, mbi.BaseAddress, mbi.RegionSize, out var commitWindowBase, out var commitWindowSize) &&
|
|
TryCommitRange(_hostMemory, commitWindowBase, commitWindowSize, commitProtect))
|
|
{
|
|
committed = true;
|
|
committedBase = commitWindowBase;
|
|
committedSize = commitWindowSize;
|
|
}
|
|
else
|
|
{
|
|
ulong largeCommitBase = faultAddress & 0xFFFFFFFFFFE00000uL;
|
|
if (TryCommitRange(_hostMemory, largeCommitBase, 2097152uL, commitProtect))
|
|
{
|
|
committed = true;
|
|
committedBase = largeCommitBase;
|
|
committedSize = 2097152uL;
|
|
}
|
|
}
|
|
|
|
if (!committed)
|
|
{
|
|
ulong region64kBase = faultAddress & 0xFFFFFFFFFFFF0000uL;
|
|
if (TryCommitRange(_hostMemory, region64kBase, 65536uL, commitProtect))
|
|
{
|
|
committed = true;
|
|
committedBase = region64kBase;
|
|
committedSize = 65536uL;
|
|
}
|
|
else if (TryCommitRange(_hostMemory, pageBase, 8192uL, commitProtect))
|
|
{
|
|
committed = true;
|
|
committedBase = pageBase;
|
|
committedSize = 8192uL;
|
|
}
|
|
else if (TryCommitRange(_hostMemory, pageBase, 4096uL, commitProtect))
|
|
{
|
|
committed = true;
|
|
committedBase = pageBase;
|
|
committedSize = 4096uL;
|
|
}
|
|
}
|
|
|
|
if (!committed)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
TryCommitRange(_hostMemory, pageBase + 4096, 4096uL, commitProtect);
|
|
if (traceLazyCommit)
|
|
{
|
|
Console.Error.WriteLine($"[LOADER][TRACE] lazy-commit#{traceIndex}: addr=0x{committedBase:X16} size=0x{committedSize:X16} access={accessType} protect=0x{commitProtect:X8}");
|
|
}
|
|
return true;
|
|
|
|
static bool TryGetLazyCommitWindow(ulong fault, ulong regionBase, ulong regionSize, out ulong baseAddress, out ulong length)
|
|
{
|
|
baseAddress = 0;
|
|
length = 0;
|
|
if (regionSize == 0 || ulong.MaxValue - regionBase < regionSize)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
ulong regionEnd = regionBase + regionSize;
|
|
ulong windowBase = fault & ~(LazyCommitWindowBytes - 1);
|
|
if (windowBase < regionBase)
|
|
{
|
|
windowBase = regionBase;
|
|
}
|
|
|
|
if (windowBase >= regionEnd)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
ulong windowEnd = Math.Min(regionEnd, windowBase + LazyCommitWindowBytes);
|
|
ulong windowSize = windowEnd - windowBase;
|
|
windowSize &= 0xFFFFFFFFFFFFF000uL;
|
|
if (windowSize == 0)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
baseAddress = windowBase;
|
|
length = windowSize;
|
|
return true;
|
|
}
|
|
|
|
// The commit protection is one of the two raw values ResolveLazyCommitProtection
|
|
// produces (0x40 RWX / 0x04 RW); the enum mapping reproduces those exactly.
|
|
static bool TryCommitRange(IHostMemory hostMemory, ulong baseAddress, ulong length, uint protection)
|
|
{
|
|
if (length == 0)
|
|
{
|
|
return false;
|
|
}
|
|
return hostMemory.Commit(baseAddress, length, protection == 64u ? HostPageProtection.ReadWriteExecute : HostPageProtection.ReadWrite);
|
|
}
|
|
|
|
static bool TryReserveRange(IHostMemory hostMemory, ulong baseAddress, ulong length)
|
|
{
|
|
if (length == 0)
|
|
{
|
|
return false;
|
|
}
|
|
return hostMemory.Reserve(baseAddress, length, HostPageProtection.ReadWrite) != 0;
|
|
}
|
|
|
|
static bool TryReserveThenCommit(IHostMemory hostMemory, ulong reserveAddress, ulong reserveSize, ulong commitAddress, ulong commitSize, uint protection)
|
|
{
|
|
if (!TryReserveRange(hostMemory, reserveAddress, reserveSize))
|
|
{
|
|
return false;
|
|
}
|
|
return TryCommitRange(hostMemory, commitAddress, commitSize, protection);
|
|
}
|
|
|
|
static bool IsAccessCompatible(ulong accessType, uint protection)
|
|
{
|
|
const uint pageNoAccess = 0x01;
|
|
const uint pageReadOnly = 0x02;
|
|
const uint pageReadWrite = 0x04;
|
|
const uint pageWriteCopy = 0x08;
|
|
const uint pageExecute = 0x10;
|
|
const uint pageExecuteRead = 0x20;
|
|
const uint pageExecuteReadWrite = 0x40;
|
|
const uint pageExecuteWriteCopy = 0x80;
|
|
const uint pageGuard = 0x100;
|
|
const uint accessMask = 0xFF;
|
|
|
|
if ((protection & pageGuard) != 0)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
uint access = protection & accessMask;
|
|
if (access == pageNoAccess)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
return accessType switch
|
|
{
|
|
0 => access is pageReadOnly or pageReadWrite or pageWriteCopy or pageExecuteRead or pageExecuteReadWrite or pageExecuteWriteCopy,
|
|
1 => access is pageReadWrite or pageWriteCopy or pageExecuteReadWrite or pageExecuteWriteCopy,
|
|
8 => access is pageExecute or pageExecuteRead or pageExecuteReadWrite or pageExecuteWriteCopy,
|
|
_ => false
|
|
};
|
|
}
|
|
}
|
|
|
|
private static bool ShouldTraceLazyCommit(int traceIndex)
|
|
{
|
|
if (string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_LAZY_COMMIT"), "1", StringComparison.Ordinal))
|
|
{
|
|
return true;
|
|
}
|
|
|
|
return traceIndex <= 16 || traceIndex % 256 == 0;
|
|
}
|
|
|
|
private static uint ResolveLazyCommitProtection(ulong accessType, uint allocationProtect)
|
|
{
|
|
if (accessType == 8 || (allocationProtect & 0xF0) != 0)
|
|
{
|
|
return 64u;
|
|
}
|
|
return 4u;
|
|
}
|
|
}
|