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 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;
}
}
+32
View File
@@ -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(
+166
View File
@@ -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;
}
}
}
+78
View File
@@ -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);
}
}
}