[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.
This commit is contained in:
PandaCatz
2026-07-11 09:41:25 -05:00
committed by GitHub
parent ef74680167
commit f43f7cde9c
3 changed files with 136 additions and 58 deletions
@@ -99,6 +99,12 @@ public sealed partial class DirectExecutionBackend
Console.Error.WriteLine($"[LOADER][TRACE] Raw sentinel recoveries: {num2} (last import index={importIndex})"); Console.Error.WriteLine($"[LOADER][TRACE] Raw sentinel recoveries: {num2} (last import index={importIndex})");
_lastReportedRawSentinelRecoveries = num2; _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) && if (IsLeafImport(importStubEntry.Nid) &&
TryDispatchLeafImport(cpuContext, importStubEntry, argPackPtr, num, out var leafResult)) 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.R13] = *(ulong*)(argPackPtr + 72);
cpuContext[CpuRegister.R14] = *(ulong*)(argPackPtr + 80); cpuContext[CpuRegister.R14] = *(ulong*)(argPackPtr + 80);
cpuContext[CpuRegister.R15] = *(ulong*)(argPackPtr + 88); cpuContext[CpuRegister.R15] = *(ulong*)(argPackPtr + 88);
cpuContext.SetXmmRegister( // The trampoline spills the SysV variadic XMM save area (xmm0..xmm7) into the
0, // 0x80 bytes immediately below the GP argpack, so variadic float args (printf
*(ulong*)(argPackPtr - 16), // %f, and powf/logf inputs) reach the handler. xmm{i} is at argPackPtr-0x80+i*16.
*(ulong*)(argPackPtr - 8)); 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; cpuContext[CpuRegister.Rsp] = (ulong)argPackPtr + 96uL;
ulong value = cpuContext[CpuRegister.Rdi]; ulong value = cpuContext[CpuRegister.Rdi];
ulong value2 = cpuContext[CpuRegister.Rsi]; ulong value2 = cpuContext[CpuRegister.Rsi];
@@ -491,6 +504,13 @@ public sealed partial class DirectExecutionBackend
Console.Error.Flush(); 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]; return cpuContext[CpuRegister.Rax];
} }
catch (Exception ex) catch (Exception ex)
@@ -622,6 +642,13 @@ public sealed partial class DirectExecutionBackend
return true; 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) => private static bool IsNoBlockLeafImport(string nid) =>
nid is nid is
"8aI7R7WaOlc" or // sceAmprCommandBufferConstructor "8aI7R7WaOlc" or // sceAmprCommandBufferConstructor
@@ -648,7 +675,7 @@ public sealed partial class DirectExecutionBackend
"xk0AcarP3V4" or // scePadOpen "xk0AcarP3V4" or // scePadOpen
"yH17Q6NWtVg" or // sceUserServiceGetEvent "yH17Q6NWtVg" or // sceUserServiceGetEvent
"D-CzAxQL0XI" or // sceUserServiceGetPlatformPrivacySetting "D-CzAxQL0XI" or // sceUserServiceGetPlatformPrivacySetting
"K-jXhbt2gn4"; // scePthreadMutexTrylock "K-jXhbt2gn4"; // pthread_mutex_trylock (scePthreadMutexTrylock is upoVrzMHFeE)
private bool ShouldLogImportResult(string nid, OrbisGen2Result result) private bool ShouldLogImportResult(string nid, OrbisGen2Result result)
{ {
@@ -715,22 +742,41 @@ public sealed partial class DirectExecutionBackend
"1G3lF1Gg1k8" or // sceKernelOpen "1G3lF1Gg1k8" or // sceKernelOpen
"gEpBkcwxUjw"; // sceKernelAprResolveFilepathsToIdsAndFileSizes "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) private bool IsLeafImport(string nid)
{ {
if (nid == "1jfXLRVzisc") if (nid == "1jfXLRVzisc") // sceKernelUsleep
{ {
return !_logUsleep; return !_logUsleep;
} }
return nid is return nid is
"9UK1vLZQft4" or "9UK1vLZQft4" or // scePthreadMutexLock
"tn3VlD0hG60" or "tn3VlD0hG60" or // scePthreadMutexUnlock
"7H0iTOciTLo" or "7H0iTOciTLo" or // pthread_mutex_lock
"2Z+PpY6CaJg" or "2Z+PpY6CaJg" or // pthread_mutex_unlock
"8aI7R7WaOlc" or "8aI7R7WaOlc" or // sceAmprCommandBufferConstructor
"zgXifHT9ErY" or // sceVideoOutIsFlipPending "zgXifHT9ErY" or // sceVideoOutIsFlipPending
"V++UgBtQhn0" or // sceAgcGetDataPacketPayloadAddress "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 "LtTouSCZjHM" or // sceAgcCbNop
"k3GhuSNmBLU" or // sceAgcCbDispatch "k3GhuSNmBLU" or // sceAgcCbDispatch
"UZbQjYAwwXM" or // sceAgcCbSetShRegistersDirect "UZbQjYAwwXM" or // sceAgcCbSetShRegistersDirect
@@ -753,24 +799,24 @@ public sealed partial class DirectExecutionBackend
"IxYiarKlXxM" or // sceAgcDmaDataPatchSetDstAddressOrOffset "IxYiarKlXxM" or // sceAgcDmaDataPatchSetDstAddressOrOffset
"3KDcnM3lrcU" or // sceAgcWaitRegMemPatchAddress "3KDcnM3lrcU" or // sceAgcWaitRegMemPatchAddress
"0fWWK5uG9rQ" or // sceAgcQueueEndOfPipeActionPatchAddress "0fWWK5uG9rQ" or // sceAgcQueueEndOfPipeActionPatchAddress
"a8uLzYY--tM" or "a8uLzYY--tM" or // sceAmprAprCommandBufferConstructor
"Qs1xtplKo0U" or "Qs1xtplKo0U" or // sceAmprAprCommandBufferDestructor
"GuchCTefuZw" or "GuchCTefuZw" or // sceAmprCommandBufferDestructor
"N-FSPA4S3nI" or "N-FSPA4S3nI" or // sceAmprCommandBufferSetBuffer
"baQO9ez2gL4" or "baQO9ez2gL4" or // sceAmprCommandBufferReset
"ULvXMDz56po" or "ULvXMDz56po" or // sceAmprCommandBufferClearBuffer
"mQ16-QdKv7k" or "mQ16-QdKv7k" or // sceAmprAprCommandBufferReadFile
"vWU-odnS+fU" or "vWU-odnS+fU" or // sceAmprMeasureCommandSizeReadFile
"sSAUCCU1dv4" or "sSAUCCU1dv4" or // sceAmprMeasureCommandSizeWriteKernelEventQueue_04_00
"C+IEj+BsAFM" or "C+IEj+BsAFM" or // sceAmprMeasureCommandSizeWriteAddressOnCompletion
"tZDDEo2tE5k" or "tZDDEo2tE5k" or // sceAmprCommandBufferGetSize
"GnxKOHEawhk" or "GnxKOHEawhk" or // sceAmprCommandBufferGetCurrentOffset
"H896Pt-yB4I" or "H896Pt-yB4I" or // sceAmprCommandBufferWriteKernelEventQueue_04_00
"sJXyWHjP-F8" or "sJXyWHjP-F8" or // sceAmprCommandBufferWriteAddressOnCompletion
"ASoW5WE-UPo" or "ASoW5WE-UPo" or // sceKernelAprSubmitCommandBufferAndGetResult
"rqwFKI4PAiM" or "rqwFKI4PAiM" or // sceKernelAprWaitCommandBuffer
"eE4Szl8sil8" or "eE4Szl8sil8" or // sceKernelAprSubmitCommandBuffer
"qvMUCyyaCSI" or "qvMUCyyaCSI" or // sceKernelAprSubmitCommandBufferAndGetId
"Vo5V8KAwCmk" or // sceSystemServiceHideSplashScreen "Vo5V8KAwCmk" or // sceSystemServiceHideSplashScreen
"TywrFKCoLGY" or // sceSaveDataInitialize3 "TywrFKCoLGY" or // sceSaveDataInitialize3
"dyIhnXq-0SM" or // sceSaveDataDirNameSearch "dyIhnXq-0SM" or // sceSaveDataDirNameSearch
@@ -1568,7 +1568,7 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
private unsafe nint CreateImportHandlerTrampoline(int importIndex) private unsafe nint CreateImportHandlerTrampoline(int importIndex)
{ {
void* ptr = VirtualAlloc(null, 192u, 12288u, 64u); void* ptr = VirtualAlloc(null, 256u, 12288u, 64u);
if (ptr == null) if (ptr == null)
{ {
return 0; return 0;
@@ -1596,20 +1596,21 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
ptr2[num++] = 82; ptr2[num++] = 82;
ptr2[num++] = 86; ptr2[num++] = 86;
ptr2[num++] = 87; ptr2[num++] = 87;
ptr2[num++] = 72; // sub rsp, 0x80 — reserve 8*16 bytes for the SysV variadic XMM save area
ptr2[num++] = 131; ptr2[num++] = 0x48; ptr2[num++] = 0x81; ptr2[num++] = 0xEC;
ptr2[num++] = 236; ptr2[num++] = 0x80; ptr2[num++] = 0x00; ptr2[num++] = 0x00; ptr2[num++] = 0x00;
ptr2[num++] = 16; // movdqu [rsp + i*0x10], xmm{i} for i = 0..7 (F3 0F 7F /r, SIB=0x24 base=rsp, disp8)
ptr2[num++] = 243; ptr2[num++] = 0xF3; ptr2[num++] = 0x0F; ptr2[num++] = 0x7F; ptr2[num++] = 0x44; ptr2[num++] = 0x24; ptr2[num++] = 0x00; // xmm0
ptr2[num++] = 15; ptr2[num++] = 0xF3; ptr2[num++] = 0x0F; ptr2[num++] = 0x7F; ptr2[num++] = 0x4C; ptr2[num++] = 0x24; ptr2[num++] = 0x10; // xmm1
ptr2[num++] = 127; ptr2[num++] = 0xF3; ptr2[num++] = 0x0F; ptr2[num++] = 0x7F; ptr2[num++] = 0x54; ptr2[num++] = 0x24; ptr2[num++] = 0x20; // xmm2
ptr2[num++] = 4; ptr2[num++] = 0xF3; ptr2[num++] = 0x0F; ptr2[num++] = 0x7F; ptr2[num++] = 0x5C; ptr2[num++] = 0x24; ptr2[num++] = 0x30; // xmm3
ptr2[num++] = 36; ptr2[num++] = 0xF3; ptr2[num++] = 0x0F; ptr2[num++] = 0x7F; ptr2[num++] = 0x64; ptr2[num++] = 0x24; ptr2[num++] = 0x40; // xmm4
ptr2[num++] = 76; ptr2[num++] = 0xF3; ptr2[num++] = 0x0F; ptr2[num++] = 0x7F; ptr2[num++] = 0x6C; ptr2[num++] = 0x24; ptr2[num++] = 0x50; // xmm5
ptr2[num++] = 141; ptr2[num++] = 0xF3; ptr2[num++] = 0x0F; ptr2[num++] = 0x7F; ptr2[num++] = 0x74; ptr2[num++] = 0x24; ptr2[num++] = 0x60; // xmm6
ptr2[num++] = 100; ptr2[num++] = 0xF3; ptr2[num++] = 0x0F; ptr2[num++] = 0x7F; ptr2[num++] = 0x7C; ptr2[num++] = 0x24; ptr2[num++] = 0x70; // xmm7
ptr2[num++] = 36; // lea r12, [rsp + 0x80] — r12 = argpack base (the 12 pushed GP regs), past the XMM area
ptr2[num++] = 16; 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++] = 72;
ptr2[num++] = 131; ptr2[num++] = 131;
ptr2[num++] = 236; ptr2[num++] = 236;
@@ -1657,6 +1658,11 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
ptr2[num++] = 131; ptr2[num++] = 131;
ptr2[num++] = 196; ptr2[num++] = 196;
ptr2[num++] = 40; 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++] = 76;
ptr2[num++] = 137; ptr2[num++] = 137;
ptr2[num++] = 228; ptr2[num++] = 228;
@@ -1680,12 +1686,12 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
ptr2[num++] = 95; ptr2[num++] = 95;
ptr2[num++] = 195; ptr2[num++] = 195;
uint num2 = default(uint); 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}"); Console.Error.WriteLine($"[LOADER][ERROR] VirtualProtect failed for import dispatch stub at 0x{(nint)ptr:X16}");
return 0; return 0;
} }
FlushInstructionCache(GetCurrentProcess(), ptr, 192u); FlushInstructionCache(GetCurrentProcess(), ptr, 256u);
return (nint)ptr; return (nint)ptr;
} }
catch catch
@@ -3885,33 +3885,59 @@ public static class KernelMemoryCompatExports
private struct RegisterPrintfArgumentSource : IPrintfArgumentSource 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 readonly CpuContext _ctx;
private int _gpIndex; private int _gpIndex;
private int _fpIndex;
private int _stackIndex;
public RegisterPrintfArgumentSource(CpuContext ctx, int gpIndex) public RegisterPrintfArgumentSource(CpuContext ctx, int gpIndex)
{ {
_ctx = ctx; _ctx = ctx;
_gpIndex = gpIndex; _gpIndex = gpIndex;
_fpIndex = 0;
_stackIndex = 0;
} }
public ulong NextGpArg() public ulong NextGpArg()
{ {
var index = _gpIndex++; var index = _gpIndex;
return index switch if (index < 6)
{ {
0 => _ctx[CpuRegister.Rdi], _gpIndex++;
1 => _ctx[CpuRegister.Rsi], return index switch
2 => _ctx[CpuRegister.Rdx], {
3 => _ctx[CpuRegister.Rcx], 0 => _ctx[CpuRegister.Rdi],
4 => _ctx[CpuRegister.R8], 1 => _ctx[CpuRegister.Rsi],
5 => _ctx[CpuRegister.R9], 2 => _ctx[CpuRegister.Rdx],
_ => ReadStackArg(_ctx, (ulong)(index - 6) * 8) 3 => _ctx[CpuRegister.Rcx],
}; 4 => _ctx[CpuRegister.R8],
_ => _ctx[CpuRegister.R9],
};
}
return ReadStackArg(_ctx, (ulong)(_stackIndex++) * 8);
} }
public double NextFloatArg() 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));
} }
} }