449 lines
18 KiB
C#
449 lines
18 KiB
C#
// Copyright (C) 2026 SharpEmu Emulator Project
|
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
|
|
using System.Runtime.InteropServices;
|
|
using SharpEmu.HLE;
|
|
|
|
namespace SharpEmu.Core.Cpu.Native;
|
|
|
|
/// <summary>
|
|
/// POSIX replacements for the kernel32 helpers the native backend embeds in
|
|
/// emitted x86-64 code. Every stub exposed here follows the Win64 calling
|
|
/// convention the emitted call sites were written for (first argument in
|
|
/// ECX, result in RAX, Win64 non-volatile registers preserved), so the
|
|
/// emission code stays identical across platforms.
|
|
/// </summary>
|
|
internal static unsafe class PosixHostStubs
|
|
{
|
|
private static readonly object Gate = new();
|
|
private static bool _initialized;
|
|
private static nint _tlsGetValueStub;
|
|
private static nint _queryPerformanceCounterStub;
|
|
private static nint _switchToThreadStub;
|
|
private static nint _sleepStub;
|
|
|
|
public static nint TlsGetValueStubAddress
|
|
{
|
|
get { EnsureInitialized(); return _tlsGetValueStub; }
|
|
}
|
|
|
|
public static nint QueryPerformanceCounterStubAddress
|
|
{
|
|
get { EnsureInitialized(); return _queryPerformanceCounterStub; }
|
|
}
|
|
|
|
public static nint SwitchToThreadStubAddress
|
|
{
|
|
get { EnsureInitialized(); return _switchToThreadStub; }
|
|
}
|
|
|
|
public static nint SleepStubAddress
|
|
{
|
|
get { EnsureInitialized(); return _sleepStub; }
|
|
}
|
|
|
|
public static nint CreateWorkerEvent()
|
|
{
|
|
if (OperatingSystem.IsMacOS())
|
|
{
|
|
return dispatch_semaphore_create(0);
|
|
}
|
|
|
|
var semaphore = Marshal.AllocHGlobal(64);
|
|
if (sem_init(semaphore, 0, 0) != 0)
|
|
{
|
|
Marshal.FreeHGlobal(semaphore);
|
|
return 0;
|
|
}
|
|
|
|
return semaphore;
|
|
}
|
|
|
|
public static bool SignalWorkerEvent(nint handle)
|
|
{
|
|
if (OperatingSystem.IsMacOS())
|
|
{
|
|
_ = dispatch_semaphore_signal(handle);
|
|
return true;
|
|
}
|
|
|
|
return sem_post(handle) == 0;
|
|
}
|
|
|
|
public static bool WaitWorkerEvent(nint handle, int timeoutMilliseconds)
|
|
{
|
|
if (OperatingSystem.IsMacOS())
|
|
{
|
|
if (timeoutMilliseconds < 0)
|
|
{
|
|
return dispatch_semaphore_wait(handle, ulong.MaxValue) == 0;
|
|
}
|
|
|
|
var deadline = dispatch_time(0, timeoutMilliseconds * 1_000_000L);
|
|
return dispatch_semaphore_wait(handle, deadline) == 0;
|
|
}
|
|
|
|
if (timeoutMilliseconds < 0)
|
|
{
|
|
while (sem_wait(handle) != 0)
|
|
{
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
var deadlineTicks = Environment.TickCount64 + timeoutMilliseconds;
|
|
while (sem_trywait(handle) != 0)
|
|
{
|
|
if (Environment.TickCount64 >= deadlineTicks)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
Thread.Sleep(1);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
public static void DestroyWorkerEvent(nint handle)
|
|
{
|
|
if (handle == 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (OperatingSystem.IsMacOS())
|
|
{
|
|
dispatch_release(handle);
|
|
return;
|
|
}
|
|
|
|
_ = sem_destroy(handle);
|
|
Marshal.FreeHGlobal(handle);
|
|
}
|
|
|
|
/// <summary>Allocates a pthread TLS key, mirroring kernel32!TlsAlloc.</summary>
|
|
public static uint TlsAlloc()
|
|
{
|
|
if (OperatingSystem.IsMacOS())
|
|
{
|
|
nuint key;
|
|
return pthread_key_create_mac(&key, 0) == 0 ? (uint)key : uint.MaxValue;
|
|
}
|
|
|
|
uint key32;
|
|
return pthread_key_create_linux(&key32, 0) == 0 ? key32 : uint.MaxValue;
|
|
}
|
|
|
|
public static bool TlsFree(uint key)
|
|
{
|
|
return OperatingSystem.IsMacOS()
|
|
? pthread_key_delete_mac((nuint)key) == 0
|
|
: pthread_key_delete_linux(key) == 0;
|
|
}
|
|
|
|
public static bool TlsSetValue(uint key, nint value)
|
|
{
|
|
return OperatingSystem.IsMacOS()
|
|
? pthread_setspecific_mac((nuint)key, value) == 0
|
|
: pthread_setspecific_linux(key, value) == 0;
|
|
}
|
|
|
|
public static nint TlsGetValue(uint key)
|
|
{
|
|
return OperatingSystem.IsMacOS()
|
|
? pthread_getspecific_mac((nuint)key)
|
|
: pthread_getspecific_linux(key);
|
|
}
|
|
|
|
/// <summary>Stable numeric id of the calling thread (kernel32!GetCurrentThreadId).</summary>
|
|
public static uint GetCurrentThreadId()
|
|
{
|
|
if (OperatingSystem.IsMacOS())
|
|
{
|
|
ulong tid;
|
|
return pthread_threadid_np(0, &tid) == 0 ? unchecked((uint)tid) : 0u;
|
|
}
|
|
|
|
return unchecked((uint)gettid());
|
|
}
|
|
|
|
/// <summary>
|
|
/// Wraps a managed callback (compiled for the SysV ABI on POSIX .NET) in a
|
|
/// thunk that accepts up to four integer arguments in the Win64 ABI the
|
|
/// emitted x86-64 call sites use. Win64 passes args in rcx/rdx/r8/r9 and
|
|
/// treats rdi/rsi as non-volatile; SysV expects rdi/rsi/rdx/rcx and
|
|
/// clobbers them, so the thunk saves rdi/rsi, shuffles the registers, keeps
|
|
/// the stack 16-byte aligned for the call, and forwards the rax result.
|
|
/// </summary>
|
|
public static nint CreateWin64ToSysVThunk(nint sysvTarget)
|
|
{
|
|
var page = (byte*)HostMemory.Alloc(
|
|
null,
|
|
4096,
|
|
HostMemory.MEM_COMMIT | HostMemory.MEM_RESERVE,
|
|
HostMemory.PAGE_EXECUTE_READWRITE);
|
|
if (page == null)
|
|
{
|
|
throw new OutOfMemoryException("Failed to allocate Win64->SysV thunk page");
|
|
}
|
|
|
|
var offset = 0;
|
|
Emit(page, ref offset, 0x57); // push rdi
|
|
Emit(page, ref offset, 0x56); // push rsi
|
|
Emit(page, ref offset, 0x48, 0x89, 0xCF); // mov rdi, rcx
|
|
Emit(page, ref offset, 0x48, 0x89, 0xD6); // mov rsi, rdx
|
|
Emit(page, ref offset, 0x4C, 0x89, 0xC2); // mov rdx, r8
|
|
Emit(page, ref offset, 0x4C, 0x89, 0xC9); // mov rcx, r9
|
|
Emit(page, ref offset, 0x48, 0x83, 0xEC, 0x08); // sub rsp, 8 (realign to 16)
|
|
EmitMovRaxImm64(page, ref offset, sysvTarget); // mov rax, target
|
|
Emit(page, ref offset, 0xFF, 0xD0); // call rax
|
|
Emit(page, ref offset, 0x48, 0x83, 0xC4, 0x08); // add rsp, 8
|
|
Emit(page, ref offset, 0x5E); // pop rsi
|
|
Emit(page, ref offset, 0x5F); // pop rdi
|
|
Emit(page, ref offset, 0xC3); // ret
|
|
|
|
if (!HostMemory.Protect(page, 4096, HostMemory.PAGE_EXECUTE_READ, out _))
|
|
{
|
|
throw new InvalidOperationException("Failed to protect Win64->SysV thunk page");
|
|
}
|
|
|
|
HostMemory.FlushInstructionCache(page, (nuint)offset);
|
|
return (nint)page;
|
|
}
|
|
|
|
private static void EnsureInitialized()
|
|
{
|
|
if (_initialized)
|
|
{
|
|
return;
|
|
}
|
|
|
|
lock (Gate)
|
|
{
|
|
if (_initialized)
|
|
{
|
|
return;
|
|
}
|
|
|
|
BuildStubs();
|
|
_initialized = true;
|
|
}
|
|
}
|
|
|
|
private static void BuildStubs()
|
|
{
|
|
var page = (byte*)HostMemory.Alloc(
|
|
null,
|
|
4096,
|
|
HostMemory.MEM_COMMIT | HostMemory.MEM_RESERVE,
|
|
HostMemory.PAGE_EXECUTE_READWRITE);
|
|
if (page == null)
|
|
{
|
|
throw new OutOfMemoryException("Failed to allocate POSIX host helper stub page");
|
|
}
|
|
|
|
var offset = 0;
|
|
_tlsGetValueStub = EmitTlsGetValue(page, ref offset);
|
|
_queryPerformanceCounterStub = EmitQueryPerformanceCounter(page, ref offset);
|
|
_switchToThreadStub = EmitSwitchToThread(page, ref offset);
|
|
_sleepStub = EmitSleep(page, ref offset);
|
|
|
|
if (!HostMemory.Protect(page, 4096, HostMemory.PAGE_EXECUTE_READ, out _))
|
|
{
|
|
throw new InvalidOperationException("Failed to protect POSIX host helper stub page");
|
|
}
|
|
|
|
HostMemory.FlushInstructionCache(page, (nuint)offset);
|
|
}
|
|
|
|
private static nint EmitTlsGetValue(byte* page, ref int offset)
|
|
{
|
|
var start = (nint)(page + offset);
|
|
if (OperatingSystem.IsMacOS())
|
|
{
|
|
// On macOS x86-64 pthread keys index the gs-based thread specific
|
|
// data array directly, so TlsGetValue(index in ecx) collapses to a
|
|
// single load that clobbers nothing but RAX.
|
|
Emit(page, ref offset, 0x89, 0xC8); // mov eax, ecx
|
|
Emit(page, ref offset, 0x65, 0x48, 0x8B, 0x04, 0xC5, 0, 0, 0, 0); // mov rax, gs:[rax*8]
|
|
Emit(page, ref offset, 0xC3); // ret
|
|
return start;
|
|
}
|
|
|
|
// Linux: call pthread_getspecific, preserving the registers that are
|
|
// volatile in SysV but non-volatile in Win64 (rsi, rdi).
|
|
var pthreadGetSpecific = NativeLibrary.GetExport(NativeLibrary.Load("libpthread.so.0"), "pthread_getspecific");
|
|
Emit(page, ref offset, 0x56); // push rsi
|
|
Emit(page, ref offset, 0x57); // push rdi
|
|
Emit(page, ref offset, 0x48, 0x83, 0xEC, 0x08); // sub rsp, 8
|
|
Emit(page, ref offset, 0x89, 0xCF); // mov edi, ecx
|
|
EmitMovRaxImm64(page, ref offset, pthreadGetSpecific); // mov rax, imm64
|
|
Emit(page, ref offset, 0xFF, 0xD0); // call rax
|
|
Emit(page, ref offset, 0x48, 0x83, 0xC4, 0x08); // add rsp, 8
|
|
Emit(page, ref offset, 0x5F); // pop rdi
|
|
Emit(page, ref offset, 0x5E); // pop rsi
|
|
Emit(page, ref offset, 0xC3); // ret
|
|
return start;
|
|
}
|
|
|
|
private static nint EmitQueryPerformanceCounter(byte* page, ref int offset)
|
|
{
|
|
// BOOL QueryPerformanceCounter(LARGE_INTEGER* out in rcx): the emitted
|
|
// consumers only need a monotonically increasing counter, which rdtsc
|
|
// provides without leaving Win64-safe registers.
|
|
var start = (nint)(page + offset);
|
|
Emit(page, ref offset, 0x0F, 0x31); // rdtsc
|
|
Emit(page, ref offset, 0x48, 0xC1, 0xE2, 0x20); // shl rdx, 32
|
|
Emit(page, ref offset, 0x48, 0x09, 0xD0); // or rax, rdx
|
|
Emit(page, ref offset, 0x48, 0x89, 0x01); // mov [rcx], rax
|
|
Emit(page, ref offset, 0xB8, 0x01, 0x00, 0x00, 0x00); // mov eax, 1
|
|
Emit(page, ref offset, 0xC3); // ret
|
|
return start;
|
|
}
|
|
|
|
private static nint EmitSwitchToThread(byte* page, ref int offset)
|
|
{
|
|
var schedYield = ResolveLibcExport("sched_yield");
|
|
var start = (nint)(page + offset);
|
|
Emit(page, ref offset, 0x56); // push rsi
|
|
Emit(page, ref offset, 0x57); // push rdi
|
|
Emit(page, ref offset, 0x48, 0x83, 0xEC, 0x08); // sub rsp, 8
|
|
EmitMovRaxImm64(page, ref offset, schedYield); // mov rax, imm64
|
|
Emit(page, ref offset, 0xFF, 0xD0); // call rax
|
|
Emit(page, ref offset, 0x48, 0x83, 0xC4, 0x08); // add rsp, 8
|
|
Emit(page, ref offset, 0x5F); // pop rdi
|
|
Emit(page, ref offset, 0x5E); // pop rsi
|
|
Emit(page, ref offset, 0xB8, 0x01, 0x00, 0x00, 0x00); // mov eax, 1
|
|
Emit(page, ref offset, 0xC3); // ret
|
|
return start;
|
|
}
|
|
|
|
private static nint EmitSleep(byte* page, ref int offset)
|
|
{
|
|
// void Sleep(DWORD milliseconds in ecx) -> nanosleep(&req, &rem), resumed on EINTR. usleep
|
|
// would be shorter, but macOS rejects usleep(>= 1 s) with EINVAL, and a signal (the
|
|
// runtime's thread activations, for one) cuts either short: a sleep that returned at once
|
|
// on an ignored error turned UE's idle HTTP thread into a hot spin.
|
|
var nanosleep = ResolveLibcExport("nanosleep");
|
|
var errorLocation = ResolveLibcExport(OperatingSystem.IsMacOS() ? "__error" : "__errno_location");
|
|
var start = (nint)(page + offset);
|
|
Emit(page, ref offset, 0x56); // push rsi
|
|
Emit(page, ref offset, 0x57); // push rdi
|
|
Emit(page, ref offset, 0x53); // push rbx
|
|
Emit(page, ref offset, 0x48, 0x83, 0xEC, 0x20); // sub rsp, 0x20: req at [rsp], rem at [rsp+16]
|
|
Emit(page, ref offset, 0x89, 0xC8); // mov eax, ecx
|
|
Emit(page, ref offset, 0x31, 0xD2); // xor edx, edx
|
|
Emit(page, ref offset, 0xB9, 0xE8, 0x03, 0x00, 0x00); // mov ecx, 1000
|
|
Emit(page, ref offset, 0xF7, 0xF1); // div ecx: eax = seconds, edx = milliseconds
|
|
Emit(page, ref offset, 0x48, 0x89, 0x04, 0x24); // mov [rsp], rax (tv_sec)
|
|
Emit(page, ref offset, 0x69, 0xD2, 0x40, 0x42, 0x0F, 0x00); // imul edx, edx, 1000000
|
|
Emit(page, ref offset, 0x48, 0x89, 0x54, 0x24, 0x08); // mov [rsp+8], rdx (tv_nsec)
|
|
// loop:
|
|
Emit(page, ref offset, 0x48, 0x89, 0xE7); // mov rdi, rsp
|
|
Emit(page, ref offset, 0x48, 0x8D, 0x74, 0x24, 0x10); // lea rsi, [rsp+16]
|
|
EmitMovRaxImm64(page, ref offset, nanosleep); // mov rax, imm64
|
|
Emit(page, ref offset, 0xFF, 0xD0); // call rax
|
|
Emit(page, ref offset, 0x85, 0xC0); // test eax, eax
|
|
Emit(page, ref offset, 0x74, 0x26); // jz done
|
|
EmitMovRaxImm64(page, ref offset, errorLocation); // mov rax, imm64
|
|
Emit(page, ref offset, 0xFF, 0xD0); // call rax
|
|
Emit(page, ref offset, 0x83, 0x38, 0x04); // cmp dword [rax], EINTR
|
|
Emit(page, ref offset, 0x75, 0x15); // jne done
|
|
Emit(page, ref offset, 0x48, 0x8B, 0x44, 0x24, 0x10); // mov rax, [rsp+16]
|
|
Emit(page, ref offset, 0x48, 0x89, 0x04, 0x24); // mov [rsp], rax
|
|
Emit(page, ref offset, 0x48, 0x8B, 0x44, 0x24, 0x18); // mov rax, [rsp+24]
|
|
Emit(page, ref offset, 0x48, 0x89, 0x44, 0x24, 0x08); // mov [rsp+8], rax
|
|
Emit(page, ref offset, 0xEB, 0xC2); // jmp loop
|
|
// done:
|
|
Emit(page, ref offset, 0x48, 0x83, 0xC4, 0x20); // add rsp, 0x20
|
|
Emit(page, ref offset, 0x5B); // pop rbx
|
|
Emit(page, ref offset, 0x5F); // pop rdi
|
|
Emit(page, ref offset, 0x5E); // pop rsi
|
|
Emit(page, ref offset, 0xC3); // ret
|
|
return start;
|
|
}
|
|
|
|
private static nint ResolveLibcExport(string name)
|
|
{
|
|
var libc = NativeLibrary.Load(OperatingSystem.IsMacOS() ? "libSystem.dylib" : "libc.so.6");
|
|
return NativeLibrary.GetExport(libc, name);
|
|
}
|
|
|
|
private static void Emit(byte* page, ref int offset, params byte[] bytes)
|
|
{
|
|
foreach (var value in bytes)
|
|
{
|
|
page[offset++] = value;
|
|
}
|
|
}
|
|
|
|
private static void EmitMovRaxImm64(byte* page, ref int offset, nint value)
|
|
{
|
|
Emit(page, ref offset, 0x48, 0xB8);
|
|
*(long*)(page + offset) = value;
|
|
offset += sizeof(long);
|
|
}
|
|
|
|
[DllImport("libc", EntryPoint = "pthread_key_create", SetLastError = true)]
|
|
private static extern int pthread_key_create_mac(nuint* key, nint destructor);
|
|
|
|
[DllImport("libpthread.so.0", EntryPoint = "pthread_key_create", SetLastError = true)]
|
|
private static extern int pthread_key_create_linux(uint* key, nint destructor);
|
|
|
|
[DllImport("libc", EntryPoint = "pthread_key_delete")]
|
|
private static extern int pthread_key_delete_mac(nuint key);
|
|
|
|
[DllImport("libpthread.so.0", EntryPoint = "pthread_key_delete")]
|
|
private static extern int pthread_key_delete_linux(uint key);
|
|
|
|
[DllImport("libc", EntryPoint = "pthread_setspecific")]
|
|
private static extern int pthread_setspecific_mac(nuint key, nint value);
|
|
|
|
[DllImport("libpthread.so.0", EntryPoint = "pthread_setspecific")]
|
|
private static extern int pthread_setspecific_linux(uint key, nint value);
|
|
|
|
[DllImport("libc", EntryPoint = "pthread_getspecific")]
|
|
private static extern nint pthread_getspecific_mac(nuint key);
|
|
|
|
[DllImport("libpthread.so.0", EntryPoint = "pthread_getspecific")]
|
|
private static extern nint pthread_getspecific_linux(uint key);
|
|
|
|
[DllImport("libc")]
|
|
private static extern int pthread_threadid_np(nint thread, ulong* threadId);
|
|
|
|
[DllImport("libc")]
|
|
private static extern int gettid();
|
|
|
|
[DllImport("libc")]
|
|
private static extern nint dispatch_semaphore_create(long value);
|
|
|
|
[DllImport("libc")]
|
|
private static extern nint dispatch_semaphore_signal(nint semaphore);
|
|
|
|
[DllImport("libc")]
|
|
private static extern nint dispatch_semaphore_wait(nint semaphore, ulong timeout);
|
|
|
|
[DllImport("libc")]
|
|
private static extern ulong dispatch_time(ulong when, long deltaNanoseconds);
|
|
|
|
[DllImport("libc")]
|
|
private static extern void dispatch_release(nint handle);
|
|
|
|
[DllImport("libc")]
|
|
private static extern int sem_init(nint semaphore, int shared, uint value);
|
|
|
|
[DllImport("libc")]
|
|
private static extern int sem_post(nint semaphore);
|
|
|
|
[DllImport("libc")]
|
|
private static extern int sem_wait(nint semaphore);
|
|
|
|
[DllImport("libc")]
|
|
private static extern int sem_trywait(nint semaphore);
|
|
|
|
[DllImport("libc")]
|
|
private static extern int sem_destroy(nint semaphore);
|
|
}
|