Files
sharpemu/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.Exceptions.cs
T
2026-06-28 23:45:26 +03:00

1159 lines
35 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.HLE;
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 = CreateExceptionHandlerTrampoline(RawVectoredHandlerPtrManaged);
if (_rawExceptionHandlerStub == 0)
{
throw new InvalidOperationException("Failed to create raw exception handler trampoline");
}
_rawExceptionHandler = (nint)AddVectoredExceptionHandler(1u, _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 = CreateExceptionHandlerTrampoline(Marshal.GetFunctionPointerForDelegate(_handlerDelegate));
if (_exceptionHandlerStub == 0)
{
throw new InvalidOperationException("Failed to create exception handler trampoline");
}
_exceptionHandler = (nint)AddVectoredExceptionHandler(1u, _exceptionHandlerStub);
Console.Error.WriteLine($"[LOADER][INFO] Exception handler installed: 0x{_exceptionHandler:X16}");
_unhandledFilterDelegate = UnhandledExceptionFilter;
_unhandledFilterHandle = GCHandle.Alloc(_unhandledFilterDelegate);
_unhandledFilterStub = CreateExceptionHandlerTrampoline(Marshal.GetFunctionPointerForDelegate(_unhandledFilterDelegate));
if (_unhandledFilterStub == 0)
{
throw new InvalidOperationException("Failed to create unhandled exception filter trampoline");
}
SetUnhandledExceptionFilter(_unhandledFilterStub);
}
private unsafe int UnhandledExceptionFilter(void* exceptionInfo)
{
try
{
EXCEPTION_RECORD* exceptionRecord = ((EXCEPTION_POINTERS*)exceptionInfo)->ExceptionRecord;
ulong rip = ReadCtxU64(((EXCEPTION_POINTERS*)exceptionInfo)->ContextRecord, 248);
ulong rsp = ReadCtxU64(((EXCEPTION_POINTERS*)exceptionInfo)->ContextRecord, 152);
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, 248);
ulong rsp = ReadCtxU64(contextRecord, 152);
if (exceptionCode == 3221225477u && TryHandleLazyCommittedPage(exceptionRecord, rip, rsp))
{
return -1;
}
if (IsBenignHostDebugException(exceptionCode))
{
return -1;
}
if (exceptionCode == MSVC_CPP_EXCEPTION)
{
return 0;
}
switch (exceptionCode)
{
case 3221225477u:
LogAccessViolationTrace(exceptionAddress, exceptionRecord);
break;
case 3221226505u:
{
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, 120);
ulong rbx = ReadCtxU64(contextRecord, 144);
ulong rcx = ReadCtxU64(contextRecord, 128);
ulong rdx = ReadCtxU64(contextRecord, 136);
ulong rsi = ReadCtxU64(contextRecord, 168);
ulong rdi = ReadCtxU64(contextRecord, 176);
ulong rbp = ReadCtxU64(contextRecord, 160);
ulong r8 = ReadCtxU64(contextRecord, 184);
ulong r9 = ReadCtxU64(contextRecord, 192);
ulong r10 = ReadCtxU64(contextRecord, 200);
ulong r11 = ReadCtxU64(contextRecord, 208);
ulong r12 = ReadCtxU64(contextRecord, 216);
ulong r13 = ReadCtxU64(contextRecord, 224);
ulong r14 = ReadCtxU64(contextRecord, 232);
ulong r15 = ReadCtxU64(contextRecord, 240);
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 == 3221225477u && 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 (VirtualQuery((void*)target, out var mbi, (nuint)sizeof(MEMORY_BASIC_INFORMATION64)) != 0)
{
Console.Error.WriteLine($"[LOADER][INFO] AV target region: base=0x{mbi.BaseAddress:X16} size=0x{mbi.RegionSize:X16} state=0x{mbi.State:X08} protect=0x{mbi.Protect: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 3221225477u:
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 2147483651u:
Console.Error.WriteLine("[LOADER][WARNING] Type: Breakpoint (int3)");
Console.Error.WriteLine("[LOADER][WARNING] Unexpected breakpoint in direct-bridge mode");
break;
case 3221225501u:
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, 248) : 0;
ulong rsp = contextRecord != null ? ReadCtxU64(contextRecord, 152) : 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 (VirtualQuery((void*)address, out var mbi, (nuint)sizeof(MEMORY_BASIC_INFORMATION64)) == 0)
{
break;
}
ulong regionBase = mbi.BaseAddress;
ulong regionEnd = regionBase + mbi.RegionSize;
if (regionEnd <= address)
{
break;
}
if (mbi.State == MEM_COMMIT &&
IsReadableProtection(mbi.Protect) &&
IsExecutableProtection(mbi.Protect))
{
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 (VirtualQuery((void*)address, out var mbi, (nuint)sizeof(MEMORY_BASIC_INFORMATION64)) == 0)
{
return false;
}
ulong regionEnd = mbi.BaseAddress + mbi.RegionSize;
if (mbi.State != MEM_COMMIT || !IsReadableProtection(mbi.Protect) || 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 (VirtualQuery((void*)faultAddress, out var mbi, (nuint)sizeof(MEMORY_BASIC_INFORMATION64)) == 0)
{
return false;
}
ulong pageBase = faultAddress & 0xFFFFFFFFFFFFF000uL;
uint commitProtect = ResolveLazyCommitProtection(accessType, mbi.AllocationProtect);
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.State:X08} base=0x{mbi.BaseAddress:X16} size=0x{mbi.RegionSize:X16} alloc=0x{mbi.AllocationProtect:X08} prot=0x{mbi.Protect:X08}");
}
if (mbi.State == 4096 && IsAccessCompatible(accessType, mbi.Protect))
{
if (traceLazyCommit)
{
Console.Error.WriteLine($"[LOADER][TRACE] lazy-commit-race#{traceIndex}: fault=0x{faultAddress:X16} protect=0x{mbi.Protect:X08}");
}
return true;
}
bool committed = false;
ulong committedBase = 0;
ulong committedSize = 0;
if (mbi.State == 65536)
{
if (TryGetLazyCommitWindow(faultAddress, mbi.BaseAddress, mbi.RegionSize, out var windowBase, out var windowSize) &&
TryReserveThenCommit(windowBase, windowSize, windowBase, windowSize, commitProtect))
{
committed = true;
committedBase = windowBase;
committedSize = windowSize;
}
else
{
ulong largeBase = faultAddress & 0xFFFFFFFFFFE00000uL;
if (TryReserveThenCommit(largeBase, 2097152uL, largeBase, 2097152uL, commitProtect))
{
committed = true;
committedBase = largeBase;
committedSize = 2097152uL;
}
}
if (!committed)
{
ulong region64kBase = faultAddress & 0xFFFFFFFFFFFF0000uL;
if (TryReserveThenCommit(region64kBase, 65536uL, region64kBase, 65536uL, commitProtect))
{
committed = true;
committedBase = region64kBase;
committedSize = 65536uL;
}
else if (TryReserveThenCommit(pageBase, 4096uL, pageBase, 4096uL, commitProtect))
{
committed = true;
committedBase = pageBase;
committedSize = 4096uL;
}
}
if (!committed)
{
return false;
}
TryCommitRange(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 != 8192)
{
return false;
}
if (TryGetLazyCommitWindow(faultAddress, mbi.BaseAddress, mbi.RegionSize, out var commitWindowBase, out var commitWindowSize) &&
TryCommitRange(commitWindowBase, commitWindowSize, commitProtect))
{
committed = true;
committedBase = commitWindowBase;
committedSize = commitWindowSize;
}
else
{
ulong largeCommitBase = faultAddress & 0xFFFFFFFFFFE00000uL;
if (TryCommitRange(largeCommitBase, 2097152uL, commitProtect))
{
committed = true;
committedBase = largeCommitBase;
committedSize = 2097152uL;
}
}
if (!committed)
{
ulong region64kBase = faultAddress & 0xFFFFFFFFFFFF0000uL;
if (TryCommitRange(region64kBase, 65536uL, commitProtect))
{
committed = true;
committedBase = region64kBase;
committedSize = 65536uL;
}
else if (TryCommitRange(pageBase, 8192uL, commitProtect))
{
committed = true;
committedBase = pageBase;
committedSize = 8192uL;
}
else if (TryCommitRange(pageBase, 4096uL, commitProtect))
{
committed = true;
committedBase = pageBase;
committedSize = 4096uL;
}
}
if (!committed)
{
return false;
}
TryCommitRange(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;
}
static unsafe bool TryCommitRange(ulong baseAddress, ulong length, uint protection)
{
if (length == 0)
{
return false;
}
return VirtualAlloc((void*)baseAddress, (nuint)length, 4096u, protection) != null;
}
static unsafe bool TryReserveRange(ulong baseAddress, ulong length)
{
if (length == 0)
{
return false;
}
return VirtualAlloc((void*)baseAddress, (nuint)length, 8192u, 4u) != null;
}
static bool TryReserveThenCommit(ulong reserveAddress, ulong reserveSize, ulong commitAddress, ulong commitSize, uint protection)
{
if (!TryReserveRange(reserveAddress, reserveSize))
{
return false;
}
return TryCommitRange(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;
}
}