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:
Kurt Himebauch
2026-07-23 08:35:59 -04:00
committed by GitHub
parent 7b950166d7
commit 2764aaab3f
8 changed files with 570 additions and 20 deletions
@@ -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;
}
}
+32
View File
@@ -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(
+166
View File
@@ -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;
}
} }
} }
+78
View File
@@ -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);
}
}
}