mirror of
https://github.com/par274/sharpemu.git
synced 2026-07-31 06:59:45 +08:00
Astro Bot stack: VEH/TBB, title clear, swapchain fallback, Psml MFSR (#528)
* Cpu/Kernel: harden VEH trampoline and keep TBB on native workers Route FastFail/CLR/stack-overflow around managed VEH, serialize managed entry with a recursive spinlock, require native workers for tbb_thead, and abandon pthread mutexes when a guest thread is torn down by worker abort so splash waiters are not left holding locks forever. * Cpu: soft-fail TBB native worker storms and cap concurrent Runs Throwing on worker/prologue faults killed the process mid tbb_thead burst (FailFast 0xC0000409). Soft-return 0x80020012, limit in-flight native Runs (default 2), and keep prewarm small so back-to-back boots do not need an artificial settle delay. * Agc/VideoOut: poison-only empty-SRT reject and clear procedural ES/PS Skip QueueSubmit only when Address-0 image slots remain; run the Astro title clear pair via CmdClearColorImage so the pass executes without descriptors that lose the device. * VideoOut: recreate swapchain with fallback extent on 0x0 surface Minimized Win32 surfaces report 0x0 / MaxImageExtent=0; deferring recreate forever left an OutOfDate swapchain with no presents. Clamp to last/default size and recreate instead of early-return. * Psml: stub MFSR init/shared/context and dispatch packet size Astro Bot asserts in GfxRenderStagePSSR when scePsmlMfsrInit is unresolved (Mfsr initialized failed). Soft HLE for the MFSR shared-resource and 800M3_2 context path plus dispatch packet size lets boot pass splash to first frame without claiming real upscaling. * Psml: stub MFSR GetDispatchMfsrPacket900 for logo PSSR Astro StartLevel ps_logo asserted GfxRenderStagePSSR.cpp:266 when GetDispatchMfsrPacket900 (RUNLFro+qok) was unresolved. Return SCE_OK from SizeInDwords so the 900 fill runs, soft-clear the guest packet buffer, and register 1000/1100 siblings for the same ABI.
This commit is contained in:
@@ -19,6 +19,9 @@ public sealed partial class DirectExecutionBackend
|
||||
private static int _lazyCommitTraceCount;
|
||||
private static int _guestAllocatorHoleRecoveries;
|
||||
private static int _auxiliaryThreadExecuteFaultRecoveries;
|
||||
private static int _auxiliaryThreadExecuteFaultSkips;
|
||||
private nint _workerAbortStack;
|
||||
private const uint WorkerAbortStackSize = 0x10000u;
|
||||
|
||||
private unsafe void SetupExceptionHandler()
|
||||
{
|
||||
@@ -435,18 +438,91 @@ public sealed partial class DirectExecutionBackend
|
||||
void* contextRecord,
|
||||
ulong rip)
|
||||
{
|
||||
if (exceptionRecord->ExceptionCode != 3221225477u ||
|
||||
rip >= 0x0000000800000000UL ||
|
||||
_activeGuestThreadState is not { Name: "tbb_thead" } activeThread)
|
||||
if (exceptionRecord->ExceptionCode != 3221225477u)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Prefer ThreadStatic active state; fall back to host-thread name when
|
||||
// concurrent TBB AVs race logging (tLT61: recover skipped, then Fatal).
|
||||
GuestThreadState? activeThread = _activeGuestThreadState;
|
||||
if (activeThread is null || activeThread.Name != "tbb_thead")
|
||||
{
|
||||
var hostName = Thread.CurrentThread.Name;
|
||||
if (hostName is null ||
|
||||
!hostName.StartsWith("SharpEmu-tbb_thead", StringComparison.Ordinal))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
activeThread = FindGuestThreadStateByHostThreadId(unchecked((int)GetCurrentThreadId()));
|
||||
if (activeThread is null || activeThread.Name != "tbb_thead")
|
||||
{
|
||||
var skip = Interlocked.Increment(ref _auxiliaryThreadExecuteFaultSkips);
|
||||
if (skip <= 8 || skip % 64 == 0)
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][WARN] tbb_recover skip #{skip}: rip=0x{rip:X16} " +
|
||||
$"host='{hostName}' active={(activeThread?.Name ?? "null")}");
|
||||
Console.Error.Flush();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
var hostExit = ActiveEntryReturnSentinelRip;
|
||||
if (hostExit < 0x10000)
|
||||
{
|
||||
hostExit = unchecked((ulong)_guestReturnStub);
|
||||
}
|
||||
|
||||
// Prefer worker-abort (SetEvent + ExitThread) over host_exit→RunEpilogue:
|
||||
// the latter FailFasts the process after TBB recover (tLT28/30 silent die).
|
||||
// Do NOT abandon mutexes here — managed HLE from inside VEH can re-enter
|
||||
// and Fatal (tLT73). NativeGuestExecutor.Run abandons after detecting abort.
|
||||
var abortRip = unchecked((ulong)_workerAbortStub);
|
||||
if (abortRip >= 0x10000)
|
||||
{
|
||||
// Do NOT SetEvent from managed VEH: that wakes the renter which may
|
||||
// TerminateThread while this thread is still inside VEH return
|
||||
// (tLTA2: recover logged, no respawning, process die). Abort stub
|
||||
// SetEvent's only after CONTINUE_EXECUTION resumes at park.
|
||||
|
||||
// Prefer the entry-stub-saved host RSP (real CreateThread stack).
|
||||
// Do not treat mid-range host stacks as guest — Astro worker stacks
|
||||
// often sit in 0x02xxxxxx_xxxx and were wrongly replaced with a
|
||||
// shared VirtualAlloc abort stack (concurrent TBB AV → die).
|
||||
var hostRspSlot = TlsGetValue(_hostRspSlotTlsIndex);
|
||||
ulong hostRsp = 0;
|
||||
if (hostRspSlot != 0)
|
||||
{
|
||||
hostRsp = *(ulong*)hostRspSlot;
|
||||
}
|
||||
|
||||
if (hostRsp < 0x10000)
|
||||
{
|
||||
hostRsp = EnsureWorkerAbortStackRsp();
|
||||
}
|
||||
|
||||
if (hostRsp >= 0x10000)
|
||||
{
|
||||
WriteCtxU64(contextRecord, 152, hostRsp & ~0xFUL);
|
||||
}
|
||||
|
||||
WriteCtxU64(contextRecord, 120, 0);
|
||||
WriteCtxU64(contextRecord, 248, abortRip);
|
||||
var recovery = Interlocked.Increment(ref _auxiliaryThreadExecuteFaultRecoveries);
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][WARN] Recovered auxiliary TBB execute fault #{recovery}: " +
|
||||
$"thread=0x{activeThread.ThreadHandle:X16} target=0x{rip:X16} " +
|
||||
$"host_rsp=0x{hostRsp:X16} -> worker_abort=0x{abortRip:X16}");
|
||||
Console.Error.WriteLine(
|
||||
"[LOADER][INFO] tbb_recover: parking native worker (SetEvent+park); " +
|
||||
"renter will TerminateThread+respawn — avoids ExitThread after VEH");
|
||||
Console.Error.Flush();
|
||||
return true;
|
||||
}
|
||||
|
||||
if (hostExit < 0x10000)
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
@@ -458,13 +534,57 @@ public sealed partial class DirectExecutionBackend
|
||||
_ = TryPatchActiveGuestReturnSlot(hostExit);
|
||||
WriteCtxU64(contextRecord, 120, 0);
|
||||
WriteCtxU64(contextRecord, 248, hostExit);
|
||||
var recovery = Interlocked.Increment(ref _auxiliaryThreadExecuteFaultRecoveries);
|
||||
var recoveryFallback = Interlocked.Increment(ref _auxiliaryThreadExecuteFaultRecoveries);
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][WARN] Recovered auxiliary TBB execute fault #{recovery}: " +
|
||||
$"[LOADER][WARN] Recovered auxiliary TBB execute fault #{recoveryFallback}: " +
|
||||
$"thread=0x{activeThread.ThreadHandle:X16} target=0x{rip:X16} -> host_exit=0x{hostExit:X16}");
|
||||
Console.Error.WriteLine(
|
||||
"[LOADER][INFO] tbb_recover: resumed at host_exit (abort stub unavailable); " +
|
||||
"subsequent FastFail/CLR must not re-enter managed VEH " +
|
||||
"(live trampoline pre-filters 0xC0000409 / 0xE0434352)");
|
||||
Console.Error.Flush();
|
||||
return true;
|
||||
}
|
||||
|
||||
private GuestThreadState? FindGuestThreadStateByHostThreadId(int hostThreadId)
|
||||
{
|
||||
if (hostThreadId == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
foreach (var thread in SnapshotGuestThreads())
|
||||
{
|
||||
if (Volatile.Read(ref thread.HostThreadId) == hostThreadId)
|
||||
{
|
||||
return thread;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private unsafe ulong EnsureWorkerAbortStackRsp()
|
||||
{
|
||||
if (_workerAbortStack == 0)
|
||||
{
|
||||
_workerAbortStack = (nint)VirtualAlloc(null, WorkerAbortStackSize, 12288u, 4u);
|
||||
if (_workerAbortStack == 0)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Grow-down stack: hand out near the top with alignment headroom.
|
||||
return (ulong)(_workerAbortStack + (nint)WorkerAbortStackSize - 0x100) & ~0xFUL;
|
||||
}
|
||||
|
||||
private unsafe bool TryRecoverGuestInt41(uint exceptionCode, void* contextRecord, ulong rip)
|
||||
{
|
||||
if (!_ignoreGuestInt41 || exceptionCode != 3221225477u || rip < 0x10000)
|
||||
|
||||
@@ -29,9 +29,29 @@ public sealed partial class DirectExecutionBackend
|
||||
private static readonly bool NativeGuestWorkersDisabled =
|
||||
string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_DISABLE_NATIVE_GUEST_WORKERS"), "1", StringComparison.Ordinal);
|
||||
|
||||
// Cap concurrent native-worker Runs. Astro's tbb_thead burst overlaps many
|
||||
// UnmanagedCallersOnly prologues; a large prewarm + unbounded concurrency
|
||||
// FailFasts (0xC0000409) mid-storm with no VEH breadcrumb. Pool size and
|
||||
// in-flight Runs are separate knobs.
|
||||
private static readonly int NativeWorkerMaxConcurrent = ReadNativeWorkerMaxConcurrent();
|
||||
|
||||
private static int ReadNativeWorkerMaxConcurrent()
|
||||
{
|
||||
if (int.TryParse(
|
||||
Environment.GetEnvironmentVariable("SHARPEMU_NATIVE_WORKER_MAX_CONCURRENT"),
|
||||
out var parsed) &&
|
||||
parsed > 0)
|
||||
{
|
||||
return Math.Clamp(parsed, 1, 64);
|
||||
}
|
||||
|
||||
return 2;
|
||||
}
|
||||
|
||||
private readonly object _nativeWorkerGate = new();
|
||||
private readonly List<NativeGuestExecutor> _allNativeWorkers = new();
|
||||
private readonly Stack<NativeGuestExecutor> _idleNativeWorkers = new();
|
||||
private readonly SemaphoreSlim _nativeWorkerRunLimiter = new(NativeWorkerMaxConcurrent);
|
||||
private bool _nativeWorkersDisposed;
|
||||
private int _nativeWorkerCreationFailedLogged;
|
||||
|
||||
@@ -49,6 +69,9 @@ public sealed partial class DirectExecutionBackend
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
private static extern uint WaitForSingleObject(nint hHandle, uint dwMilliseconds);
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
private static extern bool TerminateThread(nint hThread, uint dwExitCode);
|
||||
|
||||
// Runs an emitted guest entry stub. Preferred path is a pooled native worker
|
||||
// thread; falls back to the historical inline calli (guest frames above this
|
||||
// thread's managed frames) when workers are disabled or unavailable.
|
||||
@@ -56,40 +79,148 @@ public sealed partial class DirectExecutionBackend
|
||||
// Callers set the Active* thread-statics before emitting the stub and read the
|
||||
// yield/forced-exit flags right after this returns, so the worker outcome is
|
||||
// copied back into this thread's statics before returning.
|
||||
private unsafe int RunGuestEntryStub(void* entryStub, ulong hostRspSlot)
|
||||
private unsafe int RunGuestEntryStub(void* entryStub, ulong hostRspSlot, bool requireNativeWorker = false)
|
||||
{
|
||||
var worker = RentNativeGuestExecutor();
|
||||
if (worker is null)
|
||||
{
|
||||
TlsSetValue(_hostRspSlotTlsIndex, (nint)hostRspSlot);
|
||||
return CallNativeEntry(entryStub);
|
||||
}
|
||||
// Limit in-flight native Runs before renting so the idle pool is not
|
||||
// drained by threads blocked on the concurrency gate.
|
||||
_nativeWorkerRunLimiter.Wait();
|
||||
NativeGuestExecutor? worker = null;
|
||||
try
|
||||
{
|
||||
var state = _activeGuestThreadState;
|
||||
var nativeReturn = worker.Run(
|
||||
_activeCpuContext!,
|
||||
state,
|
||||
GuestThreadExecution.CurrentGuestThreadHandle,
|
||||
_activeEntryReturnSentinelRip,
|
||||
_activeGuestReturnSlotAddress,
|
||||
(nint)hostRspSlot,
|
||||
(nint)entryStub,
|
||||
state?.AffinityMask ?? 0,
|
||||
out var yieldRequested,
|
||||
out var yieldReason,
|
||||
out var forcedExit);
|
||||
_activeGuestThreadYieldRequested = yieldRequested;
|
||||
_activeGuestThreadYieldReason = yieldReason;
|
||||
_activeForcedGuestExit = forcedExit;
|
||||
return nativeReturn;
|
||||
// Astro can spawn a burst of tbb_thead while workers are still in
|
||||
// TerminateThread+respawn. Wait for a native worker — never fall back
|
||||
// to managed inline (FailFast) and never throw (uncaught throw mid-
|
||||
// storm was a silent process die).
|
||||
var maxAttempts = requireNativeWorker ? 500 : 48;
|
||||
for (var attempt = 0; attempt < maxAttempts; attempt++)
|
||||
{
|
||||
worker = RentNativeGuestExecutor();
|
||||
if (worker is not null)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
if (!requireNativeWorker)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
Thread.Sleep(attempt < 32 ? 1 : 4);
|
||||
}
|
||||
|
||||
if (worker is null)
|
||||
{
|
||||
if (requireNativeWorker)
|
||||
{
|
||||
var n = Interlocked.Increment(ref _tbbNativeWorkerRefuseCount);
|
||||
if (n <= 8 || n % 32 == 0)
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][ERROR] tbb_native_worker unavailable #{n} after {maxAttempts} attempts; " +
|
||||
"skipping run (no managed inline, no throw)");
|
||||
Console.Error.Flush();
|
||||
}
|
||||
|
||||
_activeGuestThreadYieldRequested = true;
|
||||
_activeGuestThreadYieldReason = "tbb_native_worker_unavailable";
|
||||
_activeForcedGuestExit = true;
|
||||
return unchecked((int)0x80020012);
|
||||
}
|
||||
|
||||
TlsSetValue(_hostRspSlotTlsIndex, (nint)hostRspSlot);
|
||||
return CallNativeEntry(entryStub);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var state = _activeGuestThreadState;
|
||||
if (state is { Name: "tbb_thead" })
|
||||
{
|
||||
var n = Interlocked.Increment(ref _tbbNativeRunEnterCount);
|
||||
if (n <= 12 || n % 64 == 0)
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][INFO] tbb_run_enter #{n} native_tid_pending handle=0x{state.ThreadHandle:X16} " +
|
||||
$"max_concurrent={NativeWorkerMaxConcurrent}");
|
||||
Console.Error.Flush();
|
||||
}
|
||||
}
|
||||
|
||||
var nativeReturn = worker.Run(
|
||||
_activeCpuContext!,
|
||||
state,
|
||||
GuestThreadExecution.CurrentGuestThreadHandle,
|
||||
_activeEntryReturnSentinelRip,
|
||||
_activeGuestReturnSlotAddress,
|
||||
(nint)hostRspSlot,
|
||||
(nint)entryStub,
|
||||
state?.AffinityMask ?? 0,
|
||||
out var yieldRequested,
|
||||
out var yieldReason,
|
||||
out var forcedExit);
|
||||
_activeGuestThreadYieldRequested = yieldRequested;
|
||||
_activeGuestThreadYieldReason = yieldReason;
|
||||
_activeForcedGuestExit = forcedExit;
|
||||
return nativeReturn;
|
||||
}
|
||||
finally
|
||||
{
|
||||
ReturnNativeGuestExecutor(worker);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
ReturnNativeGuestExecutor(worker);
|
||||
_nativeWorkerRunLimiter.Release();
|
||||
}
|
||||
}
|
||||
|
||||
private static int _tbbNativeRunEnterCount;
|
||||
private static int _tbbNativeWorkerRefuseCount;
|
||||
internal static int _tbbWorkerPrologueFaultCount;
|
||||
|
||||
private void PrewarmNativeGuestWorkers(int count)
|
||||
{
|
||||
if (!OperatingSystem.IsWindows() || NativeGuestWorkersDisabled || count <= 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var warmed = new List<NativeGuestExecutor>(count);
|
||||
for (var i = 0; i < count; i++)
|
||||
{
|
||||
var worker = NativeGuestExecutor.TryCreate(this);
|
||||
if (worker is null)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
warmed.Add(worker);
|
||||
}
|
||||
|
||||
lock (_nativeWorkerGate)
|
||||
{
|
||||
if (_nativeWorkersDisposed)
|
||||
{
|
||||
foreach (var worker in warmed)
|
||||
{
|
||||
worker.Dispose();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var worker in warmed)
|
||||
{
|
||||
_allNativeWorkers.Add(worker);
|
||||
_idleNativeWorkers.Push(worker);
|
||||
}
|
||||
}
|
||||
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][INFO] Native guest workers prewarmed: {warmed.Count}/{count} " +
|
||||
$"max_concurrent={NativeWorkerMaxConcurrent}");
|
||||
Console.Error.Flush();
|
||||
}
|
||||
|
||||
private NativeGuestExecutor? RentNativeGuestExecutor()
|
||||
{
|
||||
// NativeGuestExecutor emits a Win32 wait loop and creates it with
|
||||
@@ -400,6 +531,22 @@ public sealed partial class DirectExecutionBackend
|
||||
return false;
|
||||
}
|
||||
FlushInstructionCache(GetCurrentProcess(), _loopStub, LoopStubSize);
|
||||
return StartWorkerThread();
|
||||
}
|
||||
|
||||
private bool RestartWorkerThread()
|
||||
{
|
||||
if (_loopStub == null || _controlBlock == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
*(int*)_controlBlock = 0;
|
||||
return StartWorkerThread();
|
||||
}
|
||||
|
||||
private bool StartWorkerThread()
|
||||
{
|
||||
_threadHandle = CreateThread(
|
||||
0,
|
||||
WorkerStackReservation,
|
||||
@@ -445,6 +592,49 @@ public sealed partial class DirectExecutionBackend
|
||||
_runForcedExit = false;
|
||||
SignalWorkAvailable();
|
||||
WaitWorkCompleted();
|
||||
|
||||
// Normal path: RunEpilogue/ExitRun clears _entered before SetEvent(done).
|
||||
// TBB abort stub SetEvent's without ExitRun — _entered stays true.
|
||||
if (_entered)
|
||||
{
|
||||
var waitRc = WaitForSingleObject(_threadHandle, 500u);
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][WARN] Native guest worker tid={_nativeThreadId} aborted during run; " +
|
||||
$"wait_rc=0x{waitRc:X8} respawning");
|
||||
Console.Error.Flush();
|
||||
if (_runState is { } abortedState)
|
||||
{
|
||||
_ = GuestThreadExecution.NotifyGuestThreadAbandoned(
|
||||
abortedState.ThreadHandle,
|
||||
"tbb_worker_abort");
|
||||
Volatile.Write(ref abortedState.HostThreadId, _prevHostThreadId);
|
||||
}
|
||||
_entered = false;
|
||||
if (_threadHandle != 0)
|
||||
{
|
||||
// Abort stub parks (no ExitThread). Force-kill the parked OS
|
||||
// thread so we can recreate the loop without process teardown.
|
||||
if (waitRc != 0u)
|
||||
{
|
||||
_ = TerminateThread(_threadHandle, unchecked((uint)(-1)));
|
||||
_ = WaitForSingleObject(_threadHandle, 1000u);
|
||||
}
|
||||
CloseHandle(_threadHandle);
|
||||
_threadHandle = 0;
|
||||
_nativeThreadId = 0;
|
||||
}
|
||||
if (!RestartWorkerThread())
|
||||
{
|
||||
_runPrologueFailed = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
_runPrologueFailed = false;
|
||||
_runForcedExit = true;
|
||||
_runNativeResult = 0;
|
||||
}
|
||||
}
|
||||
|
||||
_runContext = null;
|
||||
_runState = null;
|
||||
yieldRequested = _runYieldRequested;
|
||||
@@ -452,7 +642,22 @@ public sealed partial class DirectExecutionBackend
|
||||
forcedExit = _runForcedExit;
|
||||
if (_runPrologueFailed)
|
||||
{
|
||||
throw new InvalidOperationException("Native guest worker failed to bind the run ambient (prologue fault)");
|
||||
// Never throw out of the native-worker rent path: an uncaught
|
||||
// exception mid-TBB storm kills the process with no FailFast
|
||||
// breadcrumb.
|
||||
var n = Interlocked.Increment(ref _tbbWorkerPrologueFaultCount);
|
||||
if (n <= 8 || n % 32 == 0)
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][ERROR] tbb_worker prologue fault #{n}; soft-fail run " +
|
||||
$"(tid={_nativeThreadId})");
|
||||
Console.Error.Flush();
|
||||
}
|
||||
|
||||
yieldRequested = true;
|
||||
yieldReason = "tbb_worker_prologue_fault";
|
||||
forcedExit = true;
|
||||
return unchecked((int)0x80020012);
|
||||
}
|
||||
return _runNativeResult;
|
||||
}
|
||||
@@ -546,6 +751,18 @@ public sealed partial class DirectExecutionBackend
|
||||
_activeGuestThreadState = _runState;
|
||||
backend.BindTlsBase(_runContext!);
|
||||
TlsSetValue(backend._hostRspSlotTlsIndex, _runHostRspSlot);
|
||||
if (backend._workerDoneEventTlsIndex != uint.MaxValue)
|
||||
{
|
||||
nint doneHandle = OperatingSystem.IsWindows()
|
||||
? _workCompleted!.SafeWaitHandle.DangerousGetHandle()
|
||||
: _doneSemaphore;
|
||||
TlsSetValue(backend._workerDoneEventTlsIndex, doneHandle);
|
||||
}
|
||||
if (backend._tbbAbortEligibleTlsIndex != uint.MaxValue)
|
||||
{
|
||||
nint eligible = _runState is { Name: "tbb_thead" } ? 1 : 0;
|
||||
TlsSetValue(backend._tbbAbortEligibleTlsIndex, eligible);
|
||||
}
|
||||
if (_runState is { } state)
|
||||
{
|
||||
_prevHostThreadId = Volatile.Read(ref state.HostThreadId);
|
||||
@@ -580,6 +797,14 @@ public sealed partial class DirectExecutionBackend
|
||||
Volatile.Write(ref state.HostThreadId, _prevHostThreadId);
|
||||
}
|
||||
TlsSetValue(_backend._hostRspSlotTlsIndex, _prevHostRspSlot);
|
||||
if (_backend._workerDoneEventTlsIndex != uint.MaxValue)
|
||||
{
|
||||
TlsSetValue(_backend._workerDoneEventTlsIndex, 0);
|
||||
}
|
||||
if (_backend._tbbAbortEligibleTlsIndex != uint.MaxValue)
|
||||
{
|
||||
TlsSetValue(_backend._tbbAbortEligibleTlsIndex, 0);
|
||||
}
|
||||
GuestThreadExecution.RestoreGuestThread(_prevGuestThreadHandle);
|
||||
_activeExecutionBackend = _prevBackend;
|
||||
_activeCpuContext = _prevContext;
|
||||
|
||||
@@ -214,6 +214,15 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
||||
|
||||
private nint _guestReturnStub;
|
||||
|
||||
private nint _workerAbortStub;
|
||||
private nint _vehManagedEntryLock;
|
||||
|
||||
private uint _workerDoneEventTlsIndex = uint.MaxValue;
|
||||
|
||||
private uint _tbbAbortEligibleTlsIndex = uint.MaxValue;
|
||||
|
||||
private nint _setEventAddress;
|
||||
|
||||
private nint _rawExceptionHandler;
|
||||
|
||||
private nint _rawExceptionHandlerStub;
|
||||
@@ -1041,6 +1050,8 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
||||
_selfHandlePtr = GCHandle.ToIntPtr(_selfHandle);
|
||||
_guestTlsBaseTlsIndex = TlsAlloc();
|
||||
_hostRspSlotTlsIndex = TlsAlloc();
|
||||
_workerDoneEventTlsIndex = OperatingSystem.IsWindows() ? TlsAlloc() : uint.MaxValue;
|
||||
_tbbAbortEligibleTlsIndex = OperatingSystem.IsWindows() ? TlsAlloc() : uint.MaxValue;
|
||||
if (_guestTlsBaseTlsIndex == uint.MaxValue || _hostRspSlotTlsIndex == uint.MaxValue)
|
||||
{
|
||||
throw new OutOfMemoryException("Failed to allocate native TLS slots");
|
||||
@@ -1064,6 +1075,7 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
||||
{
|
||||
throw new InvalidOperationException("Failed to resolve kernel32 thread timing functions");
|
||||
}
|
||||
_setEventAddress = kernel32 != 0 ? GetProcAddress(kernel32, "SetEvent") : 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -1087,13 +1099,29 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
||||
{
|
||||
throw new OutOfMemoryException("Failed to allocate host stack slot storage");
|
||||
}
|
||||
_vehManagedEntryLock = (nint)VirtualAlloc(null, 64u, 12288u, 4u);
|
||||
if (_vehManagedEntryLock == 0)
|
||||
{
|
||||
throw new OutOfMemoryException("Failed to allocate VEH managed-entry lock");
|
||||
}
|
||||
// owner (nint) + depth (int); recursive — nested VEH on same thread must reenter.
|
||||
*(nint*)_vehManagedEntryLock = 0;
|
||||
*(int*)(_vehManagedEntryLock + sizeof(nint)) = 0;
|
||||
_unresolvedReturnStub = CreateUnresolvedReturnStub();
|
||||
_guestReturnStub = CreateGuestReturnStub();
|
||||
if (_guestReturnStub == 0)
|
||||
{
|
||||
throw new OutOfMemoryException("Failed to allocate guest return stub");
|
||||
}
|
||||
_workerAbortStub = CreateWorkerAbortStub();
|
||||
if (_workerAbortStub == 0 && OperatingSystem.IsWindows())
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
"[LOADER][WARN] Worker abort stub unavailable; TBB execute-fault recover will use host_exit");
|
||||
}
|
||||
SetupExceptionHandler();
|
||||
// Cover the Astro TBB spawn storm (often 8–12 concurrent tbb_thead).
|
||||
PrewarmNativeGuestWorkers(Math.Max(NativeWorkerMaxConcurrent, 4));
|
||||
}
|
||||
|
||||
public bool TryExecute(CpuContext context, ulong entryPoint, Generation generation, IReadOnlyDictionary<ulong, string> importStubs, IReadOnlyDictionary<string, ulong> runtimeSymbols, CpuExecutionOptions executionOptions, out OrbisGen2Result result)
|
||||
@@ -2409,9 +2437,147 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
||||
return (nint)ptr;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// After a TBB execute-fault, VEH redirects here on a host stack.
|
||||
/// SetEvent(done) then park forever — ExitThread from VEH CONTINUE_EXECUTION
|
||||
/// was taking down the whole process (recover logged, no respawning). The
|
||||
/// renter TerminateThread's the parked worker and respawns a clean loop.
|
||||
/// </summary>
|
||||
private unsafe nint CreateWorkerAbortStub()
|
||||
{
|
||||
if (!OperatingSystem.IsWindows() ||
|
||||
_workerDoneEventTlsIndex == uint.MaxValue ||
|
||||
_tlsGetValueAddress == 0 ||
|
||||
_setEventAddress == 0)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
nint kernel32 = GetModuleHandle("kernel32.dll");
|
||||
nint getStdHandle = kernel32 != 0 ? GetProcAddress(kernel32, "GetStdHandle") : 0;
|
||||
nint writeFile = kernel32 != 0 ? GetProcAddress(kernel32, "WriteFile") : 0;
|
||||
nint flushFileBuffers = kernel32 != 0 ? GetProcAddress(kernel32, "FlushFileBuffers") : 0;
|
||||
|
||||
const uint stubSize = 256u;
|
||||
void* ptr = VirtualAlloc(null, stubSize, 12288u, 4u);
|
||||
if (ptr == null)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
byte* code = (byte*)ptr;
|
||||
int offset = 0;
|
||||
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0x83);
|
||||
EmitByte(code, ref offset, 0xEC); EmitByte(code, ref offset, 0x28); // sub rsp, 0x28
|
||||
EmitByte(code, ref offset, 0xB9);
|
||||
EmitUInt32(code, ref offset, _workerDoneEventTlsIndex); // mov ecx, tls
|
||||
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0xB8);
|
||||
*(nint*)(code + offset) = _tlsGetValueAddress;
|
||||
offset += sizeof(nint);
|
||||
EmitByte(code, ref offset, 0xFF); EmitByte(code, ref offset, 0xD0); // call TlsGetValue
|
||||
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0x85);
|
||||
EmitByte(code, ref offset, 0xC0); // test rax, rax
|
||||
EmitByte(code, ref offset, 0x74); EmitByte(code, ref offset, 0x0F); // jz skip SetEvent (15 bytes)
|
||||
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0x89);
|
||||
EmitByte(code, ref offset, 0xC1); // mov rcx, rax
|
||||
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0xB8);
|
||||
*(nint*)(code + offset) = _setEventAddress;
|
||||
offset += sizeof(nint);
|
||||
EmitByte(code, ref offset, 0xFF); EmitByte(code, ref offset, 0xD0); // call SetEvent
|
||||
|
||||
// Breadcrumb on host stack (survives silent teardown better than managed log).
|
||||
int msgAbsSlot = -1;
|
||||
if (getStdHandle != 0 && writeFile != 0)
|
||||
{
|
||||
ReadOnlySpan<byte> msg = "[LOADER][WARN] tbb_abort_stub SetEvent+park\n"u8;
|
||||
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0x83);
|
||||
EmitByte(code, ref offset, 0xEC); EmitByte(code, ref offset, 0x20); // extra shadow for WriteFile args
|
||||
EmitByte(code, ref offset, 0xB9); EmitUInt32(code, ref offset, unchecked((uint)-12));
|
||||
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0xB8);
|
||||
*(nint*)(code + offset) = getStdHandle;
|
||||
offset += sizeof(nint);
|
||||
EmitByte(code, ref offset, 0xFF); EmitByte(code, ref offset, 0xD0);
|
||||
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0x89);
|
||||
EmitByte(code, ref offset, 0xC1); // mov rcx, handle
|
||||
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0x89);
|
||||
EmitByte(code, ref offset, 0xC3); // mov rbx, handle (nonvolatile for flush)
|
||||
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0xB8);
|
||||
msgAbsSlot = offset;
|
||||
*(nint*)(code + offset) = 0;
|
||||
offset += sizeof(nint);
|
||||
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0x89);
|
||||
EmitByte(code, ref offset, 0xC2); // mov rdx, msg
|
||||
EmitByte(code, ref offset, 0x41); EmitByte(code, ref offset, 0xB8);
|
||||
EmitUInt32(code, ref offset, (uint)msg.Length);
|
||||
EmitByte(code, ref offset, 0x4C); EmitByte(code, ref offset, 0x8D);
|
||||
EmitByte(code, ref offset, 0x4C); EmitByte(code, ref offset, 0x24);
|
||||
EmitByte(code, ref offset, 0x20); // lea r9, [rsp+0x20]
|
||||
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0xC7);
|
||||
EmitByte(code, ref offset, 0x44); EmitByte(code, ref offset, 0x24);
|
||||
EmitByte(code, ref offset, 0x20); EmitUInt32(code, ref offset, 0);
|
||||
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0xC7);
|
||||
EmitByte(code, ref offset, 0x44); EmitByte(code, ref offset, 0x24);
|
||||
EmitByte(code, ref offset, 0x28); EmitUInt32(code, ref offset, 0);
|
||||
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0xB8);
|
||||
*(nint*)(code + offset) = writeFile;
|
||||
offset += sizeof(nint);
|
||||
EmitByte(code, ref offset, 0xFF); EmitByte(code, ref offset, 0xD0);
|
||||
if (flushFileBuffers != 0)
|
||||
{
|
||||
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0x89);
|
||||
EmitByte(code, ref offset, 0xD9); // mov rcx, rbx
|
||||
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0xB8);
|
||||
*(nint*)(code + offset) = flushFileBuffers;
|
||||
offset += sizeof(nint);
|
||||
EmitByte(code, ref offset, 0xFF); EmitByte(code, ref offset, 0xD0);
|
||||
}
|
||||
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0x83);
|
||||
EmitByte(code, ref offset, 0xC4); EmitByte(code, ref offset, 0x20);
|
||||
}
|
||||
|
||||
// Park: do not ExitThread (process-wide silent die after VEH redirect).
|
||||
int parkOffset = offset;
|
||||
EmitByte(code, ref offset, 0xF3); EmitByte(code, ref offset, 0x90); // pause
|
||||
EmitByte(code, ref offset, 0xEB);
|
||||
EmitByte(code, ref offset, unchecked((byte)(parkOffset - (offset + 1)))); // jmp park
|
||||
|
||||
if (msgAbsSlot >= 0)
|
||||
{
|
||||
ReadOnlySpan<byte> msgEmbed = "[LOADER][WARN] tbb_abort_stub SetEvent+park\n"u8;
|
||||
*(nint*)(code + msgAbsSlot) = (nint)ptr + offset;
|
||||
for (int i = 0; i < msgEmbed.Length; i++)
|
||||
{
|
||||
EmitByte(code, ref offset, msgEmbed[i]);
|
||||
}
|
||||
}
|
||||
|
||||
if (offset > (int)stubSize)
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][ERROR] Worker abort stub overflow: used={offset} cap={stubSize}");
|
||||
VirtualFree(ptr, 0u, 32768u);
|
||||
return 0;
|
||||
}
|
||||
|
||||
uint oldProtect = default;
|
||||
if (!VirtualProtect(ptr, stubSize, 32u, &oldProtect))
|
||||
{
|
||||
VirtualFree(ptr, 0u, 32768u);
|
||||
return 0;
|
||||
}
|
||||
FlushInstructionCache(GetCurrentProcess(), ptr, (nuint)offset);
|
||||
return (nint)ptr;
|
||||
}
|
||||
|
||||
private unsafe nint CreateExceptionHandlerTrampoline(nint managedHandler)
|
||||
{
|
||||
const uint stubSize = 256u;
|
||||
// Live VEH trampoline used by SetupExceptionHandler. Must pre-filter
|
||||
// FastFail / CLR / MSVC C++ / stack-overflow the same way as
|
||||
// WindowsFaultHandling.CreateHandlerThunk: entering managed VEH while
|
||||
// the thread is in cooperative GC mode fail-fasts with
|
||||
// "UnmanagedCallersOnly method from managed code" (tLT18–22).
|
||||
// Extra headroom for native tbb abort + recursive managed-entry spinlock.
|
||||
const uint stubSize = 2048u;
|
||||
void* ptr = VirtualAlloc(null, stubSize, 12288u, 64u);
|
||||
if (ptr == null)
|
||||
{
|
||||
@@ -2420,10 +2586,305 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
||||
|
||||
byte* code = (byte*)ptr;
|
||||
int offset = 0;
|
||||
|
||||
ReadOnlySpan<uint> nonManagedExceptionCodes =
|
||||
[
|
||||
0xE0434352u, // CLR managed exception
|
||||
0xE06D7363u, // MSVC C++ exception
|
||||
0xC0000409u, // STATUS_STACK_BUFFER_OVERRUN / FailFast
|
||||
0xC00000FDu, // STATUS_STACK_OVERFLOW
|
||||
];
|
||||
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0x8B); EmitByte(code, ref offset, 0x01); // mov rax, [rcx]
|
||||
EmitByte(code, ref offset, 0x8B); EmitByte(code, ref offset, 0x00); // mov eax, [rax] ExceptionCode
|
||||
var passJumpOffsets = stackalloc int[nonManagedExceptionCodes.Length];
|
||||
int fastFailJumpSlot = -1;
|
||||
for (int i = 0; i < nonManagedExceptionCodes.Length; i++)
|
||||
{
|
||||
EmitByte(code, ref offset, 0x3D);
|
||||
EmitUInt32(code, ref offset, nonManagedExceptionCodes[i]);
|
||||
EmitByte(code, ref offset, 0x74);
|
||||
passJumpOffsets[i] = offset;
|
||||
EmitByte(code, ref offset, 0x00);
|
||||
if (nonManagedExceptionCodes[i] == 0xC0000409u)
|
||||
{
|
||||
fastFailJumpSlot = i;
|
||||
}
|
||||
}
|
||||
|
||||
EmitByte(code, ref offset, 0xE9); // jmp mainBody (rel32; FastFail breadcrumb sits between)
|
||||
var mainBodyJumpSlot = offset;
|
||||
EmitUInt32(code, ref offset, 0u);
|
||||
|
||||
int passOffset = offset;
|
||||
EmitByte(code, ref offset, 0x31); EmitByte(code, ref offset, 0xC0); // xor eax, eax
|
||||
EmitByte(code, ref offset, 0xC3);
|
||||
|
||||
int fastFailPassOffset = offset;
|
||||
var fastFailLogInstalled = false;
|
||||
nint kernel32 = GetModuleHandle("kernel32.dll");
|
||||
nint getStdHandle = kernel32 != 0 ? GetProcAddress(kernel32, "GetStdHandle") : 0;
|
||||
nint writeFile = kernel32 != 0 ? GetProcAddress(kernel32, "WriteFile") : 0;
|
||||
if (fastFailJumpSlot >= 0 && getStdHandle != 0 && writeFile != 0)
|
||||
{
|
||||
// Prefix + Context.Rip hex (AMD64 CONTEXT.Rip @ 0xF8) + newline.
|
||||
// Keep in sync with WindowsFaultHandling.CreateHandlerThunk.
|
||||
ReadOnlySpan<byte> msg =
|
||||
"[LOADER][FATAL] VEH_PASS FastFail 0xC0000409 (live trampoline; skip managed VEH) rip=0x"u8;
|
||||
ReadOnlySpan<byte> hexDigits = "0123456789ABCDEF"u8;
|
||||
// rcx=EXCEPTION_POINTERS*: capture Rip into r10 before clobbering.
|
||||
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0x8B); EmitByte(code, ref offset, 0x41);
|
||||
EmitByte(code, ref offset, 0x08); // mov rax, [rcx+8] ContextRecord*
|
||||
EmitByte(code, ref offset, 0x4C); EmitByte(code, ref offset, 0x8B); EmitByte(code, ref offset, 0x90);
|
||||
EmitUInt32(code, ref offset, 0xF8u); // mov r10, [rax+0xF8] Rip
|
||||
EmitByte(code, ref offset, 0x50);
|
||||
EmitByte(code, ref offset, 0x51);
|
||||
EmitByte(code, ref offset, 0x52);
|
||||
EmitByte(code, ref offset, 0x41); EmitByte(code, ref offset, 0x50);
|
||||
EmitByte(code, ref offset, 0x41); EmitByte(code, ref offset, 0x51);
|
||||
EmitByte(code, ref offset, 0x41); EmitByte(code, ref offset, 0x52); // push r10 (Rip)
|
||||
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0x83);
|
||||
EmitByte(code, ref offset, 0xEC); EmitByte(code, ref offset, 0x40); // sub rsp, 0x40 (hex buf @ +0x30)
|
||||
EmitByte(code, ref offset, 0xB9); EmitUInt32(code, ref offset, unchecked((uint)-12));
|
||||
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0xB8);
|
||||
*(nint*)(code + offset) = getStdHandle;
|
||||
offset += sizeof(nint);
|
||||
EmitByte(code, ref offset, 0xFF); EmitByte(code, ref offset, 0xD0);
|
||||
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0x89);
|
||||
EmitByte(code, ref offset, 0x44); EmitByte(code, ref offset, 0x24);
|
||||
EmitByte(code, ref offset, 0x28); // mov [rsp+0x28], rax stderr handle
|
||||
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0x89); EmitByte(code, ref offset, 0xC1);
|
||||
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0xB8);
|
||||
var msgAbsSlot = offset;
|
||||
*(nint*)(code + offset) = 0;
|
||||
offset += sizeof(nint);
|
||||
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0x89); EmitByte(code, ref offset, 0xC2);
|
||||
EmitByte(code, ref offset, 0x41); EmitByte(code, ref offset, 0xB8);
|
||||
EmitUInt32(code, ref offset, (uint)msg.Length);
|
||||
EmitByte(code, ref offset, 0x4C); EmitByte(code, ref offset, 0x8D);
|
||||
EmitByte(code, ref offset, 0x4C); EmitByte(code, ref offset, 0x24);
|
||||
EmitByte(code, ref offset, 0x20);
|
||||
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0xC7);
|
||||
EmitByte(code, ref offset, 0x44); EmitByte(code, ref offset, 0x24);
|
||||
EmitByte(code, ref offset, 0x20); EmitUInt32(code, ref offset, 0);
|
||||
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0xC7);
|
||||
EmitByte(code, ref offset, 0x44); EmitByte(code, ref offset, 0x24);
|
||||
EmitByte(code, ref offset, 0x38); EmitUInt32(code, ref offset, 0); // lpOverlapped slot
|
||||
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0xB8);
|
||||
*(nint*)(code + offset) = writeFile;
|
||||
offset += sizeof(nint);
|
||||
EmitByte(code, ref offset, 0xFF); EmitByte(code, ref offset, 0xD0);
|
||||
|
||||
// Hex-encode Rip. Stack after sub 0x40: [rsp+0x40]=saved Rip (push r10).
|
||||
EmitByte(code, ref offset, 0x4C); EmitByte(code, ref offset, 0x8B);
|
||||
EmitByte(code, ref offset, 0x54); EmitByte(code, ref offset, 0x24);
|
||||
EmitByte(code, ref offset, 0x40); // mov r10, [rsp+0x40]
|
||||
EmitByte(code, ref offset, 0x49); EmitByte(code, ref offset, 0xB8);
|
||||
var hexDigitsAbsSlot = offset;
|
||||
*(nint*)(code + offset) = 0;
|
||||
offset += sizeof(nint); // mov r8, hexDigits
|
||||
EmitByte(code, ref offset, 0x4C); EmitByte(code, ref offset, 0x8D);
|
||||
EmitByte(code, ref offset, 0x5C); EmitByte(code, ref offset, 0x24);
|
||||
EmitByte(code, ref offset, 0x30); // lea r11, [rsp+0x30] hex out
|
||||
EmitByte(code, ref offset, 0xB9); EmitUInt32(code, ref offset, 16u); // ecx = 16 nibbles
|
||||
EmitByte(code, ref offset, 0x4C); EmitByte(code, ref offset, 0x89); EmitByte(code, ref offset, 0xD0); // mov rax, r10
|
||||
int hexLoopOffset = offset;
|
||||
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0xC1); EmitByte(code, ref offset, 0xC0);
|
||||
EmitByte(code, ref offset, 0x04); // rol rax, 4
|
||||
EmitByte(code, ref offset, 0x89); EmitByte(code, ref offset, 0xC2); // mov edx, eax
|
||||
EmitByte(code, ref offset, 0x83); EmitByte(code, ref offset, 0xE2); EmitByte(code, ref offset, 0x0F); // and edx, 0xF
|
||||
EmitByte(code, ref offset, 0x41); EmitByte(code, ref offset, 0x0F); EmitByte(code, ref offset, 0xB6);
|
||||
EmitByte(code, ref offset, 0x14); EmitByte(code, ref offset, 0x10); // movzx edx, byte [r8+rdx]
|
||||
EmitByte(code, ref offset, 0x41); EmitByte(code, ref offset, 0x88); EmitByte(code, ref offset, 0x13); // mov [r11], dl
|
||||
EmitByte(code, ref offset, 0x49); EmitByte(code, ref offset, 0xFF); EmitByte(code, ref offset, 0xC3); // inc r11
|
||||
EmitByte(code, ref offset, 0xFF); EmitByte(code, ref offset, 0xC9); // dec ecx
|
||||
EmitByte(code, ref offset, 0x75);
|
||||
EmitByte(code, ref offset, unchecked((byte)(hexLoopOffset - (offset + 1)))); // jnz hexLoop (rel8)
|
||||
EmitByte(code, ref offset, 0x41); EmitByte(code, ref offset, 0xC6); EmitByte(code, ref offset, 0x03);
|
||||
EmitByte(code, ref offset, 0x0A); // mov byte [r11], '\n'
|
||||
|
||||
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0x8B);
|
||||
EmitByte(code, ref offset, 0x4C); EmitByte(code, ref offset, 0x24);
|
||||
EmitByte(code, ref offset, 0x28); // mov rcx, [rsp+0x28] stderr
|
||||
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0x8D);
|
||||
EmitByte(code, ref offset, 0x54); EmitByte(code, ref offset, 0x24);
|
||||
EmitByte(code, ref offset, 0x30); // lea rdx, [rsp+0x30]
|
||||
EmitByte(code, ref offset, 0x41); EmitByte(code, ref offset, 0xB8);
|
||||
EmitUInt32(code, ref offset, 17u); // 16 hex + newline
|
||||
EmitByte(code, ref offset, 0x4C); EmitByte(code, ref offset, 0x8D);
|
||||
EmitByte(code, ref offset, 0x4C); EmitByte(code, ref offset, 0x24);
|
||||
EmitByte(code, ref offset, 0x20);
|
||||
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0xC7);
|
||||
EmitByte(code, ref offset, 0x44); EmitByte(code, ref offset, 0x24);
|
||||
EmitByte(code, ref offset, 0x20); EmitUInt32(code, ref offset, 0);
|
||||
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0xC7);
|
||||
EmitByte(code, ref offset, 0x44); EmitByte(code, ref offset, 0x24);
|
||||
EmitByte(code, ref offset, 0x38); EmitUInt32(code, ref offset, 0);
|
||||
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0xB8);
|
||||
*(nint*)(code + offset) = writeFile;
|
||||
offset += sizeof(nint);
|
||||
EmitByte(code, ref offset, 0xFF); EmitByte(code, ref offset, 0xD0);
|
||||
|
||||
// Flush redirected stderr so FastFail rip survives process teardown.
|
||||
nint flushFileBuffers = kernel32 != 0 ? GetProcAddress(kernel32, "FlushFileBuffers") : 0;
|
||||
if (flushFileBuffers != 0)
|
||||
{
|
||||
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0x8B);
|
||||
EmitByte(code, ref offset, 0x4C); EmitByte(code, ref offset, 0x24);
|
||||
EmitByte(code, ref offset, 0x28); // mov rcx, stderr
|
||||
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0xB8);
|
||||
*(nint*)(code + offset) = flushFileBuffers;
|
||||
offset += sizeof(nint);
|
||||
EmitByte(code, ref offset, 0xFF); EmitByte(code, ref offset, 0xD0);
|
||||
}
|
||||
|
||||
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0x83);
|
||||
EmitByte(code, ref offset, 0xC4); EmitByte(code, ref offset, 0x40);
|
||||
EmitByte(code, ref offset, 0x41); EmitByte(code, ref offset, 0x5A); // pop r10
|
||||
EmitByte(code, ref offset, 0x41); EmitByte(code, ref offset, 0x59);
|
||||
EmitByte(code, ref offset, 0x41); EmitByte(code, ref offset, 0x58);
|
||||
EmitByte(code, ref offset, 0x5A);
|
||||
EmitByte(code, ref offset, 0x59);
|
||||
EmitByte(code, ref offset, 0x58);
|
||||
EmitByte(code, ref offset, 0x31); EmitByte(code, ref offset, 0xC0);
|
||||
EmitByte(code, ref offset, 0xC3);
|
||||
|
||||
var msgOffset = offset;
|
||||
for (int i = 0; i < msg.Length; i++)
|
||||
{
|
||||
EmitByte(code, ref offset, msg[i]);
|
||||
}
|
||||
|
||||
var hexDigitsOffset = offset;
|
||||
for (int i = 0; i < hexDigits.Length; i++)
|
||||
{
|
||||
EmitByte(code, ref offset, hexDigits[i]);
|
||||
}
|
||||
|
||||
*(nint*)(code + msgAbsSlot) = (nint)ptr + msgOffset;
|
||||
*(nint*)(code + hexDigitsAbsSlot) = (nint)ptr + hexDigitsOffset;
|
||||
code[passJumpOffsets[fastFailJumpSlot]] =
|
||||
checked((byte)(fastFailPassOffset - (passJumpOffsets[fastFailJumpSlot] + 1)));
|
||||
fastFailLogInstalled = true;
|
||||
}
|
||||
|
||||
int mainBodyOffset = offset;
|
||||
*(int*)(code + mainBodyJumpSlot) = mainBodyOffset - (mainBodyJumpSlot + sizeof(int));
|
||||
for (int i = 0; i < nonManagedExceptionCodes.Length; i++)
|
||||
{
|
||||
if (i == fastFailJumpSlot && fastFailLogInstalled)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
code[passJumpOffsets[i]] = checked((byte)(passOffset - (passJumpOffsets[i] + 1)));
|
||||
}
|
||||
|
||||
EmitByte(code, ref offset, 0x41); EmitByte(code, ref offset, 0x54); // push r12
|
||||
EmitByte(code, ref offset, 0x41); EmitByte(code, ref offset, 0x55); // push r13
|
||||
EmitByte(code, ref offset, 0x49); EmitByte(code, ref offset, 0x89); EmitByte(code, ref offset, 0xE4); // mov r12, rsp
|
||||
EmitByte(code, ref offset, 0x49); EmitByte(code, ref offset, 0x89); EmitByte(code, ref offset, 0xCD); // mov r13, rcx
|
||||
|
||||
// Native worker EXECUTE-AV abort without managed VEH.
|
||||
// Do NOT catch read/write AVs — workers need managed lazy-commit (tLTJ
|
||||
// silent-die when every worker AV was aborted). Execute faults on
|
||||
// tbb_thead are the concurrent-managed FailFast case (tLTC).
|
||||
int tbbFallthroughJump = -1;
|
||||
if (_workerAbortStub != 0 &&
|
||||
_tlsGetValueAddress != 0 &&
|
||||
_hostRspSlotTlsIndex != uint.MaxValue)
|
||||
{
|
||||
EmitByte(code, ref offset, 0x49); EmitByte(code, ref offset, 0x8B);
|
||||
EmitByte(code, ref offset, 0x45); EmitByte(code, ref offset, 0x00); // mov rax, [r13]
|
||||
EmitByte(code, ref offset, 0x81); EmitByte(code, ref offset, 0x38);
|
||||
EmitUInt32(code, ref offset, 0xC0000005u); // cmp dword [rax], AV
|
||||
EmitByte(code, ref offset, 0x0F); EmitByte(code, ref offset, 0x85);
|
||||
tbbFallthroughJump = offset;
|
||||
EmitUInt32(code, ref offset, 0u); // jne fallthrough
|
||||
|
||||
// ExceptionInformation[0] == 8 → EXECUTE (DEP). Offset 32 on x64 RECORD.
|
||||
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0x83);
|
||||
EmitByte(code, ref offset, 0xB8); EmitUInt32(code, ref offset, 32u);
|
||||
EmitByte(code, ref offset, 0x08); // cmp qword [rax+32], 8
|
||||
EmitByte(code, ref offset, 0x0F); EmitByte(code, ref offset, 0x85);
|
||||
var tbbNotExecuteJump = offset;
|
||||
EmitUInt32(code, ref offset, 0u);
|
||||
|
||||
int tbbNotEligibleJump = -1;
|
||||
if (_tbbAbortEligibleTlsIndex != uint.MaxValue)
|
||||
{
|
||||
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0x83);
|
||||
EmitByte(code, ref offset, 0xEC); EmitByte(code, ref offset, 0x28);
|
||||
EmitByte(code, ref offset, 0xB9);
|
||||
EmitUInt32(code, ref offset, _tbbAbortEligibleTlsIndex);
|
||||
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0xB8);
|
||||
*(nint*)(code + offset) = _tlsGetValueAddress;
|
||||
offset += sizeof(nint);
|
||||
EmitByte(code, ref offset, 0xFF); EmitByte(code, ref offset, 0xD0);
|
||||
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0x83);
|
||||
EmitByte(code, ref offset, 0xC4); EmitByte(code, ref offset, 0x28);
|
||||
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0x85);
|
||||
EmitByte(code, ref offset, 0xC0);
|
||||
EmitByte(code, ref offset, 0x0F); EmitByte(code, ref offset, 0x84);
|
||||
tbbNotEligibleJump = offset;
|
||||
EmitUInt32(code, ref offset, 0u);
|
||||
}
|
||||
|
||||
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0x83);
|
||||
EmitByte(code, ref offset, 0xEC); EmitByte(code, ref offset, 0x28);
|
||||
EmitByte(code, ref offset, 0xB9);
|
||||
EmitUInt32(code, ref offset, _hostRspSlotTlsIndex);
|
||||
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0xB8);
|
||||
*(nint*)(code + offset) = _tlsGetValueAddress;
|
||||
offset += sizeof(nint);
|
||||
EmitByte(code, ref offset, 0xFF); EmitByte(code, ref offset, 0xD0);
|
||||
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0x83);
|
||||
EmitByte(code, ref offset, 0xC4); EmitByte(code, ref offset, 0x28);
|
||||
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0x85);
|
||||
EmitByte(code, ref offset, 0xC0);
|
||||
EmitByte(code, ref offset, 0x0F); EmitByte(code, ref offset, 0x84);
|
||||
var tbbNoHostRspJump = offset;
|
||||
EmitUInt32(code, ref offset, 0u);
|
||||
EmitByte(code, ref offset, 0x4C); EmitByte(code, ref offset, 0x8B);
|
||||
EmitByte(code, ref offset, 0x00); // mov r8, [rax] hostRsp
|
||||
EmitByte(code, ref offset, 0x4D); EmitByte(code, ref offset, 0x85);
|
||||
EmitByte(code, ref offset, 0xC0);
|
||||
EmitByte(code, ref offset, 0x0F); EmitByte(code, ref offset, 0x84);
|
||||
var tbbZeroRspJump = offset;
|
||||
EmitUInt32(code, ref offset, 0u);
|
||||
|
||||
EmitByte(code, ref offset, 0x49); EmitByte(code, ref offset, 0x83);
|
||||
EmitByte(code, ref offset, 0xE0); EmitByte(code, ref offset, 0xF0); // and r8, ~0xF
|
||||
EmitByte(code, ref offset, 0x4D); EmitByte(code, ref offset, 0x8B);
|
||||
EmitByte(code, ref offset, 0x4D); EmitByte(code, ref offset, 0x08); // mov r9, [r13+8]
|
||||
EmitByte(code, ref offset, 0x4D); EmitByte(code, ref offset, 0x89);
|
||||
EmitByte(code, ref offset, 0x81); EmitUInt32(code, ref offset, 0x98u); // Context.Rsp
|
||||
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0xB8);
|
||||
*(nint*)(code + offset) = _workerAbortStub;
|
||||
offset += sizeof(nint);
|
||||
EmitByte(code, ref offset, 0x49); EmitByte(code, ref offset, 0x89);
|
||||
EmitByte(code, ref offset, 0x81); EmitUInt32(code, ref offset, 0xF8u); // Context.Rip
|
||||
EmitByte(code, ref offset, 0x49); EmitByte(code, ref offset, 0xC7);
|
||||
EmitByte(code, ref offset, 0x41); EmitByte(code, ref offset, 0x78);
|
||||
EmitUInt32(code, ref offset, 0u); // Context.Rax = 0
|
||||
|
||||
EmitByte(code, ref offset, 0xB8); EmitUInt32(code, ref offset, unchecked((uint)-1));
|
||||
EmitByte(code, ref offset, 0x4C); EmitByte(code, ref offset, 0x89);
|
||||
EmitByte(code, ref offset, 0xE4); // mov rsp, r12
|
||||
EmitByte(code, ref offset, 0x41); EmitByte(code, ref offset, 0x5D);
|
||||
EmitByte(code, ref offset, 0x41); EmitByte(code, ref offset, 0x5C);
|
||||
EmitByte(code, ref offset, 0xC3);
|
||||
|
||||
int tbbFallthroughOffset = offset;
|
||||
*(int*)(code + tbbFallthroughJump) = tbbFallthroughOffset - (tbbFallthroughJump + sizeof(int));
|
||||
*(int*)(code + tbbNotExecuteJump) = tbbFallthroughOffset - (tbbNotExecuteJump + sizeof(int));
|
||||
if (tbbNotEligibleJump >= 0)
|
||||
{
|
||||
*(int*)(code + tbbNotEligibleJump) = tbbFallthroughOffset - (tbbNotEligibleJump + sizeof(int));
|
||||
}
|
||||
*(int*)(code + tbbNoHostRspJump) = tbbFallthroughOffset - (tbbNoHostRspJump + sizeof(int));
|
||||
*(int*)(code + tbbZeroRspJump) = tbbFallthroughOffset - (tbbZeroRspJump + sizeof(int));
|
||||
}
|
||||
|
||||
EmitByte(code, ref offset, 0x65); EmitByte(code, ref offset, 0x48); // mov rax, gs:[8]
|
||||
EmitByte(code, ref offset, 0x8B); EmitByte(code, ref offset, 0x04); EmitByte(code, ref offset, 0x25);
|
||||
EmitUInt32(code, ref offset, 8u);
|
||||
@@ -2440,11 +2901,67 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
||||
EmitUInt32(code, ref offset, 0u);
|
||||
|
||||
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0x83); EmitByte(code, ref offset, 0xEC); EmitByte(code, ref offset, 0x28);
|
||||
// Serialize managed VEH entry (recursive spinlock). Concurrent UnmanagedCallersOnly
|
||||
// FailFast was the tLTQ silent mid-TBB pattern (enter without abort breadcrumb).
|
||||
// Lock layout: [0]=owner UniqueThread (nint), [8]=depth (int).
|
||||
EmitByte(code, ref offset, 0x49); EmitByte(code, ref offset, 0xB9);
|
||||
*(nint*)(code + offset) = _vehManagedEntryLock;
|
||||
offset += sizeof(nint); // mov r9, lock*
|
||||
EmitByte(code, ref offset, 0x65); EmitByte(code, ref offset, 0x4C);
|
||||
EmitByte(code, ref offset, 0x8B); EmitByte(code, ref offset, 0x14);
|
||||
EmitByte(code, ref offset, 0x25); EmitUInt32(code, ref offset, 0x48u); // mov r10, gs:[0x48]
|
||||
int hostAcquireSpin = offset;
|
||||
EmitByte(code, ref offset, 0x49); EmitByte(code, ref offset, 0x8B); EmitByte(code, ref offset, 0x01); // mov rax, [r9]
|
||||
EmitByte(code, ref offset, 0x4C); EmitByte(code, ref offset, 0x39); EmitByte(code, ref offset, 0xD0); // cmp rax, r10
|
||||
EmitByte(code, ref offset, 0x0F); EmitByte(code, ref offset, 0x84);
|
||||
int hostMineJump = offset;
|
||||
EmitUInt32(code, ref offset, 0u);
|
||||
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0x85); EmitByte(code, ref offset, 0xC0); // test rax, rax
|
||||
EmitByte(code, ref offset, 0x0F); EmitByte(code, ref offset, 0x85);
|
||||
int hostPauseJump = offset;
|
||||
EmitUInt32(code, ref offset, 0u);
|
||||
EmitByte(code, ref offset, 0xF0); EmitByte(code, ref offset, 0x4C);
|
||||
EmitByte(code, ref offset, 0x0F); EmitByte(code, ref offset, 0xB1); EmitByte(code, ref offset, 0x11); // lock cmpxchg [r9], r10
|
||||
EmitByte(code, ref offset, 0x0F); EmitByte(code, ref offset, 0x85);
|
||||
int hostRetryJump = offset;
|
||||
EmitUInt32(code, ref offset, 0u);
|
||||
EmitByte(code, ref offset, 0x41); EmitByte(code, ref offset, 0xC7);
|
||||
EmitByte(code, ref offset, 0x41); EmitByte(code, ref offset, 0x08);
|
||||
EmitUInt32(code, ref offset, 1u); // mov dword [r9+8], 1
|
||||
EmitByte(code, ref offset, 0xE9);
|
||||
int hostGotJump = offset;
|
||||
EmitUInt32(code, ref offset, 0u);
|
||||
int hostPauseOffset = offset;
|
||||
EmitByte(code, ref offset, 0xF3); EmitByte(code, ref offset, 0x90); // pause
|
||||
EmitByte(code, ref offset, 0xE9);
|
||||
int hostPauseBackJump = offset;
|
||||
EmitUInt32(code, ref offset, 0u);
|
||||
int hostMineOffset = offset;
|
||||
EmitByte(code, ref offset, 0x41); EmitByte(code, ref offset, 0xFF);
|
||||
EmitByte(code, ref offset, 0x41); EmitByte(code, ref offset, 0x08); // inc dword [r9+8]
|
||||
int hostGotOffset = offset;
|
||||
*(int*)(code + hostMineJump) = hostMineOffset - (hostMineJump + sizeof(int));
|
||||
*(int*)(code + hostPauseJump) = hostPauseOffset - (hostPauseJump + sizeof(int));
|
||||
*(int*)(code + hostRetryJump) = hostAcquireSpin - (hostRetryJump + sizeof(int));
|
||||
*(int*)(code + hostGotJump) = hostGotOffset - (hostGotJump + sizeof(int));
|
||||
*(int*)(code + hostPauseBackJump) = hostAcquireSpin - (hostPauseBackJump + sizeof(int));
|
||||
EmitByte(code, ref offset, 0x4C); EmitByte(code, ref offset, 0x89); EmitByte(code, ref offset, 0xE9); // mov rcx, r13
|
||||
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0xB8);
|
||||
*(nint*)(code + offset) = managedHandler;
|
||||
offset += sizeof(nint);
|
||||
EmitByte(code, ref offset, 0xFF); EmitByte(code, ref offset, 0xD0);
|
||||
EmitByte(code, ref offset, 0x49); EmitByte(code, ref offset, 0xB9);
|
||||
*(nint*)(code + offset) = _vehManagedEntryLock;
|
||||
offset += sizeof(nint); // mov r9, lock*
|
||||
EmitByte(code, ref offset, 0x41); EmitByte(code, ref offset, 0xFF);
|
||||
EmitByte(code, ref offset, 0x49); EmitByte(code, ref offset, 0x08); // dec dword [r9+8]
|
||||
EmitByte(code, ref offset, 0x0F); EmitByte(code, ref offset, 0x85);
|
||||
int hostStillJump = offset;
|
||||
EmitUInt32(code, ref offset, 0u);
|
||||
EmitByte(code, ref offset, 0x49); EmitByte(code, ref offset, 0xC7);
|
||||
EmitByte(code, ref offset, 0x01); EmitUInt32(code, ref offset, 0u); // mov qword [r9], 0
|
||||
int hostStillOffset = offset;
|
||||
*(int*)(code + hostStillJump) = hostStillOffset - (hostStillJump + sizeof(int));
|
||||
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0x83); EmitByte(code, ref offset, 0xC4); EmitByte(code, ref offset, 0x28);
|
||||
EmitByte(code, ref offset, 0xE9);
|
||||
int hostRestoreJump = offset;
|
||||
@@ -2470,11 +2987,64 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
||||
EmitUInt32(code, ref offset, 0u);
|
||||
EmitByte(code, ref offset, 0x4C); EmitByte(code, ref offset, 0x89); EmitByte(code, ref offset, 0xDC); // mov rsp, r11
|
||||
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0x83); EmitByte(code, ref offset, 0xEC); EmitByte(code, ref offset, 0x28);
|
||||
EmitByte(code, ref offset, 0x49); EmitByte(code, ref offset, 0xB9);
|
||||
*(nint*)(code + offset) = _vehManagedEntryLock;
|
||||
offset += sizeof(nint); // mov r9, lock*
|
||||
EmitByte(code, ref offset, 0x65); EmitByte(code, ref offset, 0x4C);
|
||||
EmitByte(code, ref offset, 0x8B); EmitByte(code, ref offset, 0x14);
|
||||
EmitByte(code, ref offset, 0x25); EmitUInt32(code, ref offset, 0x48u); // mov r10, gs:[0x48]
|
||||
int guestAcquireSpin = offset;
|
||||
EmitByte(code, ref offset, 0x49); EmitByte(code, ref offset, 0x8B); EmitByte(code, ref offset, 0x01); // mov rax, [r9]
|
||||
EmitByte(code, ref offset, 0x4C); EmitByte(code, ref offset, 0x39); EmitByte(code, ref offset, 0xD0); // cmp rax, r10
|
||||
EmitByte(code, ref offset, 0x0F); EmitByte(code, ref offset, 0x84);
|
||||
int guestMineJump = offset;
|
||||
EmitUInt32(code, ref offset, 0u);
|
||||
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0x85); EmitByte(code, ref offset, 0xC0);
|
||||
EmitByte(code, ref offset, 0x0F); EmitByte(code, ref offset, 0x85);
|
||||
int guestPauseJump = offset;
|
||||
EmitUInt32(code, ref offset, 0u);
|
||||
EmitByte(code, ref offset, 0xF0); EmitByte(code, ref offset, 0x4C);
|
||||
EmitByte(code, ref offset, 0x0F); EmitByte(code, ref offset, 0xB1); EmitByte(code, ref offset, 0x11);
|
||||
EmitByte(code, ref offset, 0x0F); EmitByte(code, ref offset, 0x85);
|
||||
int guestRetryJump = offset;
|
||||
EmitUInt32(code, ref offset, 0u);
|
||||
EmitByte(code, ref offset, 0x41); EmitByte(code, ref offset, 0xC7);
|
||||
EmitByte(code, ref offset, 0x41); EmitByte(code, ref offset, 0x08);
|
||||
EmitUInt32(code, ref offset, 1u);
|
||||
EmitByte(code, ref offset, 0xE9);
|
||||
int guestGotJump = offset;
|
||||
EmitUInt32(code, ref offset, 0u);
|
||||
int guestPauseOffset = offset;
|
||||
EmitByte(code, ref offset, 0xF3); EmitByte(code, ref offset, 0x90);
|
||||
EmitByte(code, ref offset, 0xE9);
|
||||
int guestPauseBackJump = offset;
|
||||
EmitUInt32(code, ref offset, 0u);
|
||||
int guestMineOffset = offset;
|
||||
EmitByte(code, ref offset, 0x41); EmitByte(code, ref offset, 0xFF);
|
||||
EmitByte(code, ref offset, 0x41); EmitByte(code, ref offset, 0x08);
|
||||
int guestGotOffset = offset;
|
||||
*(int*)(code + guestMineJump) = guestMineOffset - (guestMineJump + sizeof(int));
|
||||
*(int*)(code + guestPauseJump) = guestPauseOffset - (guestPauseJump + sizeof(int));
|
||||
*(int*)(code + guestRetryJump) = guestAcquireSpin - (guestRetryJump + sizeof(int));
|
||||
*(int*)(code + guestGotJump) = guestGotOffset - (guestGotJump + sizeof(int));
|
||||
*(int*)(code + guestPauseBackJump) = guestAcquireSpin - (guestPauseBackJump + sizeof(int));
|
||||
EmitByte(code, ref offset, 0x4C); EmitByte(code, ref offset, 0x89); EmitByte(code, ref offset, 0xE9); // mov rcx, r13
|
||||
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0xB8);
|
||||
*(nint*)(code + offset) = managedHandler;
|
||||
offset += sizeof(nint);
|
||||
EmitByte(code, ref offset, 0xFF); EmitByte(code, ref offset, 0xD0);
|
||||
EmitByte(code, ref offset, 0x49); EmitByte(code, ref offset, 0xB9);
|
||||
*(nint*)(code + offset) = _vehManagedEntryLock;
|
||||
offset += sizeof(nint);
|
||||
EmitByte(code, ref offset, 0x41); EmitByte(code, ref offset, 0xFF);
|
||||
EmitByte(code, ref offset, 0x49); EmitByte(code, ref offset, 0x08); // dec dword [r9+8]
|
||||
EmitByte(code, ref offset, 0x0F); EmitByte(code, ref offset, 0x85);
|
||||
int guestStillJump = offset;
|
||||
EmitUInt32(code, ref offset, 0u);
|
||||
EmitByte(code, ref offset, 0x49); EmitByte(code, ref offset, 0xC7);
|
||||
EmitByte(code, ref offset, 0x01); EmitUInt32(code, ref offset, 0u);
|
||||
int guestStillOffset = offset;
|
||||
*(int*)(code + guestStillJump) = guestStillOffset - (guestStillJump + sizeof(int));
|
||||
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0x83); EmitByte(code, ref offset, 0xC4); EmitByte(code, ref offset, 0x28);
|
||||
EmitByte(code, ref offset, 0xE9);
|
||||
int guestRestoreJump = offset;
|
||||
@@ -2495,6 +3065,18 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
||||
*(int*)(code + missingHostStackJump) = passThroughOffset - (missingHostStackJump + sizeof(int));
|
||||
*(int*)(code + guestRestoreJump) = restoreOffset - (guestRestoreJump + sizeof(int));
|
||||
|
||||
if (offset > (int)stubSize)
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][ERROR] Exception handler trampoline overflow: used={offset} cap={stubSize}");
|
||||
VirtualFree(ptr, 0, 0x8000u);
|
||||
return 0;
|
||||
}
|
||||
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][INFO] VEH trampoline built: bytes={offset} native_worker_abort=" +
|
||||
$"{(_workerAbortStub != 0 && _hostRspSlotTlsIndex != uint.MaxValue)}");
|
||||
|
||||
uint oldProtect = default;
|
||||
VirtualProtect(ptr, stubSize, 32u, &oldProtect);
|
||||
FlushInstructionCache(GetCurrentProcess(), ptr, (nuint)offset);
|
||||
@@ -5149,16 +5731,27 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
||||
return GuestNativeCallExitReason.Exception;
|
||||
}
|
||||
FlushInstructionCache(GetCurrentProcess(), ptr, stubSize);
|
||||
if (!TlsSetValue(_hostRspSlotTlsIndex, (nint)hostRspSlot))
|
||||
{
|
||||
reason = "failed to bind host-RSP storage for guest thread stub";
|
||||
return GuestNativeCallExitReason.Exception;
|
||||
}
|
||||
ActiveGuestThreadYieldRequested = false;
|
||||
ActiveGuestThreadYieldReason = null;
|
||||
try
|
||||
{
|
||||
var nativeReturn = CallNativeEntry(ptr);
|
||||
// TBB execute-AV recover needs native-worker TLS (eligible/done).
|
||||
// Other guests stay on CallNativeEntry — full native-worker migration
|
||||
// increased splash hangs / UnmanagedCallersOnly (tLTN/tLTO).
|
||||
int nativeReturn;
|
||||
if (name == "tbb_thead")
|
||||
{
|
||||
nativeReturn = RunGuestEntryStub(ptr, hostRspSlot, requireNativeWorker: true);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!TlsSetValue(_hostRspSlotTlsIndex, (nint)hostRspSlot))
|
||||
{
|
||||
reason = "failed to bind host-RSP storage for guest thread stub";
|
||||
return GuestNativeCallExitReason.Exception;
|
||||
}
|
||||
nativeReturn = CallNativeEntry(ptr);
|
||||
}
|
||||
if (ActiveGuestThreadYieldRequested)
|
||||
{
|
||||
reason = ActiveGuestThreadYieldReason ?? "guest thread blocked";
|
||||
@@ -5304,16 +5897,24 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
||||
return GuestNativeCallExitReason.Exception;
|
||||
}
|
||||
FlushInstructionCache(GetCurrentProcess(), ptr, stubSize);
|
||||
if (!TlsSetValue(_hostRspSlotTlsIndex, (nint)hostRspSlot))
|
||||
{
|
||||
reason = "failed to bind host-RSP storage for guest continuation stub";
|
||||
return GuestNativeCallExitReason.Exception;
|
||||
}
|
||||
ActiveGuestThreadYieldRequested = false;
|
||||
ActiveGuestThreadYieldReason = null;
|
||||
try
|
||||
{
|
||||
var nativeReturn = CallNativeEntry(ptr);
|
||||
int nativeReturn;
|
||||
if (name == "tbb_thead")
|
||||
{
|
||||
nativeReturn = RunGuestEntryStub(ptr, hostRspSlot, requireNativeWorker: true);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!TlsSetValue(_hostRspSlotTlsIndex, (nint)hostRspSlot))
|
||||
{
|
||||
reason = "failed to bind host-RSP storage for guest continuation stub";
|
||||
return GuestNativeCallExitReason.Exception;
|
||||
}
|
||||
nativeReturn = CallNativeEntry(ptr);
|
||||
}
|
||||
if (ActiveGuestThreadYieldRequested)
|
||||
{
|
||||
reason = ActiveGuestThreadYieldReason ?? "guest thread blocked";
|
||||
@@ -6446,6 +7047,16 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
||||
VirtualFree((void*)_hostRspSlotStorage, 0u, 32768u);
|
||||
_hostRspSlotStorage = 0;
|
||||
}
|
||||
if (_vehManagedEntryLock != 0)
|
||||
{
|
||||
VirtualFree((void*)_vehManagedEntryLock, 0u, 32768u);
|
||||
_vehManagedEntryLock = 0;
|
||||
}
|
||||
if (_workerAbortStack != 0)
|
||||
{
|
||||
VirtualFree((void*)_workerAbortStack, 0u, 32768u);
|
||||
_workerAbortStack = 0;
|
||||
}
|
||||
if (_guestTlsBaseTlsIndex != uint.MaxValue)
|
||||
{
|
||||
TlsFree(_guestTlsBaseTlsIndex);
|
||||
@@ -6456,6 +7067,11 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
||||
TlsFree(_hostRspSlotTlsIndex);
|
||||
_hostRspSlotTlsIndex = uint.MaxValue;
|
||||
}
|
||||
if (_workerDoneEventTlsIndex != uint.MaxValue)
|
||||
{
|
||||
TlsFree(_workerDoneEventTlsIndex);
|
||||
_workerDoneEventTlsIndex = uint.MaxValue;
|
||||
}
|
||||
if (_unresolvedReturnStub != 0)
|
||||
{
|
||||
VirtualFree((void*)_unresolvedReturnStub, 0u, 32768u);
|
||||
@@ -6466,6 +7082,11 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
||||
VirtualFree((void*)_guestReturnStub, 0u, 32768u);
|
||||
_guestReturnStub = 0;
|
||||
}
|
||||
if (_workerAbortStub != 0)
|
||||
{
|
||||
VirtualFree((void*)_workerAbortStub, 0u, 32768u);
|
||||
_workerAbortStub = 0;
|
||||
}
|
||||
if (_guestContextTransferStub != 0)
|
||||
{
|
||||
VirtualFree((void*)_guestContextTransferStub, 0u, 32768u);
|
||||
|
||||
@@ -24,7 +24,7 @@ internal sealed unsafe partial class WindowsFaultHandling : IHostFaultHandling
|
||||
|
||||
public nint CreateHandlerThunk(nint managedCallback, uint hostRspSwitchTlsSlot, nint tlsGetValueAddress)
|
||||
{
|
||||
const uint stubSize = 256u;
|
||||
const uint stubSize = 1024u;
|
||||
void* ptr = (void*)_memory.Allocate(0, stubSize, HostPageProtection.ReadWriteExecute);
|
||||
if (ptr == null)
|
||||
{
|
||||
@@ -43,11 +43,15 @@ internal sealed unsafe partial class WindowsFaultHandling : IHostFaultHandling
|
||||
// managed code; the CLR's own VEH handles its exceptions. MSVC C++ exceptions
|
||||
// (Vulkan drivers, host CRT) are excluded too: the managed handler only ever
|
||||
// returned CONTINUE_SEARCH for them.
|
||||
//
|
||||
// FastFail (0xC0000409) is logged from this native path only: managed VEH never
|
||||
// sees it (tLT18–21 silent exits after TBB AV recovery).
|
||||
ReadOnlySpan<uint> nonManagedExceptionCodes =
|
||||
[WindowsFaultCodes.ClrManagedException, 0xE06D7363u, WindowsFaultCodes.FastFail, WindowsFaultCodes.StackOverflow];
|
||||
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0x8B); EmitByte(code, ref offset, 0x01); // mov rax, [rcx] (ExceptionRecord*)
|
||||
EmitByte(code, ref offset, 0x8B); EmitByte(code, ref offset, 0x00); // mov eax, [rax] (ExceptionCode)
|
||||
var passJumpOffsets = stackalloc int[nonManagedExceptionCodes.Length];
|
||||
int fastFailJumpSlot = -1;
|
||||
for (int i = 0; i < nonManagedExceptionCodes.Length; i++)
|
||||
{
|
||||
EmitByte(code, ref offset, 0x3D); // cmp eax, imm32
|
||||
@@ -55,13 +59,162 @@ internal sealed unsafe partial class WindowsFaultHandling : IHostFaultHandling
|
||||
EmitByte(code, ref offset, 0x74); // je pass
|
||||
passJumpOffsets[i] = offset;
|
||||
EmitByte(code, ref offset, 0x00);
|
||||
if (nonManagedExceptionCodes[i] == WindowsFaultCodes.FastFail)
|
||||
{
|
||||
fastFailJumpSlot = i;
|
||||
}
|
||||
}
|
||||
EmitByte(code, ref offset, 0xEB); EmitByte(code, ref offset, 0x03); // jmp over pass block
|
||||
EmitByte(code, ref offset, 0xE9); // jmp mainBody rel32 (FastFail breadcrumb sits between)
|
||||
var mainBodyJumpSlot = offset;
|
||||
EmitUInt32(code, ref offset, 0u);
|
||||
|
||||
int passOffset = offset;
|
||||
EmitByte(code, ref offset, 0x31); EmitByte(code, ref offset, 0xC0); // pass: xor eax, eax (EXCEPTION_CONTINUE_SEARCH)
|
||||
EmitByte(code, ref offset, 0xC3); // ret
|
||||
|
||||
// FastFail: native stderr breadcrumb with Context.Rip (no managed entry), then CONTINUE_SEARCH.
|
||||
// Keep in sync with DirectExecutionBackend.CreateExceptionHandlerTrampoline.
|
||||
int fastFailPassOffset = offset;
|
||||
var fastFailLogInstalled = false;
|
||||
if (fastFailJumpSlot >= 0 &&
|
||||
NativeLibrary.TryLoad("kernel32.dll", out var kernel32) &&
|
||||
NativeLibrary.TryGetExport(kernel32, "GetStdHandle", out var getStdHandle) &&
|
||||
NativeLibrary.TryGetExport(kernel32, "WriteFile", out var writeFile))
|
||||
{
|
||||
ReadOnlySpan<byte> msg =
|
||||
"[LOADER][FATAL] VEH_PASS FastFail 0xC0000409 (native; no managed VEH) rip=0x"u8;
|
||||
ReadOnlySpan<byte> hexDigits = "0123456789ABCDEF"u8;
|
||||
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0x8B); EmitByte(code, ref offset, 0x41);
|
||||
EmitByte(code, ref offset, 0x08); // mov rax, [rcx+8]
|
||||
EmitByte(code, ref offset, 0x4C); EmitByte(code, ref offset, 0x8B); EmitByte(code, ref offset, 0x90);
|
||||
EmitUInt32(code, ref offset, 0xF8u); // mov r10, [rax+0xF8]
|
||||
EmitByte(code, ref offset, 0x50); // push rax
|
||||
EmitByte(code, ref offset, 0x51); // push rcx
|
||||
EmitByte(code, ref offset, 0x52); // push rdx
|
||||
EmitByte(code, ref offset, 0x41); EmitByte(code, ref offset, 0x50); // push r8
|
||||
EmitByte(code, ref offset, 0x41); EmitByte(code, ref offset, 0x51); // push r9
|
||||
EmitByte(code, ref offset, 0x41); EmitByte(code, ref offset, 0x52); // push r10
|
||||
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0x83);
|
||||
EmitByte(code, ref offset, 0xEC); EmitByte(code, ref offset, 0x40); // sub rsp, 0x40
|
||||
EmitByte(code, ref offset, 0xB9); EmitUInt32(code, ref offset, unchecked((uint)-12));
|
||||
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0xB8);
|
||||
*(nint*)(code + offset) = getStdHandle;
|
||||
offset += sizeof(nint);
|
||||
EmitByte(code, ref offset, 0xFF); EmitByte(code, ref offset, 0xD0);
|
||||
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0x89);
|
||||
EmitByte(code, ref offset, 0x44); EmitByte(code, ref offset, 0x24);
|
||||
EmitByte(code, ref offset, 0x28); // mov [rsp+0x28], rax
|
||||
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0x89); EmitByte(code, ref offset, 0xC1);
|
||||
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0xB8);
|
||||
var msgAbsSlot = offset;
|
||||
*(nint*)(code + offset) = 0;
|
||||
offset += sizeof(nint);
|
||||
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0x89); EmitByte(code, ref offset, 0xC2);
|
||||
EmitByte(code, ref offset, 0x41); EmitByte(code, ref offset, 0xB8);
|
||||
EmitUInt32(code, ref offset, (uint)msg.Length);
|
||||
EmitByte(code, ref offset, 0x4C); EmitByte(code, ref offset, 0x8D);
|
||||
EmitByte(code, ref offset, 0x4C); EmitByte(code, ref offset, 0x24);
|
||||
EmitByte(code, ref offset, 0x20);
|
||||
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0xC7);
|
||||
EmitByte(code, ref offset, 0x44); EmitByte(code, ref offset, 0x24);
|
||||
EmitByte(code, ref offset, 0x20); EmitUInt32(code, ref offset, 0);
|
||||
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0xC7);
|
||||
EmitByte(code, ref offset, 0x44); EmitByte(code, ref offset, 0x24);
|
||||
EmitByte(code, ref offset, 0x38); EmitUInt32(code, ref offset, 0);
|
||||
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0xB8);
|
||||
*(nint*)(code + offset) = writeFile;
|
||||
offset += sizeof(nint);
|
||||
EmitByte(code, ref offset, 0xFF); EmitByte(code, ref offset, 0xD0);
|
||||
|
||||
EmitByte(code, ref offset, 0x4C); EmitByte(code, ref offset, 0x8B);
|
||||
EmitByte(code, ref offset, 0x54); EmitByte(code, ref offset, 0x24);
|
||||
EmitByte(code, ref offset, 0x40); // mov r10, [rsp+0x40]
|
||||
EmitByte(code, ref offset, 0x49); EmitByte(code, ref offset, 0xB8);
|
||||
var hexDigitsAbsSlot = offset;
|
||||
*(nint*)(code + offset) = 0;
|
||||
offset += sizeof(nint);
|
||||
EmitByte(code, ref offset, 0x4C); EmitByte(code, ref offset, 0x8D);
|
||||
EmitByte(code, ref offset, 0x5C); EmitByte(code, ref offset, 0x24);
|
||||
EmitByte(code, ref offset, 0x30); // lea r11, [rsp+0x30]
|
||||
EmitByte(code, ref offset, 0xB9); EmitUInt32(code, ref offset, 16u);
|
||||
EmitByte(code, ref offset, 0x4C); EmitByte(code, ref offset, 0x89); EmitByte(code, ref offset, 0xD0);
|
||||
int hexLoopOffset = offset;
|
||||
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0xC1); EmitByte(code, ref offset, 0xC0);
|
||||
EmitByte(code, ref offset, 0x04);
|
||||
EmitByte(code, ref offset, 0x89); EmitByte(code, ref offset, 0xC2);
|
||||
EmitByte(code, ref offset, 0x83); EmitByte(code, ref offset, 0xE2); EmitByte(code, ref offset, 0x0F);
|
||||
EmitByte(code, ref offset, 0x41); EmitByte(code, ref offset, 0x0F); EmitByte(code, ref offset, 0xB6);
|
||||
EmitByte(code, ref offset, 0x14); EmitByte(code, ref offset, 0x10);
|
||||
EmitByte(code, ref offset, 0x41); EmitByte(code, ref offset, 0x88); EmitByte(code, ref offset, 0x13);
|
||||
EmitByte(code, ref offset, 0x49); EmitByte(code, ref offset, 0xFF); EmitByte(code, ref offset, 0xC3);
|
||||
EmitByte(code, ref offset, 0xFF); EmitByte(code, ref offset, 0xC9);
|
||||
EmitByte(code, ref offset, 0x75);
|
||||
EmitByte(code, ref offset, unchecked((byte)(hexLoopOffset - (offset + 1)))); // jnz rel8
|
||||
EmitByte(code, ref offset, 0x41); EmitByte(code, ref offset, 0xC6); EmitByte(code, ref offset, 0x03);
|
||||
EmitByte(code, ref offset, 0x0A);
|
||||
|
||||
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0x8B);
|
||||
EmitByte(code, ref offset, 0x4C); EmitByte(code, ref offset, 0x24);
|
||||
EmitByte(code, ref offset, 0x28);
|
||||
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0x8D);
|
||||
EmitByte(code, ref offset, 0x54); EmitByte(code, ref offset, 0x24);
|
||||
EmitByte(code, ref offset, 0x30);
|
||||
EmitByte(code, ref offset, 0x41); EmitByte(code, ref offset, 0xB8);
|
||||
EmitUInt32(code, ref offset, 17u);
|
||||
EmitByte(code, ref offset, 0x4C); EmitByte(code, ref offset, 0x8D);
|
||||
EmitByte(code, ref offset, 0x4C); EmitByte(code, ref offset, 0x24);
|
||||
EmitByte(code, ref offset, 0x20);
|
||||
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0xC7);
|
||||
EmitByte(code, ref offset, 0x44); EmitByte(code, ref offset, 0x24);
|
||||
EmitByte(code, ref offset, 0x20); EmitUInt32(code, ref offset, 0);
|
||||
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0xC7);
|
||||
EmitByte(code, ref offset, 0x44); EmitByte(code, ref offset, 0x24);
|
||||
EmitByte(code, ref offset, 0x38); EmitUInt32(code, ref offset, 0);
|
||||
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0xB8);
|
||||
*(nint*)(code + offset) = writeFile;
|
||||
offset += sizeof(nint);
|
||||
EmitByte(code, ref offset, 0xFF); EmitByte(code, ref offset, 0xD0);
|
||||
|
||||
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0x83);
|
||||
EmitByte(code, ref offset, 0xC4); EmitByte(code, ref offset, 0x40);
|
||||
EmitByte(code, ref offset, 0x41); EmitByte(code, ref offset, 0x5A);
|
||||
EmitByte(code, ref offset, 0x41); EmitByte(code, ref offset, 0x59);
|
||||
EmitByte(code, ref offset, 0x41); EmitByte(code, ref offset, 0x58);
|
||||
EmitByte(code, ref offset, 0x5A);
|
||||
EmitByte(code, ref offset, 0x59);
|
||||
EmitByte(code, ref offset, 0x58);
|
||||
EmitByte(code, ref offset, 0x31); EmitByte(code, ref offset, 0xC0);
|
||||
EmitByte(code, ref offset, 0xC3);
|
||||
|
||||
var msgOffset = offset;
|
||||
for (int i = 0; i < msg.Length; i++)
|
||||
{
|
||||
EmitByte(code, ref offset, msg[i]);
|
||||
}
|
||||
|
||||
var hexDigitsOffset = offset;
|
||||
for (int i = 0; i < hexDigits.Length; i++)
|
||||
{
|
||||
EmitByte(code, ref offset, hexDigits[i]);
|
||||
}
|
||||
|
||||
*(nint*)(code + msgAbsSlot) = (nint)ptr + msgOffset;
|
||||
*(nint*)(code + hexDigitsAbsSlot) = (nint)ptr + hexDigitsOffset;
|
||||
code[passJumpOffsets[fastFailJumpSlot]] =
|
||||
checked((byte)(fastFailPassOffset - (passJumpOffsets[fastFailJumpSlot] + 1)));
|
||||
fastFailLogInstalled = true;
|
||||
}
|
||||
|
||||
int mainBodyOffset = offset;
|
||||
*(int*)(code + mainBodyJumpSlot) = mainBodyOffset - (mainBodyJumpSlot + sizeof(int));
|
||||
|
||||
for (int i = 0; i < nonManagedExceptionCodes.Length; i++)
|
||||
{
|
||||
if (i == fastFailJumpSlot && fastFailLogInstalled)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
code[passJumpOffsets[i]] = checked((byte)(passOffset - (passJumpOffsets[i] + 1)));
|
||||
}
|
||||
EmitByte(code, ref offset, 0x41); EmitByte(code, ref offset, 0x54); // push r12
|
||||
|
||||
Reference in New Issue
Block a user