diff --git a/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.cs b/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.cs index 5638f07..a09f3f6 100644 --- a/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.cs +++ b/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.cs @@ -2329,7 +2329,31 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I } } + // A trampoline only encodes its import index, so a runtime module load that sets up every + // module's stubs again reuses them: the stubs then keep their bytes and are not rewritten + // under guest threads that may be executing them. + private readonly Dictionary _importHandlerTrampolineByIndex = new(); + private unsafe nint CreateImportHandlerTrampoline(int importIndex) + { + lock (ImportStubPatchGate) + { + if (_importHandlerTrampolineByIndex.TryGetValue(importIndex, out var existing)) + { + return existing; + } + + var created = CreateImportHandlerTrampolineCore(importIndex); + if (created != 0) + { + _importHandlerTrampolineByIndex[importIndex] = created; + } + + return created; + } + } + + private unsafe nint CreateImportHandlerTrampolineCore(int importIndex) { const uint stubSize = 1024u; void* ptr = VirtualAlloc(null, stubSize, 12288u, 64u); @@ -2520,6 +2544,20 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I private unsafe bool PatchImportStubLocked(nint address, nint trampoline) { + // Every runtime module load sets up the stubs of all loaded modules again; a stub that + // already jumps to this trampoline needs no unprotect/write/restore cycle. + Span patch = stackalloc byte[16]; + patch[0] = 0x48; + patch[1] = 0xB8; + System.Buffers.Binary.BinaryPrimitives.WriteInt64LittleEndian(patch[2..], trampoline); + patch[10] = 0xFF; + patch[11] = 0xE0; + patch[12..].Fill(0x90); + if (new ReadOnlySpan((void*)address, 16).SequenceEqual(patch)) + { + return true; + } + uint flNewProtect = default(uint); if (!VirtualProtect((void*)address, 16u, 64u, &flNewProtect)) { @@ -2556,6 +2594,7 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I } } _importHandlerTrampolines.Clear(); + _importHandlerTrampolineByIndex.Clear(); } private unsafe void CreateTlsHandler() diff --git a/tests/SharpEmu.Libs.Tests/Cpu/ImportTrampolineAbiTests.cs b/tests/SharpEmu.Libs.Tests/Cpu/ImportTrampolineAbiTests.cs index 5574abf..16d7990 100644 --- a/tests/SharpEmu.Libs.Tests/Cpu/ImportTrampolineAbiTests.cs +++ b/tests/SharpEmu.Libs.Tests/Cpu/ImportTrampolineAbiTests.cs @@ -81,13 +81,13 @@ public sealed class ImportTrampolineAbiTests private static unsafe byte[] CreateTrampolineBytes() { + // GetUninitializedObject skips the field initializers, so every reference + // field the emitter touches has to be supplied here, including the + // per-index trampoline cache. var backend = (DirectExecutionBackend)RuntimeHelpers.GetUninitializedObject( typeof(DirectExecutionBackend)); - var trampolineList = typeof(DirectExecutionBackend).GetField( - "_importHandlerTrampolines", - BindingFlags.Instance | BindingFlags.NonPublic); - Assert.NotNull(trampolineList); - trampolineList.SetValue(backend, new List()); + SetPrivateField(backend, "_importHandlerTrampolines", new List()); + SetPrivateField(backend, "_importHandlerTrampolineByIndex", new Dictionary()); var createTrampoline = typeof(DirectExecutionBackend).GetMethod( "CreateImportHandlerTrampoline", @@ -106,6 +106,15 @@ public sealed class ImportTrampolineAbiTests } } + private static void SetPrivateField(DirectExecutionBackend backend, string name, object value) + { + var field = typeof(DirectExecutionBackend).GetField( + name, + BindingFlags.Instance | BindingFlags.NonPublic); + Assert.True(field is not null, $"DirectExecutionBackend has no field '{name}'."); + field!.SetValue(backend, value); + } + private static void AssertContains(ReadOnlySpan code, ReadOnlySpan expected) { Assert.True(