mirror of
https://github.com/par274/sharpemu.git
synced 2026-07-25 20:28:48 +08:00
feat: implement cosf, time, ctype tables, tracked heap access, IL2CPP lookup ABI paths (#542)
* fix: add Messenger CRT and AGC compatibility shims * fix: keep Messenger IL2CPP bootstrap on HLE shims * Fix Messenger compatibility ABI handling * Make IL2CPP ABI regression tests portable
This commit is contained in:
@@ -2168,25 +2168,18 @@ public sealed partial class DirectExecutionBackend
|
||||
}
|
||||
|
||||
var symbolNameAddress = cpuContext[CpuRegister.Rdi];
|
||||
var outputAddress = cpuContext[CpuRegister.Rsi];
|
||||
if (!TryReadAsciiZ(symbolNameAddress, 512, out var symbolName) ||
|
||||
outputAddress == 0 ||
|
||||
!TryResolveIl2CppApiAddress(symbolName, out var resolvedAddress) ||
|
||||
!TryWriteUInt64Compat(outputAddress, resolvedAddress))
|
||||
!TryResolveIl2CppApiAddress(symbolName, out var resolvedAddress))
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][WARN] il2cpp_api_lookup_symbol failed: name='{symbolName}' out=0x{outputAddress:X16}");
|
||||
if (outputAddress != 0)
|
||||
{
|
||||
_ = TryWriteUInt64Compat(outputAddress, 0);
|
||||
}
|
||||
|
||||
cpuContext[CpuRegister.Rax] = ulong.MaxValue;
|
||||
return OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
$"[LOADER][WARN] il2cpp_api_lookup_symbol failed: name='{symbolName}'");
|
||||
// il2cpp_api_lookup_symbol is a normal one-argument pointer-returning
|
||||
// function. In particular, RSI is not an output pointer; the title's
|
||||
// caller leaves it live from an earlier call. Do not write through it.
|
||||
return Il2CppApiLookupAbi.SetResult(cpuContext, resolved: false, address: 0);
|
||||
}
|
||||
|
||||
cpuContext[CpuRegister.Rax] = 0;
|
||||
return OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
return Il2CppApiLookupAbi.SetResult(cpuContext, resolved: true, resolvedAddress);
|
||||
}
|
||||
|
||||
private bool TryResolveIl2CppApiAddress(string symbolName, out ulong address)
|
||||
@@ -2196,8 +2189,23 @@ public sealed partial class DirectExecutionBackend
|
||||
return true;
|
||||
}
|
||||
|
||||
return Aerolib.Instance.TryGetByExportName(symbolName, out var symbol) &&
|
||||
TryResolveRuntimeSymbolAddress(symbol.Nid, out address);
|
||||
if (Aerolib.Instance.TryGetByExportName(symbolName, out var symbol) &&
|
||||
TryResolveRuntimeSymbolAddress(symbol.Nid, out address))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// Unity's IL2CPP API table is populated through this resolver, then the
|
||||
// returned pointers are called directly by the title. Use the callable
|
||||
// zero-return stub for missing APIs rather than a non-callable sentinel.
|
||||
if (symbolName.StartsWith("il2cpp_", StringComparison.Ordinal) &&
|
||||
_unresolvedReturnStub != 0)
|
||||
{
|
||||
address = (ulong)_unresolvedReturnStub;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private OrbisGen2Result DispatchBootstrapBridge()
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using SharpEmu.Core.Cpu;
|
||||
using SharpEmu.HLE;
|
||||
|
||||
namespace SharpEmu.Core.Cpu.Native;
|
||||
|
||||
internal static class Il2CppApiLookupAbi
|
||||
{
|
||||
internal static OrbisGen2Result SetResult(CpuContext context, bool resolved, ulong address)
|
||||
{
|
||||
// il2cpp_api_lookup_symbol is a normal pointer-returning function:
|
||||
// the result is in RAX and RSI remains caller-owned state.
|
||||
context[CpuRegister.Rax] = resolved ? address : 0;
|
||||
return OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
}
|
||||
@@ -2965,6 +2965,38 @@ public static partial class AgcExports
|
||||
$"arg1=0x{ctx[CpuRegister.Rsi]:X16} arg2=0x{ctx[CpuRegister.Rdx]:X16}");
|
||||
return ReturnPointer(ctx, commandAddress);
|
||||
}
|
||||
|
||||
// Synthetic labels for uncatalogued AGC helper NIDs; the NID is authoritative.
|
||||
#pragma warning disable SHEM006
|
||||
[SysAbiExport(
|
||||
Nid = "zlqfTyrQSPk",
|
||||
ExportName = "sceAgcUnknownZlqfTyrQSPk",
|
||||
Target = Generation.Gen5,
|
||||
LibraryName = "libSceAgc")]
|
||||
public static int UnknownZlqfTyrQSPk(CpuContext ctx)
|
||||
{
|
||||
TraceAgc(
|
||||
$"agc.unknown_zlqf rdi=0x{ctx[CpuRegister.Rdi]:X16} " +
|
||||
$"rsi=0x{ctx[CpuRegister.Rsi]:X16} rdx=0x{ctx[CpuRegister.Rdx]:X16} " +
|
||||
$"rcx=0x{ctx[CpuRegister.Rcx]:X16} r8=0x{ctx[CpuRegister.R8]:X16} r9=0x{ctx[CpuRegister.R9]:X16}");
|
||||
ctx[CpuRegister.Rax] = 0;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "dZGYu5wObJs",
|
||||
ExportName = "sceAgcUnknownDZGYu5wObJs",
|
||||
Target = Generation.Gen5,
|
||||
LibraryName = "libSceAgc")]
|
||||
public static int UnknownDZGYu5wObJs(CpuContext ctx)
|
||||
{
|
||||
TraceAgc(
|
||||
$"agc.unknown_dzgy rdi=0x{ctx[CpuRegister.Rdi]:X16} " +
|
||||
$"rsi=0x{ctx[CpuRegister.Rsi]:X16} rdx=0x{ctx[CpuRegister.Rdx]:X16} " +
|
||||
$"rcx=0x{ctx[CpuRegister.Rcx]:X16} r8=0x{ctx[CpuRegister.R8]:X16} r9=0x{ctx[CpuRegister.R9]:X16}");
|
||||
ctx[CpuRegister.Rax] = 0;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
#pragma warning restore SHEM006
|
||||
|
||||
private static void EnqueueSubmittedDcb(
|
||||
|
||||
@@ -21,6 +21,11 @@ public static class CxaGuardExports
|
||||
}
|
||||
|
||||
private static readonly ConcurrentDictionary<ulong, GuardState> _inProgress = new();
|
||||
private static readonly ConcurrentDictionary<ulong, object> _onceGates = new();
|
||||
|
||||
private const int OnceUninitialized = 0;
|
||||
private const int OnceInProgress = 1;
|
||||
private const int OnceComplete = 2;
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "3GPpjQdAMTw",
|
||||
@@ -171,6 +176,167 @@ public static class CxaGuardExports
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "DiGVep5yB5w",
|
||||
ExportName = "_ZSt13_Execute_onceRSt9once_flagPFiPvS1_PS1_ES1_",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libc")]
|
||||
public static int ExecuteOnce(CpuContext ctx)
|
||||
{
|
||||
var onceAddress = ctx[CpuRegister.Rdi];
|
||||
var callbackAddress = ctx[CpuRegister.Rsi];
|
||||
var parameter = ctx[CpuRegister.Rdx];
|
||||
if (onceAddress == 0 || callbackAddress == 0)
|
||||
{
|
||||
ctx[CpuRegister.Rax] = 0;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
if (!ctx.TryReadInt32(onceAddress, out var onceValue))
|
||||
{
|
||||
ctx[CpuRegister.Rax] = 0;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
|
||||
if (onceValue == OnceComplete)
|
||||
{
|
||||
ctx[CpuRegister.Rax] = 0;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
var gate = _onceGates.GetOrAdd(onceAddress, static _ => new object());
|
||||
lock (gate)
|
||||
{
|
||||
if (!ctx.TryReadInt32(onceAddress, out onceValue))
|
||||
{
|
||||
ctx[CpuRegister.Rax] = 0;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
|
||||
while (onceValue == OnceInProgress)
|
||||
{
|
||||
Monitor.Wait(gate, TimeSpan.FromMilliseconds(1));
|
||||
if (!ctx.TryReadInt32(onceAddress, out onceValue))
|
||||
{
|
||||
ctx[CpuRegister.Rax] = 0;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
}
|
||||
|
||||
if (onceValue == OnceComplete)
|
||||
{
|
||||
ctx[CpuRegister.Rax] = 0;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
if (!ctx.TryWriteInt32(onceAddress, OnceInProgress))
|
||||
{
|
||||
ctx[CpuRegister.Rax] = 0;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
}
|
||||
|
||||
var scheduler = GuestThreadExecution.Scheduler;
|
||||
var callbackSucceeded = false;
|
||||
string? callbackError = null;
|
||||
var allocator = ctx.Memory as IGuestMemoryAllocator;
|
||||
var hasScratchContext = false;
|
||||
ulong scratchContextAddress = 0;
|
||||
try
|
||||
{
|
||||
if (allocator is not null && allocator.TryAllocateGuestMemory(0x10, 0x10, out scratchContextAddress))
|
||||
{
|
||||
hasScratchContext = true;
|
||||
_ = ctx.TryWriteUInt64(scratchContextAddress, 0);
|
||||
_ = ctx.TryWriteUInt64(scratchContextAddress + 8, 0);
|
||||
}
|
||||
|
||||
if (scheduler is null)
|
||||
{
|
||||
callbackError = "guest scheduler unavailable";
|
||||
}
|
||||
else if (scheduler.TryCallGuestFunction(
|
||||
ctx,
|
||||
callbackAddress,
|
||||
onceAddress,
|
||||
parameter,
|
||||
scratchContextAddress,
|
||||
0,
|
||||
0,
|
||||
"std::_Execute_once",
|
||||
out var returnValue,
|
||||
out callbackError))
|
||||
{
|
||||
callbackSucceeded = returnValue != 0;
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (hasScratchContext && allocator is not null)
|
||||
{
|
||||
_ = allocator.TryFreeGuestMemory(scratchContextAddress);
|
||||
}
|
||||
}
|
||||
|
||||
lock (gate)
|
||||
{
|
||||
if (!callbackSucceeded)
|
||||
{
|
||||
_ = ctx.TryWriteInt32(onceAddress, OnceUninitialized);
|
||||
Monitor.PulseAll(gate);
|
||||
if (!string.IsNullOrWhiteSpace(callbackError))
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][WARN] std::_Execute_once callback failed: {callbackError}");
|
||||
}
|
||||
|
||||
ctx[CpuRegister.Rax] = unchecked((ulong)(int)OrbisGen2Result.ORBIS_GEN2_ERROR_TRY_AGAIN);
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_TRY_AGAIN;
|
||||
}
|
||||
|
||||
if (!ctx.TryWriteInt32(onceAddress, OnceComplete))
|
||||
{
|
||||
_ = ctx.TryWriteInt32(onceAddress, OnceUninitialized);
|
||||
Monitor.PulseAll(gate);
|
||||
ctx[CpuRegister.Rax] = unchecked((ulong)(int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
|
||||
Monitor.PulseAll(gate);
|
||||
// A completed once flag no longer needs a host-side gate. Waiters
|
||||
// already holding this gate will observe OnceComplete after the
|
||||
// pulse; future callers take the fast path before looking it up.
|
||||
_ = _onceGates.TryRemove(onceAddress, out _);
|
||||
}
|
||||
|
||||
ctx[CpuRegister.Rax] = 0;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "PsrRUg671K0",
|
||||
ExportName = "__cxa_increment_exception_refcount",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libc")]
|
||||
public static int CxaIncrementExceptionRefcount(CpuContext ctx)
|
||||
{
|
||||
_ = ctx[CpuRegister.Rdi];
|
||||
ctx[CpuRegister.Rax] = 0;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "MQFPAqQPt1s",
|
||||
ExportName = "__cxa_decrement_exception_refcount",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libc")]
|
||||
public static int CxaDecrementExceptionRefcount(CpuContext ctx)
|
||||
{
|
||||
_ = ctx[CpuRegister.Rdi];
|
||||
ctx[CpuRegister.Rax] = 0;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
private static bool TryReadGuardState(CpuContext ctx, ulong guardPtr, out ulong word, out bool initialized, out bool inProgress)
|
||||
{
|
||||
word = 0;
|
||||
|
||||
@@ -5855,7 +5855,7 @@ public static partial class KernelMemoryCompatExports
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!TryReadHostMemory(address, destination))
|
||||
if (!TryReadTrackedLibcHeap(address, destination) && !TryReadHostMemory(address, destination))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
@@ -5921,7 +5921,7 @@ public static partial class KernelMemoryCompatExports
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!TryWriteHostMemory(address, source))
|
||||
if (!TryWriteTrackedLibcHeap(address, source) && !TryWriteHostMemory(address, source))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
@@ -6663,7 +6663,7 @@ public static partial class KernelMemoryCompatExports
|
||||
}
|
||||
}
|
||||
|
||||
internal static bool TryReadTrackedLibcHeap(
|
||||
internal static unsafe bool TryReadTrackedLibcHeap(
|
||||
ulong address,
|
||||
Span<byte> destination)
|
||||
{
|
||||
@@ -6687,7 +6687,54 @@ public static partial class KernelMemoryCompatExports
|
||||
continue;
|
||||
}
|
||||
|
||||
return TryReadHostMemory(address, destination);
|
||||
try
|
||||
{
|
||||
// Marshal.AllocHGlobal allocations are not present in the
|
||||
// emulator's POSIX HostMemory map, so TryReadHostMemory
|
||||
// would reject this already-bounds-checked range.
|
||||
new ReadOnlySpan<byte>((void*)address, destination.Length).CopyTo(destination);
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static unsafe bool TryWriteTrackedLibcHeap(ulong address, ReadOnlySpan<byte> source)
|
||||
{
|
||||
if (source.IsEmpty)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
var length = (ulong)source.Length;
|
||||
lock (_libcAllocGate)
|
||||
{
|
||||
foreach (var (allocationAddress, allocation) in _libcAllocations)
|
||||
{
|
||||
var allocationSize = (ulong)allocation.Size;
|
||||
var offset = address >= allocationAddress
|
||||
? address - allocationAddress
|
||||
: ulong.MaxValue;
|
||||
if (offset > allocationSize || length > allocationSize - offset)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
source.CopyTo(new Span<byte>((void*)address, source.Length));
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -45,7 +45,11 @@ public static class LibcStdioExports
|
||||
private const ushort CtypeBlank = 0x400; // _XB ' ' and '\t'
|
||||
|
||||
private static readonly object _ctypeTableGate = new();
|
||||
private static readonly object _ctypeLowerTableGate = new();
|
||||
private static readonly object _ctypeUpperTableGate = new();
|
||||
private static nint _ctypeTableBase;
|
||||
private static nint _ctypeLowerTableBase;
|
||||
private static nint _ctypeUpperTableBase;
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "xeYO4u7uyJ0",
|
||||
@@ -716,6 +720,28 @@ public static class LibcStdioExports
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "1uJgoVq3bQU",
|
||||
ExportName = "_Getptolower",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libc")]
|
||||
public static int GetPtolower(CpuContext ctx)
|
||||
{
|
||||
ctx[CpuRegister.Rax] = unchecked((ulong)EnsureCtypeLowerTable());
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "rcQCUr0EaRU",
|
||||
ExportName = "_Getptoupper",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libc")]
|
||||
public static int GetPtoupper(CpuContext ctx)
|
||||
{
|
||||
ctx[CpuRegister.Rax] = unchecked((ulong)EnsureCtypeUpperTable());
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
private static unsafe nint EnsureCtypeTable()
|
||||
{
|
||||
lock (_ctypeTableGate)
|
||||
@@ -740,6 +766,58 @@ public static class LibcStdioExports
|
||||
}
|
||||
}
|
||||
|
||||
private static unsafe nint EnsureCtypeLowerTable()
|
||||
{
|
||||
lock (_ctypeLowerTableGate)
|
||||
{
|
||||
if (_ctypeLowerTableBase != 0)
|
||||
{
|
||||
return _ctypeLowerTableBase;
|
||||
}
|
||||
|
||||
var storage = Marshal.AllocHGlobal(CtypeTableEntryCount * sizeof(ushort));
|
||||
var entries = new Span<ushort>((void*)storage, CtypeTableEntryCount);
|
||||
for (var i = 0; i < CtypeTableEntryCount; i++)
|
||||
{
|
||||
var c = i + CtypeTableLowerBound;
|
||||
entries[i] = c == -1
|
||||
? ushort.MaxValue
|
||||
: c is >= 0 and <= 0x7F
|
||||
? (ushort)char.ToLowerInvariant((char)c)
|
||||
: (ushort)(c & 0xFF);
|
||||
}
|
||||
|
||||
_ctypeLowerTableBase = storage - (CtypeTableLowerBound * sizeof(ushort));
|
||||
return _ctypeLowerTableBase;
|
||||
}
|
||||
}
|
||||
|
||||
private static unsafe nint EnsureCtypeUpperTable()
|
||||
{
|
||||
lock (_ctypeUpperTableGate)
|
||||
{
|
||||
if (_ctypeUpperTableBase != 0)
|
||||
{
|
||||
return _ctypeUpperTableBase;
|
||||
}
|
||||
|
||||
var storage = Marshal.AllocHGlobal(CtypeTableEntryCount * sizeof(ushort));
|
||||
var entries = new Span<ushort>((void*)storage, CtypeTableEntryCount);
|
||||
for (var i = 0; i < CtypeTableEntryCount; i++)
|
||||
{
|
||||
var c = i + CtypeTableLowerBound;
|
||||
entries[i] = c == -1
|
||||
? ushort.MaxValue
|
||||
: c is >= 0 and <= 0x7F
|
||||
? (ushort)char.ToUpperInvariant((char)c)
|
||||
: (ushort)(c & 0xFF);
|
||||
}
|
||||
|
||||
_ctypeUpperTableBase = storage - (CtypeTableLowerBound * sizeof(ushort));
|
||||
return _ctypeUpperTableBase;
|
||||
}
|
||||
}
|
||||
|
||||
private static ushort ComputeCtypeFlags(int c)
|
||||
{
|
||||
var isUpper = c is >= 'A' and <= 'Z';
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System;
|
||||
using SharpEmu.HLE;
|
||||
|
||||
namespace SharpEmu.Libs.Messenger;
|
||||
|
||||
/// <summary>Small Unity/libc compatibility shims exercised by The Messenger.</summary>
|
||||
public static class MessengerCompatExports
|
||||
{
|
||||
[SysAbiExport(Nid = "wLlFkwG9UcQ", ExportName = "time", Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libc")]
|
||||
public static int Time(CpuContext ctx)
|
||||
{
|
||||
var seconds = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
|
||||
var output = ctx[CpuRegister.Rdi];
|
||||
if (output != 0 && !ctx.TryWriteUInt64(output, unchecked((ulong)seconds)))
|
||||
{
|
||||
ctx[CpuRegister.Rax] = 0;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
|
||||
ctx[CpuRegister.Rax] = unchecked((ulong)seconds);
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
[SysAbiExport(Nid = "M4YYbSFfJ8g", ExportName = "setenv", Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libc")]
|
||||
public static int Setenv(CpuContext ctx)
|
||||
{
|
||||
ctx[CpuRegister.Rax] = 0;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
[SysAbiExport(Nid = "-P6FNMzk2Kc", ExportName = "cosf", Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libc")]
|
||||
public static int Cosf(CpuContext ctx)
|
||||
{
|
||||
// AMD64 passes a scalar float in XMM0 and returns it in XMM0. RDI is
|
||||
// unrelated caller state and must not be interpreted as the argument.
|
||||
ctx.GetXmmRegister(0, out var low, out var high);
|
||||
var value = BitConverter.Int32BitsToSingle(unchecked((int)low));
|
||||
var resultBits = unchecked((uint)BitConverter.SingleToInt32Bits(MathF.Cos(value)));
|
||||
ctx.SetXmmRegister(0, (low & 0xFFFFFFFF00000000UL) | resultBits, high);
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
[SysAbiExport(Nid = "YQ0navp+YIc", ExportName = "puts", Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libc")]
|
||||
public static int Puts(CpuContext ctx)
|
||||
{
|
||||
ctx[CpuRegister.Rax] = 0;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
[SysAbiExport(Nid = "KuOuD58hqn4", ExportName = "malloc_stats_fast", Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libc")]
|
||||
public static int MallocStatsFast(CpuContext ctx)
|
||||
{
|
||||
ctx[CpuRegister.Rax] = 0;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
[SysAbiExport(Nid = "-pnj3-7a6QA", ExportName = "unity_mono_set_user_malloc_mutex", Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libunity")]
|
||||
public static int UnityMonoSetMallocMutex(CpuContext ctx)
|
||||
{
|
||||
ctx[CpuRegister.Rax] = 0;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
[SysAbiExport(Nid = "35NoyMOtYpE", ExportName = "SetDataFolder", Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libunity")]
|
||||
public static int SetDataFolder(CpuContext ctx)
|
||||
{
|
||||
ctx[CpuRegister.Rax] = 0;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
#pragma warning disable SHEM006
|
||||
[SysAbiExport(Nid = "cJ2Y4E-t258", ExportName = "il2cpp_api_register_symbol", Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libil2cpp")]
|
||||
public static int Il2CppRegisterSymbol(CpuContext ctx)
|
||||
{
|
||||
ctx[CpuRegister.Rax] = 0;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
}
|
||||
#pragma warning restore SHEM006
|
||||
@@ -0,0 +1,119 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System.Runtime.InteropServices;
|
||||
using SharpEmu.Core.Cpu.Native;
|
||||
using SharpEmu.HLE;
|
||||
using SharpEmu.Libs.Kernel;
|
||||
using SharpEmu.Libs.LibcStdio;
|
||||
using SharpEmu.Libs.Messenger;
|
||||
using Xunit;
|
||||
|
||||
namespace SharpEmu.Libs.Tests;
|
||||
|
||||
public sealed class MessengerCompatExportsTests
|
||||
{
|
||||
[Fact]
|
||||
public void Cosf_UsesScalarXmmArgumentAndReturn()
|
||||
{
|
||||
const ulong memoryBase = 0x1_0000_0000;
|
||||
var context = new CpuContext(new FakeCpuMemory(memoryBase, 0x1000), Generation.Gen5);
|
||||
var input = 0.5f;
|
||||
var inputBits = unchecked((uint)BitConverter.SingleToInt32Bits(input));
|
||||
context[CpuRegister.Rdi] = 0xDEAD_BEEF; // Must not be used as the argument.
|
||||
context.SetXmmRegister(0, 0xAABB_CCDD_0000_0000UL | inputBits, 0x1122_3344_5566_7788UL);
|
||||
|
||||
Assert.Equal((int)OrbisGen2Result.ORBIS_GEN2_OK, MessengerCompatExports.Cosf(context));
|
||||
|
||||
context.GetXmmRegister(0, out var low, out var high);
|
||||
var expectedBits = unchecked((uint)BitConverter.SingleToInt32Bits(MathF.Cos(input)));
|
||||
Assert.Equal(expectedBits, unchecked((uint)low));
|
||||
Assert.Equal(0xAABB_CCDDUL, low >> 32);
|
||||
Assert.Equal(0x1122_3344_5566_7788UL, high);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CtypeCaseTables_MapAsciiCharacters()
|
||||
{
|
||||
var context = new CpuContext(new FakeCpuMemory(0x1_0000_0000, 0x1000), Generation.Gen5);
|
||||
|
||||
Assert.Equal((int)OrbisGen2Result.ORBIS_GEN2_OK, LibcStdioExports.GetPtolower(context));
|
||||
var lower = unchecked((nint)(long)context[CpuRegister.Rax]);
|
||||
Assert.Equal((short)'a', Marshal.ReadInt16(lower + ('A' * sizeof(short))));
|
||||
Assert.Equal((short)'z', Marshal.ReadInt16(lower + ('z' * sizeof(short))));
|
||||
Assert.Equal((short)-1, Marshal.ReadInt16(lower - sizeof(short)));
|
||||
|
||||
Assert.Equal((int)OrbisGen2Result.ORBIS_GEN2_OK, LibcStdioExports.GetPtoupper(context));
|
||||
var upper = unchecked((nint)(long)context[CpuRegister.Rax]);
|
||||
Assert.Equal((short)'A', Marshal.ReadInt16(upper + ('a' * sizeof(short))));
|
||||
Assert.Equal((short)'Z', Marshal.ReadInt16(upper + ('Z' * sizeof(short))));
|
||||
Assert.Equal((short)-1, Marshal.ReadInt16(upper - sizeof(short)));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Il2CppLookup_ReturnsPointerInRaxWithoutWritingRsi()
|
||||
{
|
||||
const ulong memoryBase = 0x1_0000_0000;
|
||||
const ulong nameAddress = memoryBase + 0x100;
|
||||
const ulong outputAddress = memoryBase + 0x200;
|
||||
const ulong resolvedAddress = 0x2_0000_0000;
|
||||
var memory = new FakeCpuMemory(memoryBase, 0x1000);
|
||||
memory.WriteCString(nameAddress, "il2cpp_test");
|
||||
var context = new CpuContext(memory, Generation.Gen5);
|
||||
context[CpuRegister.Rdi] = nameAddress;
|
||||
context[CpuRegister.Rsi] = outputAddress; // Live caller state, not an output pointer.
|
||||
Assert.True(context.TryWriteUInt64(outputAddress, 0xCAFE_BABE));
|
||||
|
||||
var result = Il2CppApiLookupAbi.SetResult(context, resolved: true, resolvedAddress);
|
||||
|
||||
Assert.Equal(OrbisGen2Result.ORBIS_GEN2_OK, result);
|
||||
Assert.Equal(resolvedAddress, context[CpuRegister.Rax]);
|
||||
Assert.True(context.TryReadUInt64(outputAddress, out var output));
|
||||
Assert.Equal(0xCAFE_BABEUL, output);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Il2CppLookup_MissingApiReturnsNullWithoutWritingRsi()
|
||||
{
|
||||
const ulong memoryBase = 0x1_0000_0000;
|
||||
const ulong nameAddress = memoryBase + 0x100;
|
||||
const ulong outputAddress = memoryBase + 0x200;
|
||||
var memory = new FakeCpuMemory(memoryBase, 0x1000);
|
||||
memory.WriteCString(nameAddress, "il2cpp_missing");
|
||||
var context = new CpuContext(memory, Generation.Gen5);
|
||||
context[CpuRegister.Rdi] = nameAddress;
|
||||
context[CpuRegister.Rsi] = outputAddress;
|
||||
Assert.True(context.TryWriteUInt64(outputAddress, 0xCAFE_BABE));
|
||||
|
||||
var result = Il2CppApiLookupAbi.SetResult(context, resolved: false, address: 0);
|
||||
|
||||
Assert.Equal(OrbisGen2Result.ORBIS_GEN2_OK, result);
|
||||
Assert.Equal(0UL, context[CpuRegister.Rax]);
|
||||
Assert.True(context.TryReadUInt64(outputAddress, out var output));
|
||||
Assert.Equal(0xCAFE_BABEUL, output);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TrackedLibcHeapFallback_ReadsAndWritesHostAllocation()
|
||||
{
|
||||
var context = new CpuContext(new FakeCpuMemory(0x1_0000_0000, 0x1000), Generation.Gen5);
|
||||
context[CpuRegister.Rdi] = 16;
|
||||
Assert.Equal((int)OrbisGen2Result.ORBIS_GEN2_OK, KernelMemoryCompatExports.Malloc(context));
|
||||
var address = context[CpuRegister.Rax];
|
||||
Assert.NotEqual(0UL, address);
|
||||
|
||||
try
|
||||
{
|
||||
Assert.True(KernelMemoryCompatExports.TryWriteUInt64Compat(context, address, 0x1234_5678_9ABC_DEF0UL));
|
||||
Span<byte> bytes = stackalloc byte[8];
|
||||
Assert.True(KernelMemoryCompatExports.TryReadTrackedLibcHeap(address, bytes));
|
||||
Assert.True(KernelMemoryCompatExports.TryReadUInt64Compat(context, address, out var value));
|
||||
Assert.Equal(0x1234_5678_9ABC_DEF0UL, value);
|
||||
}
|
||||
finally
|
||||
{
|
||||
context[CpuRegister.Rdi] = address;
|
||||
_ = KernelMemoryCompatExports.Free(context);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user