From f43f7cde9c8a024e33c05ec55edac96539d66eaa Mon Sep 17 00:00:00 2001 From: PandaCatz <295914637+PandaCatz@users.noreply.github.com> Date: Sat, 11 Jul 2026 09:41:25 -0500 Subject: [PATCH] [cpu] Implement SysV variadic float ABI (xmm0-7 capture, float returns, printf %f) (#59) * [cpu] Implement SysV variadic float ABI (xmm0-7 capture, float returns, printf %f) The import trampoline spilled only xmm0 and never reloaded a return xmm0. The guest uses the System V AMD64 ABI: variadic float args pass in xmm0..xmm7 and float/double returns come back in xmm0. As a result variadic float args past the first were unavailable to HLE handlers, float returns never reached the guest, and direct printf read %f/%e/%g from GP registers instead of XMM, printing garbage and desynchronizing every following argument. - Trampoline: spill xmm0..xmm7 into a 0x80-byte save area below the GP argpack (r12 stays at the argpack base) and reload the return xmm0 in the epilogue. - Gateway: read xmm0..7 from the save area into CpuContext and write the handler's xmm0 back. XMM is caller-saved in SysV, so restoring xmm0 on return is safe for non-float imports too. - RegisterPrintfArgumentSource: read float args from xmm0..7 with independent GP/FP counters and a shared stack-overflow cursor. Every emitted byte was decoded; a unit test confirms float args read xmm0..7 (not GP) and interleaved "%d %f %d %f" stays synchronized. Build 0/0. * [cpu] Document the scalar-only leaf-import constraint at its registration site - IsLeafImport: spell out the no-XMM-args / no-XMM-return invariant the fast path relies on and what breaks if it is violated; record the 2026-07-11 audit. - Name every previously uncommented NID in the leaf list (mutex lock/unlock, usleep, the Ampr/Apr command-buffer block, the unknown AGC packet NID). - IsNoBlockLeafImport: document that it is a sub-filter of IsLeafImport and that its five extra entries currently take the full gateway path; fix the mislabeled K-jXhbt2gn4 comment (pthread_mutex_trylock, not scePthreadMutexTrylock, which is upoVrzMHFeE). - Point the DispatchImport call-site note at the audited list. Comment-only change: the comment-stripped diff is empty and the solution builds with 0 warnings / 0 errors. --- .../Native/DirectExecutionBackend.Imports.cs | 106 +++++++++++++----- .../Cpu/Native/DirectExecutionBackend.cs | 40 ++++--- .../Kernel/KernelMemoryCompatExports.cs | 48 ++++++-- 3 files changed, 136 insertions(+), 58 deletions(-) diff --git a/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.Imports.cs b/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.Imports.cs index 04b567b..32b0902 100644 --- a/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.Imports.cs +++ b/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.Imports.cs @@ -99,6 +99,12 @@ public sealed partial class DirectExecutionBackend Console.Error.WriteLine($"[LOADER][TRACE] Raw sentinel recoveries: {num2} (last import index={importIndex})"); _lastReportedRawSentinelRecoveries = num2; } + // Leaf imports take a scalar-only fast path that reads its own operands and + // intentionally bypasses the SysV variadic XMM marshalling below. This is safe + // only while the leaf set contains no float-variadic or float-returning function. + // The constraint and the audited list live on IsLeafImport — do not add a + // function that consumes xmm0-7 args or returns in xmm0 to the leaf set; + // such functions must stay on the full gateway path below. if (IsLeafImport(importStubEntry.Nid) && TryDispatchLeafImport(cpuContext, importStubEntry, argPackPtr, num, out var leafResult)) { @@ -118,10 +124,17 @@ public sealed partial class DirectExecutionBackend cpuContext[CpuRegister.R13] = *(ulong*)(argPackPtr + 72); cpuContext[CpuRegister.R14] = *(ulong*)(argPackPtr + 80); cpuContext[CpuRegister.R15] = *(ulong*)(argPackPtr + 88); - cpuContext.SetXmmRegister( - 0, - *(ulong*)(argPackPtr - 16), - *(ulong*)(argPackPtr - 8)); + // The trampoline spills the SysV variadic XMM save area (xmm0..xmm7) into the + // 0x80 bytes immediately below the GP argpack, so variadic float args (printf + // %f, and powf/logf inputs) reach the handler. xmm{i} is at argPackPtr-0x80+i*16. + for (var xmmIndex = 0; xmmIndex < 8; xmmIndex++) + { + var xmmSlot = argPackPtr - 0x80 + (xmmIndex * 16); + cpuContext.SetXmmRegister( + xmmIndex, + *(ulong*)xmmSlot, + *(ulong*)(xmmSlot + 8)); + } cpuContext[CpuRegister.Rsp] = (ulong)argPackPtr + 96uL; ulong value = cpuContext[CpuRegister.Rdi]; ulong value2 = cpuContext[CpuRegister.Rsi]; @@ -491,6 +504,13 @@ public sealed partial class DirectExecutionBackend Console.Error.Flush(); } } + // Publish the handler's XMM0 back into the argpack's xmm0 save slot; the + // trampoline epilogue reloads it into the guest's XMM0, delivering float/double + // return values (powf/logf/wcstod). Harmless for int/pointer returns (XMM is + // volatile across a SysV call, so the guest never relies on a preserved XMM0). + cpuContext.GetXmmRegister(0, out var returnXmm0Low, out var returnXmm0High); + *(ulong*)(argPackPtr - 0x80) = returnXmm0Low; + *(ulong*)(argPackPtr - 0x80 + 8) = returnXmm0High; return cpuContext[CpuRegister.Rax]; } catch (Exception ex) @@ -622,6 +642,13 @@ public sealed partial class DirectExecutionBackend return true; } + // Subset of the leaf set that additionally skips the import-call-frame + // bookkeeping. The same scalar-only (no XMM args, no XMM return) constraint + // as IsLeafImport applies — see the note there before adding entries. + // NOTE: this filter is only consulted after IsLeafImport accepts the NID; + // entries listed here but not in IsLeafImport (scePadRead, scePadOpen, + // sceUserServiceGetEvent, sceUserServiceGetPlatformPrivacySetting, + // pthread_mutex_trylock) currently take the full gateway path. private static bool IsNoBlockLeafImport(string nid) => nid is "8aI7R7WaOlc" or // sceAmprCommandBufferConstructor @@ -648,7 +675,7 @@ public sealed partial class DirectExecutionBackend "xk0AcarP3V4" or // scePadOpen "yH17Q6NWtVg" or // sceUserServiceGetEvent "D-CzAxQL0XI" or // sceUserServiceGetPlatformPrivacySetting - "K-jXhbt2gn4"; // scePthreadMutexTrylock + "K-jXhbt2gn4"; // pthread_mutex_trylock (scePthreadMutexTrylock is upoVrzMHFeE) private bool ShouldLogImportResult(string nid, OrbisGen2Result result) { @@ -715,22 +742,41 @@ public sealed partial class DirectExecutionBackend "1G3lF1Gg1k8" or // sceKernelOpen "gEpBkcwxUjw"; // sceKernelAprResolveFilepathsToIdsAndFileSizes + // LEAF-IMPORT REGISTRATION — scalar-only constraint. + // + // TryDispatchLeafImport is a fast path that never copies the trampoline's XMM + // save area into CpuContext and never publishes an XMM0 return back to the + // guest (see DispatchImport). Every NID listed here must therefore satisfy BOTH: + // 1. no float/double parameters (nothing passed in xmm0..xmm7), and + // 2. no float/double return value (nothing returned in xmm0). + // Integer/pointer arguments and returns only. va_list-based functions + // (vsnprintf) are safe: SysV va_list floats are read from the caller-built + // reg_save_area in guest memory, not from the callee's incoming XMM registers. + // + // If a function that consumes XMM arguments or returns in xmm0 (e.g. powf, + // logf, wcstod, sceVideoOutColorSettingsSetGamma_) is ever added here, its + // float arguments will silently arrive stale and its return value will be + // dropped. Such functions must stay on the full gateway path — do not list them. + // + // Audited 2026-07-11 (PR #59): every NID below maps to a registered + // SysAbiExport whose handler neither reads nor writes CpuContext XMM state + // and whose known guest signature is integer/pointer only. private bool IsLeafImport(string nid) { - if (nid == "1jfXLRVzisc") + if (nid == "1jfXLRVzisc") // sceKernelUsleep { return !_logUsleep; } return nid is - "9UK1vLZQft4" or - "tn3VlD0hG60" or - "7H0iTOciTLo" or - "2Z+PpY6CaJg" or - "8aI7R7WaOlc" or + "9UK1vLZQft4" or // scePthreadMutexLock + "tn3VlD0hG60" or // scePthreadMutexUnlock + "7H0iTOciTLo" or // pthread_mutex_lock + "2Z+PpY6CaJg" or // pthread_mutex_unlock + "8aI7R7WaOlc" or // sceAmprCommandBufferConstructor "zgXifHT9ErY" or // sceVideoOutIsFlipPending "V++UgBtQhn0" or // sceAgcGetDataPacketPayloadAddress - "qj7QZpgr9Uw" or // Gen5 graphics type-2 packet + "qj7QZpgr9Uw" or // sceAgcUnknownQj7QZpgr9Uw (unknown NID; observed scalar: command-buffer pointer in, packet address out) "LtTouSCZjHM" or // sceAgcCbNop "k3GhuSNmBLU" or // sceAgcCbDispatch "UZbQjYAwwXM" or // sceAgcCbSetShRegistersDirect @@ -753,24 +799,24 @@ public sealed partial class DirectExecutionBackend "IxYiarKlXxM" or // sceAgcDmaDataPatchSetDstAddressOrOffset "3KDcnM3lrcU" or // sceAgcWaitRegMemPatchAddress "0fWWK5uG9rQ" or // sceAgcQueueEndOfPipeActionPatchAddress - "a8uLzYY--tM" or - "Qs1xtplKo0U" or - "GuchCTefuZw" or - "N-FSPA4S3nI" or - "baQO9ez2gL4" or - "ULvXMDz56po" or - "mQ16-QdKv7k" or - "vWU-odnS+fU" or - "sSAUCCU1dv4" or - "C+IEj+BsAFM" or - "tZDDEo2tE5k" or - "GnxKOHEawhk" or - "H896Pt-yB4I" or - "sJXyWHjP-F8" or - "ASoW5WE-UPo" or - "rqwFKI4PAiM" or - "eE4Szl8sil8" or - "qvMUCyyaCSI" or + "a8uLzYY--tM" or // sceAmprAprCommandBufferConstructor + "Qs1xtplKo0U" or // sceAmprAprCommandBufferDestructor + "GuchCTefuZw" or // sceAmprCommandBufferDestructor + "N-FSPA4S3nI" or // sceAmprCommandBufferSetBuffer + "baQO9ez2gL4" or // sceAmprCommandBufferReset + "ULvXMDz56po" or // sceAmprCommandBufferClearBuffer + "mQ16-QdKv7k" or // sceAmprAprCommandBufferReadFile + "vWU-odnS+fU" or // sceAmprMeasureCommandSizeReadFile + "sSAUCCU1dv4" or // sceAmprMeasureCommandSizeWriteKernelEventQueue_04_00 + "C+IEj+BsAFM" or // sceAmprMeasureCommandSizeWriteAddressOnCompletion + "tZDDEo2tE5k" or // sceAmprCommandBufferGetSize + "GnxKOHEawhk" or // sceAmprCommandBufferGetCurrentOffset + "H896Pt-yB4I" or // sceAmprCommandBufferWriteKernelEventQueue_04_00 + "sJXyWHjP-F8" or // sceAmprCommandBufferWriteAddressOnCompletion + "ASoW5WE-UPo" or // sceKernelAprSubmitCommandBufferAndGetResult + "rqwFKI4PAiM" or // sceKernelAprWaitCommandBuffer + "eE4Szl8sil8" or // sceKernelAprSubmitCommandBuffer + "qvMUCyyaCSI" or // sceKernelAprSubmitCommandBufferAndGetId "Vo5V8KAwCmk" or // sceSystemServiceHideSplashScreen "TywrFKCoLGY" or // sceSaveDataInitialize3 "dyIhnXq-0SM" or // sceSaveDataDirNameSearch diff --git a/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.cs b/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.cs index c7a9839..1c06fa8 100644 --- a/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.cs +++ b/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.cs @@ -1568,7 +1568,7 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I private unsafe nint CreateImportHandlerTrampoline(int importIndex) { - void* ptr = VirtualAlloc(null, 192u, 12288u, 64u); + void* ptr = VirtualAlloc(null, 256u, 12288u, 64u); if (ptr == null) { return 0; @@ -1596,20 +1596,21 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I ptr2[num++] = 82; ptr2[num++] = 86; ptr2[num++] = 87; - ptr2[num++] = 72; - ptr2[num++] = 131; - ptr2[num++] = 236; - ptr2[num++] = 16; - ptr2[num++] = 243; - ptr2[num++] = 15; - ptr2[num++] = 127; - ptr2[num++] = 4; - ptr2[num++] = 36; - ptr2[num++] = 76; - ptr2[num++] = 141; - ptr2[num++] = 100; - ptr2[num++] = 36; - ptr2[num++] = 16; + // sub rsp, 0x80 — reserve 8*16 bytes for the SysV variadic XMM save area + ptr2[num++] = 0x48; ptr2[num++] = 0x81; ptr2[num++] = 0xEC; + ptr2[num++] = 0x80; ptr2[num++] = 0x00; ptr2[num++] = 0x00; ptr2[num++] = 0x00; + // movdqu [rsp + i*0x10], xmm{i} for i = 0..7 (F3 0F 7F /r, SIB=0x24 base=rsp, disp8) + ptr2[num++] = 0xF3; ptr2[num++] = 0x0F; ptr2[num++] = 0x7F; ptr2[num++] = 0x44; ptr2[num++] = 0x24; ptr2[num++] = 0x00; // xmm0 + ptr2[num++] = 0xF3; ptr2[num++] = 0x0F; ptr2[num++] = 0x7F; ptr2[num++] = 0x4C; ptr2[num++] = 0x24; ptr2[num++] = 0x10; // xmm1 + ptr2[num++] = 0xF3; ptr2[num++] = 0x0F; ptr2[num++] = 0x7F; ptr2[num++] = 0x54; ptr2[num++] = 0x24; ptr2[num++] = 0x20; // xmm2 + ptr2[num++] = 0xF3; ptr2[num++] = 0x0F; ptr2[num++] = 0x7F; ptr2[num++] = 0x5C; ptr2[num++] = 0x24; ptr2[num++] = 0x30; // xmm3 + ptr2[num++] = 0xF3; ptr2[num++] = 0x0F; ptr2[num++] = 0x7F; ptr2[num++] = 0x64; ptr2[num++] = 0x24; ptr2[num++] = 0x40; // xmm4 + ptr2[num++] = 0xF3; ptr2[num++] = 0x0F; ptr2[num++] = 0x7F; ptr2[num++] = 0x6C; ptr2[num++] = 0x24; ptr2[num++] = 0x50; // xmm5 + ptr2[num++] = 0xF3; ptr2[num++] = 0x0F; ptr2[num++] = 0x7F; ptr2[num++] = 0x74; ptr2[num++] = 0x24; ptr2[num++] = 0x60; // xmm6 + ptr2[num++] = 0xF3; ptr2[num++] = 0x0F; ptr2[num++] = 0x7F; ptr2[num++] = 0x7C; ptr2[num++] = 0x24; ptr2[num++] = 0x70; // xmm7 + // lea r12, [rsp + 0x80] — r12 = argpack base (the 12 pushed GP regs), past the XMM area + ptr2[num++] = 0x4C; ptr2[num++] = 0x8D; ptr2[num++] = 0xA4; ptr2[num++] = 0x24; + ptr2[num++] = 0x80; ptr2[num++] = 0x00; ptr2[num++] = 0x00; ptr2[num++] = 0x00; ptr2[num++] = 72; ptr2[num++] = 131; ptr2[num++] = 236; @@ -1657,6 +1658,11 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I ptr2[num++] = 131; ptr2[num++] = 196; ptr2[num++] = 40; + // movdqu xmm0, [r12 - 0x80] — reload the return XMM0 the gateway wrote into the + // argpack's xmm0 save slot (float/double returns: powf/logf/wcstod). SysV/Win64 + // XMM regs are volatile across calls, so an unconditional reload is ABI-safe. + ptr2[num++] = 0xF3; ptr2[num++] = 0x41; ptr2[num++] = 0x0F; ptr2[num++] = 0x6F; + ptr2[num++] = 0x84; ptr2[num++] = 0x24; ptr2[num++] = 0x80; ptr2[num++] = 0xFF; ptr2[num++] = 0xFF; ptr2[num++] = 0xFF; ptr2[num++] = 76; ptr2[num++] = 137; ptr2[num++] = 228; @@ -1680,12 +1686,12 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I ptr2[num++] = 95; ptr2[num++] = 195; uint num2 = default(uint); - if (!VirtualProtect(ptr, 192u, 32u, &num2)) + if (!VirtualProtect(ptr, 256u, 32u, &num2)) { Console.Error.WriteLine($"[LOADER][ERROR] VirtualProtect failed for import dispatch stub at 0x{(nint)ptr:X16}"); return 0; } - FlushInstructionCache(GetCurrentProcess(), ptr, 192u); + FlushInstructionCache(GetCurrentProcess(), ptr, 256u); return (nint)ptr; } catch diff --git a/src/SharpEmu.Libs/Kernel/KernelMemoryCompatExports.cs b/src/SharpEmu.Libs/Kernel/KernelMemoryCompatExports.cs index f884a59..ece78e8 100644 --- a/src/SharpEmu.Libs/Kernel/KernelMemoryCompatExports.cs +++ b/src/SharpEmu.Libs/Kernel/KernelMemoryCompatExports.cs @@ -3885,33 +3885,59 @@ public static class KernelMemoryCompatExports private struct RegisterPrintfArgumentSource : IPrintfArgumentSource { + // SysV AMD64: variadic integer/pointer args use the GP registers (rdi..r9, up to + // 6) and variadic float/double args use xmm0..xmm7 (8) — INDEPENDENT counters. + // Args beyond the registers spill to the stack in source order, so GP-overflow + // and FP-overflow share one stack cursor. private readonly CpuContext _ctx; private int _gpIndex; + private int _fpIndex; + private int _stackIndex; public RegisterPrintfArgumentSource(CpuContext ctx, int gpIndex) { _ctx = ctx; _gpIndex = gpIndex; + _fpIndex = 0; + _stackIndex = 0; } public ulong NextGpArg() { - var index = _gpIndex++; - return index switch + var index = _gpIndex; + if (index < 6) { - 0 => _ctx[CpuRegister.Rdi], - 1 => _ctx[CpuRegister.Rsi], - 2 => _ctx[CpuRegister.Rdx], - 3 => _ctx[CpuRegister.Rcx], - 4 => _ctx[CpuRegister.R8], - 5 => _ctx[CpuRegister.R9], - _ => ReadStackArg(_ctx, (ulong)(index - 6) * 8) - }; + _gpIndex++; + return index switch + { + 0 => _ctx[CpuRegister.Rdi], + 1 => _ctx[CpuRegister.Rsi], + 2 => _ctx[CpuRegister.Rdx], + 3 => _ctx[CpuRegister.Rcx], + 4 => _ctx[CpuRegister.R8], + _ => _ctx[CpuRegister.R9], + }; + } + + return ReadStackArg(_ctx, (ulong)(_stackIndex++) * 8); } public double NextFloatArg() { - return BitConverter.Int64BitsToDouble(unchecked((long)NextGpArg())); + // Float/double variadic args live in xmm0..xmm7 (low 64 bits), NOT the GP + // registers. Reading them from GP prints garbage and desynchronizes every + // subsequent argument. + ulong bits; + if (_fpIndex < 8) + { + _ctx.GetXmmRegister(_fpIndex++, out bits, out _); + } + else + { + bits = ReadStackArg(_ctx, (ulong)(_stackIndex++) * 8); + } + + return BitConverter.Int64BitsToDouble(unchecked((long)bits)); } }