sharpemu/tests/SharpEmu.Libs.Tests/Cpu/WindowsExceptionTrampolineT...

239 lines
9.2 KiB
C#

// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Reflection;
using System.Runtime.InteropServices;
using SharpEmu.Core.Cpu.Native;
using SharpEmu.HLE;
using Xunit;
namespace SharpEmu.Libs.Tests.Cpu;
public sealed class WindowsExceptionTrampolineTests
{
private static bool CanRun => OperatingSystem.IsWindows() &&
RuntimeInformation.ProcessArchitecture == Architecture.X64;
[UnmanagedFunctionPointer(CallingConvention.Winapi)]
private delegate int ExceptionCallback(nint exceptionPointers);
[Fact]
public async Task ConcurrentCallbacksCanEnterWhileAnotherCallbackWaits()
{
if (!CanRun) return;
using var firstEntered = new ManualResetEventSlim();
using var secondEntered = new ManualResetEventSlim();
using var releaseFirst = new ManualResetEventSlim();
var callbackCount = 0;
using var trampoline = new TestTrampoline(_ =>
{
if (Interlocked.Increment(ref callbackCount) == 1)
{
firstEntered.Set();
releaseFirst.Wait();
}
else
{
secondEntered.Set();
}
return -1;
});
Task<int>? firstCallback = null;
Task<int>? secondCallback = null;
try
{
firstCallback = StartCallback(trampoline.Address);
Assert.True(firstEntered.Wait(TimeSpan.FromSeconds(5)), "The first callback did not enter.");
secondCallback = StartCallback(trampoline.Address);
Assert.True(secondEntered.Wait(TimeSpan.FromSeconds(5)),
"The second callback was blocked by the first callback.");
}
finally
{
// Release the first callback before joining either thread, including a failed assertion.
releaseFirst.Set();
if (firstCallback is not null) await firstCallback;
if (secondCallback is not null) await secondCallback;
}
Assert.Equal(-1, await firstCallback!);
Assert.Equal(-1, await secondCallback!);
Assert.Equal(2, callbackCount);
}
[Theory]
[InlineData(0xE0434352u)]
[InlineData(0xE06D7363u)]
[InlineData(0xC00000FDu)]
[InlineData(0x40010006u)]
[InlineData(0x4001000Au)]
[InlineData(0xC0000005u)]
public void HostExceptionsBypassManagedCallback(uint exceptionCode)
{
if (!CanRun) return;
var callbackEntered = false;
using var trampoline = new TestTrampoline(_ =>
{
callbackEntered = true;
return -1;
});
Assert.Equal(0, InvokeCallback(trampoline.Address, exceptionCode));
Assert.False(callbackEntered);
}
[Fact]
public unsafe void CallbackPreservesNonvolatileRegistersAndStackBounds()
{
if (!CanRun) return;
using var trampoline = new TestTrampoline(_ =>
{
GC.KeepAlive(new byte[256]);
return -1;
});
var probe = CreateRegisterProbe(trampoline.Address);
try
{
Span<ulong> observed = stackalloc ulong[14];
Span<byte> exceptionRecord = stackalloc byte[0xA0];
Span<byte> contextRecord = stackalloc byte[0x4D0];
exceptionRecord.Clear();
contextRecord.Clear();
fixed (byte* recordAddress = exceptionRecord)
fixed (byte* contextAddress = contextRecord)
fixed (ulong* observedAddress = observed)
{
*(uint*)recordAddress = 0xC000001D;
nint* exceptionPointers = stackalloc nint[2];
exceptionPointers[0] = (nint)recordAddress;
exceptionPointers[1] = (nint)contextAddress;
var result = ((delegate* unmanaged<nint, nint, int>)probe)(
(nint)exceptionPointers, (nint)observedAddress);
Assert.Equal(-1, result);
}
for (var registerIndex = 0; registerIndex < 8; registerIndex++)
{
Assert.Equal(0x1111111111111111UL * (ulong)(registerIndex + 1), observed[registerIndex]);
}
Assert.Equal(observed[8], observed[11]);
Assert.Equal(observed[9], observed[12]);
Assert.Equal(observed[10], observed[13]);
}
finally
{
Assert.True(HostMemory.Free((void*)probe, 0, HostMemory.MEM_RELEASE));
}
}
private static Task<int> StartCallback(nint trampoline) => Task.Factory.StartNew(
() => InvokeCallback(trampoline, 0xC000001D), CancellationToken.None,
TaskCreationOptions.LongRunning, TaskScheduler.Default);
private static unsafe int InvokeCallback(nint trampoline, uint exceptionCode)
{
byte* exceptionRecord = stackalloc byte[0xA0];
byte* contextRecord = stackalloc byte[0x4D0];
new Span<byte>(exceptionRecord, 0xA0).Clear();
new Span<byte>(contextRecord, 0x4D0).Clear();
*(uint*)exceptionRecord = exceptionCode;
nint* exceptionPointers = stackalloc nint[2];
exceptionPointers[0] = (nint)exceptionRecord;
exceptionPointers[1] = (nint)contextRecord;
return ((delegate* unmanaged<nint, int>)trampoline)((nint)exceptionPointers);
}
private static unsafe nint CreateRegisterProbe(nint trampoline)
{
var instructions = new List<byte>();
void WriteImmediate(ulong value) => instructions.AddRange(BitConverter.GetBytes(value));
// Save the caller's registers and reserve aligned call space plus two local values.
instructions.AddRange([0x53, 0x55, 0x56, 0x57, 0x41, 0x54, 0x41, 0x55, 0x41, 0x56, 0x41, 0x57]);
instructions.AddRange([0x48, 0x83, 0xEC, 0x38, 0x48, 0x89, 0x54, 0x24, 0x30]);
ReadOnlySpan<byte> registerCodes = [3, 5, 6, 7, 12, 13, 14, 15];
for (var registerIndex = 0; registerIndex < registerCodes.Length; registerIndex++)
{
var registerCode = registerCodes[registerIndex];
instructions.Add(registerCode < 8 ? (byte)0x48 : (byte)0x49);
instructions.Add((byte)(0xB8 + (registerCode & 7)));
WriteImmediate(0x1111111111111111UL * (ulong)(registerIndex + 1));
}
// Record RSP and both thread stack bounds before and after the callback.
instructions.AddRange([0x48, 0x89, 0x62, 0x40]);
instructions.AddRange([0x65, 0x48, 0x8B, 0x04, 0x25, 8, 0, 0, 0, 0x48, 0x89, 0x42, 0x48]);
instructions.AddRange([0x65, 0x48, 0x8B, 0x04, 0x25, 16, 0, 0, 0, 0x48, 0x89, 0x42, 0x50]);
instructions.AddRange([0x48, 0xB8]);
WriteImmediate((ulong)trampoline);
instructions.AddRange([0xFF, 0xD0, 0x89, 0x44, 0x24, 0x28, 0x48, 0x8B, 0x44, 0x24, 0x30]);
for (var registerIndex = 0; registerIndex < registerCodes.Length; registerIndex++)
{
var registerCode = registerCodes[registerIndex];
instructions.Add(registerCode < 8 ? (byte)0x48 : (byte)0x4C);
instructions.Add(0x89);
instructions.Add((byte)(0x40 | ((registerCode & 7) << 3)));
instructions.Add((byte)(registerIndex * sizeof(ulong)));
}
instructions.AddRange([0x48, 0x89, 0x60, 0x58]);
instructions.AddRange([0x65, 0x48, 0x8B, 0x14, 0x25, 8, 0, 0, 0, 0x48, 0x89, 0x50, 0x60]);
instructions.AddRange([0x65, 0x48, 0x8B, 0x14, 0x25, 16, 0, 0, 0, 0x48, 0x89, 0x50, 0x68]);
instructions.AddRange([0x8B, 0x44, 0x24, 0x28, 0x48, 0x83, 0xC4, 0x38,
0x41, 0x5F, 0x41, 0x5E, 0x41, 0x5D, 0x41, 0x5C, 0x5F, 0x5E, 0x5D, 0x5B, 0xC3]);
var address = (byte*)HostMemory.Alloc(null, (nuint)instructions.Count,
HostMemory.MEM_COMMIT | HostMemory.MEM_RESERVE, HostMemory.PAGE_EXECUTE_READWRITE);
Assert.NotEqual(0, (nint)address);
instructions.ToArray().CopyTo(new Span<byte>(address, instructions.Count));
return (nint)address;
}
private sealed class TestTrampoline : IDisposable
{
private readonly DirectExecutionBackend _backend;
private readonly ExceptionCallback _callback;
public nint Address { get; }
public TestTrampoline(ExceptionCallback callback)
{
_callback = callback;
var modules = new ModuleManager();
modules.Freeze();
_backend = new DirectExecutionBackend(modules);
try
{
var createTrampoline = typeof(DirectExecutionBackend).GetMethod(
"CreateExceptionHandlerTrampoline", BindingFlags.Instance | BindingFlags.NonPublic);
Assert.NotNull(createTrampoline);
Address = (nint)createTrampoline.Invoke(_backend,
[Marshal.GetFunctionPointerForDelegate(callback)])!;
Assert.NotEqual(0, Address);
}
catch
{
_backend.Dispose();
throw;
}
}
public unsafe void Dispose()
{
try
{
Assert.True(HostMemory.Free((void*)Address, 0, HostMemory.MEM_RELEASE));
}
finally
{
GC.KeepAlive(_callback);
_backend.Dispose();
}
}
}
}