[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})");
_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
@@ -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
@@ -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));
}
}