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 symbolNameAddress = cpuContext[CpuRegister.Rdi];
|
||||||
var outputAddress = cpuContext[CpuRegister.Rsi];
|
|
||||||
if (!TryReadAsciiZ(symbolNameAddress, 512, out var symbolName) ||
|
if (!TryReadAsciiZ(symbolNameAddress, 512, out var symbolName) ||
|
||||||
outputAddress == 0 ||
|
!TryResolveIl2CppApiAddress(symbolName, out var resolvedAddress))
|
||||||
!TryResolveIl2CppApiAddress(symbolName, out var resolvedAddress) ||
|
|
||||||
!TryWriteUInt64Compat(outputAddress, resolvedAddress))
|
|
||||||
{
|
{
|
||||||
Console.Error.WriteLine(
|
Console.Error.WriteLine(
|
||||||
$"[LOADER][WARN] il2cpp_api_lookup_symbol failed: name='{symbolName}' out=0x{outputAddress:X16}");
|
$"[LOADER][WARN] il2cpp_api_lookup_symbol failed: name='{symbolName}'");
|
||||||
if (outputAddress != 0)
|
// il2cpp_api_lookup_symbol is a normal one-argument pointer-returning
|
||||||
{
|
// function. In particular, RSI is not an output pointer; the title's
|
||||||
_ = TryWriteUInt64Compat(outputAddress, 0);
|
// caller leaves it live from an earlier call. Do not write through it.
|
||||||
}
|
return Il2CppApiLookupAbi.SetResult(cpuContext, resolved: false, address: 0);
|
||||||
|
|
||||||
cpuContext[CpuRegister.Rax] = ulong.MaxValue;
|
|
||||||
return OrbisGen2Result.ORBIS_GEN2_OK;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
cpuContext[CpuRegister.Rax] = 0;
|
return Il2CppApiLookupAbi.SetResult(cpuContext, resolved: true, resolvedAddress);
|
||||||
return OrbisGen2Result.ORBIS_GEN2_OK;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool TryResolveIl2CppApiAddress(string symbolName, out ulong address)
|
private bool TryResolveIl2CppApiAddress(string symbolName, out ulong address)
|
||||||
@@ -2196,8 +2189,23 @@ public sealed partial class DirectExecutionBackend
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
return Aerolib.Instance.TryGetByExportName(symbolName, out var symbol) &&
|
if (Aerolib.Instance.TryGetByExportName(symbolName, out var symbol) &&
|
||||||
TryResolveRuntimeSymbolAddress(symbol.Nid, out address);
|
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()
|
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}");
|
$"arg1=0x{ctx[CpuRegister.Rsi]:X16} arg2=0x{ctx[CpuRegister.Rdx]:X16}");
|
||||||
return ReturnPointer(ctx, commandAddress);
|
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
|
#pragma warning restore SHEM006
|
||||||
|
|
||||||
private static void EnqueueSubmittedDcb(
|
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, 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(
|
[SysAbiExport(
|
||||||
Nid = "3GPpjQdAMTw",
|
Nid = "3GPpjQdAMTw",
|
||||||
@@ -171,6 +176,167 @@ public static class CxaGuardExports
|
|||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
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)
|
private static bool TryReadGuardState(CpuContext ctx, ulong guardPtr, out ulong word, out bool initialized, out bool inProgress)
|
||||||
{
|
{
|
||||||
word = 0;
|
word = 0;
|
||||||
|
|||||||
@@ -5855,7 +5855,7 @@ public static partial class KernelMemoryCompatExports
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!TryReadHostMemory(address, destination))
|
if (!TryReadTrackedLibcHeap(address, destination) && !TryReadHostMemory(address, destination))
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -5921,7 +5921,7 @@ public static partial class KernelMemoryCompatExports
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!TryWriteHostMemory(address, source))
|
if (!TryWriteTrackedLibcHeap(address, source) && !TryWriteHostMemory(address, source))
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -6663,7 +6663,7 @@ public static partial class KernelMemoryCompatExports
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal static bool TryReadTrackedLibcHeap(
|
internal static unsafe bool TryReadTrackedLibcHeap(
|
||||||
ulong address,
|
ulong address,
|
||||||
Span<byte> destination)
|
Span<byte> destination)
|
||||||
{
|
{
|
||||||
@@ -6687,7 +6687,54 @@ public static partial class KernelMemoryCompatExports
|
|||||||
continue;
|
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 const ushort CtypeBlank = 0x400; // _XB ' ' and '\t'
|
||||||
|
|
||||||
private static readonly object _ctypeTableGate = new();
|
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 _ctypeTableBase;
|
||||||
|
private static nint _ctypeLowerTableBase;
|
||||||
|
private static nint _ctypeUpperTableBase;
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "xeYO4u7uyJ0",
|
Nid = "xeYO4u7uyJ0",
|
||||||
@@ -716,6 +720,28 @@ public static class LibcStdioExports
|
|||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
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()
|
private static unsafe nint EnsureCtypeTable()
|
||||||
{
|
{
|
||||||
lock (_ctypeTableGate)
|
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)
|
private static ushort ComputeCtypeFlags(int c)
|
||||||
{
|
{
|
||||||
var isUpper = c is >= 'A' and <= 'Z';
|
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