mirror of
https://github.com/par274/sharpemu.git
synced 2026-07-18 17:06:12 +08:00
5e76554514
Comprehensive refactoring of the system time subsystem to unify clock dispatching, support precise clock extensions, and secure memory boundaries against partial state corruption.
Centralized Clock Dispatch Engine:
- Extracted shared elapsed-tick calculation and clock-routing math into a unified internal static bool ResolveClockTime() dispatch engine under KernelRuntimeCompatExports.cs.
- Moved all clock identifiers from KernelMemoryCompatExports to KernelRuntimeCompatExports as internal const int constants to eliminate cross-file duplication while preserving raw compiler switch-case layout optimizations.
- Added native alias mapping support for CLOCK_REALTIME_PRECISE (9) and CLOCK_MONOTONIC_PRECISE (11).
- Hardened the Orbis sceKernelClockGettime path by routing it through the new dispatcher, resolving a pre-existing logic flaw where any non-zero clock_id incorrectly fell back to monotonic time. Invalid IDs now properly fail with ORBIS_GEN2_ERROR_INVALID_ARGUMENT.
Coalesced Single-Transaction Memory Writes:
- Replaced consecutive isolated 8-byte scalar writes across POSIX clock_gettime, gettimeofday, and Orbis sceKernelClockGettime/sceKernelGettimeofday with safe single-transaction 16-byte stackalloc byte buffer writes via BinaryPrimitives and ctx.Memory.TryWrite. This entirely prevents partial memory state corruption on virtual page boundaries.
- Implemented a single 8-byte coalesced zero-fill transaction for the deprecated/legacy timezone buffer (timezoneAddress != 0), aligning it with standard FreeBSD stub behavior.
- Standardized POSIX failure path routines. Write faults cleanly issue TrySetErrno(ctx, Efault) while safely omitting explicit manual Rax writes, letting the import dispatcher natively sign-extend the return -1 value to 0xFFFFFFFFFFFFFFFF.
Zero-Alloc Host RDTSC Execution Stub:
- Patched CreateRdtscReader() to stream native architecture opcodes out of stack-allocated spans directly into host executable memory zones (VirtualAlloc) via unsafe { Buffer.MemoryCopy(...) }, completely removing the high-frequency .ToArray() runtime allocation overhead on the hot path.
Files: KernelRuntimeCompatExports.cs, KernelMemoryCompatExports.cs
6316 lines
207 KiB
C#
6316 lines
207 KiB
C#
// Copyright (C) 2026 SharpEmu Emulator Project
|
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
|
|
using SharpEmu.HLE;
|
|
using SharpEmu.Libs.Ampr;
|
|
using System.Buffers;
|
|
using System.Buffers.Binary;
|
|
using System.Collections.Concurrent;
|
|
using System.Text;
|
|
using System.Runtime.InteropServices;
|
|
using System.Globalization;
|
|
|
|
namespace SharpEmu.Libs.Kernel;
|
|
|
|
public static class KernelMemoryCompatExports
|
|
{
|
|
private const int MaxGuestStringLength = 4096;
|
|
private const int WideCharSize = sizeof(ushort);
|
|
private const int MemsetChunkSize = 16 * 1024;
|
|
private const int TlsModuleBlockSize = 0x10000;
|
|
private const int O_WRONLY = 0x1;
|
|
private const int O_RDWR = 0x2;
|
|
private const int O_APPEND = 0x8;
|
|
private const int O_CREAT = 0x0200;
|
|
private const int O_TRUNC = 0x0400;
|
|
private const int O_DIRECTORY = 0x00020000;
|
|
private const int OrbisKernelMapFixed = 0x0010;
|
|
private const int OrbisKernelMapOpMapDirect = 0;
|
|
private const int OrbisKernelMapOpUnmap = 1;
|
|
private const int OrbisKernelMapOpProtect = 2;
|
|
private const int OrbisKernelMapOpMapFlexible = 3;
|
|
private const int OrbisKernelMapOpTypeProtect = 4;
|
|
private const int OrbisKernelBatchMapEntrySize = 32;
|
|
private const int OrbisKernelBatchMapEntryStartOffset = 0;
|
|
private const int OrbisKernelBatchMapEntryOffsetOffset = 8;
|
|
private const int OrbisKernelBatchMapEntryLengthOffset = 16;
|
|
private const int OrbisKernelBatchMapEntryProtectionOffset = 24;
|
|
private const int OrbisKernelBatchMapEntryTypeOffset = 25;
|
|
private const int OrbisKernelBatchMapEntryOperationOffset = 28;
|
|
private const ulong OrbisPageSize = 0x4000;
|
|
private const int OrbisProtCpuRead = 0x01;
|
|
private const int OrbisProtCpuWrite = 0x02;
|
|
private const int OrbisProtCpuExec = 0x04;
|
|
private const int OrbisProtGpuRead = 0x10;
|
|
private const int OrbisProtGpuWrite = 0x20;
|
|
private const int OrbisProtCpuReadWrite = OrbisProtCpuRead | OrbisProtCpuWrite;
|
|
private const int SeekSet = 0;
|
|
private const int SeekCur = 1;
|
|
private const int SeekEnd = 2;
|
|
private const ulong DirectMemorySizeBytes = 16384UL * 1024 * 1024;
|
|
private const ulong UnsetMainDirectMemoryPoolBase = ulong.MaxValue;
|
|
private const ulong FlexibleMemorySizeBytes = 448UL * 1024 * 1024;
|
|
private const int OrbisVirtualQueryInfoSize = 72;
|
|
private const int OrbisKernelMaximumNameLength = 32;
|
|
private const uint MemCommit = 0x1000;
|
|
private const uint HostPageNoAccess = 0x01;
|
|
private const uint HostPageReadOnly = 0x02;
|
|
private const uint HostPageReadWrite = 0x04;
|
|
private const uint HostPageWriteCopy = 0x08;
|
|
private const uint HostPageExecute = 0x10;
|
|
private const uint HostPageExecuteRead = 0x20;
|
|
private const uint HostPageExecuteReadWrite = 0x40;
|
|
private const uint HostPageExecuteWriteCopy = 0x80;
|
|
private const uint HostPageGuard = 0x100;
|
|
private const int Enomem = 12;
|
|
private const int Efault = 14;
|
|
private const int Einval = 22;
|
|
private const int Erange = 34;
|
|
private const int Struncate = 80;
|
|
private const nuint DefaultLibcHeapAlignment = 16;
|
|
private const ushort KernelStatModeDirectory = 0x41FF;
|
|
private const ushort KernelStatModeRegular = 0x81FF;
|
|
private const int KernelStatSize = 120;
|
|
private const int KernelStatStDevOffset = 0;
|
|
private const int KernelStatStInoOffset = 4;
|
|
private const int KernelStatStModeOffset = 8;
|
|
private const int KernelStatStNlinkOffset = 10;
|
|
private const int KernelStatStUidOffset = 12;
|
|
private const int KernelStatStGidOffset = 16;
|
|
private const int KernelStatStRdevOffset = 20;
|
|
private const int KernelStatStAtimOffset = 24;
|
|
private const int KernelStatStMtimOffset = 40;
|
|
private const int KernelStatStCtimOffset = 56;
|
|
private const int KernelStatStSizeOffset = 72;
|
|
private const int KernelStatStBlocksOffset = 80;
|
|
private const int KernelStatStBlksizeOffset = 88;
|
|
private const int KernelStatStFlagsOffset = 92;
|
|
private const int KernelStatStGenOffset = 96;
|
|
private const int KernelStatStLspareOffset = 100;
|
|
private const int KernelStatStBirthtimOffset = 104;
|
|
|
|
private static readonly object _fdGate = new();
|
|
private static readonly Dictionary<int, FileStream> _openFiles = new();
|
|
private static readonly Dictionary<int, OpenDirectory> _openDirectories = new();
|
|
private static readonly object _libcAllocGate = new();
|
|
private static readonly object _memoryGate = new();
|
|
private static readonly object _tlsGate = new();
|
|
private static readonly object _ioTraceGate = new();
|
|
private static readonly object _statCacheGate = new();
|
|
private static readonly object _guestMountGate = new();
|
|
private static readonly Dictionary<ulong, DirectAllocation> _directAllocations = new();
|
|
private static readonly Dictionary<ulong, LibcHeapAllocation> _libcAllocations = new();
|
|
private static readonly Dictionary<ulong, MappedRegion> _mappedRegions = new();
|
|
private static readonly Dictionary<ulong, string> _mappedRegionNames = new();
|
|
private static readonly Dictionary<ulong, ulong> _tlsModuleBlocks = new();
|
|
private static readonly Dictionary<string, string> _guestMounts = new(StringComparer.OrdinalIgnoreCase);
|
|
private static readonly HashSet<string> _tracedStatResults = new(StringComparer.Ordinal);
|
|
private static readonly HashSet<string> _negativeStatCache = new(StringComparer.OrdinalIgnoreCase);
|
|
private static readonly ConcurrentDictionary<string, ulong> _aprFileSizeCache = new(StringComparer.OrdinalIgnoreCase);
|
|
private static long _nextFileDescriptor = 2;
|
|
private static ulong _nextPhysicalAddress;
|
|
private static ulong _nextVirtualAddress;
|
|
private static ulong _mainDirectMemoryPoolBase = UnsetMainDirectMemoryPoolBase;
|
|
private static ulong _allocatedFlexibleBytes;
|
|
private static ulong _threadAtexitCountCallback;
|
|
private static ulong _threadAtexitReportCallback;
|
|
private static ulong _threadDtorsCallback;
|
|
private static int _nullMemsetRecoveryCount;
|
|
private static int _nonCanonicalMemsetRecoveryCount;
|
|
private static int _inaccessibleMemsetRecoveryCount;
|
|
private static int _hostMemoryWriteFallbackCount;
|
|
private static int _hostMemoryReadFallbackCount;
|
|
private static int _nullWcscpyRecoveryCount;
|
|
private static string? _cachedApp0Root;
|
|
private static string? _cachedDownload0Root;
|
|
|
|
[StructLayout(LayoutKind.Sequential)]
|
|
private struct MemoryBasicInformation
|
|
{
|
|
public nint BaseAddress;
|
|
public nint AllocationBase;
|
|
public uint AllocationProtect;
|
|
public nuint RegionSize;
|
|
public uint State;
|
|
public uint Protect;
|
|
public uint Type;
|
|
}
|
|
|
|
[DllImport("kernel32.dll", SetLastError = true)]
|
|
private static extern nuint VirtualQuery(nint lpAddress, out MemoryBasicInformation lpBuffer, nuint dwLength);
|
|
|
|
[DllImport("kernel32.dll", SetLastError = true)]
|
|
[return: MarshalAs(UnmanagedType.Bool)]
|
|
private static extern bool VirtualProtect(nint lpAddress, nuint dwSize, uint flNewProtect, out uint lpflOldProtect);
|
|
|
|
private sealed class OpenDirectory
|
|
{
|
|
public required string Path { get; init; }
|
|
public required string[] Entries { get; init; }
|
|
public int NextIndex { get; set; }
|
|
}
|
|
|
|
private readonly record struct DirectAllocation(ulong Start, ulong Length, int MemoryType);
|
|
private readonly record struct LibcHeapAllocation(nint BaseAddress, nuint Size, nuint Alignment);
|
|
private readonly record struct MappedRegion(ulong Address, ulong Length, int Protection, bool IsFlexible, bool IsDirect, ulong DirectStart);
|
|
private readonly record struct BatchMapEntry(ulong Start, ulong Offset, ulong Length, byte Protection, byte Type, int Operation);
|
|
|
|
public static void RegisterGuestPathMount(string guestMountPoint, string hostRoot)
|
|
{
|
|
ArgumentException.ThrowIfNullOrWhiteSpace(guestMountPoint);
|
|
ArgumentException.ThrowIfNullOrWhiteSpace(hostRoot);
|
|
|
|
var normalizedMountPoint = NormalizeGuestStatCachePath(guestMountPoint);
|
|
if (normalizedMountPoint is null || normalizedMountPoint == "/")
|
|
{
|
|
throw new ArgumentException("Guest mount point must name a directory.", nameof(guestMountPoint));
|
|
}
|
|
|
|
var normalizedHostRoot = Path.GetFullPath(hostRoot);
|
|
Directory.CreateDirectory(normalizedHostRoot);
|
|
lock (_guestMountGate)
|
|
{
|
|
_guestMounts[normalizedMountPoint] = normalizedHostRoot;
|
|
}
|
|
|
|
lock (_statCacheGate)
|
|
{
|
|
_negativeStatCache.RemoveWhere(path =>
|
|
string.Equals(path, normalizedMountPoint, StringComparison.OrdinalIgnoreCase) ||
|
|
path.StartsWith(normalizedMountPoint + "/", StringComparison.OrdinalIgnoreCase));
|
|
}
|
|
}
|
|
|
|
internal static bool TryAllocateHleData(
|
|
CpuContext ctx,
|
|
ulong length,
|
|
ulong alignment,
|
|
out ulong address)
|
|
{
|
|
address = 0;
|
|
if (length == 0 || length > int.MaxValue)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var mappedLength = AlignUp(length, 0x1000UL);
|
|
var effectiveAlignment = Math.Max(alignment, 0x1000UL);
|
|
lock (_memoryGate)
|
|
{
|
|
var desiredAddress = AlignUp(
|
|
_nextVirtualAddress == 0 ? 0x1_0000_0000UL : _nextVirtualAddress,
|
|
effectiveAlignment);
|
|
if (!TryReserveGuestVirtualRange(ctx, desiredAddress, mappedLength, OrbisProtCpuReadWrite, effectiveAlignment, out address) ||
|
|
address == 0)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
_nextVirtualAddress = Math.Max(_nextVirtualAddress, address + mappedLength);
|
|
_mappedRegions[address] = new MappedRegion(
|
|
address,
|
|
mappedLength,
|
|
OrbisProtCpuReadWrite,
|
|
IsFlexible: false,
|
|
IsDirect: false,
|
|
DirectStart: 0);
|
|
}
|
|
|
|
var zeroes = new byte[(int)Math.Min(mappedLength, (ulong)MemsetChunkSize)];
|
|
for (ulong offset = 0; offset < mappedLength;)
|
|
{
|
|
var chunkLength = (int)Math.Min((ulong)zeroes.Length, mappedLength - offset);
|
|
if (!ctx.Memory.TryWrite(address + offset, zeroes.AsSpan(0, chunkLength)))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
offset += (ulong)chunkLength;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
internal static void RegisterReservedVirtualRange(ulong address, ulong length)
|
|
{
|
|
if (address == 0 || length == 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
lock (_memoryGate)
|
|
{
|
|
_mappedRegions[address] = new MappedRegion(
|
|
address,
|
|
length,
|
|
Protection: 0,
|
|
IsFlexible: false,
|
|
IsDirect: false,
|
|
DirectStart: 0);
|
|
}
|
|
}
|
|
|
|
[SysAbiExport(
|
|
Nid = "8zTFvBIAIN8",
|
|
ExportName = "memset",
|
|
Target = Generation.Gen4 | Generation.Gen5,
|
|
LibraryName = "libc")]
|
|
public static int Memset(CpuContext ctx)
|
|
{
|
|
var destination = ctx[CpuRegister.Rdi];
|
|
var value = (byte)(ctx[CpuRegister.Rsi] & 0xFF);
|
|
var length = ctx[CpuRegister.Rdx];
|
|
if (length == 0)
|
|
{
|
|
ctx[CpuRegister.Rax] = destination;
|
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
|
}
|
|
|
|
if (destination == 0)
|
|
{
|
|
if (length <= 0x20)
|
|
{
|
|
var recoveryIndex = Interlocked.Increment(ref _nullMemsetRecoveryCount);
|
|
if (recoveryIndex <= 8)
|
|
{
|
|
Console.Error.WriteLine(
|
|
$"[LOADER][WARNING] memset null-dst recovery#{recoveryIndex}: rip=0x{ctx.Rip:X16} len=0x{length:X} val=0x{value:X2}");
|
|
}
|
|
|
|
ctx[CpuRegister.Rax] = 0;
|
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
|
}
|
|
|
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
|
}
|
|
|
|
const ulong CanonicalUserUpper = 0x0000800000000000UL;
|
|
if (destination >= CanonicalUserUpper && length <= 0x40)
|
|
{
|
|
var recoveryIndex = Interlocked.Increment(ref _nonCanonicalMemsetRecoveryCount);
|
|
if (recoveryIndex <= 8)
|
|
{
|
|
Console.Error.WriteLine(
|
|
$"[LOADER][WARNING] memset non-canonical-dst recovery#{recoveryIndex}: rip=0x{ctx.Rip:X16} dst=0x{destination:X16} len=0x{length:X} val=0x{value:X2}");
|
|
}
|
|
|
|
ctx[CpuRegister.Rax] = destination;
|
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
|
}
|
|
|
|
const ulong MaxSane = 512UL * 1024 * 1024;
|
|
if (destination < 0x1000 || destination >= CanonicalUserUpper || length > MaxSane)
|
|
{
|
|
Console.WriteLine("!!! CRITICAL: Bad Memset Call !!!");
|
|
Console.WriteLine($"Called from RIP: 0x{ctx.Rip:X}");
|
|
Console.WriteLine($"dst=0x{destination:X} val=0x{value:X2} len=0x{length:X}");
|
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
|
}
|
|
|
|
var chunk = new byte[MemsetChunkSize];
|
|
Array.Fill(chunk, value);
|
|
var remaining = length;
|
|
var cursor = destination;
|
|
while (remaining > 0)
|
|
{
|
|
var take = (int)Math.Min((ulong)chunk.Length, remaining);
|
|
if (!TryWriteCompat(ctx, cursor, chunk.AsSpan(0, take)))
|
|
{
|
|
if (length <= 0x40)
|
|
{
|
|
var recoveryIndex = Interlocked.Increment(ref _inaccessibleMemsetRecoveryCount);
|
|
if (recoveryIndex <= 8)
|
|
{
|
|
Console.Error.WriteLine(
|
|
$"[LOADER][WARNING] memset inaccessible-dst recovery#{recoveryIndex}: rip=0x{ctx.Rip:X16} dst=0x{destination:X16} len=0x{length:X} val=0x{value:X2}");
|
|
}
|
|
|
|
ctx[CpuRegister.Rax] = destination;
|
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
|
}
|
|
|
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
|
}
|
|
|
|
cursor += (ulong)take;
|
|
remaining -= (ulong)take;
|
|
}
|
|
|
|
ctx[CpuRegister.Rax] = destination;
|
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
|
}
|
|
|
|
[SysAbiExport(
|
|
Nid = "j4ViWNHEgww",
|
|
ExportName = "strlen",
|
|
Target = Generation.Gen4 | Generation.Gen5,
|
|
LibraryName = "libc")]
|
|
public static int Strlen(CpuContext ctx)
|
|
{
|
|
if (!TryReadCString(ctx, ctx[CpuRegister.Rdi], 1_048_576, out var bytes))
|
|
{
|
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
|
}
|
|
|
|
ctx[CpuRegister.Rax] = unchecked((ulong)bytes.Length);
|
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
|
}
|
|
|
|
[SysAbiExport(
|
|
Nid = "5jNubw4vlAA",
|
|
ExportName = "strnlen",
|
|
Target = Generation.Gen4 | Generation.Gen5,
|
|
LibraryName = "libc")]
|
|
public static int Strnlen(CpuContext ctx)
|
|
{
|
|
var maxLength = ctx[CpuRegister.Rsi];
|
|
if (!TryReadCString(ctx, ctx[CpuRegister.Rdi], maxLength, out var bytes))
|
|
{
|
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
|
}
|
|
|
|
ctx[CpuRegister.Rax] = unchecked((ulong)bytes.Length);
|
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
|
}
|
|
|
|
[SysAbiExport(
|
|
Nid = "LHMrG7e8G78",
|
|
ExportName = "wcslen",
|
|
Target = Generation.Gen4 | Generation.Gen5,
|
|
LibraryName = "libc")]
|
|
public static int Wcslen(CpuContext ctx)
|
|
{
|
|
return WcslenCore(ctx, ctx[CpuRegister.Rdi]);
|
|
}
|
|
|
|
[SysAbiExport(
|
|
Nid = "WkkeywLJcgU",
|
|
ExportName = "wcslen",
|
|
Target = Generation.Gen4 | Generation.Gen5,
|
|
LibraryName = "libc")]
|
|
public static int WcslenWkkey(CpuContext ctx)
|
|
{
|
|
return WcslenCore(ctx, ctx[CpuRegister.Rdi]);
|
|
}
|
|
|
|
private static int WcslenCore(CpuContext ctx, ulong address)
|
|
{
|
|
if (string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_WIDE"), "1", StringComparison.Ordinal))
|
|
{
|
|
Span<byte> probe = stackalloc byte[32];
|
|
if (TryReadCompat(ctx, address, probe))
|
|
{
|
|
Console.Error.WriteLine(
|
|
$"[LOADER][TRACE] wcslen probe @0x{address:X16}: {Convert.ToHexString(probe).ToLowerInvariant()}");
|
|
}
|
|
else
|
|
{
|
|
Console.Error.WriteLine($"[LOADER][TRACE] wcslen probe @0x{address:X16}: <unreadable>");
|
|
}
|
|
}
|
|
|
|
if (!TryReadWideCString(ctx, address, 1_048_576, out var units))
|
|
{
|
|
Console.Error.WriteLine($"[LOADER][WARN] wcslen: unreadable string at 0x{address:X16}");
|
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
|
}
|
|
|
|
ctx[CpuRegister.Rax] = unchecked((ulong)units.Length);
|
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
|
}
|
|
|
|
[SysAbiExport(
|
|
Nid = "Ovb2dSJOAuE",
|
|
ExportName = "strcmp",
|
|
Target = Generation.Gen4 | Generation.Gen5,
|
|
LibraryName = "libc")]
|
|
public static int Strcmp(CpuContext ctx)
|
|
{
|
|
var left = ctx[CpuRegister.Rdi];
|
|
var right = ctx[CpuRegister.Rsi];
|
|
if (!TryCompareStrings(ctx, left, right, limit: ulong.MaxValue, out var compare))
|
|
{
|
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
|
}
|
|
|
|
ctx[CpuRegister.Rax] = unchecked((ulong)compare);
|
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
|
}
|
|
|
|
[SysAbiExport(
|
|
Nid = "fV2xHER+bKE",
|
|
ExportName = "wcscoll",
|
|
Target = Generation.Gen4 | Generation.Gen5,
|
|
LibraryName = "libc")]
|
|
public static int Wcscoll(CpuContext ctx)
|
|
{
|
|
return WcscollCore(ctx, ctx[CpuRegister.Rdi], ctx[CpuRegister.Rsi]);
|
|
}
|
|
|
|
[SysAbiExport(
|
|
Nid = "pNtJdE3x49E",
|
|
ExportName = "wcscmp",
|
|
Target = Generation.Gen4 | Generation.Gen5,
|
|
LibraryName = "libc")]
|
|
public static int Wcscmp(CpuContext ctx)
|
|
{
|
|
return WcscmpCore(ctx, ctx[CpuRegister.Rdi], ctx[CpuRegister.Rsi]);
|
|
}
|
|
|
|
private static int WcscollCore(CpuContext ctx, ulong left, ulong right)
|
|
{
|
|
return WcscmpCore(ctx, left, right);
|
|
}
|
|
|
|
private static int WcscmpCore(CpuContext ctx, ulong left, ulong right)
|
|
{
|
|
if (!TryCompareWideStrings(ctx, left, right, limit: ulong.MaxValue, out var compare))
|
|
{
|
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
|
}
|
|
|
|
ctx[CpuRegister.Rax] = unchecked((ulong)compare);
|
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
|
}
|
|
|
|
[SysAbiExport(
|
|
Nid = "FM5NPnLqBc8",
|
|
ExportName = "wcscpy",
|
|
Target = Generation.Gen4 | Generation.Gen5,
|
|
LibraryName = "libc")]
|
|
public static int WcscpyFm5(CpuContext ctx)
|
|
{
|
|
return WcscpyCore(ctx, ctx[CpuRegister.Rdi], ctx[CpuRegister.Rsi]);
|
|
}
|
|
|
|
private static int WcscpyCore(CpuContext ctx, ulong destination, ulong source)
|
|
{
|
|
if (source == 0)
|
|
{
|
|
var recoveryIndex = Interlocked.Increment(ref _nullWcscpyRecoveryCount);
|
|
if (recoveryIndex <= 8)
|
|
{
|
|
Console.Error.WriteLine(
|
|
$"[LOADER][WARNING] wcscpy null-src recovery#{recoveryIndex}: rip=0x{ctx.Rip:X16} dst=0x{destination:X16}");
|
|
}
|
|
|
|
if (!TryWriteWideTerminator(ctx, destination))
|
|
{
|
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
|
}
|
|
|
|
ctx[CpuRegister.Rax] = destination;
|
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
|
}
|
|
|
|
if (!TryReadWideCString(ctx, source, 1_048_576, out var units))
|
|
{
|
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
|
}
|
|
|
|
if (!TryWriteCompat(ctx, destination, EncodeWideUnitsWithTerminator(units)))
|
|
{
|
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
|
}
|
|
|
|
ctx[CpuRegister.Rax] = destination;
|
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
|
}
|
|
|
|
[SysAbiExport(
|
|
Nid = "aesyjrHVWy4",
|
|
ExportName = "strncmp",
|
|
Target = Generation.Gen4 | Generation.Gen5,
|
|
LibraryName = "libc")]
|
|
public static int Strncmp(CpuContext ctx)
|
|
{
|
|
var left = ctx[CpuRegister.Rdi];
|
|
var right = ctx[CpuRegister.Rsi];
|
|
var limit = ctx[CpuRegister.Rdx];
|
|
if (!TryCompareStrings(ctx, left, right, limit, out var compare))
|
|
{
|
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
|
}
|
|
|
|
ctx[CpuRegister.Rax] = unchecked((ulong)compare);
|
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
|
}
|
|
|
|
[SysAbiExport(
|
|
Nid = "0nV21JjYCH8",
|
|
ExportName = "wcsncpy",
|
|
Target = Generation.Gen4 | Generation.Gen5,
|
|
LibraryName = "libc")]
|
|
public static int Wcsncpy(CpuContext ctx)
|
|
{
|
|
return WcsncpyCore(ctx, ctx[CpuRegister.Rdi], ctx[CpuRegister.Rsi], ctx[CpuRegister.Rdx]);
|
|
}
|
|
|
|
[SysAbiExport(
|
|
Nid = "E8wCoUEbfzk",
|
|
ExportName = "wcsncmp",
|
|
Target = Generation.Gen4 | Generation.Gen5,
|
|
LibraryName = "libc")]
|
|
public static int Wcsncmp(CpuContext ctx)
|
|
{
|
|
return WcsncmpCore(ctx, ctx[CpuRegister.Rdi], ctx[CpuRegister.Rsi], ctx[CpuRegister.Rdx]);
|
|
}
|
|
|
|
private static int WcsncmpCore(CpuContext ctx, ulong left, ulong right, ulong limit)
|
|
{
|
|
if (!TryCompareWideStrings(ctx, left, right, limit, out var compare))
|
|
{
|
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
|
}
|
|
|
|
ctx[CpuRegister.Rax] = unchecked((ulong)compare);
|
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
|
}
|
|
|
|
[SysAbiExport(
|
|
Nid = "eLdDw6l0-bU",
|
|
ExportName = "snprintf",
|
|
Target = Generation.Gen4 | Generation.Gen5,
|
|
LibraryName = "libc")]
|
|
public static int Snprintf(CpuContext ctx)
|
|
{
|
|
return SnprintfCore(ctx);
|
|
}
|
|
|
|
[SysAbiExport(
|
|
Nid = "Q2V+iqvjgC0",
|
|
ExportName = "vsnprintf",
|
|
Target = Generation.Gen4 | Generation.Gen5,
|
|
LibraryName = "libc")]
|
|
public static int Vsnprintf(CpuContext ctx)
|
|
{
|
|
return VsnprintfCore(ctx);
|
|
}
|
|
|
|
[SysAbiExport(
|
|
Nid = "nJz16JE1txM",
|
|
ExportName = "swprintf",
|
|
Target = Generation.Gen4 | Generation.Gen5,
|
|
LibraryName = "libc")]
|
|
public static int Swprintf(CpuContext ctx)
|
|
{
|
|
return SwprintfCore(ctx);
|
|
}
|
|
|
|
[SysAbiExport(
|
|
Nid = "u0XOsuOmOzc",
|
|
ExportName = "vswprintf",
|
|
Target = Generation.Gen4 | Generation.Gen5,
|
|
LibraryName = "libc")]
|
|
public static int Vswprintf(CpuContext ctx)
|
|
{
|
|
return VswprintfCore(ctx);
|
|
}
|
|
|
|
[SysAbiExport(
|
|
Nid = "Im55VJ-Bekc",
|
|
ExportName = "swprintf_s",
|
|
Target = Generation.Gen4 | Generation.Gen5,
|
|
LibraryName = "libc")]
|
|
public static int SwprintfS(CpuContext ctx)
|
|
{
|
|
return SwprintfCore(ctx);
|
|
}
|
|
|
|
[SysAbiExport(
|
|
Nid = "oDoV9tyHTbA",
|
|
ExportName = "vswprintf_s",
|
|
Target = Generation.Gen4 | Generation.Gen5,
|
|
LibraryName = "libc")]
|
|
public static int VswprintfS(CpuContext ctx)
|
|
{
|
|
return VswprintfCore(ctx);
|
|
}
|
|
|
|
[SysAbiExport(
|
|
Nid = "GMpvxPFW924",
|
|
ExportName = "vprintf",
|
|
Target = Generation.Gen4 | Generation.Gen5,
|
|
LibraryName = "libc")]
|
|
public static int Vprintf(CpuContext ctx)
|
|
{
|
|
var formatAddress = ctx[CpuRegister.Rdi];
|
|
var vaListAddress = ctx[CpuRegister.Rsi];
|
|
if (!TryReadCString(ctx, formatAddress, 1_048_576, out var formatBytes))
|
|
{
|
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
|
}
|
|
|
|
var format = Encoding.UTF8.GetString(formatBytes);
|
|
string rendered;
|
|
if (!TryCreateVaListCursor(ctx, vaListAddress, out var vaCursor))
|
|
{
|
|
rendered = format;
|
|
}
|
|
else
|
|
{
|
|
rendered = FormatString(ctx, format, ref vaCursor);
|
|
}
|
|
|
|
Console.Write(rendered);
|
|
ctx[CpuRegister.Rax] = unchecked((ulong)Encoding.UTF8.GetByteCount(rendered));
|
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
|
}
|
|
|
|
[SysAbiExport(
|
|
Nid = "kiZSXIWd9vg",
|
|
ExportName = "strcpy",
|
|
Target = Generation.Gen4 | Generation.Gen5,
|
|
LibraryName = "libc")]
|
|
public static int Strcpy(CpuContext ctx)
|
|
{
|
|
var destination = ctx[CpuRegister.Rdi];
|
|
var source = ctx[CpuRegister.Rsi];
|
|
if (!TryReadCString(ctx, source, 1_048_576, out var bytes))
|
|
{
|
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
|
}
|
|
|
|
var payload = new byte[bytes.Length + 1];
|
|
bytes.CopyTo(payload.AsSpan());
|
|
if (!TryWriteCompat(ctx, destination, payload))
|
|
{
|
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
|
}
|
|
|
|
ctx[CpuRegister.Rax] = destination;
|
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
|
}
|
|
|
|
[SysAbiExport(
|
|
Nid = "6f5f-qx4ucA",
|
|
ExportName = "wcscpy_s",
|
|
Target = Generation.Gen4 | Generation.Gen5,
|
|
LibraryName = "libc")]
|
|
public static int WcscpyS(CpuContext ctx)
|
|
{
|
|
return WcscpySCore(ctx, ctx[CpuRegister.Rdi], ctx[CpuRegister.Rsi], ctx[CpuRegister.Rdx]);
|
|
}
|
|
|
|
[SysAbiExport(
|
|
Nid = "6sJWiWSRuqk",
|
|
ExportName = "strncpy",
|
|
Target = Generation.Gen4 | Generation.Gen5,
|
|
LibraryName = "libc")]
|
|
public static int Strncpy(CpuContext ctx)
|
|
{
|
|
var destination = ctx[CpuRegister.Rdi];
|
|
var source = ctx[CpuRegister.Rsi];
|
|
var count = (int)Math.Min(ctx[CpuRegister.Rdx], int.MaxValue);
|
|
if (count < 0)
|
|
{
|
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
|
}
|
|
|
|
var payload = new byte[count];
|
|
Span<byte> one = stackalloc byte[1];
|
|
var copied = 0;
|
|
while (copied < count)
|
|
{
|
|
if (!TryReadCompat(ctx, source + (ulong)copied, one))
|
|
{
|
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
|
}
|
|
|
|
payload[copied] = one[0];
|
|
copied++;
|
|
if (one[0] == 0)
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!TryWriteCompat(ctx, destination, payload))
|
|
{
|
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
|
}
|
|
|
|
ctx[CpuRegister.Rax] = destination;
|
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
|
}
|
|
|
|
[SysAbiExport(
|
|
Nid = "Slmz4HMpNGs",
|
|
ExportName = "wcsncpy_s",
|
|
Target = Generation.Gen4 | Generation.Gen5,
|
|
LibraryName = "libc")]
|
|
public static int WcsncpyS(CpuContext ctx)
|
|
{
|
|
return WcsncpySCore(ctx, ctx[CpuRegister.Rdi], ctx[CpuRegister.Rsi], ctx[CpuRegister.Rdx], ctx[CpuRegister.Rcx]);
|
|
}
|
|
|
|
private static int WcsncpyCore(CpuContext ctx, ulong destination, ulong source, ulong countValue)
|
|
{
|
|
var count = (int)Math.Min(countValue, int.MaxValue);
|
|
if (count < 0 || count > (int.MaxValue / WideCharSize))
|
|
{
|
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
|
}
|
|
|
|
var payload = new byte[count * WideCharSize];
|
|
for (var copied = 0; copied < count; copied++)
|
|
{
|
|
if (!TryReadUInt16Compat(ctx, source + ((ulong)copied * WideCharSize), out var unit))
|
|
{
|
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
|
}
|
|
|
|
BinaryPrimitives.WriteUInt16LittleEndian(
|
|
payload.AsSpan(copied * WideCharSize, WideCharSize),
|
|
unit);
|
|
|
|
if (unit == 0)
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!TryWriteCompat(ctx, destination, payload))
|
|
{
|
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
|
}
|
|
|
|
ctx[CpuRegister.Rax] = destination;
|
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
|
}
|
|
|
|
[SysAbiExport(
|
|
Nid = "Ezzq78ZgHPs",
|
|
ExportName = "wcschr",
|
|
Target = Generation.Gen4 | Generation.Gen5,
|
|
LibraryName = "libc")]
|
|
public static int Wcschr(CpuContext ctx)
|
|
{
|
|
return WcschrCore(ctx, ctx[CpuRegister.Rdi], unchecked((ushort)ctx[CpuRegister.Rsi]));
|
|
}
|
|
|
|
private static int WcschrCore(CpuContext ctx, ulong address, ushort needle)
|
|
{
|
|
for (ulong index = 0; index < 1_048_576; index++)
|
|
{
|
|
if (!TryReadUInt16Compat(ctx, address + (index * WideCharSize), out var unit))
|
|
{
|
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
|
}
|
|
|
|
if (unit == needle)
|
|
{
|
|
ctx[CpuRegister.Rax] = address + (index * WideCharSize);
|
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
|
}
|
|
|
|
if (unit == 0)
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
|
|
ctx[CpuRegister.Rax] = 0;
|
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
|
}
|
|
|
|
private static int WcscpySCore(CpuContext ctx, ulong destination, ulong destinationCount, ulong source)
|
|
{
|
|
if (destination == 0 || destinationCount == 0)
|
|
{
|
|
ctx[CpuRegister.Rax] = Einval;
|
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
|
}
|
|
|
|
if (source == 0)
|
|
{
|
|
if (!TryZeroWideDestination(ctx, destination, destinationCount))
|
|
{
|
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
|
}
|
|
|
|
ctx[CpuRegister.Rax] = Einval;
|
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
|
}
|
|
|
|
var probeLimit = Math.Min(destinationCount, 1_048_576UL);
|
|
if (!TryReadWideCStringBounded(ctx, source, probeLimit, out var units, out var terminated))
|
|
{
|
|
_ = TryZeroWideDestination(ctx, destination, destinationCount);
|
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
|
}
|
|
|
|
if (!terminated || (ulong)units.Length + 1 > destinationCount)
|
|
{
|
|
if (!TryZeroWideDestination(ctx, destination, destinationCount))
|
|
{
|
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
|
}
|
|
|
|
ctx[CpuRegister.Rax] = Erange;
|
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
|
}
|
|
|
|
if (!TryWriteCompat(ctx, destination, EncodeWideUnitsWithTerminator(units)))
|
|
{
|
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
|
}
|
|
|
|
ctx[CpuRegister.Rax] = 0;
|
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
|
}
|
|
|
|
private static int WcsncpySCore(CpuContext ctx, ulong destination, ulong destinationCount, ulong source, ulong count)
|
|
{
|
|
if (destination == 0 || destinationCount == 0)
|
|
{
|
|
ctx[CpuRegister.Rax] = Einval;
|
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
|
}
|
|
|
|
if (source == 0)
|
|
{
|
|
if (!TryZeroWideDestination(ctx, destination, destinationCount))
|
|
{
|
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
|
}
|
|
|
|
ctx[CpuRegister.Rax] = Einval;
|
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
|
}
|
|
|
|
if (count == 0)
|
|
{
|
|
if (!TryWriteWideTerminator(ctx, destination))
|
|
{
|
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
|
}
|
|
|
|
ctx[CpuRegister.Rax] = 0;
|
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
|
}
|
|
|
|
if (count == ulong.MaxValue)
|
|
{
|
|
var copyLimit = Math.Min(destinationCount - 1, 1_048_576UL);
|
|
if (!TryReadWideCStringBounded(ctx, source, copyLimit, out var truncatedUnits, out var terminated))
|
|
{
|
|
_ = TryZeroWideDestination(ctx, destination, destinationCount);
|
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
|
}
|
|
|
|
if (!TryWriteCompat(ctx, destination, EncodeWideUnitsWithTerminator(truncatedUnits)))
|
|
{
|
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
|
}
|
|
|
|
ctx[CpuRegister.Rax] = terminated ? 0UL : Struncate;
|
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
|
}
|
|
|
|
var boundedCount = Math.Min(count, 1_048_576UL);
|
|
if (!TryReadWideCStringBounded(ctx, source, boundedCount, out var units, out var sourceTerminated))
|
|
{
|
|
_ = TryZeroWideDestination(ctx, destination, destinationCount);
|
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
|
}
|
|
|
|
var requiredUnits = sourceTerminated ? (ulong)units.Length + 1 : boundedCount + 1;
|
|
if (requiredUnits > destinationCount)
|
|
{
|
|
if (!TryZeroWideDestination(ctx, destination, destinationCount))
|
|
{
|
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
|
}
|
|
|
|
ctx[CpuRegister.Rax] = Erange;
|
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
|
}
|
|
|
|
if (!TryWriteCompat(ctx, destination, EncodeWideUnitsWithTerminator(units)))
|
|
{
|
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
|
}
|
|
|
|
ctx[CpuRegister.Rax] = 0;
|
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
|
}
|
|
|
|
[SysAbiExport(
|
|
Nid = "Q3VBxCXhUHs",
|
|
ExportName = "memcpy",
|
|
Target = Generation.Gen4 | Generation.Gen5,
|
|
LibraryName = "libc")]
|
|
public static int Memcpy(CpuContext ctx)
|
|
{
|
|
var destination = ctx[CpuRegister.Rdi];
|
|
var source = ctx[CpuRegister.Rsi];
|
|
var count = (int)Math.Min(ctx[CpuRegister.Rdx], int.MaxValue);
|
|
if (count < 0)
|
|
{
|
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
|
}
|
|
|
|
var payload = GC.AllocateUninitializedArray<byte>(count);
|
|
if (count > 0 && (!TryReadCompat(ctx, source, payload) || !TryWriteCompat(ctx, destination, payload)))
|
|
{
|
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
|
}
|
|
|
|
ctx[CpuRegister.Rax] = destination;
|
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
|
}
|
|
|
|
[SysAbiExport(
|
|
Nid = "+P6FRGH4LfA",
|
|
ExportName = "memmove",
|
|
Target = Generation.Gen4 | Generation.Gen5,
|
|
LibraryName = "libc")]
|
|
public static int Memmove(CpuContext ctx)
|
|
{
|
|
return Memcpy(ctx);
|
|
}
|
|
|
|
[SysAbiExport(
|
|
Nid = "gQX+4GDQjpM",
|
|
ExportName = "malloc",
|
|
Target = Generation.Gen4 | Generation.Gen5,
|
|
LibraryName = "libc")]
|
|
public static int Malloc(CpuContext ctx)
|
|
{
|
|
ctx[CpuRegister.Rax] =
|
|
TryAllocateLibcHeap(ctx[CpuRegister.Rdi], DefaultLibcHeapAlignment, zeroFill: false, out var address)
|
|
? address
|
|
: 0;
|
|
TraceLibcAllocation(
|
|
ctx,
|
|
"malloc",
|
|
size: ctx[CpuRegister.Rdi],
|
|
alignment: DefaultLibcHeapAlignment,
|
|
resultAddress: ctx[CpuRegister.Rax]);
|
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
|
}
|
|
|
|
[SysAbiExport(
|
|
Nid = "tIhsqj0qsFE",
|
|
ExportName = "free",
|
|
Target = Generation.Gen4 | Generation.Gen5,
|
|
LibraryName = "libc")]
|
|
public static int Free(CpuContext ctx)
|
|
{
|
|
FreeLibcHeap(ctx[CpuRegister.Rdi]);
|
|
ctx[CpuRegister.Rax] = 0;
|
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
|
}
|
|
|
|
[SysAbiExport(
|
|
Nid = "2X5agFjKxMc",
|
|
ExportName = "calloc",
|
|
Target = Generation.Gen4 | Generation.Gen5,
|
|
LibraryName = "libc")]
|
|
public static int Calloc(CpuContext ctx)
|
|
{
|
|
ctx[CpuRegister.Rax] =
|
|
TryMultiplyAllocationSize(ctx[CpuRegister.Rdi], ctx[CpuRegister.Rsi], out var totalSize) &&
|
|
TryAllocateLibcHeapCore(totalSize, DefaultLibcHeapAlignment, zeroFill: true, out var address)
|
|
? address
|
|
: 0;
|
|
TraceLibcAllocation(
|
|
ctx,
|
|
"calloc",
|
|
size: ctx[CpuRegister.Rdi],
|
|
count: ctx[CpuRegister.Rsi],
|
|
alignment: DefaultLibcHeapAlignment,
|
|
resultAddress: ctx[CpuRegister.Rax]);
|
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
|
}
|
|
|
|
[SysAbiExport(
|
|
Nid = "Y7aJ1uydPMo",
|
|
ExportName = "realloc",
|
|
Target = Generation.Gen4 | Generation.Gen5,
|
|
LibraryName = "libc")]
|
|
public static int Realloc(CpuContext ctx)
|
|
{
|
|
var existingAddress = ctx[CpuRegister.Rdi];
|
|
var requestedSize = ctx[CpuRegister.Rsi];
|
|
|
|
if (existingAddress == 0)
|
|
{
|
|
ctx[CpuRegister.Rax] =
|
|
TryAllocateLibcHeap(requestedSize, DefaultLibcHeapAlignment, zeroFill: false, out var freshAddress)
|
|
? freshAddress
|
|
: 0;
|
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
|
}
|
|
|
|
if (requestedSize == 0)
|
|
{
|
|
FreeLibcHeap(existingAddress);
|
|
ctx[CpuRegister.Rax] = 0;
|
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
|
}
|
|
|
|
ctx[CpuRegister.Rax] =
|
|
TryReallocateLibcHeap(existingAddress, requestedSize, out var resizedAddress)
|
|
? resizedAddress
|
|
: 0;
|
|
TraceLibcAllocation(
|
|
ctx,
|
|
"realloc",
|
|
size: requestedSize,
|
|
existingAddress: existingAddress,
|
|
resultAddress: ctx[CpuRegister.Rax]);
|
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
|
}
|
|
|
|
[SysAbiExport(
|
|
Nid = "Ujf3KzMvRmI",
|
|
ExportName = "memalign",
|
|
Target = Generation.Gen4 | Generation.Gen5,
|
|
LibraryName = "libc")]
|
|
public static int Memalign(CpuContext ctx)
|
|
{
|
|
ctx[CpuRegister.Rax] =
|
|
TryAllocateAlignedLibcHeap(
|
|
alignmentValue: ctx[CpuRegister.Rdi],
|
|
requestedSize: ctx[CpuRegister.Rsi],
|
|
requireSizeMultiple: false,
|
|
out var address)
|
|
? address
|
|
: 0;
|
|
TraceLibcAllocation(
|
|
ctx,
|
|
"memalign",
|
|
size: ctx[CpuRegister.Rsi],
|
|
alignment: ctx[CpuRegister.Rdi],
|
|
resultAddress: ctx[CpuRegister.Rax]);
|
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
|
}
|
|
|
|
[SysAbiExport(
|
|
Nid = "2Btkg8k24Zg",
|
|
ExportName = "aligned_alloc",
|
|
Target = Generation.Gen4 | Generation.Gen5,
|
|
LibraryName = "libc")]
|
|
public static int AlignedAlloc(CpuContext ctx)
|
|
{
|
|
ctx[CpuRegister.Rax] =
|
|
TryAllocateAlignedLibcHeap(
|
|
alignmentValue: ctx[CpuRegister.Rdi],
|
|
requestedSize: ctx[CpuRegister.Rsi],
|
|
requireSizeMultiple: true,
|
|
out var address)
|
|
? address
|
|
: 0;
|
|
TraceLibcAllocation(
|
|
ctx,
|
|
"aligned_alloc",
|
|
size: ctx[CpuRegister.Rsi],
|
|
alignment: ctx[CpuRegister.Rdi],
|
|
resultAddress: ctx[CpuRegister.Rax]);
|
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
|
}
|
|
|
|
[SysAbiExport(
|
|
Nid = "cVSk9y8URbc",
|
|
ExportName = "posix_memalign",
|
|
Target = Generation.Gen4 | Generation.Gen5,
|
|
LibraryName = "libc")]
|
|
public static int PosixMemalign(CpuContext ctx)
|
|
{
|
|
var outPointerAddress = ctx[CpuRegister.Rdi];
|
|
if (outPointerAddress == 0)
|
|
{
|
|
ctx[CpuRegister.Rax] = Einval;
|
|
TraceLibcAllocation(
|
|
ctx,
|
|
"posix_memalign",
|
|
size: ctx[CpuRegister.Rdx],
|
|
alignment: ctx[CpuRegister.Rsi],
|
|
existingAddress: outPointerAddress,
|
|
resultAddress: 0,
|
|
errorCode: Einval);
|
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
|
}
|
|
|
|
if (!TryValidateAlignedAllocation(
|
|
ctx[CpuRegister.Rsi],
|
|
ctx[CpuRegister.Rdx],
|
|
requireSizeMultiple: false,
|
|
requirePointerSizedAlignment: true,
|
|
out var alignment,
|
|
out var requestedSize))
|
|
{
|
|
_ = TryWriteUInt64Compat(ctx, outPointerAddress, 0);
|
|
ctx[CpuRegister.Rax] = Einval;
|
|
TraceLibcAllocation(
|
|
ctx,
|
|
"posix_memalign",
|
|
size: ctx[CpuRegister.Rdx],
|
|
alignment: ctx[CpuRegister.Rsi],
|
|
existingAddress: outPointerAddress,
|
|
resultAddress: 0,
|
|
errorCode: Einval);
|
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
|
}
|
|
|
|
if (!TryAllocateLibcHeapCore(requestedSize, alignment, zeroFill: false, out var address))
|
|
{
|
|
_ = TryWriteUInt64Compat(ctx, outPointerAddress, 0);
|
|
ctx[CpuRegister.Rax] = Enomem;
|
|
TraceLibcAllocation(
|
|
ctx,
|
|
"posix_memalign",
|
|
size: requestedSize,
|
|
alignment: alignment,
|
|
existingAddress: outPointerAddress,
|
|
resultAddress: 0,
|
|
errorCode: Enomem);
|
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
|
}
|
|
|
|
if (!TryWriteUInt64Compat(ctx, outPointerAddress, address))
|
|
{
|
|
FreeLibcHeap(address);
|
|
ctx[CpuRegister.Rax] = Einval;
|
|
TraceLibcAllocation(
|
|
ctx,
|
|
"posix_memalign",
|
|
size: requestedSize,
|
|
alignment: alignment,
|
|
existingAddress: outPointerAddress,
|
|
resultAddress: 0,
|
|
errorCode: Einval);
|
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
|
}
|
|
|
|
ctx[CpuRegister.Rax] = 0;
|
|
TraceLibcAllocation(
|
|
ctx,
|
|
"posix_memalign",
|
|
size: requestedSize,
|
|
alignment: alignment,
|
|
existingAddress: outPointerAddress,
|
|
resultAddress: address);
|
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
|
}
|
|
|
|
[SysAbiExport(
|
|
Nid = "DfivPArhucg",
|
|
ExportName = "memcmp",
|
|
Target = Generation.Gen4 | Generation.Gen5,
|
|
LibraryName = "libc")]
|
|
public static int Memcmp(CpuContext ctx)
|
|
{
|
|
var left = ctx[CpuRegister.Rdi];
|
|
var right = ctx[CpuRegister.Rsi];
|
|
var count = (int)Math.Min(ctx[CpuRegister.Rdx], int.MaxValue);
|
|
if (count < 0)
|
|
{
|
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
|
}
|
|
|
|
Span<byte> leftByte = stackalloc byte[1];
|
|
Span<byte> rightByte = stackalloc byte[1];
|
|
for (var i = 0; i < count; i++)
|
|
{
|
|
if (!TryReadCompat(ctx, left + (ulong)i, leftByte) ||
|
|
!TryReadCompat(ctx, right + (ulong)i, rightByte))
|
|
{
|
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
|
}
|
|
|
|
var diff = leftByte[0] - rightByte[0];
|
|
if (diff != 0)
|
|
{
|
|
ctx[CpuRegister.Rax] = unchecked((ulong)diff);
|
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
|
}
|
|
}
|
|
|
|
ctx[CpuRegister.Rax] = 0;
|
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
|
}
|
|
|
|
[SysAbiExport(
|
|
Nid = "QrZZdJ8XsX0",
|
|
ExportName = "fputs",
|
|
Target = Generation.Gen4 | Generation.Gen5,
|
|
LibraryName = "libc")]
|
|
public static int Fputs(CpuContext ctx)
|
|
{
|
|
var textAddress = ctx[CpuRegister.Rdi];
|
|
var stream = ctx[CpuRegister.Rsi];
|
|
if (textAddress == 0)
|
|
{
|
|
ctx[CpuRegister.Rax] = unchecked((ulong)(-1L));
|
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
|
}
|
|
|
|
if (!TryReadNullTerminatedUtf8(ctx, textAddress, MaxGuestStringLength, out var text))
|
|
{
|
|
ctx[CpuRegister.Rax] = unchecked((ulong)(-1L));
|
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
|
}
|
|
|
|
if (stream == 0)
|
|
{
|
|
Console.Error.Write(text);
|
|
Console.Error.Flush();
|
|
}
|
|
else
|
|
{
|
|
Console.Out.Write(text);
|
|
Console.Out.Flush();
|
|
}
|
|
|
|
ctx[CpuRegister.Rax] = unchecked((ulong)text.Length);
|
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
|
}
|
|
|
|
[SysAbiExport(
|
|
Nid = "6c3rCVE-fTU",
|
|
ExportName = "_open",
|
|
Target = Generation.Gen4 | Generation.Gen5,
|
|
LibraryName = "libKernel")]
|
|
public static int KernelOpenUnderscore(CpuContext ctx)
|
|
{
|
|
var pathAddress = ctx[CpuRegister.Rdi];
|
|
var flags = unchecked((int)ctx[CpuRegister.Rsi]);
|
|
if (!TryReadNullTerminatedUtf8(ctx, pathAddress, MaxGuestStringLength, out var guestPath))
|
|
{
|
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
|
}
|
|
|
|
var hostPath = ResolveGuestPath(guestPath);
|
|
var access = ResolveOpenAccess(flags);
|
|
var mode = ResolveOpenMode(flags, access);
|
|
try
|
|
{
|
|
if (IsMutatingOpen(flags) && IsReadOnlyGuestMutationPath(guestPath))
|
|
{
|
|
LogOpenTrace($"_open readonly path='{guestPath}' host='{hostPath}' flags=0x{flags:X8}");
|
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_PERMISSION_DENIED;
|
|
}
|
|
|
|
var wantsDirectory = (flags & O_DIRECTORY) != 0;
|
|
if (wantsDirectory || Directory.Exists(hostPath))
|
|
{
|
|
if (!Directory.Exists(hostPath))
|
|
{
|
|
LogOpenTrace($"_open miss path='{guestPath}' host='{hostPath}' flags=0x{flags:X8} directory=1");
|
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
|
}
|
|
|
|
if (access != FileAccess.Read || (flags & (O_CREAT | O_TRUNC | O_APPEND)) != 0)
|
|
{
|
|
LogOpenTrace($"_open invalid-dir path='{guestPath}' host='{hostPath}' flags=0x{flags:X8}");
|
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
|
}
|
|
|
|
var directoryFd = (int)Interlocked.Increment(ref _nextFileDescriptor);
|
|
lock (_fdGate)
|
|
{
|
|
_openDirectories[directoryFd] = new OpenDirectory
|
|
{
|
|
Path = hostPath,
|
|
Entries = EnumerateDirectoryEntries(hostPath),
|
|
NextIndex = 0
|
|
};
|
|
}
|
|
|
|
LogOpenTrace($"_open dir path='{guestPath}' host='{hostPath}' flags=0x{flags:X8} fd={directoryFd}");
|
|
ctx[CpuRegister.Rax] = unchecked((ulong)directoryFd);
|
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
|
}
|
|
|
|
EnsureOpenParentDirectoryExists(guestPath, hostPath, flags);
|
|
var stream = new FileStream(hostPath, mode, access, FileShare.ReadWrite);
|
|
if ((flags & O_APPEND) != 0)
|
|
{
|
|
stream.Seek(0, SeekOrigin.End);
|
|
}
|
|
|
|
var fd = (int)Interlocked.Increment(ref _nextFileDescriptor);
|
|
lock (_fdGate)
|
|
{
|
|
_openFiles[fd] = stream;
|
|
}
|
|
|
|
if (IsMutatingOpen(flags))
|
|
{
|
|
InvalidateNegativeStatCacheForPathAndAncestors(guestPath);
|
|
InvalidateAprFileSizeCache(hostPath);
|
|
}
|
|
|
|
LogOpenTrace($"_open file path='{guestPath}' host='{hostPath}' flags=0x{flags:X8} fd={fd}");
|
|
ctx[CpuRegister.Rax] = unchecked((ulong)fd);
|
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
|
}
|
|
catch (Exception ex) when (ex is IOException or UnauthorizedAccessException)
|
|
{
|
|
LogOpenTrace($"_open fail path='{guestPath}' host='{hostPath}' flags=0x{flags:X8} ex={ex.GetType().Name}: {ex.Message}");
|
|
return ex is UnauthorizedAccessException
|
|
? (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT
|
|
: (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
|
}
|
|
}
|
|
|
|
[SysAbiExport(
|
|
Nid = "NNtFaKJbPt0",
|
|
ExportName = "_close",
|
|
Target = Generation.Gen4 | Generation.Gen5,
|
|
LibraryName = "libKernel")]
|
|
public static int KernelCloseUnderscore(CpuContext ctx) => KernelCloseCore(ctx, unchecked((int)ctx[CpuRegister.Rdi]));
|
|
|
|
[SysAbiExport(
|
|
Nid = "bY-PO6JhzhQ",
|
|
ExportName = "close",
|
|
Target = Generation.Gen4 | Generation.Gen5,
|
|
LibraryName = "libKernel")]
|
|
public static int PosixClose(CpuContext ctx) => KernelCloseCore(ctx, unchecked((int)ctx[CpuRegister.Rdi]));
|
|
|
|
[SysAbiExport(
|
|
Nid = "UK2Tl2DWUns",
|
|
ExportName = "sceKernelClose",
|
|
Target = Generation.Gen4 | Generation.Gen5,
|
|
LibraryName = "libKernel")]
|
|
public static int KernelClose(CpuContext ctx) => KernelCloseCore(ctx, unchecked((int)ctx[CpuRegister.Rdi]));
|
|
|
|
[SysAbiExport(
|
|
Nid = "eV9wAD2riIA",
|
|
ExportName = "sceKernelStat",
|
|
Target = Generation.Gen4 | Generation.Gen5,
|
|
LibraryName = "libKernel")]
|
|
public static int KernelStat(CpuContext ctx)
|
|
{
|
|
var pathAddress = ctx[CpuRegister.Rdi];
|
|
var statAddress = ctx[CpuRegister.Rsi];
|
|
if (pathAddress == 0 || statAddress == 0)
|
|
{
|
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
|
}
|
|
|
|
if (!TryReadNullTerminatedUtf8(ctx, pathAddress, MaxGuestStringLength, out var guestPath))
|
|
{
|
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
|
}
|
|
|
|
var hostPath = ResolveGuestPath(guestPath);
|
|
var statCacheKey = GetNegativeStatCacheKey(guestPath);
|
|
if (statCacheKey is not null && IsNegativeStatCached(statCacheKey))
|
|
{
|
|
LogUniqueStatTrace(guestPath, hostPath, found: false);
|
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
|
}
|
|
|
|
if (!TryWriteHostPathStat(ctx, statAddress, hostPath))
|
|
{
|
|
if (statCacheKey is not null)
|
|
{
|
|
AddNegativeStatCache(statCacheKey);
|
|
}
|
|
|
|
LogUniqueStatTrace(guestPath, hostPath, found: false);
|
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
|
}
|
|
|
|
if (statCacheKey is not null)
|
|
{
|
|
RemoveNegativeStatCache(statCacheKey);
|
|
}
|
|
|
|
LogUniqueStatTrace(guestPath, hostPath, found: true);
|
|
ctx[CpuRegister.Rax] = 0;
|
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
|
}
|
|
|
|
[SysAbiExport(
|
|
Nid = "gEpBkcwxUjw",
|
|
ExportName = "sceKernelAprResolveFilepathsToIdsAndFileSizes",
|
|
Target = Generation.Gen4 | Generation.Gen5,
|
|
LibraryName = "libKernel")]
|
|
public static int KernelAprResolveFilepathsToIdsAndFileSizes(CpuContext ctx)
|
|
{
|
|
var pathListAddress = ctx[CpuRegister.Rdi];
|
|
var count = ctx[CpuRegister.Rsi];
|
|
var idsAddress = ctx[CpuRegister.Rdx];
|
|
var sizesAddress = ctx[CpuRegister.Rcx];
|
|
if (pathListAddress == 0 || count == 0 || sizesAddress == 0 || count > 1024)
|
|
{
|
|
KernelRuntimeCompatExports.TrySetErrno(ctx, Einval);
|
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
|
}
|
|
|
|
for (ulong i = 0; i < count; i++)
|
|
{
|
|
if (idsAddress != 0 &&
|
|
!TryWriteUInt32Compat(ctx, idsAddress + (i * sizeof(uint)), uint.MaxValue))
|
|
{
|
|
KernelRuntimeCompatExports.TrySetErrno(ctx, Efault);
|
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
|
}
|
|
|
|
if (!TryResolveAprFilepath(ctx, pathListAddress, i, out var guestPath))
|
|
{
|
|
KernelRuntimeCompatExports.TrySetErrno(ctx, Efault);
|
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
|
}
|
|
|
|
var hostPath = ResolveGuestPath(guestPath);
|
|
if (!TryGetAprFileSize(hostPath, out var fileSize))
|
|
{
|
|
LogIoTrace("apr_resolve", guestPath, $"host='{hostPath}' index={i} count={count} result=not_found");
|
|
KernelRuntimeCompatExports.TrySetErrno(ctx, 2);
|
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
|
}
|
|
|
|
var fileId = AmprFileRegistry.Register(guestPath, hostPath);
|
|
LogIoTrace("apr_resolve", guestPath, $"host='{hostPath}' index={i} count={count} id=0x{fileId:X8} size={fileSize}");
|
|
|
|
if (idsAddress != 0 &&
|
|
!TryWriteUInt32Compat(ctx, idsAddress + (i * sizeof(uint)), fileId))
|
|
{
|
|
KernelRuntimeCompatExports.TrySetErrno(ctx, Efault);
|
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
|
}
|
|
|
|
if (!TryWriteUInt64Compat(ctx, sizesAddress + (i * sizeof(ulong)), fileSize))
|
|
{
|
|
KernelRuntimeCompatExports.TrySetErrno(ctx, Efault);
|
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
|
}
|
|
}
|
|
|
|
ctx[CpuRegister.Rax] = 0;
|
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
|
}
|
|
|
|
[SysAbiExport(
|
|
Nid = "kBwCPsYX-m4",
|
|
ExportName = "sceKernelFstat",
|
|
Target = Generation.Gen4 | Generation.Gen5,
|
|
LibraryName = "libKernel")]
|
|
public static int KernelFstat(CpuContext ctx)
|
|
{
|
|
var fd = unchecked((int)ctx[CpuRegister.Rdi]);
|
|
var statAddress = ctx[CpuRegister.Rsi];
|
|
if (statAddress == 0)
|
|
{
|
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
|
}
|
|
|
|
if (!TryWriteOpenDescriptorStat(ctx, fd, statAddress))
|
|
{
|
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
|
}
|
|
|
|
ctx[CpuRegister.Rax] = 0;
|
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
|
}
|
|
|
|
[SysAbiExport(
|
|
Nid = "AUXVxWeJU-A",
|
|
ExportName = "sceKernelUnlink",
|
|
Target = Generation.Gen4 | Generation.Gen5,
|
|
LibraryName = "libKernel")]
|
|
public static int KernelUnlink(CpuContext ctx)
|
|
{
|
|
var pathAddress = ctx[CpuRegister.Rdi];
|
|
if (pathAddress == 0)
|
|
{
|
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
|
}
|
|
|
|
if (!TryReadNullTerminatedUtf8(ctx, pathAddress, MaxGuestStringLength, out var guestPath))
|
|
{
|
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
|
}
|
|
|
|
var hostPath = ResolveGuestPath(guestPath);
|
|
if (IsReadOnlyGuestMutationPath(guestPath))
|
|
{
|
|
LogOpenTrace($"unlink readonly path='{guestPath}' host='{hostPath}'");
|
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_PERMISSION_DENIED;
|
|
}
|
|
|
|
try
|
|
{
|
|
if (Directory.Exists(hostPath))
|
|
{
|
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_PERMISSION_DENIED;
|
|
}
|
|
|
|
if (!File.Exists(hostPath))
|
|
{
|
|
AddNegativeStatCacheForGuestPath(guestPath);
|
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
|
}
|
|
|
|
File.Delete(hostPath);
|
|
InvalidateNegativeStatCacheForPathAndAncestors(guestPath);
|
|
InvalidateAprFileSizeCache(hostPath);
|
|
AddNegativeStatCacheForGuestPath(guestPath);
|
|
LogOpenTrace($"unlink path='{guestPath}' host='{hostPath}'");
|
|
ctx[CpuRegister.Rax] = 0;
|
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
|
}
|
|
catch (UnauthorizedAccessException)
|
|
{
|
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_PERMISSION_DENIED;
|
|
}
|
|
catch (IOException)
|
|
{
|
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY;
|
|
}
|
|
}
|
|
|
|
[SysAbiExport(
|
|
Nid = "1-LFLmRFxxM",
|
|
ExportName = "sceKernelMkdir",
|
|
Target = Generation.Gen4 | Generation.Gen5,
|
|
LibraryName = "libKernel")]
|
|
public static int KernelMkdir(CpuContext ctx)
|
|
{
|
|
var pathAddress = ctx[CpuRegister.Rdi];
|
|
if (pathAddress == 0)
|
|
{
|
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
|
}
|
|
|
|
if (!TryReadNullTerminatedUtf8(ctx, pathAddress, MaxGuestStringLength, out var guestPath))
|
|
{
|
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
|
}
|
|
|
|
var hostPath = ResolveGuestPath(guestPath);
|
|
if (IsReadOnlyGuestMutationPath(guestPath))
|
|
{
|
|
LogOpenTrace($"mkdir readonly path='{guestPath}' host='{hostPath}'");
|
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_PERMISSION_DENIED;
|
|
}
|
|
|
|
try
|
|
{
|
|
if (File.Exists(hostPath) || Directory.Exists(hostPath))
|
|
{
|
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_ALREADY_EXISTS;
|
|
}
|
|
|
|
var parentDirectory = Path.GetDirectoryName(hostPath);
|
|
if (string.IsNullOrWhiteSpace(parentDirectory) || !Directory.Exists(parentDirectory))
|
|
{
|
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
|
}
|
|
|
|
Directory.CreateDirectory(hostPath);
|
|
if (!Directory.Exists(hostPath))
|
|
{
|
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
|
}
|
|
|
|
InvalidateNegativeStatCacheForPathAndAncestors(guestPath);
|
|
LogOpenTrace($"mkdir path='{guestPath}' host='{hostPath}'");
|
|
ctx[CpuRegister.Rax] = 0;
|
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
|
}
|
|
catch (UnauthorizedAccessException)
|
|
{
|
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_PERMISSION_DENIED;
|
|
}
|
|
catch (IOException)
|
|
{
|
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
|
}
|
|
}
|
|
|
|
[SysAbiExport(
|
|
Nid = "naInUjYt3so",
|
|
ExportName = "sceKernelRmdir",
|
|
Target = Generation.Gen4 | Generation.Gen5,
|
|
LibraryName = "libKernel")]
|
|
public static int KernelRmdir(CpuContext ctx)
|
|
{
|
|
var pathAddress = ctx[CpuRegister.Rdi];
|
|
if (pathAddress == 0)
|
|
{
|
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
|
}
|
|
|
|
if (!TryReadNullTerminatedUtf8(ctx, pathAddress, MaxGuestStringLength, out var guestPath))
|
|
{
|
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
|
}
|
|
|
|
var hostPath = ResolveGuestPath(guestPath);
|
|
if (IsReadOnlyGuestMutationPath(guestPath))
|
|
{
|
|
LogOpenTrace($"rmdir readonly path='{guestPath}' host='{hostPath}'");
|
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_PERMISSION_DENIED;
|
|
}
|
|
|
|
try
|
|
{
|
|
if (!Directory.Exists(hostPath))
|
|
{
|
|
AddNegativeStatCacheForGuestPath(guestPath);
|
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
|
}
|
|
|
|
Directory.Delete(hostPath, recursive: false);
|
|
InvalidateNegativeStatCacheForPathAndAncestors(guestPath);
|
|
AddNegativeStatCacheForGuestPath(guestPath);
|
|
LogOpenTrace($"rmdir path='{guestPath}' host='{hostPath}'");
|
|
ctx[CpuRegister.Rax] = 0;
|
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
|
}
|
|
catch (UnauthorizedAccessException)
|
|
{
|
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_PERMISSION_DENIED;
|
|
}
|
|
catch (IOException)
|
|
{
|
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY;
|
|
}
|
|
}
|
|
|
|
private static int KernelCloseCore(CpuContext ctx, int fd)
|
|
{
|
|
if (fd is 0 or 1 or 2)
|
|
{
|
|
ctx[CpuRegister.Rax] = 0;
|
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
|
}
|
|
|
|
FileStream? stream;
|
|
lock (_fdGate)
|
|
{
|
|
if (_openFiles.Remove(fd, out stream))
|
|
{
|
|
}
|
|
else if (_openDirectories.Remove(fd))
|
|
{
|
|
ctx[CpuRegister.Rax] = 0;
|
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
|
}
|
|
else
|
|
{
|
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
|
}
|
|
}
|
|
|
|
stream.Dispose();
|
|
ctx[CpuRegister.Rax] = 0;
|
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
|
}
|
|
|
|
[SysAbiExport(
|
|
Nid = "DRuBt2pvICk",
|
|
ExportName = "_read",
|
|
Target = Generation.Gen4 | Generation.Gen5,
|
|
LibraryName = "libKernel")]
|
|
public static int KernelReadUnderscore(CpuContext ctx)
|
|
{
|
|
var fd = unchecked((int)ctx[CpuRegister.Rdi]);
|
|
var bufferAddress = ctx[CpuRegister.Rsi];
|
|
var requested = (int)Math.Min(ctx[CpuRegister.Rdx], int.MaxValue);
|
|
if (requested < 0 || (requested > 0 && bufferAddress == 0))
|
|
{
|
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
|
}
|
|
|
|
if (requested == 0 || fd == 0)
|
|
{
|
|
ctx[CpuRegister.Rax] = 0;
|
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
|
}
|
|
|
|
FileStream? stream;
|
|
lock (_fdGate)
|
|
{
|
|
_openFiles.TryGetValue(fd, out stream);
|
|
}
|
|
|
|
if (stream is null)
|
|
{
|
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
|
}
|
|
|
|
long positionBefore;
|
|
try
|
|
{
|
|
positionBefore = stream.Position;
|
|
}
|
|
catch (IOException)
|
|
{
|
|
positionBefore = -1;
|
|
}
|
|
|
|
var buffer = GC.AllocateUninitializedArray<byte>(requested);
|
|
var read = stream.Read(buffer, 0, requested);
|
|
if (read > 0 && !ctx.Memory.TryWrite(bufferAddress, buffer.AsSpan(0, read)))
|
|
{
|
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
|
}
|
|
|
|
long positionAfter;
|
|
try
|
|
{
|
|
positionAfter = stream.Position;
|
|
}
|
|
catch (IOException)
|
|
{
|
|
positionAfter = -1;
|
|
}
|
|
|
|
LogIoTrace(
|
|
"read",
|
|
stream.Name,
|
|
$"fd={fd} req={requested} read={read} pos={positionBefore}->{positionAfter} preview='{PreviewIoBytes(buffer, read, 64)}' hex={PreviewIoHex(buffer, read, 32)} guest_tail={PreviewGuestHex(ctx, bufferAddress + (ulong)Math.Max(read, 0), 32)}");
|
|
|
|
ctx[CpuRegister.Rax] = unchecked((ulong)read);
|
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
|
}
|
|
|
|
[SysAbiExport(
|
|
Nid = "AqBioC2vF3I",
|
|
ExportName = "read",
|
|
Target = Generation.Gen4 | Generation.Gen5,
|
|
LibraryName = "libKernel")]
|
|
public static int PosixRead(CpuContext ctx) => KernelReadUnderscore(ctx);
|
|
|
|
[SysAbiExport(
|
|
Nid = "Cg4srZ6TKbU",
|
|
ExportName = "sceKernelRead",
|
|
Target = Generation.Gen4 | Generation.Gen5,
|
|
LibraryName = "libKernel")]
|
|
public static int KernelRead(CpuContext ctx) => KernelReadUnderscore(ctx);
|
|
|
|
[SysAbiExport(
|
|
Nid = "Oy6IpwgtYOk",
|
|
ExportName = "lseek",
|
|
Target = Generation.Gen4 | Generation.Gen5,
|
|
LibraryName = "libKernel")]
|
|
public static int PosixLseek(CpuContext ctx)
|
|
{
|
|
var result = KernelLseekCore(
|
|
unchecked((int)ctx[CpuRegister.Rdi]),
|
|
unchecked((long)ctx[CpuRegister.Rsi]),
|
|
unchecked((int)ctx[CpuRegister.Rdx]),
|
|
out var position);
|
|
|
|
if (result != OrbisGen2Result.ORBIS_GEN2_OK)
|
|
{
|
|
ctx[CpuRegister.Rax] = ulong.MaxValue;
|
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
|
}
|
|
|
|
ctx[CpuRegister.Rax] = unchecked((ulong)position);
|
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
|
}
|
|
|
|
[SysAbiExport(
|
|
Nid = "oib76F-12fk",
|
|
ExportName = "sceKernelLseek",
|
|
Target = Generation.Gen4 | Generation.Gen5,
|
|
LibraryName = "libKernel")]
|
|
public static int KernelLseek(CpuContext ctx)
|
|
{
|
|
var result = KernelLseekCore(
|
|
unchecked((int)ctx[CpuRegister.Rdi]),
|
|
unchecked((long)ctx[CpuRegister.Rsi]),
|
|
unchecked((int)ctx[CpuRegister.Rdx]),
|
|
out var position);
|
|
|
|
if (result != OrbisGen2Result.ORBIS_GEN2_OK)
|
|
{
|
|
return (int)result;
|
|
}
|
|
|
|
ctx[CpuRegister.Rax] = unchecked((ulong)position);
|
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
|
}
|
|
|
|
[SysAbiExport(
|
|
Nid = "taRWhTJFTgE",
|
|
ExportName = "sceKernelGetdirentries",
|
|
Target = Generation.Gen4 | Generation.Gen5,
|
|
LibraryName = "libKernel")]
|
|
public static int KernelGetdirentries(CpuContext ctx)
|
|
{
|
|
return KernelGetdirentriesCore(
|
|
ctx,
|
|
unchecked((int)ctx[CpuRegister.Rdi]),
|
|
ctx[CpuRegister.Rsi],
|
|
unchecked((int)Math.Min(ctx[CpuRegister.Rdx], int.MaxValue)),
|
|
ctx[CpuRegister.Rcx]);
|
|
}
|
|
|
|
[SysAbiExport(
|
|
Nid = "j2AIqSqJP0w",
|
|
ExportName = "sceKernelGetdents",
|
|
Target = Generation.Gen4 | Generation.Gen5,
|
|
LibraryName = "libKernel")]
|
|
public static int KernelGetdents(CpuContext ctx)
|
|
{
|
|
return KernelGetdirentriesCore(
|
|
ctx,
|
|
unchecked((int)ctx[CpuRegister.Rdi]),
|
|
ctx[CpuRegister.Rsi],
|
|
unchecked((int)Math.Min(ctx[CpuRegister.Rdx], int.MaxValue)),
|
|
0);
|
|
}
|
|
|
|
private static OrbisGen2Result KernelLseekCore(int fd, long offset, int whence, out long position)
|
|
{
|
|
position = -1;
|
|
|
|
FileStream? stream;
|
|
lock (_fdGate)
|
|
{
|
|
_openFiles.TryGetValue(fd, out stream);
|
|
}
|
|
|
|
if (stream is null)
|
|
{
|
|
LogIoTrace("lseek", $"fd:{fd}", $"offset={offset} whence={whence} result=badfd");
|
|
return OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
|
}
|
|
|
|
SeekOrigin origin;
|
|
switch (whence)
|
|
{
|
|
case SeekSet:
|
|
origin = SeekOrigin.Begin;
|
|
break;
|
|
case SeekCur:
|
|
origin = SeekOrigin.Current;
|
|
break;
|
|
case SeekEnd:
|
|
origin = SeekOrigin.End;
|
|
break;
|
|
default:
|
|
LogIoTrace("lseek", stream.Name, $"fd={fd} offset={offset} whence={whence} result=invalid_whence");
|
|
return OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
|
}
|
|
|
|
try
|
|
{
|
|
position = stream.Seek(offset, origin);
|
|
}
|
|
catch (IOException ex)
|
|
{
|
|
LogIoTrace("lseek", stream.Name, $"fd={fd} offset={offset} whence={whence} result=io_error ex={ex.Message}");
|
|
return OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
|
}
|
|
catch (ArgumentException ex)
|
|
{
|
|
LogIoTrace("lseek", stream.Name, $"fd={fd} offset={offset} whence={whence} result=invalid ex={ex.Message}");
|
|
return OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
|
}
|
|
|
|
LogIoTrace("lseek", stream.Name, $"fd={fd} offset={offset} whence={whence} pos={position}");
|
|
return OrbisGen2Result.ORBIS_GEN2_OK;
|
|
}
|
|
|
|
[SysAbiExport(
|
|
Nid = "FxVZqBAA7ks",
|
|
ExportName = "_write",
|
|
Target = Generation.Gen4 | Generation.Gen5,
|
|
LibraryName = "libKernel")]
|
|
public static int KernelWriteUnderscore(CpuContext ctx)
|
|
{
|
|
var fd = unchecked((int)ctx[CpuRegister.Rdi]);
|
|
var bufferAddress = ctx[CpuRegister.Rsi];
|
|
var requested = (int)Math.Min(ctx[CpuRegister.Rdx], int.MaxValue);
|
|
if (requested < 0 || (requested > 0 && bufferAddress == 0))
|
|
{
|
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
|
}
|
|
|
|
var payload = requested == 0
|
|
? Array.Empty<byte>()
|
|
: GC.AllocateUninitializedArray<byte>(requested);
|
|
if (requested > 0 && !ctx.Memory.TryRead(bufferAddress, payload))
|
|
{
|
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
|
}
|
|
|
|
if (fd == 1 || fd == 2)
|
|
{
|
|
var text = Encoding.UTF8.GetString(payload);
|
|
if (fd == 1)
|
|
{
|
|
Console.Out.Write(text);
|
|
Console.Out.Flush();
|
|
}
|
|
else
|
|
{
|
|
Console.Error.Write(text);
|
|
Console.Error.Flush();
|
|
}
|
|
|
|
ctx[CpuRegister.Rax] = unchecked((ulong)requested);
|
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
|
}
|
|
|
|
FileStream? stream;
|
|
lock (_fdGate)
|
|
{
|
|
_openFiles.TryGetValue(fd, out stream);
|
|
}
|
|
|
|
if (stream is null)
|
|
{
|
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
|
}
|
|
|
|
stream.Write(payload, 0, requested);
|
|
stream.Flush();
|
|
ctx[CpuRegister.Rax] = unchecked((ulong)requested);
|
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
|
}
|
|
|
|
[SysAbiExport(
|
|
Nid = "FN4gaPmuFV8",
|
|
ExportName = "write",
|
|
Target = Generation.Gen4 | Generation.Gen5,
|
|
LibraryName = "libKernel")]
|
|
public static int PosixWrite(CpuContext ctx) => KernelWriteUnderscore(ctx);
|
|
|
|
[SysAbiExport(
|
|
Nid = "4wSze92BhLI",
|
|
ExportName = "sceKernelWrite",
|
|
Target = Generation.Gen4 | Generation.Gen5,
|
|
LibraryName = "libKernel")]
|
|
public static int KernelWrite(CpuContext ctx) => KernelWriteUnderscore(ctx);
|
|
|
|
[SysAbiExport(
|
|
Nid = "lLMT9vJAck0",
|
|
ExportName = "clock_gettime",
|
|
Target = Generation.Gen4 | Generation.Gen5,
|
|
LibraryName = "libKernel")]
|
|
public static int ClockGettime(CpuContext ctx)
|
|
{
|
|
var clockId = unchecked((int)ctx[CpuRegister.Rdi]);
|
|
var timespecAddress = ctx[CpuRegister.Rsi];
|
|
if (timespecAddress == 0)
|
|
{
|
|
KernelRuntimeCompatExports.TrySetErrno(ctx, Einval);
|
|
ctx[CpuRegister.Rax] = unchecked((ulong)-1);
|
|
return -1;
|
|
}
|
|
|
|
long seconds;
|
|
long nanoseconds;
|
|
if (!KernelRuntimeCompatExports.ResolveClockTime(clockId, out seconds, out nanoseconds))
|
|
{
|
|
KernelRuntimeCompatExports.TrySetErrno(ctx, Einval);
|
|
ctx[CpuRegister.Rax] = unchecked((ulong)-1);
|
|
return -1;
|
|
}
|
|
|
|
Span<byte> timespecBuffer = stackalloc byte[16];
|
|
BinaryPrimitives.WriteInt64LittleEndian(timespecBuffer, seconds);
|
|
BinaryPrimitives.WriteInt64LittleEndian(timespecBuffer[sizeof(long)..], nanoseconds);
|
|
|
|
if (!ctx.Memory.TryWrite(timespecAddress, timespecBuffer))
|
|
{
|
|
KernelRuntimeCompatExports.TrySetErrno(ctx, Efault);
|
|
ctx[CpuRegister.Rax] = unchecked((ulong)-1);
|
|
return -1;
|
|
}
|
|
|
|
ctx[CpuRegister.Rax] = 0;
|
|
return 0;
|
|
}
|
|
|
|
[SysAbiExport(
|
|
Nid = "vNe1w4diLCs",
|
|
ExportName = "__tls_get_addr",
|
|
Target = Generation.Gen4 | Generation.Gen5,
|
|
LibraryName = "libKernel")]
|
|
public static int TlsGetAddr(CpuContext ctx)
|
|
{
|
|
var tlsInfoAddress = ctx[CpuRegister.Rdi];
|
|
if (tlsInfoAddress == 0)
|
|
{
|
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
|
}
|
|
|
|
if (!ctx.TryReadUInt64(tlsInfoAddress, out var moduleId) ||
|
|
!ctx.TryReadUInt64(tlsInfoAddress + sizeof(ulong), out var offset))
|
|
{
|
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
|
}
|
|
|
|
ctx[CpuRegister.Rax] = ResolveTlsAddress(ctx, moduleId, offset);
|
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
|
}
|
|
|
|
private static ulong ResolveTlsAddress(CpuContext ctx, ulong moduleId, ulong offset)
|
|
{
|
|
if (ctx.FsBase == 0)
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
if (moduleId <= 1)
|
|
{
|
|
return unchecked(ctx.FsBase + offset);
|
|
}
|
|
|
|
var key = (ctx.FsBase << 16) ^ (moduleId & 0xFFFFUL);
|
|
ulong moduleBase;
|
|
lock (_tlsGate)
|
|
{
|
|
if (!_tlsModuleBlocks.TryGetValue(key, out moduleBase))
|
|
{
|
|
var block = Marshal.AllocHGlobal(TlsModuleBlockSize);
|
|
Marshal.Copy(new byte[TlsModuleBlockSize], 0, block, TlsModuleBlockSize);
|
|
|
|
moduleBase = unchecked((ulong)block);
|
|
_tlsModuleBlocks[key] = moduleBase;
|
|
}
|
|
}
|
|
|
|
return unchecked(moduleBase + offset);
|
|
}
|
|
|
|
[SysAbiExport(
|
|
Nid = "pB-yGZ2nQ9o",
|
|
ExportName = "_sceKernelSetThreadAtexitCount",
|
|
Target = Generation.Gen4 | Generation.Gen5,
|
|
LibraryName = "libKernel")]
|
|
public static int KernelSetThreadAtexitCount(CpuContext ctx)
|
|
{
|
|
_threadAtexitCountCallback = ctx[CpuRegister.Rdi];
|
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
|
}
|
|
|
|
[SysAbiExport(
|
|
Nid = "WhCc1w3EhSI",
|
|
ExportName = "_sceKernelSetThreadAtexitReport",
|
|
Target = Generation.Gen4 | Generation.Gen5,
|
|
LibraryName = "libKernel")]
|
|
public static int KernelSetThreadAtexitReport(CpuContext ctx)
|
|
{
|
|
_threadAtexitReportCallback = ctx[CpuRegister.Rdi];
|
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
|
}
|
|
|
|
[SysAbiExport(
|
|
Nid = "rNhWz+lvOMU",
|
|
ExportName = "_sceKernelSetThreadDtors",
|
|
Target = Generation.Gen4 | Generation.Gen5,
|
|
LibraryName = "libKernel")]
|
|
public static int KernelSetThreadDtors(CpuContext ctx)
|
|
{
|
|
_threadDtorsCallback = ctx[CpuRegister.Rdi];
|
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
|
}
|
|
|
|
[SysAbiExport(
|
|
Nid = "Tz4RNUCBbGI",
|
|
ExportName = "_sceKernelRtldThreadAtexitIncrement",
|
|
Target = Generation.Gen4 | Generation.Gen5,
|
|
LibraryName = "libKernel")]
|
|
public static int KernelRtldThreadAtexitIncrement(CpuContext ctx)
|
|
{
|
|
return KernelRtldThreadAtexitAdjust(ctx, delta: +1);
|
|
}
|
|
|
|
[SysAbiExport(
|
|
Nid = "8OnWXlgQlvo",
|
|
ExportName = "_sceKernelRtldThreadAtexitDecrement",
|
|
Target = Generation.Gen4 | Generation.Gen5,
|
|
LibraryName = "libKernel")]
|
|
public static int KernelRtldThreadAtexitDecrement(CpuContext ctx)
|
|
{
|
|
return KernelRtldThreadAtexitAdjust(ctx, delta: -1);
|
|
}
|
|
|
|
[SysAbiExport(
|
|
Nid = "pO96TwzOm5E",
|
|
ExportName = "sceKernelGetDirectMemorySize",
|
|
Target = Generation.Gen4 | Generation.Gen5,
|
|
LibraryName = "libKernel")]
|
|
public static int KernelGetDirectMemorySize(CpuContext ctx)
|
|
{
|
|
ctx[CpuRegister.Rax] = DirectMemorySizeBytes;
|
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
|
}
|
|
|
|
[SysAbiExport(
|
|
Nid = "C0f7TJcbfac",
|
|
ExportName = "sceKernelAvailableDirectMemorySize",
|
|
Target = Generation.Gen4 | Generation.Gen5,
|
|
LibraryName = "libKernel")]
|
|
public static int KernelAvailableDirectMemorySize(CpuContext ctx)
|
|
{
|
|
var arg0 = ctx[CpuRegister.Rdi];
|
|
var arg1 = ctx[CpuRegister.Rsi];
|
|
var arg2 = ctx[CpuRegister.Rdx];
|
|
var arg3 = ctx[CpuRegister.Rcx];
|
|
var arg4 = ctx[CpuRegister.R8];
|
|
|
|
ulong used = 0;
|
|
lock (_memoryGate)
|
|
{
|
|
foreach (var allocation in _directAllocations.Values)
|
|
{
|
|
used = Math.Min(DirectMemorySizeBytes, used + allocation.Length);
|
|
}
|
|
}
|
|
|
|
var totalAvailable = used >= DirectMemorySizeBytes
|
|
? 0UL
|
|
: DirectMemorySizeBytes - used;
|
|
|
|
if (arg1 != 0 || arg2 != 0 || arg3 != 0 || arg4 != 0)
|
|
{
|
|
var searchStartRaw = unchecked((long)arg0);
|
|
var searchEndRaw = unchecked((long)arg1);
|
|
var alignment = arg2 == 0 ? 0x1000UL : arg2;
|
|
var outAddress = arg3;
|
|
var outSize = arg4;
|
|
if (outAddress == 0 || outSize == 0)
|
|
{
|
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
|
}
|
|
|
|
var searchStart = searchStartRaw < 0 ? 0UL : (ulong)searchStartRaw;
|
|
var searchEnd = searchEndRaw <= 0
|
|
? DirectMemorySizeBytes
|
|
: Math.Min((ulong)searchEndRaw, DirectMemorySizeBytes);
|
|
if (searchStart >= searchEnd)
|
|
{
|
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
|
}
|
|
|
|
if (!TryFindAvailableDirectMemorySpanLocked(searchStart, searchEnd, alignment, out var candidate, out var rangeAvailable))
|
|
{
|
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
|
}
|
|
|
|
if (!ctx.TryWriteUInt64(outAddress, candidate) || !ctx.TryWriteUInt64(outSize, rangeAvailable))
|
|
{
|
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
|
}
|
|
|
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
|
}
|
|
|
|
var outSizeAddress = arg0;
|
|
if (outSizeAddress == 0)
|
|
{
|
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
|
}
|
|
|
|
if (!ctx.TryWriteUInt64(outSizeAddress, totalAvailable))
|
|
{
|
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
|
}
|
|
|
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
|
}
|
|
|
|
[SysAbiExport(
|
|
Nid = "aNz11fnnzi4",
|
|
ExportName = "sceKernelAvailableFlexibleMemorySize",
|
|
Target = Generation.Gen4 | Generation.Gen5,
|
|
LibraryName = "libKernel")]
|
|
public static int KernelAvailableFlexibleMemorySize(CpuContext ctx)
|
|
{
|
|
var outSizeAddress = ctx[CpuRegister.Rdi];
|
|
if (outSizeAddress == 0)
|
|
{
|
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
|
}
|
|
|
|
ulong available;
|
|
lock (_memoryGate)
|
|
{
|
|
available = _allocatedFlexibleBytes >= FlexibleMemorySizeBytes
|
|
? 0
|
|
: FlexibleMemorySizeBytes - _allocatedFlexibleBytes;
|
|
}
|
|
|
|
if (!ctx.TryWriteUInt64(outSizeAddress, available))
|
|
{
|
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
|
}
|
|
|
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
|
}
|
|
|
|
[SysAbiExport(
|
|
Nid = "rTXw65xmLIA",
|
|
ExportName = "sceKernelAllocateDirectMemory",
|
|
Target = Generation.Gen4 | Generation.Gen5,
|
|
LibraryName = "libKernel")]
|
|
public static int KernelAllocateDirectMemory(CpuContext ctx)
|
|
{
|
|
var searchStartRaw = unchecked((long)ctx[CpuRegister.Rdi]);
|
|
var searchEndRaw = unchecked((long)ctx[CpuRegister.Rsi]);
|
|
var length = ctx[CpuRegister.Rdx];
|
|
var alignment = ctx[CpuRegister.Rcx];
|
|
var memoryType = unchecked((int)ctx[CpuRegister.R8]);
|
|
var outAddress = ctx[CpuRegister.R9];
|
|
|
|
if (length == 0 || outAddress == 0)
|
|
{
|
|
TraceDirectMemoryCall(
|
|
ctx,
|
|
"allocate_direct",
|
|
length,
|
|
alignment,
|
|
memoryType,
|
|
outAddress,
|
|
result: OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
|
}
|
|
|
|
var limit = DirectMemorySizeBytes;
|
|
ulong searchStart;
|
|
ulong searchEnd;
|
|
|
|
if (searchEndRaw <= 0)
|
|
{
|
|
searchEnd = limit;
|
|
}
|
|
else
|
|
{
|
|
searchEnd = (ulong)searchEndRaw;
|
|
if (searchEnd > limit)
|
|
{
|
|
searchEnd = limit;
|
|
}
|
|
}
|
|
|
|
if (searchStartRaw < 0)
|
|
{
|
|
searchStart = 0;
|
|
}
|
|
else
|
|
{
|
|
searchStart = (ulong)searchStartRaw;
|
|
}
|
|
|
|
if (searchStart >= searchEnd)
|
|
{
|
|
searchStart = 0;
|
|
}
|
|
|
|
var align = alignment == 0 ? 0x1000UL : alignment;
|
|
ulong selectedAddress;
|
|
lock (_memoryGate)
|
|
{
|
|
if (!TryAllocateDirectMemoryLocked(searchStart, searchEnd, length, align, memoryType, DirectMemorySizeBytes, out selectedAddress))
|
|
{
|
|
TraceDirectMemoryCall(
|
|
ctx,
|
|
"allocate_direct",
|
|
length,
|
|
align,
|
|
memoryType,
|
|
outAddress,
|
|
result: OrbisGen2Result.ORBIS_GEN2_ERROR_TRY_AGAIN);
|
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_TRY_AGAIN;
|
|
}
|
|
}
|
|
|
|
if (!ctx.TryWriteUInt64(outAddress, selectedAddress))
|
|
{
|
|
TraceDirectMemoryCall(
|
|
ctx,
|
|
"allocate_direct",
|
|
length,
|
|
align,
|
|
memoryType,
|
|
outAddress,
|
|
selectedAddress,
|
|
OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
|
}
|
|
|
|
TraceDirectMemoryCall(
|
|
ctx,
|
|
"allocate_direct",
|
|
length,
|
|
align,
|
|
memoryType,
|
|
outAddress,
|
|
selectedAddress,
|
|
OrbisGen2Result.ORBIS_GEN2_OK);
|
|
|
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
|
}
|
|
|
|
[SysAbiExport(
|
|
Nid = "B+vc2AO2Zrc",
|
|
ExportName = "sceKernelAllocateMainDirectMemory",
|
|
Target = Generation.Gen4 | Generation.Gen5,
|
|
LibraryName = "libKernel")]
|
|
public static int KernelAllocateMainDirectMemory(CpuContext ctx)
|
|
{
|
|
var length = ctx[CpuRegister.Rdi];
|
|
var alignment = ctx[CpuRegister.Rsi];
|
|
var memoryType = unchecked((int)ctx[CpuRegister.Rdx]);
|
|
var outAddress = ctx[CpuRegister.Rcx];
|
|
if (outAddress == 0 || length == 0)
|
|
{
|
|
TraceDirectMemoryCall(
|
|
ctx,
|
|
"allocate_main_direct",
|
|
length,
|
|
alignment,
|
|
memoryType,
|
|
outAddress,
|
|
result: OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
|
}
|
|
|
|
var effectiveAlignment = alignment == 0 ? 0x1000UL : alignment;
|
|
ulong aligned;
|
|
lock (_memoryGate)
|
|
{
|
|
var allocationLimit = DirectMemorySizeBytes;
|
|
if (_mainDirectMemoryPoolBase != UnsetMainDirectMemoryPoolBase &&
|
|
!TryAddU64(_mainDirectMemoryPoolBase, DirectMemorySizeBytes, out allocationLimit))
|
|
{
|
|
allocationLimit = ulong.MaxValue;
|
|
}
|
|
|
|
if (!TryAllocateDirectMemoryLocked(0, allocationLimit, length, effectiveAlignment, memoryType, allocationLimit, out aligned))
|
|
{
|
|
var poolBase = _mainDirectMemoryPoolBase == UnsetMainDirectMemoryPoolBase
|
|
? AlignUp(GetDirectMemoryHighWaterMarkLocked(), effectiveAlignment)
|
|
: _mainDirectMemoryPoolBase;
|
|
|
|
if (_mainDirectMemoryPoolBase == UnsetMainDirectMemoryPoolBase &&
|
|
TryAddU64(poolBase, DirectMemorySizeBytes, out var shiftedLimit) &&
|
|
TryAllocateDirectMemoryLocked(0, shiftedLimit, length, effectiveAlignment, memoryType, shiftedLimit, out aligned))
|
|
{
|
|
_mainDirectMemoryPoolBase = poolBase;
|
|
if (ShouldTraceDirectMemory())
|
|
{
|
|
Console.Error.WriteLine(
|
|
$"[LOADER][TRACE] main_direct_pool: base=0x{poolBase:X16} limit=0x{shiftedLimit:X16}");
|
|
}
|
|
}
|
|
else
|
|
{
|
|
TraceDirectMemoryCall(
|
|
ctx,
|
|
"allocate_main_direct",
|
|
length,
|
|
effectiveAlignment,
|
|
memoryType,
|
|
outAddress,
|
|
result: OrbisGen2Result.ORBIS_GEN2_ERROR_TRY_AGAIN);
|
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_TRY_AGAIN;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!ctx.TryWriteUInt64(outAddress, aligned))
|
|
{
|
|
TraceDirectMemoryCall(
|
|
ctx,
|
|
"allocate_main_direct",
|
|
length,
|
|
effectiveAlignment,
|
|
memoryType,
|
|
outAddress,
|
|
aligned,
|
|
OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
|
}
|
|
|
|
TraceDirectMemoryCall(
|
|
ctx,
|
|
"allocate_main_direct",
|
|
length,
|
|
effectiveAlignment,
|
|
memoryType,
|
|
outAddress,
|
|
aligned,
|
|
OrbisGen2Result.ORBIS_GEN2_OK);
|
|
|
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
|
}
|
|
|
|
[SysAbiExport(
|
|
Nid = "MBuItvba6z8",
|
|
ExportName = "sceKernelReleaseDirectMemory",
|
|
Target = Generation.Gen4 | Generation.Gen5,
|
|
LibraryName = "libKernel")]
|
|
public static int KernelReleaseDirectMemory(CpuContext ctx)
|
|
{
|
|
var start = ctx[CpuRegister.Rdi];
|
|
var length = ctx[CpuRegister.Rsi];
|
|
if (length == 0)
|
|
{
|
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
|
}
|
|
|
|
lock (_memoryGate)
|
|
{
|
|
if (!_directAllocations.TryGetValue(start, out var allocation) || allocation.Length != length)
|
|
{
|
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
|
}
|
|
|
|
_directAllocations.Remove(start);
|
|
_nextPhysicalAddress = GetDirectMemoryHighWaterMarkLocked();
|
|
}
|
|
|
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
|
}
|
|
|
|
[SysAbiExport(
|
|
Nid = "hwVSPCmp5tM",
|
|
ExportName = "sceKernelCheckedReleaseDirectMemory",
|
|
Target = Generation.Gen4 | Generation.Gen5,
|
|
LibraryName = "libKernel")]
|
|
public static int KernelCheckedReleaseDirectMemory(CpuContext ctx)
|
|
{
|
|
var start = ctx[CpuRegister.Rdi];
|
|
var length = ctx[CpuRegister.Rsi];
|
|
|
|
if (length == 0)
|
|
{
|
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
|
}
|
|
|
|
lock (_memoryGate)
|
|
{
|
|
if (!_directAllocations.TryGetValue(start, out var allocation) ||
|
|
allocation.Length != length)
|
|
{
|
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
|
}
|
|
|
|
_directAllocations.Remove(start);
|
|
_nextPhysicalAddress = GetDirectMemoryHighWaterMarkLocked();
|
|
}
|
|
|
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
|
}
|
|
|
|
[SysAbiExport(
|
|
Nid = "L-Q3LEjIbgA",
|
|
ExportName = "sceKernelMapDirectMemory",
|
|
Target = Generation.Gen4 | Generation.Gen5,
|
|
LibraryName = "libKernel")]
|
|
public static int KernelMapDirectMemory(CpuContext ctx)
|
|
{
|
|
var inOutAddressPointer = ctx[CpuRegister.Rdi];
|
|
var length = ctx[CpuRegister.Rsi];
|
|
var protection = unchecked((int)ctx[CpuRegister.Rdx]);
|
|
var flags = ctx[CpuRegister.Rcx];
|
|
var directMemoryStart = ctx[CpuRegister.R8];
|
|
var alignment = ctx[CpuRegister.R9];
|
|
if (ShouldTraceDirectMemory())
|
|
{
|
|
Console.Error.WriteLine(
|
|
$"[LOADER][TRACE] map_direct: inout=0x{inOutAddressPointer:X16} len=0x{length:X16} prot=0x{protection:X8} flags=0x{flags:X16} direct=0x{directMemoryStart:X16} align=0x{alignment:X16}");
|
|
}
|
|
if (inOutAddressPointer == 0 || length == 0)
|
|
{
|
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
|
}
|
|
|
|
if (!ctx.TryReadUInt64(inOutAddressPointer, out var requestedAddress))
|
|
{
|
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
|
}
|
|
|
|
ulong mappedAddress;
|
|
lock (_memoryGate)
|
|
{
|
|
var effectiveAlignment = alignment == 0 ? 0x1000UL : alignment;
|
|
var fixedMapping = (flags & 0x10UL) != 0;
|
|
var desiredAddress = requestedAddress != 0
|
|
? requestedAddress
|
|
: directMemoryStart != 0
|
|
? AlignUp(directMemoryStart, effectiveAlignment)
|
|
: AlignUp(_nextVirtualAddress == 0 ? 0x1_0000_0000UL : _nextVirtualAddress, effectiveAlignment);
|
|
|
|
var reserved = false;
|
|
if (fixedMapping && requestedAddress != 0)
|
|
{
|
|
mappedAddress = requestedAddress;
|
|
reserved = IsGuestRangeBacked(ctx, requestedAddress, length);
|
|
if (!reserved)
|
|
{
|
|
TryReserveExactGuestVirtualRange(ctx, requestedAddress, length, protection);
|
|
reserved = IsGuestRangeBacked(ctx, requestedAddress, length);
|
|
}
|
|
|
|
if (!reserved)
|
|
{
|
|
mappedAddress = 0;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
reserved = TryReserveGuestVirtualRange(ctx, desiredAddress, length, protection, effectiveAlignment, out mappedAddress);
|
|
}
|
|
if (ShouldTraceDirectMemory())
|
|
{
|
|
Console.Error.WriteLine(
|
|
$"[LOADER][TRACE] map_direct reserve: requested=0x{requestedAddress:X16} desired=0x{desiredAddress:X16} reserved={reserved} mapped=0x{mappedAddress:X16}");
|
|
}
|
|
if (!reserved && !fixedMapping)
|
|
{
|
|
if (mappedAddress == 0)
|
|
{
|
|
mappedAddress = requestedAddress != 0
|
|
? requestedAddress
|
|
: AllocateMappedGuestAddress(ctx, length, effectiveAlignment);
|
|
if (ShouldTraceDirectMemory())
|
|
{
|
|
Console.Error.WriteLine($"[LOADER][TRACE] map_direct fallback mapped=0x{mappedAddress:X16}");
|
|
}
|
|
}
|
|
}
|
|
|
|
if (mappedAddress == 0)
|
|
{
|
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
|
}
|
|
|
|
_nextVirtualAddress = Math.Max(_nextVirtualAddress, mappedAddress + length);
|
|
_mappedRegions[mappedAddress] = new MappedRegion(
|
|
mappedAddress,
|
|
length,
|
|
protection,
|
|
IsFlexible: false,
|
|
IsDirect: true,
|
|
DirectStart: directMemoryStart);
|
|
}
|
|
|
|
if (!ctx.TryWriteUInt64(inOutAddressPointer, mappedAddress))
|
|
{
|
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
|
}
|
|
|
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
|
}
|
|
|
|
[SysAbiExport(
|
|
Nid = "NcaWUxfMNIQ",
|
|
ExportName = "sceKernelMapNamedDirectMemory",
|
|
Target = Generation.Gen4 | Generation.Gen5,
|
|
LibraryName = "libKernel")]
|
|
public static int KernelMapNamedDirectMemory(CpuContext ctx)
|
|
{
|
|
return KernelMapDirectMemory(ctx);
|
|
}
|
|
|
|
[SysAbiExport(
|
|
Nid = "mL8NDH86iQI",
|
|
ExportName = "sceKernelMapNamedFlexibleMemory",
|
|
Target = Generation.Gen4 | Generation.Gen5,
|
|
LibraryName = "libKernel")]
|
|
public static int KernelMapNamedFlexibleMemory(CpuContext ctx)
|
|
{
|
|
var inOutAddressPointer = ctx[CpuRegister.Rdi];
|
|
var length = ctx[CpuRegister.Rsi];
|
|
var protection = unchecked((int)ctx[CpuRegister.Rdx]);
|
|
var flags = ctx[CpuRegister.Rcx];
|
|
if (ShouldTraceDirectMemory())
|
|
{
|
|
Console.Error.WriteLine(
|
|
$"[LOADER][TRACE] map_flexible: inout=0x{inOutAddressPointer:X16} len=0x{length:X16} prot=0x{protection:X8} flags=0x{flags:X16}");
|
|
}
|
|
if (inOutAddressPointer == 0 || length == 0)
|
|
{
|
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
|
}
|
|
|
|
if (!ctx.TryReadUInt64(inOutAddressPointer, out var requestedAddress))
|
|
{
|
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
|
}
|
|
|
|
ulong mappedAddress;
|
|
lock (_memoryGate)
|
|
{
|
|
var fixedMapping = (flags & 0x10UL) != 0;
|
|
var desiredAddress = requestedAddress != 0
|
|
? requestedAddress
|
|
: AlignUp(_nextVirtualAddress == 0 ? 0x1_0000_0000UL : _nextVirtualAddress, 0x1000UL);
|
|
|
|
if (fixedMapping && requestedAddress != 0)
|
|
{
|
|
mappedAddress = requestedAddress;
|
|
if (!IsGuestRangeBacked(ctx, requestedAddress, length))
|
|
{
|
|
TryReserveExactGuestVirtualRange(ctx, requestedAddress, length, protection);
|
|
if (!IsGuestRangeBacked(ctx, requestedAddress, length))
|
|
{
|
|
mappedAddress = 0;
|
|
}
|
|
}
|
|
}
|
|
else if (!TryReserveGuestVirtualRange(ctx, desiredAddress, length, protection, OrbisPageSize, out mappedAddress))
|
|
{
|
|
mappedAddress = AllocateMappedGuestAddress(ctx, length, 0x1000UL);
|
|
}
|
|
|
|
if (ShouldTraceDirectMemory())
|
|
{
|
|
Console.Error.WriteLine(
|
|
$"[LOADER][TRACE] map_flexible reserve: requested=0x{requestedAddress:X16} desired=0x{desiredAddress:X16} mapped=0x{mappedAddress:X16}");
|
|
}
|
|
|
|
if (mappedAddress == 0)
|
|
{
|
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
|
}
|
|
|
|
_nextVirtualAddress = Math.Max(_nextVirtualAddress, mappedAddress + length);
|
|
_allocatedFlexibleBytes = Math.Min(FlexibleMemorySizeBytes, _allocatedFlexibleBytes + length);
|
|
_mappedRegions[mappedAddress] = new MappedRegion(
|
|
mappedAddress,
|
|
length,
|
|
protection,
|
|
IsFlexible: true,
|
|
IsDirect: false,
|
|
DirectStart: 0);
|
|
}
|
|
|
|
if (!ctx.TryWriteUInt64(inOutAddressPointer, mappedAddress))
|
|
{
|
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
|
}
|
|
|
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
|
}
|
|
|
|
[SysAbiExport(
|
|
Nid = "IWIBBdTHit4",
|
|
ExportName = "sceKernelMapFlexibleMemory",
|
|
Target = Generation.Gen4 | Generation.Gen5,
|
|
LibraryName = "libKernel")]
|
|
public static int KernelMapFlexibleMemory(CpuContext ctx)
|
|
{
|
|
return KernelMapNamedFlexibleMemory(ctx);
|
|
}
|
|
|
|
[SysAbiExport(
|
|
Nid = "4h6F1LLbTiw",
|
|
ExportName = "sceKernelMapFlexibleMemoryInternal",
|
|
Target = Generation.Gen4 | Generation.Gen5,
|
|
LibraryName = "libKernel")]
|
|
public static int KernelMapFlexibleMemoryInternal(CpuContext ctx)
|
|
{
|
|
return KernelMapNamedFlexibleMemory(ctx);
|
|
}
|
|
|
|
[SysAbiExport(
|
|
Nid = "2SKEx6bSq-4",
|
|
ExportName = "sceKernelBatchMap",
|
|
Target = Generation.Gen4 | Generation.Gen5,
|
|
LibraryName = "libKernel")]
|
|
public static int KernelBatchMap(CpuContext ctx)
|
|
{
|
|
return KernelBatchMapCore(ctx, OrbisKernelMapFixed);
|
|
}
|
|
|
|
[SysAbiExport(
|
|
Nid = "kBJzF8x4SyE",
|
|
ExportName = "sceKernelBatchMap2",
|
|
Target = Generation.Gen4 | Generation.Gen5,
|
|
LibraryName = "libKernel")]
|
|
public static int KernelBatchMap2(CpuContext ctx)
|
|
{
|
|
return KernelBatchMapCore(ctx, unchecked((int)ctx[CpuRegister.Rcx]));
|
|
}
|
|
|
|
[SysAbiExport(
|
|
Nid = "cQke9UuBQOk",
|
|
ExportName = "sceKernelMunmap",
|
|
Target = Generation.Gen4 | Generation.Gen5,
|
|
LibraryName = "libKernel")]
|
|
public static int KernelMunmap(CpuContext ctx)
|
|
{
|
|
var address = ctx[CpuRegister.Rdi];
|
|
var length = ctx[CpuRegister.Rsi];
|
|
if (address == 0 || length == 0)
|
|
{
|
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
|
}
|
|
|
|
lock (_memoryGate)
|
|
{
|
|
if (!_mappedRegions.TryGetValue(address, out var mappedRegion) || mappedRegion.Length != length)
|
|
{
|
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
|
}
|
|
|
|
_mappedRegions.Remove(address);
|
|
if (mappedRegion.IsFlexible)
|
|
{
|
|
_allocatedFlexibleBytes = mappedRegion.Length >= _allocatedFlexibleBytes
|
|
? 0
|
|
: _allocatedFlexibleBytes - mappedRegion.Length;
|
|
}
|
|
}
|
|
|
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
|
}
|
|
|
|
[SysAbiExport(
|
|
Nid = "DGMG3JshrZU",
|
|
ExportName = "sceKernelSetVirtualRangeName",
|
|
Target = Generation.Gen4 | Generation.Gen5,
|
|
LibraryName = "libKernel")]
|
|
public static int KernelSetVirtualRangeName(CpuContext ctx)
|
|
{
|
|
var address = ctx[CpuRegister.Rdi];
|
|
var length = ctx[CpuRegister.Rsi];
|
|
var nameAddress = ctx[CpuRegister.Rdx];
|
|
if (nameAddress == 0)
|
|
{
|
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
|
}
|
|
|
|
if (!TryReadCString(ctx, nameAddress, OrbisKernelMaximumNameLength, out var nameBytes))
|
|
{
|
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
|
}
|
|
|
|
var name = Encoding.UTF8.GetString(nameBytes);
|
|
lock (_memoryGate)
|
|
{
|
|
if (!TryFindVirtualQueryRegionLocked(address, findNext: false, out var region) ||
|
|
length > region.Length ||
|
|
address < region.Address ||
|
|
length > region.Address + region.Length - address)
|
|
{
|
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
|
}
|
|
|
|
_mappedRegionNames[region.Address] = name;
|
|
}
|
|
|
|
ctx[CpuRegister.Rax] = 0;
|
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
|
}
|
|
|
|
[SysAbiExport(
|
|
Nid = "rVjRvHJ0X6c",
|
|
ExportName = "sceKernelVirtualQuery",
|
|
Target = Generation.Gen4 | Generation.Gen5,
|
|
LibraryName = "libKernel")]
|
|
public static int KernelVirtualQuery(CpuContext ctx)
|
|
{
|
|
var queryAddress = ctx[CpuRegister.Rdi];
|
|
var flags = unchecked((int)ctx[CpuRegister.Rsi]);
|
|
var infoAddress = ctx[CpuRegister.Rdx];
|
|
var infoSize = ctx[CpuRegister.Rcx];
|
|
if (infoAddress == 0 || infoSize < OrbisVirtualQueryInfoSize)
|
|
{
|
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
|
}
|
|
|
|
MappedRegion region;
|
|
var memoryType = 0;
|
|
lock (_memoryGate)
|
|
{
|
|
if (!TryFindVirtualQueryRegionLocked(queryAddress, findNext: (flags & 0x1) != 0, out region))
|
|
{
|
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
|
}
|
|
|
|
if (region.IsDirect && TryFindDirectAllocationLocked(region.DirectStart, out var allocation))
|
|
{
|
|
memoryType = allocation.MemoryType;
|
|
}
|
|
}
|
|
|
|
Span<byte> payload = stackalloc byte[OrbisVirtualQueryInfoSize];
|
|
payload.Clear();
|
|
|
|
var regionEnd = TryAddU64(region.Address, region.Length, out var exclusiveEnd)
|
|
? exclusiveEnd
|
|
: ulong.MaxValue;
|
|
var stateFlags = 0u;
|
|
if (region.IsFlexible)
|
|
{
|
|
stateFlags |= 0x01u;
|
|
}
|
|
|
|
if (region.IsDirect)
|
|
{
|
|
stateFlags |= 0x02u;
|
|
}
|
|
|
|
stateFlags |= 0x10u;
|
|
|
|
BinaryPrimitives.WriteUInt64LittleEndian(payload[0..8], region.Address);
|
|
BinaryPrimitives.WriteUInt64LittleEndian(payload[8..16], regionEnd);
|
|
BinaryPrimitives.WriteUInt64LittleEndian(payload[16..24], region.DirectStart);
|
|
BinaryPrimitives.WriteInt32LittleEndian(payload[24..28], region.Protection);
|
|
BinaryPrimitives.WriteInt32LittleEndian(payload[28..32], memoryType);
|
|
payload[32] = unchecked((byte)stateFlags);
|
|
|
|
string name;
|
|
lock (_memoryGate)
|
|
{
|
|
name = _mappedRegionNames.GetValueOrDefault(region.Address)
|
|
?? (region.IsDirect
|
|
? "direct"
|
|
: region.IsFlexible
|
|
? "flexible"
|
|
: string.Empty);
|
|
}
|
|
if (!string.IsNullOrEmpty(name))
|
|
{
|
|
var nameBytes = Encoding.ASCII.GetBytes(name);
|
|
nameBytes.AsSpan(0, Math.Min(nameBytes.Length, OrbisKernelMaximumNameLength))
|
|
.CopyTo(payload.Slice(33, OrbisKernelMaximumNameLength));
|
|
}
|
|
|
|
if (!TryWriteCompat(ctx, infoAddress, payload))
|
|
{
|
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
|
}
|
|
|
|
ctx[CpuRegister.Rax] = 0;
|
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
|
}
|
|
|
|
[SysAbiExport(
|
|
Nid = "WFcfL2lzido",
|
|
ExportName = "sceKernelQueryMemoryProtection",
|
|
Target = Generation.Gen4 | Generation.Gen5,
|
|
LibraryName = "libKernel")]
|
|
public static int KernelQueryMemoryProtection(CpuContext ctx)
|
|
{
|
|
var queryAddress = ctx[CpuRegister.Rdi];
|
|
var startOut = ctx[CpuRegister.Rsi];
|
|
var endOut = ctx[CpuRegister.Rdx];
|
|
var protectionOut = ctx[CpuRegister.Rcx];
|
|
|
|
lock (_memoryGate)
|
|
{
|
|
foreach (var region in _mappedRegions.Values)
|
|
{
|
|
if (queryAddress < region.Address || queryAddress >= region.Address + region.Length)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (startOut != 0 && !ctx.TryWriteUInt64(startOut, region.Address))
|
|
{
|
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
|
}
|
|
|
|
if (endOut != 0 && !ctx.TryWriteUInt64(endOut, region.Address + region.Length - 1))
|
|
{
|
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
|
}
|
|
|
|
if (protectionOut != 0 && !ctx.TryWriteInt32(protectionOut, region.Protection))
|
|
{
|
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
|
}
|
|
|
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
|
}
|
|
}
|
|
|
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
|
}
|
|
|
|
[SysAbiExport(
|
|
Nid = "BHouLQzh0X0",
|
|
ExportName = "sceKernelDirectMemoryQuery",
|
|
Target = Generation.Gen4 | Generation.Gen5,
|
|
LibraryName = "libKernel")]
|
|
public static int KernelDirectMemoryQuery(CpuContext ctx)
|
|
{
|
|
var offset = ctx[CpuRegister.Rdi];
|
|
_ = ctx[CpuRegister.Rsi]; // flags
|
|
var infoAddress = ctx[CpuRegister.Rdx];
|
|
var infoSize = ctx[CpuRegister.Rcx];
|
|
if (infoAddress == 0 || infoSize < 24)
|
|
{
|
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
|
}
|
|
|
|
lock (_memoryGate)
|
|
{
|
|
foreach (var block in _directAllocations.Values)
|
|
{
|
|
if (offset < block.Start || offset >= block.Start + block.Length)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (!ctx.TryWriteUInt64(infoAddress, block.Start) ||
|
|
!ctx.TryWriteUInt64(infoAddress + sizeof(ulong), block.Start + block.Length) ||
|
|
!ctx.TryWriteInt32(infoAddress + (sizeof(ulong) * 2), block.MemoryType))
|
|
{
|
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
|
}
|
|
|
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
|
}
|
|
}
|
|
|
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
|
}
|
|
|
|
[SysAbiExport(
|
|
Nid = "vSMAm3cxYTY",
|
|
ExportName = "sceKernelMprotect",
|
|
Target = Generation.Gen4 | Generation.Gen5,
|
|
LibraryName = "libKernel")]
|
|
public static int KernelMprotect(CpuContext ctx)
|
|
{
|
|
var address = ctx[CpuRegister.Rdi];
|
|
var length = ctx[CpuRegister.Rsi];
|
|
var protection = unchecked((int)ctx[CpuRegister.Rdx]);
|
|
if (address == 0 || length == 0)
|
|
{
|
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
|
}
|
|
|
|
if (!TryNormalizeProtectRange(address, length, out var alignedAddress, out var alignedLength))
|
|
{
|
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
|
}
|
|
|
|
if (!TryProtectHostRange(alignedAddress, alignedLength, protection))
|
|
{
|
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
|
}
|
|
|
|
lock (_memoryGate)
|
|
{
|
|
_ = TryApplyMappedRegionProtectionLocked(alignedAddress, alignedLength, protection);
|
|
}
|
|
|
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
|
}
|
|
|
|
[SysAbiExport(
|
|
Nid = "9bfdLIyuwCY",
|
|
ExportName = "sceKernelMtypeprotect",
|
|
Target = Generation.Gen4 | Generation.Gen5,
|
|
LibraryName = "libKernel")]
|
|
public static int KernelMtypeprotect(CpuContext ctx)
|
|
{
|
|
var address = ctx[CpuRegister.Rdi];
|
|
var length = ctx[CpuRegister.Rsi];
|
|
var memoryType = unchecked((int)ctx[CpuRegister.Rdx]);
|
|
var protection = unchecked((int)ctx[CpuRegister.Rcx]);
|
|
if (address == 0 || length == 0)
|
|
{
|
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
|
}
|
|
|
|
if (!TryNormalizeProtectRange(address, length, out var alignedAddress, out var alignedLength))
|
|
{
|
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
|
}
|
|
|
|
if (!TryProtectHostRange(alignedAddress, alignedLength, protection))
|
|
{
|
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
|
}
|
|
|
|
lock (_memoryGate)
|
|
{
|
|
_ = TryApplyMappedRegionProtectionLocked(alignedAddress, alignedLength, protection, memoryType);
|
|
}
|
|
|
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
|
}
|
|
|
|
private static int KernelRtldThreadAtexitAdjust(CpuContext ctx, int delta)
|
|
{
|
|
var counterAddress = ctx[CpuRegister.Rdi];
|
|
if (counterAddress == 0)
|
|
{
|
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
|
}
|
|
|
|
if (!ctx.TryReadUInt64(counterAddress, out var value))
|
|
{
|
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
|
}
|
|
|
|
var adjusted = delta >= 0
|
|
? unchecked(value + (ulong)delta)
|
|
: value >= (ulong)(-delta)
|
|
? unchecked(value - (ulong)(-delta))
|
|
: 0UL;
|
|
if (!ctx.TryWriteUInt64(counterAddress, adjusted))
|
|
{
|
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
|
}
|
|
|
|
ctx[CpuRegister.Rax] = adjusted;
|
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
|
}
|
|
|
|
private static int SnprintfCore(CpuContext ctx)
|
|
{
|
|
var destination = ctx[CpuRegister.Rdi];
|
|
var bufferSize = ctx[CpuRegister.Rsi];
|
|
var formatAddress = ctx[CpuRegister.Rdx];
|
|
|
|
if (!TryReadCString(ctx, formatAddress, 1_048_576, out var formatBytes))
|
|
{
|
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
|
}
|
|
|
|
var format = Encoding.UTF8.GetString(formatBytes);
|
|
var result = FormatString(ctx, format);
|
|
|
|
return WriteSnprintfOutput(ctx, destination, bufferSize, result);
|
|
}
|
|
|
|
private static int VsnprintfCore(CpuContext ctx)
|
|
{
|
|
var destination = ctx[CpuRegister.Rdi];
|
|
var bufferSize = ctx[CpuRegister.Rsi];
|
|
var formatAddress = ctx[CpuRegister.Rdx];
|
|
var vaListAddress = ctx[CpuRegister.Rcx];
|
|
|
|
if (!TryReadCString(ctx, formatAddress, 1_048_576, out var formatBytes))
|
|
{
|
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
|
}
|
|
|
|
var format = Encoding.UTF8.GetString(formatBytes);
|
|
if (!TryCreateVaListCursor(ctx, vaListAddress, out var vaCursor))
|
|
{
|
|
return WriteSnprintfOutput(ctx, destination, bufferSize, formatBytes);
|
|
}
|
|
|
|
var rendered = FormatString(ctx, format, ref vaCursor);
|
|
|
|
return WriteSnprintfOutput(ctx, destination, bufferSize, rendered);
|
|
}
|
|
|
|
private static int SwprintfCore(CpuContext ctx)
|
|
{
|
|
var destination = ctx[CpuRegister.Rdi];
|
|
var bufferSize = ctx[CpuRegister.Rsi];
|
|
var formatAddress = ctx[CpuRegister.Rdx];
|
|
|
|
if (!TryReadWideCString(ctx, formatAddress, 1_048_576, out var formatUnits))
|
|
{
|
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
|
}
|
|
|
|
var format = DecodeWideUnits(formatUnits);
|
|
var rendered = FormatString(ctx, format);
|
|
TraceWidePrintf(ctx, "swprintf", destination, bufferSize, format, rendered);
|
|
return WriteSwprintfOutput(ctx, destination, bufferSize, rendered);
|
|
}
|
|
|
|
private static int VswprintfCore(CpuContext ctx)
|
|
{
|
|
var destination = ctx[CpuRegister.Rdi];
|
|
var bufferSize = ctx[CpuRegister.Rsi];
|
|
var formatAddress = ctx[CpuRegister.Rdx];
|
|
var vaListAddress = ctx[CpuRegister.Rcx];
|
|
|
|
if (!TryReadWideCString(ctx, formatAddress, 1_048_576, out var formatUnits))
|
|
{
|
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
|
}
|
|
|
|
var format = DecodeWideUnits(formatUnits);
|
|
string rendered;
|
|
if (!TryCreateVaListCursor(ctx, vaListAddress, out var vaCursor))
|
|
{
|
|
rendered = format;
|
|
}
|
|
else
|
|
{
|
|
TraceWidePrintfVaList(ctx, "vswprintf", format, vaListAddress, vaCursor);
|
|
rendered = FormatString(ctx, format, ref vaCursor);
|
|
}
|
|
|
|
TraceWidePrintf(ctx, "vswprintf", destination, bufferSize, format, rendered);
|
|
return WriteSwprintfOutput(ctx, destination, bufferSize, rendered);
|
|
}
|
|
|
|
private static bool TryCreateVaListCursor(CpuContext ctx, ulong vaListAddress, out SysVAmd64VaListCursor cursor)
|
|
{
|
|
cursor = default;
|
|
if (vaListAddress == 0)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
Span<byte> vaList = stackalloc byte[24];
|
|
if (!TryReadCompat(ctx, vaListAddress, vaList))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
cursor = new SysVAmd64VaListCursor(
|
|
ctx,
|
|
vaListAddress,
|
|
BinaryPrimitives.ReadUInt32LittleEndian(vaList),
|
|
BinaryPrimitives.ReadUInt32LittleEndian(vaList[4..]),
|
|
BinaryPrimitives.ReadUInt64LittleEndian(vaList[8..]),
|
|
BinaryPrimitives.ReadUInt64LittleEndian(vaList[16..]));
|
|
return true;
|
|
}
|
|
|
|
private static int WriteSnprintfOutput(
|
|
CpuContext ctx,
|
|
ulong destination,
|
|
ulong bufferSize,
|
|
ReadOnlySpan<byte> outputBytes)
|
|
{
|
|
if (bufferSize != 0 && destination != 0)
|
|
{
|
|
var maxWritable = (int)Math.Min((ulong)int.MaxValue, bufferSize - 1);
|
|
var copyLength = Math.Min(maxWritable, outputBytes.Length);
|
|
if (copyLength > 0 && !ctx.Memory.TryWrite(destination, outputBytes[..copyLength]))
|
|
{
|
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
|
}
|
|
|
|
Span<byte> nullTerminator = stackalloc byte[1];
|
|
if (!ctx.Memory.TryWrite(destination + (ulong)copyLength, nullTerminator))
|
|
{
|
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
|
}
|
|
}
|
|
|
|
ctx[CpuRegister.Rax] = unchecked((ulong)outputBytes.Length);
|
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
|
}
|
|
|
|
private static int WriteSnprintfOutput(
|
|
CpuContext ctx,
|
|
ulong destination,
|
|
ulong bufferSize,
|
|
string output)
|
|
{
|
|
if (bufferSize == 0 || destination == 0)
|
|
{
|
|
ctx[CpuRegister.Rax] = unchecked((ulong)Encoding.UTF8.GetByteCount(output));
|
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
|
}
|
|
|
|
return WriteSnprintfOutput(ctx, destination, bufferSize, Encoding.UTF8.GetBytes(output));
|
|
}
|
|
|
|
private static int WriteSwprintfOutput(
|
|
CpuContext ctx,
|
|
ulong destination,
|
|
ulong bufferSize,
|
|
string rendered)
|
|
{
|
|
if (bufferSize != 0 && destination != 0)
|
|
{
|
|
var maxWritableChars = (int)Math.Min((ulong)int.MaxValue, bufferSize - 1);
|
|
var copyLength = Math.Min(maxWritableChars, rendered.Length);
|
|
if (copyLength > 0)
|
|
{
|
|
var outputBytes = Encoding.Unicode.GetBytes(rendered[..copyLength]);
|
|
if (!TryWriteCompat(ctx, destination, outputBytes))
|
|
{
|
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
|
}
|
|
}
|
|
|
|
Span<byte> nullTerminator = stackalloc byte[WideCharSize];
|
|
if (!TryWriteCompat(ctx, destination + ((ulong)copyLength * WideCharSize), nullTerminator))
|
|
{
|
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
|
}
|
|
}
|
|
|
|
ctx[CpuRegister.Rax] = unchecked((ulong)rendered.Length);
|
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
|
}
|
|
|
|
private static void TraceWidePrintf(CpuContext ctx, string exportName, ulong destination, ulong bufferSize, string format, string rendered)
|
|
{
|
|
if (!string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_WIDE_PRINTF"), "1", StringComparison.Ordinal))
|
|
{
|
|
return;
|
|
}
|
|
|
|
var formatPreview = format.Length > 160 ? format[..160] + "..." : format;
|
|
var renderedPreview = rendered.Length > 160 ? rendered[..160] + "..." : rendered;
|
|
Console.Error.WriteLine(
|
|
$"[LOADER][TRACE] {exportName}: dst=0x{destination:X16} count=0x{bufferSize:X} len={rendered.Length} fmt='{formatPreview}' rendered='{renderedPreview}'");
|
|
}
|
|
|
|
private static void TraceWidePrintfVaList(
|
|
CpuContext ctx,
|
|
string exportName,
|
|
string format,
|
|
ulong vaListAddress,
|
|
SysVAmd64VaListCursor cursor)
|
|
{
|
|
if (!ShouldTraceWidePrintfArgs(format))
|
|
{
|
|
return;
|
|
}
|
|
|
|
Console.Error.WriteLine(
|
|
$"[LOADER][TRACE] {exportName}: va_list=0x{vaListAddress:X16} gp_offset={cursor.GpOffset} fp_offset={cursor.FpOffset} overflow=0x{cursor.OverflowArgArea:X16} reg_save=0x{cursor.RegSaveArea:X16}");
|
|
|
|
if (cursor.RegSaveArea != 0)
|
|
{
|
|
for (var i = 0; i < 6; i++)
|
|
{
|
|
var slotAddress = cursor.RegSaveArea + ((ulong)i * 8);
|
|
var valueText = TryReadUInt64Compat(ctx, slotAddress, out var value)
|
|
? $"0x{value:X16}"
|
|
: "<unreadable>";
|
|
Console.Error.WriteLine(
|
|
$"[LOADER][TRACE] {exportName}: reg_save.gp[{i}] @0x{slotAddress:X16} = {valueText}");
|
|
}
|
|
}
|
|
|
|
if (cursor.OverflowArgArea != 0)
|
|
{
|
|
for (var i = 0; i < 4; i++)
|
|
{
|
|
var slotAddress = cursor.OverflowArgArea + ((ulong)i * 8);
|
|
var valueText = TryReadUInt64Compat(ctx, slotAddress, out var value)
|
|
? $"0x{value:X16}"
|
|
: "<unreadable>";
|
|
Console.Error.WriteLine(
|
|
$"[LOADER][TRACE] {exportName}: overflow[{i}] @0x{slotAddress:X16} = {valueText}");
|
|
}
|
|
}
|
|
}
|
|
|
|
private static bool ShouldTraceWidePrintfArgs(string format)
|
|
{
|
|
if (!string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_WIDE_PRINTF_ARGS"), "1", StringComparison.Ordinal))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var filter = Environment.GetEnvironmentVariable("SHARPEMU_LOG_WIDE_PRINTF_FILTER");
|
|
return string.IsNullOrEmpty(filter) || format.Contains(filter, StringComparison.Ordinal);
|
|
}
|
|
|
|
private static void TracePrintfStringArgument(CpuContext ctx, string lengthMod, ulong address)
|
|
{
|
|
if (!ShouldTraceWidePrintfArgs(lengthMod == "l" ? "%ls" : "%s"))
|
|
{
|
|
return;
|
|
}
|
|
|
|
var widePreview = "<not-wide>";
|
|
if (address != 0 && TryReadWideCString(ctx, address, 64, out var wideUnits))
|
|
{
|
|
widePreview = SanitizeTracePreview(DecodeWideUnits(wideUnits), 64);
|
|
}
|
|
else if (address == 0)
|
|
{
|
|
widePreview = "<null>";
|
|
}
|
|
else
|
|
{
|
|
widePreview = "<unreadable>";
|
|
}
|
|
|
|
var utf8Preview = "<null>";
|
|
if (address != 0 && TryReadCString(ctx, address, 128, out var utf8Bytes))
|
|
{
|
|
utf8Preview = SanitizeTracePreview(Encoding.UTF8.GetString(utf8Bytes), 64);
|
|
}
|
|
else if (address != 0)
|
|
{
|
|
utf8Preview = "<unreadable>";
|
|
}
|
|
|
|
var rawPreview = "<null>";
|
|
if (address != 0)
|
|
{
|
|
Span<byte> raw = stackalloc byte[32];
|
|
rawPreview = TryReadCompat(ctx, address, raw)
|
|
? Convert.ToHexString(raw).ToLowerInvariant()
|
|
: "<unreadable>";
|
|
}
|
|
|
|
Console.Error.WriteLine(
|
|
$"[LOADER][TRACE] printf-arg %{lengthMod}s addr=0x{address:X16} wide='{widePreview}' utf8='{utf8Preview}' raw={rawPreview}");
|
|
}
|
|
|
|
private static string SanitizeTracePreview(string value, int maxLength)
|
|
{
|
|
if (string.IsNullOrEmpty(value))
|
|
{
|
|
return value;
|
|
}
|
|
|
|
var sanitized = value
|
|
.Replace("\r", "\\r", StringComparison.Ordinal)
|
|
.Replace("\n", "\\n", StringComparison.Ordinal)
|
|
.Replace("\t", "\\t", StringComparison.Ordinal);
|
|
return sanitized.Length > maxLength
|
|
? sanitized[..maxLength] + "..."
|
|
: sanitized;
|
|
}
|
|
|
|
private readonly record struct PosixTm(
|
|
int Second,
|
|
int Minute,
|
|
int Hour,
|
|
int Day,
|
|
int Month,
|
|
int Year,
|
|
int WeekDay,
|
|
int YearDay,
|
|
int IsDst)
|
|
{
|
|
public DateTime ToDateTime()
|
|
{
|
|
var year = Math.Clamp(1900 + Year, 1, 9999);
|
|
var month = Math.Clamp(Month + 1, 1, 12);
|
|
var day = Math.Clamp(Day, 1, DateTime.DaysInMonth(year, month));
|
|
var hour = Math.Clamp(Hour, 0, 23);
|
|
var minute = Math.Clamp(Minute, 0, 59);
|
|
var second = Math.Clamp(Second, 0, 60);
|
|
if (second == 60)
|
|
{
|
|
second = 59;
|
|
}
|
|
|
|
return new DateTime(year, month, day, hour, minute, second, DateTimeKind.Unspecified);
|
|
}
|
|
}
|
|
|
|
private static bool TryReadPosixTm(CpuContext ctx, ulong address, out PosixTm tm)
|
|
{
|
|
tm = default;
|
|
if (address == 0)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
Span<byte> bytes = stackalloc byte[9 * sizeof(int)];
|
|
if (!TryReadCompat(ctx, address, bytes))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
tm = new PosixTm(
|
|
BinaryPrimitives.ReadInt32LittleEndian(bytes[0..4]),
|
|
BinaryPrimitives.ReadInt32LittleEndian(bytes[4..8]),
|
|
BinaryPrimitives.ReadInt32LittleEndian(bytes[8..12]),
|
|
BinaryPrimitives.ReadInt32LittleEndian(bytes[12..16]),
|
|
BinaryPrimitives.ReadInt32LittleEndian(bytes[16..20]),
|
|
BinaryPrimitives.ReadInt32LittleEndian(bytes[20..24]),
|
|
BinaryPrimitives.ReadInt32LittleEndian(bytes[24..28]),
|
|
BinaryPrimitives.ReadInt32LittleEndian(bytes[28..32]),
|
|
BinaryPrimitives.ReadInt32LittleEndian(bytes[32..36]));
|
|
return true;
|
|
}
|
|
|
|
private static string FormatWcsftime(string format, PosixTm tm)
|
|
{
|
|
var timestamp = tm.ToDateTime();
|
|
var culture = CultureInfo.InvariantCulture;
|
|
var builder = new StringBuilder(format.Length + 16);
|
|
for (var i = 0; i < format.Length; i++)
|
|
{
|
|
var ch = format[i];
|
|
if (ch != '%' || i + 1 >= format.Length)
|
|
{
|
|
builder.Append(ch);
|
|
continue;
|
|
}
|
|
|
|
i++;
|
|
builder.Append(format[i] switch
|
|
{
|
|
'%' => "%",
|
|
'a' => timestamp.ToString("ddd", culture),
|
|
'A' => timestamp.ToString("dddd", culture),
|
|
'b' or 'h' => timestamp.ToString("MMM", culture),
|
|
'B' => timestamp.ToString("MMMM", culture),
|
|
'c' => timestamp.ToString("ddd MMM dd HH:mm:ss yyyy", culture),
|
|
'd' => timestamp.ToString("dd", culture),
|
|
'e' => timestamp.Day.ToString().PadLeft(2, ' '),
|
|
'F' => timestamp.ToString("yyyy-MM-dd", culture),
|
|
'H' => timestamp.ToString("HH", culture),
|
|
'I' => timestamp.ToString("hh", culture),
|
|
'j' => timestamp.DayOfYear.ToString("D3", culture),
|
|
'm' => timestamp.ToString("MM", culture),
|
|
'M' => timestamp.ToString("mm", culture),
|
|
'n' => "\n",
|
|
'p' => timestamp.ToString("tt", culture),
|
|
'R' => timestamp.ToString("HH:mm", culture),
|
|
'S' => timestamp.ToString("ss", culture),
|
|
't' => "\t",
|
|
'T' => timestamp.ToString("HH:mm:ss", culture),
|
|
'u' => (((int)timestamp.DayOfWeek + 6) % 7 + 1).ToString(culture),
|
|
'w' => ((int)timestamp.DayOfWeek).ToString(culture),
|
|
'x' => timestamp.ToString("MM/dd/yy", culture),
|
|
'X' => timestamp.ToString("HH:mm:ss", culture),
|
|
'y' => timestamp.ToString("yy", culture),
|
|
'Y' => timestamp.ToString("yyyy", culture),
|
|
'Z' => tm.IsDst == 0 ? "UTC" : "DST",
|
|
'z' => "+0000",
|
|
_ => "%" + format[i]
|
|
});
|
|
}
|
|
|
|
return builder.ToString();
|
|
}
|
|
|
|
private static void TraceLibcAllocation(
|
|
CpuContext ctx,
|
|
string operation,
|
|
ulong size,
|
|
ulong alignment = 0,
|
|
ulong count = 0,
|
|
ulong existingAddress = 0,
|
|
ulong resultAddress = 0,
|
|
int? errorCode = null)
|
|
{
|
|
if (!string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_LIBC_ALLOC"), "1", StringComparison.Ordinal))
|
|
{
|
|
return;
|
|
}
|
|
|
|
var returnRip = 0UL;
|
|
var stackPointer = ctx[CpuRegister.Rsp];
|
|
if (stackPointer != 0)
|
|
{
|
|
_ = ctx.TryReadUInt64(stackPointer, out returnRip);
|
|
}
|
|
|
|
Console.Error.WriteLine(
|
|
$"[LOADER][TRACE] {operation}: ret=0x{returnRip:X16} size=0x{size:X16} count=0x{count:X16} align=0x{alignment:X16} in=0x{existingAddress:X16} result=0x{resultAddress:X16} errno={(errorCode.HasValue ? errorCode.Value : 0)}");
|
|
}
|
|
|
|
internal static string FormatStringFromVarArgs(CpuContext ctx, string format, int firstGpArgIndex)
|
|
{
|
|
var argumentSource = new RegisterPrintfArgumentSource(ctx, Math.Max(0, firstGpArgIndex));
|
|
return FormatString(ctx, format, ref argumentSource);
|
|
}
|
|
|
|
private static string FormatString(CpuContext ctx, string format)
|
|
{
|
|
return FormatStringFromVarArgs(ctx, format, firstGpArgIndex: 3);
|
|
}
|
|
|
|
private static string FormatString<TArgumentSource>(
|
|
CpuContext ctx,
|
|
string format,
|
|
ref TArgumentSource argumentSource)
|
|
where TArgumentSource : struct, IPrintfArgumentSource
|
|
{
|
|
var sb = new StringBuilder(format.Length + 32);
|
|
|
|
for (var i = 0; i < format.Length; i++)
|
|
{
|
|
if (format[i] != '%')
|
|
{
|
|
sb.Append(format[i]);
|
|
continue;
|
|
}
|
|
|
|
i++;
|
|
if (i >= format.Length)
|
|
{
|
|
sb.Append('%');
|
|
break;
|
|
}
|
|
|
|
var leftAlign = false;
|
|
var showSign = false;
|
|
var spaceForSign = false;
|
|
var padWithZero = false;
|
|
var alternateForm = false;
|
|
|
|
while (i < format.Length)
|
|
{
|
|
switch (format[i])
|
|
{
|
|
case '-': leftAlign = true; i++; continue;
|
|
case '+': showSign = true; i++; continue;
|
|
case ' ': spaceForSign = true; i++; continue;
|
|
case '0': padWithZero = true; i++; continue;
|
|
case '#': alternateForm = true; i++; continue;
|
|
}
|
|
break;
|
|
}
|
|
|
|
var width = 0;
|
|
if (i < format.Length && format[i] == '*')
|
|
{
|
|
width = unchecked((int)argumentSource.NextGpArg());
|
|
i++;
|
|
if (width < 0)
|
|
{
|
|
leftAlign = true;
|
|
width = -width;
|
|
}
|
|
}
|
|
else if (i < format.Length && char.IsDigit(format[i]))
|
|
{
|
|
while (i < format.Length && char.IsDigit(format[i]))
|
|
{
|
|
width = width * 10 + (format[i] - '0');
|
|
i++;
|
|
}
|
|
}
|
|
|
|
var precision = -1;
|
|
if (i < format.Length && format[i] == '.')
|
|
{
|
|
i++;
|
|
if (i < format.Length && format[i] == '*')
|
|
{
|
|
precision = unchecked((int)argumentSource.NextGpArg());
|
|
i++;
|
|
}
|
|
else if (i < format.Length && char.IsDigit(format[i]))
|
|
{
|
|
precision = 0;
|
|
while (i < format.Length && char.IsDigit(format[i]))
|
|
{
|
|
precision = precision * 10 + (format[i] - '0');
|
|
i++;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
precision = 0;
|
|
}
|
|
}
|
|
|
|
var lengthMod = "";
|
|
if (i < format.Length)
|
|
{
|
|
if (i + 1 < format.Length &&
|
|
((format[i] == 'h' && format[i + 1] == 'h') ||
|
|
(format[i] == 'l' && format[i + 1] == 'l')))
|
|
{
|
|
lengthMod = format.Substring(i, 2);
|
|
i += 2;
|
|
}
|
|
else if (format[i] is 'h' or 'l' or 'j' or 'z' or 't' or 'L')
|
|
{
|
|
lengthMod = format[i].ToString();
|
|
i++;
|
|
}
|
|
}
|
|
|
|
if (i >= format.Length)
|
|
{
|
|
sb.Append('%');
|
|
break;
|
|
}
|
|
|
|
var specifier = format[i];
|
|
|
|
switch (specifier)
|
|
{
|
|
case '%':
|
|
sb.Append('%');
|
|
break;
|
|
|
|
case 'd':
|
|
case 'i':
|
|
{
|
|
long value = lengthMod switch
|
|
{
|
|
"hh" => unchecked((sbyte)argumentSource.NextGpArg()),
|
|
"h" => unchecked((short)argumentSource.NextGpArg()),
|
|
"l" => unchecked((long)argumentSource.NextGpArg()),
|
|
"ll" => unchecked((long)argumentSource.NextGpArg()),
|
|
"j" => unchecked((long)argumentSource.NextGpArg()),
|
|
"z" => unchecked((long)argumentSource.NextGpArg()),
|
|
"t" => unchecked((long)argumentSource.NextGpArg()),
|
|
_ => unchecked((int)argumentSource.NextGpArg())
|
|
};
|
|
|
|
var formatted = value.ToString();
|
|
if (showSign && value >= 0)
|
|
formatted = "+" + formatted;
|
|
else if (spaceForSign && value >= 0)
|
|
formatted = " " + formatted;
|
|
|
|
sb.Append(PadString(formatted, width, leftAlign, padWithZero && !leftAlign));
|
|
}
|
|
break;
|
|
|
|
case 'u':
|
|
{
|
|
ulong value = lengthMod switch
|
|
{
|
|
"hh" => (byte)argumentSource.NextGpArg(),
|
|
"h" => (ushort)argumentSource.NextGpArg(),
|
|
"l" => argumentSource.NextGpArg(),
|
|
"ll" => argumentSource.NextGpArg(),
|
|
"j" => argumentSource.NextGpArg(),
|
|
"z" => argumentSource.NextGpArg(),
|
|
"t" => argumentSource.NextGpArg(),
|
|
_ => (uint)argumentSource.NextGpArg()
|
|
};
|
|
|
|
var formatted = value.ToString();
|
|
sb.Append(PadString(formatted, width, leftAlign, padWithZero && !leftAlign));
|
|
}
|
|
break;
|
|
|
|
case 'x':
|
|
case 'X':
|
|
{
|
|
ulong value = lengthMod switch
|
|
{
|
|
"hh" => (byte)argumentSource.NextGpArg(),
|
|
"h" => (ushort)argumentSource.NextGpArg(),
|
|
"l" => argumentSource.NextGpArg(),
|
|
"ll" => argumentSource.NextGpArg(),
|
|
"j" => argumentSource.NextGpArg(),
|
|
"z" => argumentSource.NextGpArg(),
|
|
"t" => argumentSource.NextGpArg(),
|
|
_ => (uint)argumentSource.NextGpArg()
|
|
};
|
|
|
|
var formatted = specifier == 'x'
|
|
? value.ToString("x")
|
|
: value.ToString("X");
|
|
|
|
if (alternateForm && value != 0)
|
|
formatted = specifier == 'x' ? "0x" + formatted : "0X" + formatted;
|
|
|
|
sb.Append(PadString(formatted, width, leftAlign, padWithZero && !leftAlign));
|
|
}
|
|
break;
|
|
|
|
case 'o':
|
|
{
|
|
ulong value = lengthMod switch
|
|
{
|
|
"hh" => (byte)argumentSource.NextGpArg(),
|
|
"h" => (ushort)argumentSource.NextGpArg(),
|
|
"l" => argumentSource.NextGpArg(),
|
|
"ll" => argumentSource.NextGpArg(),
|
|
"j" => argumentSource.NextGpArg(),
|
|
"z" => argumentSource.NextGpArg(),
|
|
"t" => argumentSource.NextGpArg(),
|
|
_ => (uint)argumentSource.NextGpArg()
|
|
};
|
|
|
|
var formatted = Convert.ToString((long)value, 8);
|
|
if (alternateForm && value != 0)
|
|
formatted = "0" + formatted;
|
|
|
|
sb.Append(PadString(formatted, width, leftAlign, padWithZero && !leftAlign));
|
|
}
|
|
break;
|
|
|
|
case 'p':
|
|
{
|
|
var value = argumentSource.NextGpArg();
|
|
var formatted = value == 0
|
|
? "(nil)"
|
|
: $"0x{value:X}";
|
|
sb.Append(formatted);
|
|
}
|
|
break;
|
|
|
|
case 's':
|
|
{
|
|
var strAddr = argumentSource.NextGpArg();
|
|
TracePrintfStringArgument(ctx, lengthMod, strAddr);
|
|
if (strAddr == 0)
|
|
{
|
|
sb.Append("(null)");
|
|
}
|
|
else if (lengthMod == "l")
|
|
{
|
|
if (TryReadWideCString(ctx, strAddr, 1_048_576, out var wideUnits))
|
|
{
|
|
var str = DecodeWideUnits(wideUnits);
|
|
if (precision >= 0 && str.Length > precision)
|
|
str = str.Substring(0, precision);
|
|
sb.Append(PadString(str, width, leftAlign, false));
|
|
}
|
|
else
|
|
{
|
|
sb.Append("(null)");
|
|
}
|
|
}
|
|
else if (TryReadCString(ctx, strAddr, 1_048_576, out var strBytes))
|
|
{
|
|
var str = Encoding.UTF8.GetString(strBytes);
|
|
if (precision >= 0 && str.Length > precision)
|
|
str = str.Substring(0, precision);
|
|
sb.Append(PadString(str, width, leftAlign, false));
|
|
}
|
|
else
|
|
{
|
|
sb.Append("(null)");
|
|
}
|
|
}
|
|
break;
|
|
|
|
case 'c':
|
|
{
|
|
string renderedChar;
|
|
if (lengthMod == "l")
|
|
{
|
|
var scalar = unchecked((ushort)argumentSource.NextGpArg());
|
|
renderedChar = TryConvertWideScalarToString(scalar, out var wideCharText)
|
|
? wideCharText
|
|
: "?";
|
|
}
|
|
else
|
|
{
|
|
renderedChar = ((char)(byte)argumentSource.NextGpArg()).ToString();
|
|
}
|
|
|
|
sb.Append(PadString(renderedChar, width, leftAlign, false));
|
|
}
|
|
break;
|
|
|
|
case 'f':
|
|
case 'F':
|
|
case 'e':
|
|
case 'E':
|
|
case 'g':
|
|
case 'G':
|
|
{
|
|
var value = argumentSource.NextFloatArg();
|
|
|
|
var formatStr = precision >= 0
|
|
? $"{{0:{specifier}{precision}}}"
|
|
: $"{{0:{specifier}}}";
|
|
var formatted = string.Format(formatStr, value);
|
|
|
|
if (showSign && value >= 0)
|
|
formatted = "+" + formatted;
|
|
else if (spaceForSign && value >= 0)
|
|
formatted = " " + formatted;
|
|
|
|
sb.Append(PadString(formatted, width, leftAlign, padWithZero && !leftAlign));
|
|
}
|
|
break;
|
|
|
|
case 'n':
|
|
{
|
|
var addr = argumentSource.NextGpArg();
|
|
if (addr != 0)
|
|
{
|
|
_ = ctx.TryWriteInt32(addr, sb.Length);
|
|
}
|
|
}
|
|
break;
|
|
|
|
default:
|
|
sb.Append('%');
|
|
sb.Append(specifier);
|
|
break;
|
|
}
|
|
}
|
|
|
|
return sb.ToString();
|
|
}
|
|
|
|
private static ulong ReadStackArg(CpuContext ctx, ulong offset)
|
|
{
|
|
var rsp = ctx[CpuRegister.Rsp];
|
|
if (!ctx.TryReadUInt64(rsp + offset + 8, out var value)) // +8 to skip return address
|
|
{
|
|
return 0;
|
|
}
|
|
return value;
|
|
}
|
|
|
|
private static string PadString(string str, int width, bool leftAlign, bool padWithZero)
|
|
{
|
|
if (width <= str.Length)
|
|
return str;
|
|
|
|
var padChar = padWithZero ? '0' : ' ';
|
|
var padLength = width - str.Length;
|
|
var padding = new string(padChar, padLength);
|
|
|
|
return leftAlign ? str + padding : padding + str;
|
|
}
|
|
|
|
private interface IPrintfArgumentSource
|
|
{
|
|
ulong NextGpArg();
|
|
|
|
double NextFloatArg();
|
|
}
|
|
|
|
private struct RegisterPrintfArgumentSource : IPrintfArgumentSource
|
|
{
|
|
// SysV AMD64: variadic integer/pointer args use the GP registers (rdi..r9, up to
|
|
// 6) and variadic float/double args use xmm0..xmm7 (8) — INDEPENDENT counters.
|
|
// Args beyond the registers spill to the stack in source order, so GP-overflow
|
|
// and FP-overflow share one stack cursor.
|
|
private readonly CpuContext _ctx;
|
|
private int _gpIndex;
|
|
private int _fpIndex;
|
|
private int _stackIndex;
|
|
|
|
public RegisterPrintfArgumentSource(CpuContext ctx, int gpIndex)
|
|
{
|
|
_ctx = ctx;
|
|
_gpIndex = gpIndex;
|
|
_fpIndex = 0;
|
|
_stackIndex = 0;
|
|
}
|
|
|
|
public ulong NextGpArg()
|
|
{
|
|
var index = _gpIndex;
|
|
if (index < 6)
|
|
{
|
|
_gpIndex++;
|
|
return index switch
|
|
{
|
|
0 => _ctx[CpuRegister.Rdi],
|
|
1 => _ctx[CpuRegister.Rsi],
|
|
2 => _ctx[CpuRegister.Rdx],
|
|
3 => _ctx[CpuRegister.Rcx],
|
|
4 => _ctx[CpuRegister.R8],
|
|
_ => _ctx[CpuRegister.R9],
|
|
};
|
|
}
|
|
|
|
return ReadStackArg(_ctx, (ulong)(_stackIndex++) * 8);
|
|
}
|
|
|
|
public double NextFloatArg()
|
|
{
|
|
// Float/double variadic args live in xmm0..xmm7 (low 64 bits), NOT the GP
|
|
// registers. Reading them from GP prints garbage and desynchronizes every
|
|
// subsequent argument.
|
|
ulong bits;
|
|
if (_fpIndex < 8)
|
|
{
|
|
_ctx.GetXmmRegister(_fpIndex++, out bits, out _);
|
|
}
|
|
else
|
|
{
|
|
bits = ReadStackArg(_ctx, (ulong)(_stackIndex++) * 8);
|
|
}
|
|
|
|
return BitConverter.Int64BitsToDouble(unchecked((long)bits));
|
|
}
|
|
}
|
|
|
|
private struct SysVAmd64VaListCursor : IPrintfArgumentSource
|
|
{
|
|
private const uint GpSaveAreaLimit = 48;
|
|
private const uint FpSaveAreaLimit = 176;
|
|
|
|
private readonly CpuContext _ctx;
|
|
private readonly ulong _vaListAddress;
|
|
private uint _gpOffset;
|
|
private uint _fpOffset;
|
|
private ulong _overflowArgArea;
|
|
private readonly ulong _regSaveArea;
|
|
|
|
public SysVAmd64VaListCursor(
|
|
CpuContext ctx,
|
|
ulong vaListAddress,
|
|
uint gpOffset,
|
|
uint fpOffset,
|
|
ulong overflowArgArea,
|
|
ulong regSaveArea)
|
|
{
|
|
_ctx = ctx;
|
|
_vaListAddress = vaListAddress;
|
|
_gpOffset = gpOffset;
|
|
_fpOffset = fpOffset;
|
|
_overflowArgArea = overflowArgArea;
|
|
_regSaveArea = regSaveArea;
|
|
}
|
|
|
|
public ulong NextGpArg()
|
|
{
|
|
ulong readAddress;
|
|
if (_regSaveArea != 0 && _gpOffset <= GpSaveAreaLimit - 8)
|
|
{
|
|
readAddress = _regSaveArea + _gpOffset;
|
|
_gpOffset += 8;
|
|
}
|
|
else
|
|
{
|
|
readAddress = _overflowArgArea;
|
|
_overflowArgArea += 8;
|
|
}
|
|
|
|
return TryReadUInt64Compat(_ctx, readAddress, out var value) ? value : 0;
|
|
}
|
|
|
|
public double NextFloatArg()
|
|
{
|
|
ulong readAddress;
|
|
if (_regSaveArea != 0 && _fpOffset <= FpSaveAreaLimit - 16)
|
|
{
|
|
readAddress = _regSaveArea + _fpOffset;
|
|
_fpOffset += 16;
|
|
}
|
|
else
|
|
{
|
|
readAddress = _overflowArgArea;
|
|
_overflowArgArea += 8;
|
|
}
|
|
|
|
return TryReadUInt64Compat(_ctx, readAddress, out var rawBits)
|
|
? BitConverter.Int64BitsToDouble(unchecked((long)rawBits))
|
|
: 0.0;
|
|
}
|
|
|
|
public uint GpOffset => _gpOffset;
|
|
|
|
public uint FpOffset => _fpOffset;
|
|
|
|
public ulong OverflowArgArea => _overflowArgArea;
|
|
|
|
public ulong RegSaveArea => _regSaveArea;
|
|
}
|
|
|
|
private static ulong AllocateMappedGuestAddress(CpuContext ctx, ulong length, ulong alignment)
|
|
{
|
|
if (length == 0)
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
var effectiveAlignment = alignment == 0 ? 0x1000UL : alignment;
|
|
if (_nextVirtualAddress == 0)
|
|
{
|
|
_nextVirtualAddress = 0x0100_0000UL;
|
|
}
|
|
|
|
var probeCandidates = new[]
|
|
{
|
|
8UL * 1024 * 1024,
|
|
2UL * 1024 * 1024,
|
|
512UL * 1024,
|
|
128UL * 1024,
|
|
0x1000UL,
|
|
};
|
|
|
|
foreach (var probeCandidate in probeCandidates)
|
|
{
|
|
var cursor = AlignUp(_nextVirtualAddress, effectiveAlignment);
|
|
for (var i = 0; i < 0x4000; i++)
|
|
{
|
|
if (IsMappedGuestRangeAvailable(ctx, cursor, length, probeCandidate))
|
|
{
|
|
_nextVirtualAddress = cursor + length;
|
|
return cursor;
|
|
}
|
|
|
|
cursor = AlignUp(cursor + 0x1000UL, effectiveAlignment);
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
private static bool TryReserveGuestVirtualRange(
|
|
CpuContext ctx,
|
|
ulong desiredAddress,
|
|
ulong length,
|
|
int protection,
|
|
ulong alignment,
|
|
out ulong mappedAddress)
|
|
{
|
|
var executable = (protection & OrbisProtCpuExec) != 0;
|
|
return KernelVirtualRangeAllocator.TryReserve(
|
|
ctx,
|
|
desiredAddress,
|
|
length,
|
|
executable,
|
|
alignment,
|
|
allowSearch: true,
|
|
allowAllocateAtAlternative: false,
|
|
"reserve range",
|
|
out mappedAddress);
|
|
}
|
|
|
|
private static bool TryReserveExactGuestVirtualRange(
|
|
CpuContext ctx,
|
|
ulong desiredAddress,
|
|
ulong length,
|
|
int protection)
|
|
{
|
|
var executable = (protection & OrbisProtCpuExec) != 0;
|
|
return KernelVirtualRangeAllocator.TryReserve(
|
|
ctx,
|
|
desiredAddress,
|
|
length,
|
|
executable,
|
|
alignment: 0,
|
|
allowSearch: false,
|
|
allowAllocateAtAlternative: false,
|
|
"reserve fixed range",
|
|
out _);
|
|
}
|
|
|
|
private static bool IsGuestRangeBacked(CpuContext ctx, ulong address, ulong length)
|
|
{
|
|
if (address == 0 || length == 0 || ulong.MaxValue - address < length - 1)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
Span<byte> probe = stackalloc byte[1];
|
|
return ctx.Memory.TryRead(address, probe) &&
|
|
ctx.Memory.TryRead(address + length - 1, probe);
|
|
}
|
|
|
|
private static bool IsMappedGuestRangeAvailable(
|
|
CpuContext ctx,
|
|
ulong address,
|
|
ulong length,
|
|
ulong minimumReadableSpan)
|
|
{
|
|
if (length == 0)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (ulong.MaxValue - address < length - 1)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var end = address + length - 1;
|
|
foreach (var region in _mappedRegions.Values)
|
|
{
|
|
var regionEnd = region.Address + region.Length - 1;
|
|
if (address <= regionEnd && end >= region.Address)
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
var probeLength = Math.Min(length, Math.Max(0x1000UL, minimumReadableSpan));
|
|
var probeEnd = address + probeLength - 1;
|
|
Span<byte> probe = stackalloc byte[1];
|
|
return ctx.Memory.TryRead(address, probe) &&
|
|
ctx.Memory.TryRead(probeEnd, probe);
|
|
}
|
|
|
|
private static FileAccess ResolveOpenAccess(int flags)
|
|
{
|
|
if ((flags & O_RDWR) == O_RDWR)
|
|
{
|
|
return FileAccess.ReadWrite;
|
|
}
|
|
|
|
if ((flags & O_WRONLY) == O_WRONLY)
|
|
{
|
|
return FileAccess.Write;
|
|
}
|
|
|
|
return FileAccess.Read;
|
|
}
|
|
|
|
private static FileMode ResolveOpenMode(int flags, FileAccess access)
|
|
{
|
|
var create = (flags & O_CREAT) != 0;
|
|
var truncate = (flags & O_TRUNC) != 0;
|
|
if (create && truncate)
|
|
{
|
|
return FileMode.Create;
|
|
}
|
|
|
|
if (create)
|
|
{
|
|
return FileMode.OpenOrCreate;
|
|
}
|
|
|
|
if (truncate)
|
|
{
|
|
return access == FileAccess.Read ? FileMode.Open : FileMode.Truncate;
|
|
}
|
|
|
|
return FileMode.Open;
|
|
}
|
|
|
|
private static string ResolveGuestPath(string guestPath)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(guestPath))
|
|
{
|
|
return guestPath;
|
|
}
|
|
|
|
if (TryResolveRegisteredGuestMount(guestPath, out var mountedPath))
|
|
{
|
|
return mountedPath;
|
|
}
|
|
|
|
if (guestPath.StartsWith("/devlog/app/", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
var relative = NormalizeMountRelativePath(guestPath["/devlog/app/".Length..]);
|
|
return Path.Combine(ResolveDevlogAppRoot(), relative);
|
|
}
|
|
|
|
if (guestPath.StartsWith("devlog/app/", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
var relative = NormalizeMountRelativePath(guestPath["devlog/app/".Length..]);
|
|
return Path.Combine(ResolveDevlogAppRoot(), relative);
|
|
}
|
|
|
|
if (string.Equals(guestPath, "/devlog/app", StringComparison.OrdinalIgnoreCase) ||
|
|
string.Equals(guestPath, "devlog/app", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
return ResolveDevlogAppRoot();
|
|
}
|
|
|
|
if (guestPath.StartsWith("/temp0/", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
var relative = NormalizeMountRelativePath(guestPath["/temp0/".Length..]);
|
|
return Path.Combine(ResolveTemp0Root(), relative);
|
|
}
|
|
|
|
if (string.Equals(guestPath, "/temp0", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
return ResolveTemp0Root();
|
|
}
|
|
|
|
if (guestPath.StartsWith("/download0/", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
var relative = NormalizeMountRelativePath(guestPath["/download0/".Length..]);
|
|
return Path.Combine(ResolveDownload0Root(), relative);
|
|
}
|
|
|
|
if (guestPath.StartsWith("download0/", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
var relative = NormalizeMountRelativePath(guestPath["download0/".Length..]);
|
|
return Path.Combine(ResolveDownload0Root(), relative);
|
|
}
|
|
|
|
if (string.Equals(guestPath, "/download0", StringComparison.OrdinalIgnoreCase) ||
|
|
string.Equals(guestPath, "download0", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
return ResolveDownload0Root();
|
|
}
|
|
|
|
if (guestPath.StartsWith("/hostapp/", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
var relative = NormalizeMountRelativePath(guestPath["/hostapp/".Length..]);
|
|
return Path.Combine(ResolveHostappRoot(), relative);
|
|
}
|
|
|
|
if (guestPath.StartsWith("hostapp/", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
var relative = NormalizeMountRelativePath(guestPath["hostapp/".Length..]);
|
|
return Path.Combine(ResolveHostappRoot(), relative);
|
|
}
|
|
|
|
if (string.Equals(guestPath, "/hostapp", StringComparison.OrdinalIgnoreCase) ||
|
|
string.Equals(guestPath, "hostapp", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
return ResolveHostappRoot();
|
|
}
|
|
|
|
var app0Root = ResolveApp0Root();
|
|
if (!string.IsNullOrWhiteSpace(app0Root))
|
|
{
|
|
if (string.Equals(guestPath, "$", StringComparison.Ordinal) ||
|
|
string.Equals(guestPath, "$/", StringComparison.Ordinal) ||
|
|
string.Equals(guestPath, "$\\", StringComparison.Ordinal))
|
|
{
|
|
return app0Root;
|
|
}
|
|
|
|
if (guestPath.StartsWith("$/", StringComparison.Ordinal) ||
|
|
guestPath.StartsWith("$\\", StringComparison.Ordinal))
|
|
{
|
|
var relative = NormalizeMountRelativePath(guestPath[2..]);
|
|
return Path.Combine(app0Root, relative);
|
|
}
|
|
|
|
if (string.Equals(guestPath, "/app0", StringComparison.OrdinalIgnoreCase) ||
|
|
string.Equals(guestPath, "app0", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
return app0Root;
|
|
}
|
|
|
|
if (guestPath.StartsWith("/app0/", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
var relative = NormalizeMountRelativePath(guestPath["/app0/".Length..]);
|
|
return Path.Combine(app0Root, relative);
|
|
}
|
|
|
|
if (guestPath.StartsWith("app0/", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
var relative = NormalizeMountRelativePath(guestPath["app0/".Length..]);
|
|
return Path.Combine(app0Root, relative);
|
|
}
|
|
|
|
if (!Path.IsPathFullyQualified(guestPath) &&
|
|
!guestPath.StartsWith("/", StringComparison.Ordinal) &&
|
|
!guestPath.StartsWith("\\", StringComparison.Ordinal))
|
|
{
|
|
var relative = guestPath.Replace('/', Path.DirectorySeparatorChar);
|
|
return Path.Combine(app0Root, relative);
|
|
}
|
|
}
|
|
|
|
return guestPath;
|
|
}
|
|
|
|
private static bool TryResolveRegisteredGuestMount(string guestPath, out string hostPath)
|
|
{
|
|
hostPath = string.Empty;
|
|
var normalizedGuestPath = NormalizeGuestStatCachePath(guestPath);
|
|
if (normalizedGuestPath is null)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
string? matchedMountPoint = null;
|
|
string? matchedHostRoot = null;
|
|
lock (_guestMountGate)
|
|
{
|
|
foreach (var (mountPoint, hostRoot) in _guestMounts)
|
|
{
|
|
if ((string.Equals(normalizedGuestPath, mountPoint, StringComparison.OrdinalIgnoreCase) ||
|
|
normalizedGuestPath.StartsWith(mountPoint + "/", StringComparison.OrdinalIgnoreCase)) &&
|
|
(matchedMountPoint is null || mountPoint.Length > matchedMountPoint.Length))
|
|
{
|
|
matchedMountPoint = mountPoint;
|
|
matchedHostRoot = hostRoot;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (matchedMountPoint is null || matchedHostRoot is null)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var relativePath = normalizedGuestPath[matchedMountPoint.Length..].TrimStart('/');
|
|
var candidate = Path.GetFullPath(Path.Combine(
|
|
matchedHostRoot,
|
|
NormalizeMountRelativePath(relativePath)));
|
|
var rootWithSeparator = Path.TrimEndingDirectorySeparator(matchedHostRoot) + Path.DirectorySeparatorChar;
|
|
if (!string.Equals(candidate, matchedHostRoot, StringComparison.OrdinalIgnoreCase) &&
|
|
!candidate.StartsWith(rootWithSeparator, StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
hostPath = candidate;
|
|
return true;
|
|
}
|
|
|
|
private static string? ResolveApp0Root()
|
|
{
|
|
var cached = Volatile.Read(ref _cachedApp0Root);
|
|
if (!string.IsNullOrWhiteSpace(cached))
|
|
{
|
|
return cached;
|
|
}
|
|
|
|
var configured = Environment.GetEnvironmentVariable("SHARPEMU_APP0_DIR");
|
|
if (string.IsNullOrWhiteSpace(configured))
|
|
{
|
|
return null;
|
|
}
|
|
|
|
Interlocked.CompareExchange(ref _cachedApp0Root, configured, null);
|
|
return _cachedApp0Root;
|
|
}
|
|
|
|
private static string NormalizeMountRelativePath(string relativePath)
|
|
{
|
|
return relativePath
|
|
.TrimStart('/', '\\')
|
|
.Replace('/', Path.DirectorySeparatorChar)
|
|
.Replace('\\', Path.DirectorySeparatorChar);
|
|
}
|
|
|
|
private static string ResolveDevlogAppRoot()
|
|
{
|
|
var configuredRoot = Environment.GetEnvironmentVariable("SHARPEMU_DEVLOG_APP_DIR");
|
|
string root;
|
|
if (!string.IsNullOrWhiteSpace(configuredRoot))
|
|
{
|
|
root = Path.GetFullPath(configuredRoot);
|
|
}
|
|
else
|
|
{
|
|
root = Path.GetFullPath(Path.Combine(Environment.CurrentDirectory, "logs", "devlog", "app"));
|
|
}
|
|
|
|
Directory.CreateDirectory(root);
|
|
return root;
|
|
}
|
|
|
|
private static string ResolveTemp0Root()
|
|
{
|
|
const string temp0VariableName = "SHARPEMU_TEMP0_DIR";
|
|
var configuredRoot = Environment.GetEnvironmentVariable(temp0VariableName);
|
|
if (!string.IsNullOrWhiteSpace(configuredRoot))
|
|
{
|
|
return Path.GetFullPath(configuredRoot);
|
|
}
|
|
|
|
var app0Root = Environment.GetEnvironmentVariable("SHARPEMU_APP0_DIR");
|
|
var appName = string.IsNullOrWhiteSpace(app0Root)
|
|
? "default"
|
|
: Path.GetFileName(Path.TrimEndingDirectorySeparator(app0Root));
|
|
if (string.IsNullOrWhiteSpace(appName))
|
|
{
|
|
appName = "default";
|
|
}
|
|
|
|
var invalidChars = Path.GetInvalidFileNameChars();
|
|
appName = new string(appName.Select(ch => invalidChars.Contains(ch) ? '_' : ch).ToArray());
|
|
var root = Path.Combine(Path.GetTempPath(), "SharpEmu", appName, "temp0");
|
|
Environment.SetEnvironmentVariable(temp0VariableName, root);
|
|
return root;
|
|
}
|
|
|
|
private static string ResolveDownload0Root()
|
|
{
|
|
var cached = Volatile.Read(ref _cachedDownload0Root);
|
|
if (!string.IsNullOrWhiteSpace(cached))
|
|
{
|
|
return cached;
|
|
}
|
|
|
|
const string download0VariableName = "SHARPEMU_DOWNLOAD0_DIR";
|
|
var configuredRoot = Environment.GetEnvironmentVariable(download0VariableName);
|
|
string root;
|
|
if (!string.IsNullOrWhiteSpace(configuredRoot))
|
|
{
|
|
root = Path.GetFullPath(configuredRoot);
|
|
}
|
|
else
|
|
{
|
|
root = Path.Combine(GetPerAppWritableRoot(), "download0");
|
|
Environment.SetEnvironmentVariable(download0VariableName, root);
|
|
}
|
|
|
|
Directory.CreateDirectory(root);
|
|
Interlocked.CompareExchange(ref _cachedDownload0Root, root, null);
|
|
return _cachedDownload0Root;
|
|
}
|
|
|
|
private static string ResolveHostappRoot()
|
|
{
|
|
const string hostappVariableName = "SHARPEMU_HOSTAPP_DIR";
|
|
var configuredRoot = Environment.GetEnvironmentVariable(hostappVariableName);
|
|
string root;
|
|
if (!string.IsNullOrWhiteSpace(configuredRoot))
|
|
{
|
|
root = Path.GetFullPath(configuredRoot);
|
|
}
|
|
else
|
|
{
|
|
root = Path.GetFullPath(Path.Combine(Environment.CurrentDirectory, "logs", "hostapp"));
|
|
Environment.SetEnvironmentVariable(hostappVariableName, root);
|
|
}
|
|
|
|
Directory.CreateDirectory(root);
|
|
return root;
|
|
}
|
|
|
|
private static string GetPerAppWritableRoot()
|
|
{
|
|
var app0Root = Environment.GetEnvironmentVariable("SHARPEMU_APP0_DIR");
|
|
var appName = string.IsNullOrWhiteSpace(app0Root)
|
|
? "default"
|
|
: Path.GetFileName(Path.TrimEndingDirectorySeparator(app0Root));
|
|
if (string.IsNullOrWhiteSpace(appName))
|
|
{
|
|
appName = "default";
|
|
}
|
|
|
|
var invalidChars = Path.GetInvalidFileNameChars();
|
|
appName = new string(appName.Select(ch => invalidChars.Contains(ch) ? '_' : ch).ToArray());
|
|
return Path.Combine(Path.GetTempPath(), "SharpEmu", appName);
|
|
}
|
|
|
|
private static void EnsureOpenParentDirectoryExists(string guestPath, string hostPath, int flags)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(hostPath))
|
|
{
|
|
return;
|
|
}
|
|
|
|
var shouldCreateParent =
|
|
(flags & O_CREAT) != 0 ||
|
|
guestPath.StartsWith("/devlog/app/", StringComparison.OrdinalIgnoreCase) ||
|
|
guestPath.StartsWith("devlog/app/", StringComparison.OrdinalIgnoreCase);
|
|
if (!shouldCreateParent)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var parentDirectory = Path.GetDirectoryName(hostPath);
|
|
if (!string.IsNullOrWhiteSpace(parentDirectory))
|
|
{
|
|
Directory.CreateDirectory(parentDirectory);
|
|
}
|
|
}
|
|
|
|
private static bool IsMutatingOpen(int flags) =>
|
|
(flags & (O_WRONLY | O_RDWR | O_CREAT | O_TRUNC | O_APPEND)) != 0;
|
|
|
|
private static bool IsReadOnlyGuestMutationPath(string guestPath)
|
|
{
|
|
var normalized = NormalizeGuestStatCachePath(guestPath);
|
|
return normalized is not null &&
|
|
(string.Equals(normalized, "/app0", StringComparison.OrdinalIgnoreCase) ||
|
|
normalized.StartsWith("/app0/", StringComparison.OrdinalIgnoreCase));
|
|
}
|
|
|
|
private static bool TryReadCString(CpuContext ctx, ulong address, ulong maxLength, out byte[] bytes)
|
|
{
|
|
return TryReadBytesUntilNull(ctx, address, maxLength, 1_048_576, out bytes);
|
|
}
|
|
|
|
private static bool TryReadBytesUntilNull(
|
|
CpuContext ctx,
|
|
ulong address,
|
|
ulong maxLength,
|
|
int hardLimit,
|
|
out byte[] bytes)
|
|
{
|
|
bytes = Array.Empty<byte>();
|
|
if (address == 0)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var limit = (int)Math.Min(maxLength, (ulong)Math.Max(0, hardLimit));
|
|
if (limit == 0)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
const int maxChunkSize = 4096;
|
|
const int inlineChunkSize = 256;
|
|
Span<byte> inlineChunk = stackalloc byte[inlineChunkSize];
|
|
var firstPageRemaining = maxChunkSize - (int)(address & (maxChunkSize - 1));
|
|
var firstReadLength = Math.Min(limit, Math.Min(inlineChunkSize, firstPageRemaining));
|
|
var firstSpan = inlineChunk[..firstReadLength];
|
|
ulong offset = 0;
|
|
if (TryReadCompat(ctx, address, firstSpan))
|
|
{
|
|
var nulIndex = firstSpan.IndexOf((byte)0);
|
|
if (nulIndex >= 0)
|
|
{
|
|
bytes = firstSpan[..nulIndex].ToArray();
|
|
return true;
|
|
}
|
|
|
|
offset = unchecked((ulong)firstReadLength);
|
|
}
|
|
|
|
var chunk = GC.AllocateUninitializedArray<byte>(Math.Min(maxChunkSize, limit));
|
|
var writer = new ArrayBufferWriter<byte>(Math.Min(limit, 256));
|
|
if (offset != 0)
|
|
{
|
|
firstSpan.CopyTo(writer.GetSpan(firstReadLength));
|
|
writer.Advance(firstReadLength);
|
|
}
|
|
|
|
while (offset < (ulong)limit)
|
|
{
|
|
var current = address + offset;
|
|
if (current < address)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var pageRemaining = maxChunkSize - (int)(current & (maxChunkSize - 1));
|
|
var remaining = (int)Math.Min((ulong)limit - offset, (ulong)Math.Min(chunk.Length, pageRemaining));
|
|
var span = chunk.AsSpan(0, remaining);
|
|
if (TryReadCompat(ctx, current, span))
|
|
{
|
|
var nulIndex = span.IndexOf((byte)0);
|
|
var copyLength = nulIndex >= 0 ? nulIndex : remaining;
|
|
if (copyLength > 0)
|
|
{
|
|
span[..copyLength].CopyTo(writer.GetSpan(copyLength));
|
|
writer.Advance(copyLength);
|
|
}
|
|
|
|
if (nulIndex >= 0)
|
|
{
|
|
bytes = writer.WrittenSpan.ToArray();
|
|
return true;
|
|
}
|
|
|
|
offset += (ulong)remaining;
|
|
continue;
|
|
}
|
|
|
|
var one = chunk.AsSpan(0, 1);
|
|
if (!TryReadCompat(ctx, current, one))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (one[0] == 0)
|
|
{
|
|
bytes = writer.WrittenSpan.ToArray();
|
|
return true;
|
|
}
|
|
|
|
one.CopyTo(writer.GetSpan(1));
|
|
writer.Advance(1);
|
|
offset++;
|
|
}
|
|
|
|
bytes = writer.WrittenSpan.ToArray();
|
|
return true;
|
|
}
|
|
|
|
private static bool TryCompareStrings(CpuContext ctx, ulong left, ulong right, ulong limit, out int compare)
|
|
{
|
|
compare = 0;
|
|
if (left == 0 || right == 0)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var max = limit == ulong.MaxValue ? 1_048_576UL : Math.Min(limit, 1_048_576UL);
|
|
Span<byte> leftByte = stackalloc byte[1];
|
|
Span<byte> rightByte = stackalloc byte[1];
|
|
for (ulong i = 0; i < max; i++)
|
|
{
|
|
if (!TryReadCompat(ctx, left + i, leftByte) ||
|
|
!TryReadCompat(ctx, right + i, rightByte))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
compare = leftByte[0] - rightByte[0];
|
|
if (compare != 0 || leftByte[0] == 0 || rightByte[0] == 0)
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
|
|
compare = 0;
|
|
return true;
|
|
}
|
|
|
|
private static bool TryReadWideCString(CpuContext ctx, ulong address, ulong maxLength, out ushort[] units)
|
|
{
|
|
units = Array.Empty<ushort>();
|
|
if (address == 0)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var limit = (int)Math.Min(maxLength, 1_048_576UL);
|
|
var buffer = new List<ushort>(Math.Min(limit, 256));
|
|
for (var i = 0; i < limit; i++)
|
|
{
|
|
if (!TryReadUInt16Compat(ctx, address + ((ulong)i * WideCharSize), out var unit))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (unit == 0)
|
|
{
|
|
units = buffer.ToArray();
|
|
return true;
|
|
}
|
|
|
|
buffer.Add(unit);
|
|
}
|
|
|
|
units = buffer.ToArray();
|
|
return true;
|
|
}
|
|
|
|
private static bool TryReadWideCStringBounded(CpuContext ctx, ulong address, ulong maxLength, out ushort[] units, out bool terminated)
|
|
{
|
|
units = Array.Empty<ushort>();
|
|
terminated = false;
|
|
if (address == 0)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var limit = (int)Math.Min(maxLength, 1_048_576UL);
|
|
var buffer = new List<ushort>(Math.Min(limit, 256));
|
|
for (var i = 0; i < limit; i++)
|
|
{
|
|
if (!TryReadUInt16Compat(ctx, address + ((ulong)i * WideCharSize), out var unit))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (unit == 0)
|
|
{
|
|
terminated = true;
|
|
units = buffer.ToArray();
|
|
return true;
|
|
}
|
|
|
|
buffer.Add(unit);
|
|
}
|
|
|
|
units = buffer.ToArray();
|
|
return true;
|
|
}
|
|
|
|
private static bool TryCompareWideStrings(CpuContext ctx, ulong left, ulong right, ulong limit, out int compare)
|
|
{
|
|
compare = 0;
|
|
if (left == 0 || right == 0)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var max = limit == ulong.MaxValue ? 1_048_576UL : Math.Min(limit, 1_048_576UL);
|
|
for (ulong i = 0; i < max; i++)
|
|
{
|
|
if (!TryReadUInt16Compat(ctx, left + (i * WideCharSize), out var leftUnit) ||
|
|
!TryReadUInt16Compat(ctx, right + (i * WideCharSize), out var rightUnit))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
compare = leftUnit == rightUnit ? 0 : leftUnit < rightUnit ? -1 : 1;
|
|
if (compare != 0 || leftUnit == 0 || rightUnit == 0)
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
|
|
compare = 0;
|
|
return true;
|
|
}
|
|
|
|
private static byte[] EncodeWideUnits(ReadOnlySpan<ushort> units)
|
|
{
|
|
var bytes = new byte[units.Length * WideCharSize];
|
|
for (var i = 0; i < units.Length; i++)
|
|
{
|
|
BinaryPrimitives.WriteUInt16LittleEndian(
|
|
bytes.AsSpan(i * WideCharSize, WideCharSize),
|
|
units[i]);
|
|
}
|
|
|
|
return bytes;
|
|
}
|
|
|
|
private static string DecodeWideUnits(ReadOnlySpan<ushort> units)
|
|
{
|
|
if (units.IsEmpty)
|
|
{
|
|
return string.Empty;
|
|
}
|
|
|
|
return new string(MemoryMarshal.Cast<ushort, char>(units));
|
|
}
|
|
|
|
private static bool TryConvertWideScalarToString(ushort scalar, out string text)
|
|
{
|
|
text = ((char)scalar).ToString();
|
|
return true;
|
|
}
|
|
|
|
private static byte[] EncodeWideUnitsWithTerminator(ReadOnlySpan<ushort> units)
|
|
{
|
|
var bytes = new byte[(units.Length + 1) * WideCharSize];
|
|
EncodeWideUnits(units).CopyTo(bytes, 0);
|
|
return bytes;
|
|
}
|
|
|
|
private static bool TryWriteWideTerminator(CpuContext ctx, ulong address)
|
|
{
|
|
Span<byte> terminator = stackalloc byte[WideCharSize];
|
|
terminator.Clear();
|
|
return TryWriteCompat(ctx, address, terminator);
|
|
}
|
|
|
|
private static bool TryZeroWideDestination(CpuContext ctx, ulong destination, ulong destinationCount)
|
|
{
|
|
return destination == 0 || destinationCount == 0 || TryWriteWideTerminator(ctx, destination);
|
|
}
|
|
|
|
private static bool TryReadNullTerminatedUtf8(CpuContext ctx, ulong address, int maxLength, out string value)
|
|
{
|
|
value = string.Empty;
|
|
if (address == 0 || maxLength <= 0)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (!TryReadBytesUntilNull(ctx, address, (ulong)maxLength, maxLength, out var bytes))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
value = Encoding.UTF8.GetString(bytes);
|
|
return true;
|
|
}
|
|
|
|
private static bool TryResolveAprFilepath(CpuContext ctx, ulong pathListAddress, ulong index, out string guestPath)
|
|
{
|
|
guestPath = string.Empty;
|
|
if (TryReadAprPathPointer(ctx, pathListAddress + (index * sizeof(ulong)), out guestPath))
|
|
{
|
|
return true;
|
|
}
|
|
|
|
if (index != 0)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (TryReadAprPathText(ctx, pathListAddress, out guestPath))
|
|
{
|
|
return true;
|
|
}
|
|
|
|
const ulong scanLimit = 0x40;
|
|
for (ulong offset = 0; offset < scanLimit; offset += sizeof(ulong))
|
|
{
|
|
if (TryReadAprPathPointer(ctx, pathListAddress + offset, out guestPath))
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
private static bool TryReadAprPathPointer(CpuContext ctx, ulong pointerAddress, out string guestPath)
|
|
{
|
|
guestPath = string.Empty;
|
|
if (!ctx.TryReadUInt64(pointerAddress, out var candidatePath) || candidatePath == 0)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
return TryReadAprPathText(ctx, candidatePath, out guestPath);
|
|
}
|
|
|
|
private static bool TryReadAprPathText(CpuContext ctx, ulong address, out string guestPath)
|
|
{
|
|
guestPath = string.Empty;
|
|
if (TryReadNullTerminatedUtf8(ctx, address, MaxGuestStringLength, out guestPath) &&
|
|
!string.IsNullOrWhiteSpace(guestPath))
|
|
{
|
|
return true;
|
|
}
|
|
|
|
if (!TryReadWideCString(ctx, address, MaxGuestStringLength, out var wideUnits))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
guestPath = DecodeWideUnits(wideUnits);
|
|
return !string.IsNullOrWhiteSpace(guestPath);
|
|
}
|
|
|
|
private static bool TryReadCompat(CpuContext ctx, ulong address, Span<byte> destination)
|
|
{
|
|
if (destination.IsEmpty)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
if (ctx.Memory.TryRead(address, destination))
|
|
{
|
|
return true;
|
|
}
|
|
|
|
if (!TryReadHostMemory(address, destination))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var recoveryIndex = Interlocked.Increment(ref _hostMemoryReadFallbackCount);
|
|
if (recoveryIndex <= 8)
|
|
{
|
|
Console.Error.WriteLine(
|
|
$"[LOADER][WARNING] host-read fallback#{recoveryIndex}: addr=0x{address:X16} len=0x{destination.Length:X}");
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
private static bool TryReadUInt32Compat(CpuContext ctx, ulong address, out uint value)
|
|
{
|
|
Span<byte> bytes = stackalloc byte[sizeof(uint)];
|
|
if (!TryReadCompat(ctx, address, bytes))
|
|
{
|
|
value = 0;
|
|
return false;
|
|
}
|
|
|
|
value = BinaryPrimitives.ReadUInt32LittleEndian(bytes);
|
|
return true;
|
|
}
|
|
|
|
private static bool TryReadUInt16Compat(CpuContext ctx, ulong address, out ushort value)
|
|
{
|
|
Span<byte> bytes = stackalloc byte[sizeof(ushort)];
|
|
if (!TryReadCompat(ctx, address, bytes))
|
|
{
|
|
value = 0;
|
|
return false;
|
|
}
|
|
|
|
value = BinaryPrimitives.ReadUInt16LittleEndian(bytes);
|
|
return true;
|
|
}
|
|
|
|
internal static bool TryReadUInt64Compat(CpuContext ctx, ulong address, out ulong value)
|
|
{
|
|
Span<byte> bytes = stackalloc byte[sizeof(ulong)];
|
|
if (!TryReadCompat(ctx, address, bytes))
|
|
{
|
|
value = 0;
|
|
return false;
|
|
}
|
|
|
|
value = BinaryPrimitives.ReadUInt64LittleEndian(bytes);
|
|
return true;
|
|
}
|
|
|
|
private static bool TryWriteCompat(CpuContext ctx, ulong address, ReadOnlySpan<byte> source)
|
|
{
|
|
if (source.IsEmpty)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
if (ctx.Memory.TryWrite(address, source))
|
|
{
|
|
return true;
|
|
}
|
|
|
|
if (!TryWriteHostMemory(address, source))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var recoveryIndex = Interlocked.Increment(ref _hostMemoryWriteFallbackCount);
|
|
if (recoveryIndex <= 8)
|
|
{
|
|
Console.Error.WriteLine(
|
|
$"[LOADER][WARNING] host-write fallback#{recoveryIndex}: addr=0x{address:X16} len=0x{source.Length:X}");
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
private static bool TryWriteUInt32Compat(CpuContext ctx, ulong address, uint value)
|
|
{
|
|
Span<byte> bytes = stackalloc byte[sizeof(uint)];
|
|
BinaryPrimitives.WriteUInt32LittleEndian(bytes, value);
|
|
return TryWriteCompat(ctx, address, bytes);
|
|
}
|
|
|
|
internal static bool TryWriteUInt64Compat(CpuContext ctx, ulong address, ulong value)
|
|
{
|
|
Span<byte> bytes = stackalloc byte[sizeof(ulong)];
|
|
BinaryPrimitives.WriteUInt64LittleEndian(bytes, value);
|
|
return TryWriteCompat(ctx, address, bytes);
|
|
}
|
|
|
|
private static int KernelBatchMapCore(CpuContext ctx, int flags)
|
|
{
|
|
var entriesAddress = ctx[CpuRegister.Rdi];
|
|
var entryCount = unchecked((int)ctx[CpuRegister.Rsi]);
|
|
var processedOutAddress = ctx[CpuRegister.Rdx];
|
|
var processedCount = 0;
|
|
var result = (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
|
|
|
for (var index = 0; index < entryCount; index++)
|
|
{
|
|
var entryAddress = entriesAddress + (ulong)(index * OrbisKernelBatchMapEntrySize);
|
|
if (!TryReadBatchMapEntry(ctx, entryAddress, out var entry) ||
|
|
entry.Length == 0 ||
|
|
entry.Operation < OrbisKernelMapOpMapDirect ||
|
|
entry.Operation > OrbisKernelMapOpTypeProtect)
|
|
{
|
|
result = (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
|
break;
|
|
}
|
|
|
|
result = entry.Operation switch
|
|
{
|
|
OrbisKernelMapOpMapDirect => InvokeKernelMemoryOperation(
|
|
ctx,
|
|
KernelMapDirectMemory,
|
|
entryAddress + OrbisKernelBatchMapEntryStartOffset,
|
|
entry.Length,
|
|
entry.Protection,
|
|
unchecked((ulong)(uint)flags),
|
|
entry.Offset,
|
|
0),
|
|
OrbisKernelMapOpUnmap => InvokeKernelMemoryOperation(
|
|
ctx,
|
|
KernelMunmap,
|
|
entry.Start,
|
|
entry.Length),
|
|
OrbisKernelMapOpProtect => InvokeKernelMemoryOperation(
|
|
ctx,
|
|
KernelMprotect,
|
|
entry.Start,
|
|
entry.Length,
|
|
entry.Protection),
|
|
OrbisKernelMapOpMapFlexible => InvokeKernelMemoryOperation(
|
|
ctx,
|
|
KernelMapNamedFlexibleMemory,
|
|
entryAddress + OrbisKernelBatchMapEntryStartOffset,
|
|
entry.Length,
|
|
entry.Protection,
|
|
unchecked((ulong)(uint)flags)),
|
|
OrbisKernelMapOpTypeProtect => InvokeKernelMemoryOperation(
|
|
ctx,
|
|
KernelMtypeprotect,
|
|
entry.Start,
|
|
entry.Length,
|
|
entry.Type,
|
|
entry.Protection),
|
|
_ => (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT,
|
|
};
|
|
|
|
if (result != (int)OrbisGen2Result.ORBIS_GEN2_OK)
|
|
{
|
|
break;
|
|
}
|
|
|
|
processedCount++;
|
|
}
|
|
|
|
if (processedOutAddress != 0 && !ctx.TryWriteInt32(processedOutAddress, processedCount))
|
|
{
|
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
private static int InvokeKernelMemoryOperation(
|
|
CpuContext ctx,
|
|
Func<CpuContext, int> operation,
|
|
ulong rdi = 0,
|
|
ulong rsi = 0,
|
|
ulong rdx = 0,
|
|
ulong rcx = 0,
|
|
ulong r8 = 0,
|
|
ulong r9 = 0)
|
|
{
|
|
var savedRdi = ctx[CpuRegister.Rdi];
|
|
var savedRsi = ctx[CpuRegister.Rsi];
|
|
var savedRdx = ctx[CpuRegister.Rdx];
|
|
var savedRcx = ctx[CpuRegister.Rcx];
|
|
var savedR8 = ctx[CpuRegister.R8];
|
|
var savedR9 = ctx[CpuRegister.R9];
|
|
|
|
ctx[CpuRegister.Rdi] = rdi;
|
|
ctx[CpuRegister.Rsi] = rsi;
|
|
ctx[CpuRegister.Rdx] = rdx;
|
|
ctx[CpuRegister.Rcx] = rcx;
|
|
ctx[CpuRegister.R8] = r8;
|
|
ctx[CpuRegister.R9] = r9;
|
|
|
|
try
|
|
{
|
|
return operation(ctx);
|
|
}
|
|
finally
|
|
{
|
|
ctx[CpuRegister.Rdi] = savedRdi;
|
|
ctx[CpuRegister.Rsi] = savedRsi;
|
|
ctx[CpuRegister.Rdx] = savedRdx;
|
|
ctx[CpuRegister.Rcx] = savedRcx;
|
|
ctx[CpuRegister.R8] = savedR8;
|
|
ctx[CpuRegister.R9] = savedR9;
|
|
}
|
|
}
|
|
|
|
private static bool TryReadBatchMapEntry(CpuContext ctx, ulong entryAddress, out BatchMapEntry entry)
|
|
{
|
|
entry = default;
|
|
if (!ctx.TryReadUInt64(entryAddress + OrbisKernelBatchMapEntryStartOffset, out var start) ||
|
|
!ctx.TryReadUInt64(entryAddress + OrbisKernelBatchMapEntryOffsetOffset, out var offset) ||
|
|
!ctx.TryReadUInt64(entryAddress + OrbisKernelBatchMapEntryLengthOffset, out var length))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
Span<byte> protection = stackalloc byte[1];
|
|
Span<byte> memoryType = stackalloc byte[1];
|
|
if (!TryReadCompat(ctx, entryAddress + OrbisKernelBatchMapEntryProtectionOffset, protection) ||
|
|
!TryReadCompat(ctx, entryAddress + OrbisKernelBatchMapEntryTypeOffset, memoryType) ||
|
|
!TryReadUInt32Compat(ctx, entryAddress + OrbisKernelBatchMapEntryOperationOffset, out var operation))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
entry = new BatchMapEntry(start, offset, length, protection[0], memoryType[0], unchecked((int)operation));
|
|
return true;
|
|
}
|
|
|
|
private static bool TryApplyMappedRegionProtectionLocked(
|
|
ulong address,
|
|
ulong length,
|
|
int protection,
|
|
int? memoryType = null)
|
|
{
|
|
if (length == 0 || !TryAddU64(address, length, out var endAddress))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var affected = new List<MappedRegion>();
|
|
var cursor = address;
|
|
foreach (var region in _mappedRegions.Values.OrderBy(static region => region.Address))
|
|
{
|
|
if (!TryAddU64(region.Address, region.Length, out var regionEnd) || regionEnd <= cursor)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (region.Address > cursor)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
affected.Add(region);
|
|
cursor = regionEnd >= endAddress ? endAddress : regionEnd;
|
|
if (cursor == endAddress)
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (cursor != endAddress)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
foreach (var region in affected)
|
|
{
|
|
_mappedRegions.Remove(region.Address);
|
|
}
|
|
|
|
foreach (var region in affected)
|
|
{
|
|
var regionEnd = region.Address + region.Length;
|
|
var protectStart = Math.Max(region.Address, address);
|
|
var protectEnd = Math.Min(regionEnd, endAddress);
|
|
|
|
if (region.Address < protectStart)
|
|
{
|
|
AddMappedRegionSliceLocked(region, region.Address, protectStart, region.Protection);
|
|
}
|
|
|
|
AddMappedRegionSliceLocked(region, protectStart, protectEnd, protection);
|
|
|
|
if (protectEnd < regionEnd)
|
|
{
|
|
AddMappedRegionSliceLocked(region, protectEnd, regionEnd, region.Protection);
|
|
}
|
|
|
|
if (memoryType.HasValue && region.IsDirect && TryFindDirectAllocationLocked(region.DirectStart, out var allocation))
|
|
{
|
|
_directAllocations[allocation.Start] = allocation with { MemoryType = memoryType.Value };
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
private static void AddMappedRegionSliceLocked(
|
|
MappedRegion source,
|
|
ulong start,
|
|
ulong end,
|
|
int protection)
|
|
{
|
|
if (end <= start)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var directStart = source.IsDirect
|
|
? unchecked(source.DirectStart + (start - source.Address))
|
|
: 0UL;
|
|
|
|
_mappedRegions[start] = source with
|
|
{
|
|
Address = start,
|
|
Length = end - start,
|
|
Protection = protection,
|
|
DirectStart = directStart,
|
|
};
|
|
}
|
|
|
|
private static bool TryFindDirectAllocationLocked(ulong directStart, out DirectAllocation allocation)
|
|
{
|
|
foreach (var candidate in _directAllocations.Values)
|
|
{
|
|
if (!TryAddU64(candidate.Start, candidate.Length, out var candidateEnd))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (directStart >= candidate.Start && directStart < candidateEnd)
|
|
{
|
|
allocation = candidate;
|
|
return true;
|
|
}
|
|
}
|
|
|
|
allocation = default;
|
|
return false;
|
|
}
|
|
|
|
private static bool TryNormalizeProtectRange(
|
|
ulong address,
|
|
ulong length,
|
|
out ulong alignedAddress,
|
|
out ulong alignedLength)
|
|
{
|
|
alignedAddress = 0;
|
|
alignedLength = 0;
|
|
if (length == 0 || !TryAddU64(address, length, out var endAddress))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
alignedAddress = AlignDown(address, OrbisPageSize);
|
|
var alignedEnd = AlignUp(endAddress, OrbisPageSize);
|
|
alignedLength = alignedEnd - alignedAddress;
|
|
return alignedLength != 0;
|
|
}
|
|
|
|
private static bool TryProtectHostRange(ulong address, ulong length, int orbisProtection)
|
|
{
|
|
if (length == 0 || length > nuint.MaxValue)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var hostProtection = ResolveHostProtection(orbisProtection);
|
|
if (!VirtualProtect((nint)address, (nuint)length, hostProtection, out _))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
private static uint ResolveHostProtection(int orbisProtection)
|
|
{
|
|
var read = (orbisProtection & (OrbisProtCpuRead | OrbisProtGpuRead)) != 0;
|
|
var write = (orbisProtection & (OrbisProtCpuWrite | OrbisProtGpuWrite)) != 0;
|
|
var execute = (orbisProtection & OrbisProtCpuExec) != 0;
|
|
|
|
if (execute)
|
|
{
|
|
return write
|
|
? HostPageExecuteReadWrite
|
|
: read
|
|
? HostPageExecuteRead
|
|
: HostPageExecute;
|
|
}
|
|
|
|
return write
|
|
? HostPageReadWrite
|
|
: read
|
|
? HostPageReadOnly
|
|
: HostPageNoAccess;
|
|
}
|
|
|
|
private static bool TryFindVirtualQueryRegionLocked(ulong queryAddress, bool findNext, out MappedRegion region)
|
|
{
|
|
region = default;
|
|
var foundNext = false;
|
|
foreach (var candidate in _mappedRegions.Values)
|
|
{
|
|
if (TryAddU64(candidate.Address, candidate.Length, out var candidateEnd) &&
|
|
queryAddress >= candidate.Address &&
|
|
queryAddress < candidateEnd)
|
|
{
|
|
region = candidate;
|
|
return true;
|
|
}
|
|
|
|
if (!findNext || candidate.Address < queryAddress)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (!foundNext || candidate.Address < region.Address)
|
|
{
|
|
region = candidate;
|
|
foundNext = true;
|
|
}
|
|
}
|
|
|
|
return foundNext;
|
|
}
|
|
|
|
private static void TraceDirectMemoryCall(
|
|
CpuContext ctx,
|
|
string operation,
|
|
ulong length,
|
|
ulong alignment,
|
|
int memoryType,
|
|
ulong outAddress,
|
|
ulong selectedAddress = 0,
|
|
OrbisGen2Result? result = null)
|
|
{
|
|
if (!ShouldTraceDirectMemory())
|
|
{
|
|
return;
|
|
}
|
|
|
|
var returnRip = 0UL;
|
|
var stackPointer = ctx[CpuRegister.Rsp];
|
|
if (stackPointer != 0)
|
|
{
|
|
_ = ctx.TryReadUInt64(stackPointer, out returnRip);
|
|
}
|
|
|
|
Console.Error.WriteLine(
|
|
$"[LOADER][TRACE] {operation}: ret=0x{returnRip:X16} len=0x{length:X16} align=0x{alignment:X16} type=0x{memoryType:X8} out=0x{outAddress:X16} selected=0x{selectedAddress:X16} result={result?.ToString() ?? "<pending>"}");
|
|
}
|
|
|
|
private static bool ShouldTraceDirectMemory()
|
|
{
|
|
return string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_DIRECT_MEMORY"), "1", StringComparison.Ordinal);
|
|
}
|
|
|
|
private static bool TryAllocateDirectMemoryLocked(
|
|
ulong searchStart,
|
|
ulong searchEnd,
|
|
ulong length,
|
|
ulong alignment,
|
|
int memoryType,
|
|
ulong allocationLimit,
|
|
out ulong selectedAddress)
|
|
{
|
|
selectedAddress = 0;
|
|
if (length == 0 || searchStart >= searchEnd)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var effectiveAlignment = alignment == 0 ? 0x1000UL : alignment;
|
|
if (!TryFindAllocatableDirectMemoryRangeLocked(searchStart, searchEnd, length, effectiveAlignment, allocationLimit, out var freePosition) ||
|
|
!TryAddU64(freePosition, length, out var endAddress))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
_directAllocations[freePosition] = new DirectAllocation(freePosition, length, memoryType);
|
|
_nextPhysicalAddress = endAddress;
|
|
selectedAddress = freePosition;
|
|
return true;
|
|
}
|
|
|
|
private static bool TryFindAllocatableDirectMemoryRangeLocked(
|
|
ulong searchStart,
|
|
ulong searchEnd,
|
|
ulong length,
|
|
ulong alignment,
|
|
ulong allocationLimit,
|
|
out ulong selectedAddress)
|
|
{
|
|
selectedAddress = 0;
|
|
if (length == 0 || searchStart >= searchEnd)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var effectiveEnd = Math.Min(searchEnd, allocationLimit);
|
|
var candidate = AlignUp(searchStart, alignment);
|
|
if (candidate >= effectiveEnd)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var allocations = new List<DirectAllocation>(_directAllocations.Values);
|
|
allocations.Sort(static (left, right) => left.Start.CompareTo(right.Start));
|
|
|
|
foreach (var allocation in allocations)
|
|
{
|
|
if (!TryAddU64(allocation.Start, allocation.Length, out var allocationEnd))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (allocationEnd <= candidate)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var gapEnd = Math.Min(allocation.Start, effectiveEnd);
|
|
if (candidate < gapEnd &&
|
|
TryAddU64(candidate, length, out var candidateEnd) &&
|
|
candidateEnd <= gapEnd)
|
|
{
|
|
selectedAddress = candidate;
|
|
return true;
|
|
}
|
|
|
|
if (allocation.Start >= effectiveEnd)
|
|
{
|
|
break;
|
|
}
|
|
|
|
candidate = AlignUp(Math.Max(candidate, allocationEnd), alignment);
|
|
if (candidate >= effectiveEnd)
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
if (!TryAddU64(candidate, length, out var endAddress) || endAddress > effectiveEnd)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
selectedAddress = candidate;
|
|
return true;
|
|
}
|
|
|
|
private static bool TryFindAvailableDirectMemorySpanLocked(
|
|
ulong searchStart,
|
|
ulong searchEnd,
|
|
ulong alignment,
|
|
out ulong spanStart,
|
|
out ulong spanLength)
|
|
{
|
|
spanStart = 0;
|
|
spanLength = 0;
|
|
if (searchStart >= searchEnd)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var effectiveEnd = Math.Min(searchEnd, DirectMemorySizeBytes);
|
|
var candidate = AlignUp(searchStart, alignment);
|
|
if (candidate >= effectiveEnd)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var allocations = new List<DirectAllocation>(_directAllocations.Values);
|
|
allocations.Sort(static (left, right) => left.Start.CompareTo(right.Start));
|
|
|
|
foreach (var allocation in allocations)
|
|
{
|
|
if (!TryAddU64(allocation.Start, allocation.Length, out var allocationEnd))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (allocationEnd <= candidate)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var gapEnd = Math.Min(allocation.Start, effectiveEnd);
|
|
if (candidate < gapEnd)
|
|
{
|
|
spanStart = candidate;
|
|
spanLength = gapEnd - candidate;
|
|
return true;
|
|
}
|
|
|
|
if (allocation.Start >= effectiveEnd)
|
|
{
|
|
break;
|
|
}
|
|
|
|
candidate = AlignUp(Math.Max(candidate, allocationEnd), alignment);
|
|
if (candidate >= effectiveEnd)
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
spanStart = candidate;
|
|
spanLength = effectiveEnd - candidate;
|
|
return spanLength != 0;
|
|
}
|
|
|
|
private static ulong GetDirectMemoryHighWaterMarkLocked()
|
|
{
|
|
ulong highWaterMark = 0;
|
|
foreach (var allocation in _directAllocations.Values)
|
|
{
|
|
if (!TryAddU64(allocation.Start, allocation.Length, out var endAddress))
|
|
{
|
|
return ulong.MaxValue;
|
|
}
|
|
|
|
if (endAddress > highWaterMark)
|
|
{
|
|
highWaterMark = endAddress;
|
|
}
|
|
}
|
|
|
|
return highWaterMark;
|
|
}
|
|
|
|
private static bool TryReadHostMemory(ulong address, Span<byte> destination)
|
|
{
|
|
if (destination.IsEmpty || !IsHostRangeAccessible(address, (ulong)destination.Length, writeAccess: false))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
try
|
|
{
|
|
var temporary = new byte[destination.Length];
|
|
Marshal.Copy((nint)address, temporary, 0, temporary.Length);
|
|
temporary.AsSpan().CopyTo(destination);
|
|
return true;
|
|
}
|
|
catch
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
internal static bool TryReadTrackedLibcHeap(
|
|
ulong address,
|
|
Span<byte> destination)
|
|
{
|
|
if (destination.IsEmpty)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
var length = (ulong)destination.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;
|
|
}
|
|
|
|
return TryReadHostMemory(address, destination);
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
private static bool TryAllocateLibcHeap(ulong requestedSize, nuint alignment, bool zeroFill, out ulong address)
|
|
{
|
|
address = 0;
|
|
return TryConvertAllocationSize(requestedSize, out var size) &&
|
|
TryAllocateLibcHeapCore(size, alignment, zeroFill, out address);
|
|
}
|
|
|
|
private static unsafe bool TryAllocateLibcHeapCore(nuint requestedSize, nuint alignment, bool zeroFill, out ulong address)
|
|
{
|
|
address = 0;
|
|
alignment = NormalizeLibcAlignment(alignment);
|
|
var actualSize = requestedSize == 0 ? 1u : requestedSize;
|
|
|
|
nuint totalSize;
|
|
try
|
|
{
|
|
checked
|
|
{
|
|
totalSize = actualSize + alignment - 1 + (nuint)IntPtr.Size;
|
|
}
|
|
}
|
|
catch (OverflowException)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
nint baseAddress;
|
|
try
|
|
{
|
|
baseAddress = Marshal.AllocHGlobal(checked((nint)totalSize));
|
|
}
|
|
catch (OutOfMemoryException)
|
|
{
|
|
return false;
|
|
}
|
|
catch (OverflowException)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (baseAddress == 0)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var alignedAddress = AlignUp(unchecked((ulong)baseAddress) + (ulong)IntPtr.Size, (ulong)alignment);
|
|
lock (_libcAllocGate)
|
|
{
|
|
_libcAllocations[alignedAddress] = new LibcHeapAllocation(baseAddress, actualSize, alignment);
|
|
}
|
|
|
|
try
|
|
{
|
|
if (zeroFill)
|
|
{
|
|
NativeMemory.Clear((void*)alignedAddress, actualSize);
|
|
}
|
|
}
|
|
catch
|
|
{
|
|
FreeLibcHeap(alignedAddress);
|
|
return false;
|
|
}
|
|
|
|
address = alignedAddress;
|
|
return true;
|
|
}
|
|
|
|
private static unsafe bool TryReallocateLibcHeap(ulong existingAddress, ulong requestedSize, out ulong resizedAddress)
|
|
{
|
|
resizedAddress = 0;
|
|
if (existingAddress == 0)
|
|
{
|
|
return TryAllocateLibcHeap(requestedSize, DefaultLibcHeapAlignment, zeroFill: false, out resizedAddress);
|
|
}
|
|
|
|
if (requestedSize == 0)
|
|
{
|
|
FreeLibcHeap(existingAddress);
|
|
return true;
|
|
}
|
|
|
|
LibcHeapAllocation allocation;
|
|
lock (_libcAllocGate)
|
|
{
|
|
if (!_libcAllocations.TryGetValue(existingAddress, out allocation))
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
if (!TryAllocateLibcHeap(requestedSize, allocation.Alignment, zeroFill: false, out resizedAddress))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var bytesToCopy = Math.Min(allocation.Size, (nuint)requestedSize);
|
|
Buffer.MemoryCopy(
|
|
source: (void*)existingAddress,
|
|
destination: (void*)resizedAddress,
|
|
destinationSizeInBytes: checked((long)Math.Max(bytesToCopy, 1u)),
|
|
sourceBytesToCopy: checked((long)bytesToCopy));
|
|
FreeLibcHeap(existingAddress);
|
|
return true;
|
|
}
|
|
|
|
private static bool TryAllocateAlignedLibcHeap(ulong alignmentValue, ulong requestedSize, bool requireSizeMultiple, out ulong address)
|
|
{
|
|
address = 0;
|
|
return TryValidateAlignedAllocation(
|
|
alignmentValue,
|
|
requestedSize,
|
|
requireSizeMultiple,
|
|
requirePointerSizedAlignment: false,
|
|
out var alignment,
|
|
out var size) &&
|
|
TryAllocateLibcHeapCore(size, alignment, zeroFill: false, out address);
|
|
}
|
|
|
|
private static bool TryValidateAlignedAllocation(
|
|
ulong alignmentValue,
|
|
ulong requestedSize,
|
|
bool requireSizeMultiple,
|
|
bool requirePointerSizedAlignment,
|
|
out nuint alignment,
|
|
out nuint size)
|
|
{
|
|
alignment = 0;
|
|
size = 0;
|
|
if (!TryConvertAllocationSize(requestedSize, out size) ||
|
|
alignmentValue == 0 ||
|
|
alignmentValue > (ulong)nint.MaxValue)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
alignment = (nuint)alignmentValue;
|
|
if (!IsPowerOfTwo(alignment))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (requirePointerSizedAlignment && alignment % (nuint)IntPtr.Size != 0)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (alignment < (nuint)IntPtr.Size)
|
|
{
|
|
alignment = (nuint)IntPtr.Size;
|
|
}
|
|
|
|
if (requireSizeMultiple && size % alignment != 0)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
private static void FreeLibcHeap(ulong address)
|
|
{
|
|
if (address == 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
LibcHeapAllocation allocation;
|
|
lock (_libcAllocGate)
|
|
{
|
|
if (!_libcAllocations.Remove(address, out allocation))
|
|
{
|
|
return;
|
|
}
|
|
}
|
|
|
|
Marshal.FreeHGlobal(allocation.BaseAddress);
|
|
}
|
|
|
|
private static bool TryMultiplyAllocationSize(ulong left, ulong right, out nuint size)
|
|
{
|
|
size = 0;
|
|
if (!TryConvertAllocationSize(left, out var leftSize) ||
|
|
!TryConvertAllocationSize(right, out var rightSize))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
try
|
|
{
|
|
checked
|
|
{
|
|
size = leftSize * rightSize;
|
|
}
|
|
}
|
|
catch (OverflowException)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
private static bool TryConvertAllocationSize(ulong requestedSize, out nuint size)
|
|
{
|
|
size = 0;
|
|
if (requestedSize > (ulong)nint.MaxValue)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
size = (nuint)requestedSize;
|
|
return true;
|
|
}
|
|
|
|
private static nuint NormalizeLibcAlignment(nuint alignment)
|
|
{
|
|
if (alignment < DefaultLibcHeapAlignment)
|
|
{
|
|
return DefaultLibcHeapAlignment;
|
|
}
|
|
|
|
return alignment;
|
|
}
|
|
|
|
private static bool IsPowerOfTwo(nuint value)
|
|
{
|
|
return value != 0 && (value & (value - 1)) == 0;
|
|
}
|
|
|
|
private static bool TryWriteHostMemory(ulong address, ReadOnlySpan<byte> source)
|
|
{
|
|
if (source.IsEmpty || !IsHostRangeAccessible(address, (ulong)source.Length, writeAccess: true))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
try
|
|
{
|
|
var temporary = source.ToArray();
|
|
Marshal.Copy(temporary, 0, (nint)address, temporary.Length);
|
|
return true;
|
|
}
|
|
catch
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
private static bool IsHostRangeAccessible(ulong address, ulong length, bool writeAccess)
|
|
{
|
|
if (address == 0 || length == 0)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
const ulong canonicalUpper = 0x0000800000000000UL;
|
|
if (address >= canonicalUpper)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (ulong.MaxValue - address < length - 1)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (!TryQueryHostPage(address, out var startInfo) || !HasRequiredProtection(startInfo.Protect, writeAccess))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var endAddress = address + length - 1;
|
|
if (endAddress == address)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
if (!TryQueryHostPage(endAddress, out var endInfo) || !HasRequiredProtection(endInfo.Protect, writeAccess))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
private static bool TryQueryHostPage(ulong address, out MemoryBasicInformation info)
|
|
{
|
|
info = default;
|
|
var size = (nuint)Marshal.SizeOf<MemoryBasicInformation>();
|
|
if (VirtualQuery((nint)address, out info, size) == 0)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
return info.State == MemCommit;
|
|
}
|
|
|
|
private static bool HasRequiredProtection(uint protect, bool writeAccess)
|
|
{
|
|
if ((protect & (HostPageNoAccess | HostPageGuard)) != 0)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
const uint readableMask = HostPageReadOnly | HostPageReadWrite | HostPageWriteCopy | HostPageExecuteRead | HostPageExecuteReadWrite | HostPageExecuteWriteCopy;
|
|
const uint writableMask = HostPageReadWrite | HostPageWriteCopy | HostPageExecuteReadWrite | HostPageExecuteWriteCopy;
|
|
var expected = writeAccess ? writableMask : readableMask;
|
|
return (protect & expected) != 0;
|
|
}
|
|
|
|
private static bool TryWriteOpenDescriptorStat(CpuContext ctx, int fd, ulong statAddress)
|
|
{
|
|
if (fd is 0 or 1 or 2)
|
|
{
|
|
var now = DateTime.UtcNow;
|
|
LogIoTrace("fstat", $"stdio:{fd}", $"fd={fd} size=0 dir=0");
|
|
return TryWriteKernelStat(ctx, statAddress, isDirectory: false, size: 0, now, now, now, $"stdio:{fd}");
|
|
}
|
|
|
|
string? hostPath = null;
|
|
bool isDirectory = false;
|
|
lock (_fdGate)
|
|
{
|
|
if (_openDirectories.TryGetValue(fd, out var directory))
|
|
{
|
|
hostPath = directory.Path;
|
|
isDirectory = true;
|
|
}
|
|
else if (_openFiles.TryGetValue(fd, out var stream))
|
|
{
|
|
hostPath = stream.Name;
|
|
}
|
|
}
|
|
|
|
if (!string.IsNullOrWhiteSpace(hostPath))
|
|
{
|
|
long size = 0;
|
|
if (!isDirectory)
|
|
{
|
|
try
|
|
{
|
|
size = new FileInfo(hostPath!).Length;
|
|
}
|
|
catch (IOException)
|
|
{
|
|
size = -1;
|
|
}
|
|
}
|
|
|
|
LogIoTrace("fstat", hostPath!, $"fd={fd} size={size} dir={(isDirectory ? 1 : 0)}");
|
|
}
|
|
|
|
return !string.IsNullOrWhiteSpace(hostPath) && TryWriteHostPathStat(ctx, statAddress, hostPath!, isDirectory);
|
|
}
|
|
|
|
private static bool TryWriteHostPathStat(CpuContext ctx, ulong statAddress, string hostPath)
|
|
{
|
|
var isDirectory = Directory.Exists(hostPath);
|
|
if (!isDirectory && !File.Exists(hostPath))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
return TryWriteHostPathStat(ctx, statAddress, hostPath, isDirectory);
|
|
}
|
|
|
|
private static bool TryGetAprFileSize(string hostPath, out ulong size)
|
|
{
|
|
size = 0;
|
|
|
|
string cachePath;
|
|
try
|
|
{
|
|
cachePath = Path.GetFullPath(hostPath);
|
|
}
|
|
catch
|
|
{
|
|
cachePath = hostPath;
|
|
}
|
|
|
|
if (_aprFileSizeCache.TryGetValue(cachePath, out size))
|
|
{
|
|
return true;
|
|
}
|
|
|
|
try
|
|
{
|
|
var fileInfo = new FileInfo(cachePath);
|
|
if (fileInfo.Exists)
|
|
{
|
|
var length = fileInfo.Length;
|
|
size = length < 0 ? 0UL : unchecked((ulong)length);
|
|
_aprFileSizeCache.TryAdd(cachePath, size);
|
|
return true;
|
|
}
|
|
|
|
if (!new DirectoryInfo(cachePath).Exists)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
size = 65536;
|
|
_aprFileSizeCache.TryAdd(cachePath, size);
|
|
return true;
|
|
}
|
|
catch
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
private static bool TryWriteHostPathStat(CpuContext ctx, ulong statAddress, string hostPath, bool isDirectory)
|
|
{
|
|
if (isDirectory)
|
|
{
|
|
if (!Directory.Exists(hostPath))
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
else if (!File.Exists(hostPath))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
try
|
|
{
|
|
var lastAccessUtc = File.GetLastAccessTimeUtc(hostPath);
|
|
var lastWriteUtc = File.GetLastWriteTimeUtc(hostPath);
|
|
var creationUtc = File.GetCreationTimeUtc(hostPath);
|
|
var size = isDirectory ? 65536L : new FileInfo(hostPath).Length;
|
|
return TryWriteKernelStat(ctx, statAddress, isDirectory, size, lastAccessUtc, lastWriteUtc, creationUtc, hostPath);
|
|
}
|
|
catch
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
private static bool TryWriteKernelStat(
|
|
CpuContext ctx,
|
|
ulong statAddress,
|
|
bool isDirectory,
|
|
long size,
|
|
DateTime lastAccessUtc,
|
|
DateTime lastWriteUtc,
|
|
DateTime creationUtc,
|
|
string inodeSeed)
|
|
{
|
|
Span<byte> payload = stackalloc byte[KernelStatSize];
|
|
payload.Clear();
|
|
|
|
var seedBytes = Encoding.UTF8.GetBytes(inodeSeed);
|
|
BinaryPrimitives.WriteUInt32LittleEndian(payload[KernelStatStDevOffset..], 0);
|
|
BinaryPrimitives.WriteUInt32LittleEndian(payload[KernelStatStInoOffset..], ComputeDirectoryEntryHash(seedBytes));
|
|
BinaryPrimitives.WriteUInt16LittleEndian(payload[KernelStatStModeOffset..], isDirectory ? KernelStatModeDirectory : KernelStatModeRegular);
|
|
BinaryPrimitives.WriteUInt16LittleEndian(payload[KernelStatStNlinkOffset..], 1);
|
|
BinaryPrimitives.WriteUInt32LittleEndian(payload[KernelStatStUidOffset..], 0);
|
|
BinaryPrimitives.WriteUInt32LittleEndian(payload[KernelStatStGidOffset..], 0);
|
|
BinaryPrimitives.WriteUInt32LittleEndian(payload[KernelStatStRdevOffset..], 0);
|
|
WriteKernelTimespec(payload[KernelStatStAtimOffset..], lastAccessUtc);
|
|
WriteKernelTimespec(payload[KernelStatStMtimOffset..], lastWriteUtc);
|
|
WriteKernelTimespec(payload[KernelStatStCtimOffset..], lastWriteUtc);
|
|
BinaryPrimitives.WriteInt64LittleEndian(payload[KernelStatStSizeOffset..], size);
|
|
BinaryPrimitives.WriteInt64LittleEndian(payload[KernelStatStBlocksOffset..], isDirectory ? 128 : (size + 511) / 512);
|
|
BinaryPrimitives.WriteUInt32LittleEndian(payload[KernelStatStBlksizeOffset..], isDirectory ? 65536U : 512U);
|
|
BinaryPrimitives.WriteUInt32LittleEndian(payload[KernelStatStFlagsOffset..], 0);
|
|
BinaryPrimitives.WriteUInt32LittleEndian(payload[KernelStatStGenOffset..], 0);
|
|
BinaryPrimitives.WriteInt32LittleEndian(payload[KernelStatStLspareOffset..], 0);
|
|
WriteKernelTimespec(payload[KernelStatStBirthtimOffset..], creationUtc);
|
|
return TryWriteCompat(ctx, statAddress, payload);
|
|
}
|
|
|
|
private static void WriteKernelTimespec(Span<byte> destination, DateTime utcTime)
|
|
{
|
|
var timestamp = utcTime.Kind == DateTimeKind.Utc ? utcTime : utcTime.ToUniversalTime();
|
|
var dto = new DateTimeOffset(timestamp);
|
|
BinaryPrimitives.WriteInt64LittleEndian(destination, dto.ToUnixTimeSeconds());
|
|
var ticksWithinSecond = timestamp.Ticks % TimeSpan.TicksPerSecond;
|
|
BinaryPrimitives.WriteInt64LittleEndian(destination[sizeof(long)..], ticksWithinSecond * 100);
|
|
}
|
|
|
|
private static int KernelGetdirentriesCore(CpuContext ctx, int fd, ulong bufferAddress, int requested, ulong basePointerAddress)
|
|
{
|
|
if (fd < 0 || bufferAddress == 0 || requested < 512)
|
|
{
|
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
|
}
|
|
|
|
OpenDirectory? directory;
|
|
lock (_fdGate)
|
|
{
|
|
_openDirectories.TryGetValue(fd, out directory);
|
|
}
|
|
|
|
if (directory is null)
|
|
{
|
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
|
}
|
|
|
|
var currentIndex = directory.NextIndex;
|
|
if (basePointerAddress != 0 && !TryWriteUInt64Compat(ctx, basePointerAddress, (ulong)currentIndex))
|
|
{
|
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
|
}
|
|
|
|
if (currentIndex >= directory.Entries.Length)
|
|
{
|
|
ctx[CpuRegister.Rax] = 0;
|
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
|
}
|
|
|
|
var entryName = directory.Entries[currentIndex];
|
|
directory.NextIndex = currentIndex + 1;
|
|
|
|
var entryBytes = Encoding.UTF8.GetBytes(entryName);
|
|
var nameLength = Math.Min(entryBytes.Length, 255);
|
|
var entryPath = Path.Combine(directory.Path, entryName);
|
|
var entryType = Directory.Exists(entryPath) ? (byte)4 : (byte)8;
|
|
|
|
var payload = new byte[512];
|
|
BinaryPrimitives.WriteUInt32LittleEndian(payload.AsSpan(0, sizeof(uint)), ComputeDirectoryEntryHash(entryBytes.AsSpan(0, nameLength)));
|
|
BinaryPrimitives.WriteUInt16LittleEndian(payload.AsSpan(4, sizeof(ushort)), 512);
|
|
payload[6] = entryType;
|
|
payload[7] = unchecked((byte)nameLength);
|
|
entryBytes.AsSpan(0, nameLength).CopyTo(payload.AsSpan(8));
|
|
|
|
if (!TryWriteCompat(ctx, bufferAddress, payload))
|
|
{
|
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
|
}
|
|
|
|
ctx[CpuRegister.Rax] = 512;
|
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
|
}
|
|
|
|
private static string[] EnumerateDirectoryEntries(string hostPath)
|
|
{
|
|
return Directory.EnumerateFileSystemEntries(hostPath)
|
|
.Select(Path.GetFileName)
|
|
.Where(static name => !string.IsNullOrEmpty(name))
|
|
.OrderBy(static name => name, StringComparer.OrdinalIgnoreCase)
|
|
.ToArray()!;
|
|
}
|
|
|
|
private static uint ComputeDirectoryEntryHash(ReadOnlySpan<byte> utf8Name)
|
|
{
|
|
const uint offsetBasis = 2166136261;
|
|
const uint prime = 16777619;
|
|
|
|
var hash = offsetBasis;
|
|
for (var i = 0; i < utf8Name.Length; i++)
|
|
{
|
|
hash ^= utf8Name[i];
|
|
hash *= prime;
|
|
}
|
|
|
|
return hash;
|
|
}
|
|
|
|
private static void LogOpenTrace(string message)
|
|
{
|
|
if (!string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_OPEN"), "1", StringComparison.Ordinal))
|
|
{
|
|
return;
|
|
}
|
|
|
|
Console.Error.WriteLine($"[LOADER][TRACE] {message}");
|
|
}
|
|
|
|
private static void LogIoTrace(string operation, string path, string detail)
|
|
{
|
|
if (!string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_IO"), "1", StringComparison.Ordinal))
|
|
{
|
|
return;
|
|
}
|
|
|
|
var filter = Environment.GetEnvironmentVariable("SHARPEMU_LOG_IO_FILTER");
|
|
if (!string.IsNullOrWhiteSpace(filter) &&
|
|
path.IndexOf(filter, StringComparison.OrdinalIgnoreCase) < 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
Console.Error.WriteLine($"[LOADER][TRACE] {operation} path='{path}' {detail}");
|
|
}
|
|
|
|
private static void LogUniqueStatTrace(string guestPath, string hostPath, bool found)
|
|
{
|
|
if (!string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_IO"), "1", StringComparison.Ordinal))
|
|
{
|
|
return;
|
|
}
|
|
|
|
var result = found ? "found" : "not_found";
|
|
lock (_ioTraceGate)
|
|
{
|
|
if (!_tracedStatResults.Add($"{result}\0{guestPath}"))
|
|
{
|
|
return;
|
|
}
|
|
}
|
|
|
|
LogIoTrace("stat", guestPath, $"host='{hostPath}' result={result}");
|
|
}
|
|
|
|
private static string? GetNegativeStatCacheKey(string guestPath)
|
|
{
|
|
var normalized = NormalizeGuestStatCachePath(guestPath);
|
|
return IsReadOnlyGuestStatPath(normalized) ? normalized : null;
|
|
}
|
|
|
|
private static string? NormalizeGuestStatCachePath(string guestPath)
|
|
{
|
|
var normalized = guestPath.Replace('\\', '/').TrimEnd('/');
|
|
if (normalized.Length == 0)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
if (normalized[0] != '/')
|
|
{
|
|
normalized = "/" + normalized;
|
|
}
|
|
|
|
return normalized;
|
|
}
|
|
|
|
private static bool IsReadOnlyGuestStatPath(string? normalizedGuestPath) =>
|
|
normalizedGuestPath is not null &&
|
|
(string.Equals(normalizedGuestPath, "/app0", StringComparison.OrdinalIgnoreCase) ||
|
|
normalizedGuestPath.StartsWith("/app0/", StringComparison.OrdinalIgnoreCase) ||
|
|
string.Equals(normalizedGuestPath, "/hostapp", StringComparison.OrdinalIgnoreCase) ||
|
|
normalizedGuestPath.StartsWith("/hostapp/", StringComparison.OrdinalIgnoreCase) ||
|
|
string.Equals(normalizedGuestPath, "/devlog/app", StringComparison.OrdinalIgnoreCase) ||
|
|
normalizedGuestPath.StartsWith("/devlog/app/", StringComparison.OrdinalIgnoreCase) ||
|
|
string.Equals(normalizedGuestPath, "/temp0", StringComparison.OrdinalIgnoreCase) ||
|
|
normalizedGuestPath.StartsWith("/temp0/", StringComparison.OrdinalIgnoreCase) ||
|
|
string.Equals(normalizedGuestPath, "/download0", StringComparison.OrdinalIgnoreCase) ||
|
|
normalizedGuestPath.StartsWith("/download0/", StringComparison.OrdinalIgnoreCase));
|
|
|
|
private static bool IsNegativeStatCached(string cacheKey)
|
|
{
|
|
lock (_statCacheGate)
|
|
{
|
|
return _negativeStatCache.Contains(cacheKey);
|
|
}
|
|
}
|
|
|
|
private static void AddNegativeStatCache(string cacheKey)
|
|
{
|
|
lock (_statCacheGate)
|
|
{
|
|
_negativeStatCache.Add(cacheKey);
|
|
}
|
|
}
|
|
|
|
private static void RemoveNegativeStatCache(string cacheKey)
|
|
{
|
|
lock (_statCacheGate)
|
|
{
|
|
_negativeStatCache.Remove(cacheKey);
|
|
}
|
|
}
|
|
|
|
private static void AddNegativeStatCacheForGuestPath(string guestPath)
|
|
{
|
|
var cacheKey = GetNegativeStatCacheKey(guestPath);
|
|
if (cacheKey is not null)
|
|
{
|
|
AddNegativeStatCache(cacheKey);
|
|
}
|
|
}
|
|
|
|
private static void InvalidateNegativeStatCacheForPathAndAncestors(string guestPath)
|
|
{
|
|
var normalized = NormalizeGuestStatCachePath(guestPath);
|
|
if (normalized is null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
lock (_statCacheGate)
|
|
{
|
|
var current = normalized;
|
|
while (true)
|
|
{
|
|
_negativeStatCache.Remove(current);
|
|
var slash = current.LastIndexOf('/');
|
|
if (slash <= 0)
|
|
{
|
|
break;
|
|
}
|
|
|
|
current = current[..slash];
|
|
}
|
|
|
|
_negativeStatCache.Remove("/");
|
|
}
|
|
}
|
|
|
|
private static void InvalidateAprFileSizeCache(string hostPath)
|
|
{
|
|
try
|
|
{
|
|
hostPath = Path.GetFullPath(hostPath);
|
|
}
|
|
catch
|
|
{
|
|
// The cache key remains the original path when normalization fails.
|
|
}
|
|
|
|
_aprFileSizeCache.TryRemove(hostPath, out _);
|
|
}
|
|
|
|
private static string PreviewIoBytes(byte[] buffer, int count, int maxBytes)
|
|
{
|
|
if (count <= 0)
|
|
{
|
|
return string.Empty;
|
|
}
|
|
|
|
var previewLength = Math.Min(count, maxBytes);
|
|
var text = Encoding.UTF8.GetString(buffer, 0, previewLength);
|
|
return SanitizeTracePreview(text, maxBytes);
|
|
}
|
|
|
|
private static string PreviewIoHex(byte[] buffer, int count, int maxBytes)
|
|
{
|
|
if (count <= 0)
|
|
{
|
|
return string.Empty;
|
|
}
|
|
|
|
var previewLength = Math.Min(count, maxBytes);
|
|
return Convert.ToHexString(buffer, 0, previewLength);
|
|
}
|
|
|
|
private static string PreviewGuestHex(CpuContext ctx, ulong address, int maxBytes)
|
|
{
|
|
if (address == 0 || maxBytes <= 0)
|
|
{
|
|
return string.Empty;
|
|
}
|
|
|
|
var bytes = GC.AllocateUninitializedArray<byte>(maxBytes);
|
|
if (!ctx.Memory.TryRead(address, bytes))
|
|
{
|
|
return "<unreadable>";
|
|
}
|
|
|
|
return Convert.ToHexString(bytes);
|
|
}
|
|
|
|
private static ulong AlignUp(ulong value, ulong alignment)
|
|
{
|
|
if (alignment <= 1)
|
|
{
|
|
return value;
|
|
}
|
|
|
|
var mask = alignment - 1;
|
|
return (value + mask) & ~mask;
|
|
}
|
|
|
|
private static ulong AlignDown(ulong value, ulong alignment)
|
|
{
|
|
if (alignment <= 1)
|
|
{
|
|
return value;
|
|
}
|
|
|
|
var mask = alignment - 1;
|
|
return value & ~mask;
|
|
}
|
|
|
|
private static bool TryAddU64(ulong left, ulong right, out ulong sum)
|
|
{
|
|
sum = left + right;
|
|
return sum >= left;
|
|
}
|
|
}
|