Files
sharpemu/src/SharpEmu.Core/Loader/SelfLoader.cs
2026-07-15 01:31:16 +03:00

2435 lines
81 KiB
C#

// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Buffers.Binary;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Text;
using System.Linq;
using SharpEmu.Core;
using SharpEmu.Core.Cpu;
using SharpEmu.Core.Memory;
using SharpEmu.HLE;
using SharpEmu.Logging;
namespace SharpEmu.Core.Loader;
public sealed class SelfLoader : ISelfLoader
{
private static readonly SharpEmuLogger Log = SharpEmuLog.For("Loader");
private const uint ElfMagic = 0x7F454C46;
private const uint Ps4SelfMagic = 0x4F153D1D;
private const uint Ps5SelfMagic = 0x5414F5EE;
private const ulong SelfSegmentFlag = 0x800;
private const int PageSize = 0x1000;
private const ulong ImportStubBaseAddress = 0x0000_7000_0000_0000UL;
private const ulong ImportStubAddressStride = 0x0000_0000_0100_0000UL;
private const ulong ImportStubSlotSize = 0x10;
private const byte StubTrapOpcode = 0xCC;
private const byte StubReturnOpcode = 0xC3;
private const int DynamicEntrySize = 16;
private const int ElfSymbolSize = 24;
private const int ElfRelocationSize = 24;
private const int ElfSectionHeaderSize = 64;
private const uint SectionTypeSymbolTable = 2;
private const uint SectionTypeRela = 4;
private const long DtNull = 0;
private const long DtPltRelSize = 0x02;
private const long DtPltGot = 0x03;
private const long DtStrTab = 0x05;
private const long DtSymTab = 0x06;
private const long DtRela = 0x07;
private const long DtRelaSize = 0x08;
private const long DtInit = 0x0C;
private const long DtStrSize = 0x0A;
private const long DtJmpRel = 0x17;
private const long DtInitArray = 0x19;
private const long DtInitArraySize = 0x1B;
private const long DtPreInitArray = 0x20;
private const long DtPreInitArraySize = 0x21;
private const long DtSceJmpRel = 0x61000029;
private const long DtScePltRelSize = 0x6100002D;
private const long DtSceRela = 0x6100002F;
private const long DtSceRelaSize = 0x61000031;
private const long DtSceStrTab = 0x61000035;
private const long DtSceStrSize = 0x61000037;
private const long DtSceSymTab = 0x61000039;
private const long DtSceSymTabSize = 0x6100003F;
private const uint RelocationTypeAbsolute64 = 1;
private const uint RelocationTypeGlobalData = 6;
private const uint RelocationTypeJumpSlot = 7;
private const uint RelocationTypeRelative = 8;
private const uint RelocationTypeTlsModuleId = 16;
private const ulong Ps5MainImageBase = 0x0000000800000000UL;
private const ulong Ps4MainImageBase = 0x0000000000400000UL;
private const ulong Ps5ModuleSearchStart = 0x0000000804000000UL;
private const ulong Ps5ModuleSearchEnd = 0x0000000900000000UL;
private const ulong Ps4ModuleSearchStart = 0x0000000002000000UL;
private const ulong Ps4ModuleSearchEnd = 0x0000000040000000UL;
private const ulong ModulePlacementStep = 0x00200000UL;
private const ulong FocusRelocGuestStart = 0x0000000807BA25B0UL;
private const ulong FocusRelocGuestEnd = 0x0000000807BA2608UL;
private const byte SymbolBindLocal = 0;
private const byte SymbolBindGlobal = 1;
private const byte SymbolBindWeak = 2;
private const byte SymbolTypeObject = 1;
private IModuleManager? _moduleManager;
private uint _nextTlsModuleId = 1;
private static readonly IReadOnlyDictionary<ulong, string> EmptyImportStubs = new Dictionary<ulong, string>();
private static readonly IReadOnlyDictionary<string, ulong> EmptyRuntimeSymbols =
new Dictionary<string, ulong>(StringComparer.Ordinal);
private static readonly IReadOnlyList<ulong> EmptyInitializerFunctions = Array.Empty<ulong>();
private static readonly int SelfHeaderSize = Unsafe.SizeOf<SelfHeader>();
private static readonly int SelfSegmentSize = Unsafe.SizeOf<SelfSegment>();
private static readonly int ProgramHeaderSize = Unsafe.SizeOf<ProgramHeader>();
public SelfImage Load(ReadOnlySpan<byte> imageData, IVirtualMemory virtualMemory)
{
return Load(imageData, virtualMemory, fs: null, mountRoot: null);
}
public SelfImage Load(ReadOnlySpan<byte> imageData, IVirtualMemory virtualMemory, IFileSystem? fs, string? mountRoot)
{
return LoadCore(
imageData,
virtualMemory,
fs,
mountRoot,
clearVirtualMemory: true,
readParamJson: true);
}
public SelfImage Load(ReadOnlySpan<byte> imageData, IVirtualMemory virtualMemory, IModuleManager moduleManager)
{
return Load(imageData, virtualMemory, moduleManager, fs: null, mountRoot: null);
}
public SelfImage Load(ReadOnlySpan<byte> imageData, IVirtualMemory virtualMemory, IModuleManager moduleManager, IFileSystem? fs, string? mountRoot)
{
_moduleManager = moduleManager;
return LoadCore(
imageData,
virtualMemory,
fs,
mountRoot,
clearVirtualMemory: true,
readParamJson: true);
}
public SelfImage LoadAdditional(ReadOnlySpan<byte> imageData, IVirtualMemory virtualMemory, IModuleManager moduleManager, IFileSystem? fs, string? mountRoot)
{
_moduleManager = moduleManager;
return LoadCore(
imageData,
virtualMemory,
fs,
mountRoot,
clearVirtualMemory: false,
readParamJson: false);
}
private SelfImage LoadCore(
ReadOnlySpan<byte> imageData,
IVirtualMemory virtualMemory,
IFileSystem? fs,
string? mountRoot,
bool clearVirtualMemory,
bool readParamJson)
{
ArgumentNullException.ThrowIfNull(virtualMemory);
if (imageData.IsEmpty)
{
throw new InvalidDataException("Input image is empty.");
}
var applicationInfo = readParamJson
? TryLoadParamJson(fs, mountRoot)
: default;
if (clearVirtualMemory)
{
virtualMemory.Clear();
_nextTlsModuleId = 1;
}
var tlsModuleId = _nextTlsModuleId == 0 ? 1u : _nextTlsModuleId;
Log.Debug($"TLS load_start clear={clearVirtualMemory} next={_nextTlsModuleId} assigned={tlsModuleId}");
var loadContext = ParseLayout(imageData);
var elfHeader = ReadUnmanaged<ElfHeader>(imageData, loadContext.ElfOffset);
ValidateElfHeader(elfHeader);
var programHeaders = ParseProgramHeaders(imageData, loadContext, elfHeader);
var totalImageSize = CalculateTotalImageSize(programHeaders);
Console.WriteLine($"Total image size needed: 0x{totalImageSize:X} ({totalImageSize} bytes)");
var isNextGen = elfHeader.AbiVersion == 2;
var imageBase = DetermineRequestedImageBase(virtualMemory, totalImageSize, isNextGen, clearVirtualMemory);
if (virtualMemory is PhysicalVirtualMemory physicalVm)
{
if (clearVirtualMemory)
{
if (!physicalVm.TryAllocateAtExact(imageBase, totalImageSize, executable: true, out var allocatedBase))
{
throw new InvalidOperationException(
$"Could not allocate main image at required base 0x{imageBase:X16} (size=0x{totalImageSize:X}).");
}
imageBase = allocatedBase;
}
else if (!TryAllocateAdditionalImageAtExact(physicalVm, imageBase, totalImageSize, isNextGen, out imageBase))
{
var allocatedBase = physicalVm.AllocateAt(imageBase, totalImageSize, executable: true);
if (allocatedBase != imageBase)
{
Console.WriteLine($"[LOADER] Could not allocate module at preferred base 0x{imageBase:X16}");
Console.WriteLine($"[LOADER] Allocated module at 0x{allocatedBase:X16} instead.");
}
imageBase = allocatedBase;
}
}
MapLoadSegments(imageData, loadContext, programHeaders, virtualMemory, imageBase);
var importStubs = ResolveAndPatchImportStubs(
imageData,
loadContext,
elfHeader,
programHeaders,
virtualMemory,
imageBase,
_moduleManager,
tlsModuleId,
out var importedRelocations);
var effectiveImportStubs = importStubs.Count == 0
? new Dictionary<ulong, string>()
: new Dictionary<ulong, string>(importStubs);
var runtimeSymbols = new Dictionary<string, ulong>(StringComparer.Ordinal);
RegisterRuntimeSymbolsAndHooks(
imageData,
loadContext,
programHeaders,
elfHeader,
virtualMemory,
imageBase,
effectiveImportStubs,
runtimeSymbols);
var finalizedImportStubs = effectiveImportStubs.Count == 0
? EmptyImportStubs
: effectiveImportStubs;
var finalizedRuntimeSymbols = runtimeSymbols.Count == 0
? EmptyRuntimeSymbols
: runtimeSymbols;
CollectInitializerFunctions(
imageData,
loadContext,
programHeaders,
virtualMemory,
imageBase,
out var initFunctionEntryPoint,
out var preInitializerFunctions,
out var initializerFunctions);
var procParamAddress = ResolveProcParamAddress(programHeaders, imageBase);
Console.WriteLine($"[LOADER] ELF e_entry: 0x{elfHeader.EntryPoint:X16}");
Console.WriteLine($"[LOADER] Generation: {(isNextGen ? "Gen5 (PS5)" : "Gen4 (PS4)")}");
Console.WriteLine($"[LOADER] Using image base: 0x{imageBase:X16}");
Console.WriteLine($"[LOADER] Final entry point: 0x{elfHeader.EntryPoint + imageBase:X16}");
if (procParamAddress != 0)
{
Console.WriteLine($"[LOADER] ProcParam: 0x{procParamAddress:X16}");
}
int count = ((IReadOnlyList<ProgramHeader>)programHeaders).Count;
int phCountToLog = count < 3 ? count : 3;
for (var i = 0; i < phCountToLog; i++)
{
var ph = programHeaders[i];
Console.WriteLine($"[LOADER] PH[{i}]: type={ph.HeaderType}, vaddr=0x{ph.VirtualAddress:X16} -> 0x{ph.VirtualAddress + imageBase:X16}, memsz=0x{ph.MemorySize:X}");
}
if (_nextTlsModuleId == tlsModuleId && _nextTlsModuleId < uint.MaxValue)
{
_nextTlsModuleId++;
}
Log.Debug($"TLS load_done assigned={tlsModuleId} next={_nextTlsModuleId}");
return new SelfImage(
loadContext.IsSelf,
elfHeader,
programHeaders,
virtualMemory.SnapshotRegions(),
finalizedImportStubs,
finalizedRuntimeSymbols,
importedRelocations,
preInitializerFunctions,
initializerFunctions,
initFunctionEntryPoint,
imageBase,
procParamAddress,
applicationInfo.Title,
applicationInfo.TitleId,
applicationInfo.Version);
}
private static (string? Title, string? TitleId, string? Version) TryLoadParamJson(
IFileSystem? fs,
string? mountRoot)
{
if (fs == null)
{
Console.WriteLine("[LOADER] param.json not found (no filesystem provided).");
return default;
}
string[] possiblePaths = string.IsNullOrEmpty(mountRoot)
? new[] { "sce_sys/param.json", "param.json" }
: new[] { $"{mountRoot}/sce_sys/param.json", $"{mountRoot}/param.json" };
string? foundPath = null;
foreach (var path in possiblePaths)
{
if (fs.Exists(path))
{
foundPath = path;
break;
}
}
if (foundPath == null)
{
Console.WriteLine("[LOADER] param.json not found (no root path / unknown layout).");
return default;
}
var applicationInfo = Ps5ParamJsonReader.TryReadPs5Param(fs, foundPath);
Console.WriteLine($"[LOADER] Loading param.json at {foundPath}");
Console.WriteLine(
$"[LOADER] Title: {applicationInfo.Title ?? "(unknown)"}, " +
$"TitleId: {applicationInfo.TitleId ?? "(unknown)"}, " +
$"Version: {applicationInfo.Version ?? "(unknown)"}");
return applicationInfo;
}
private static LoadContext ParseLayout(ReadOnlySpan<byte> imageData)
{
if (imageData.Length < Unsafe.SizeOf<ElfHeader>())
{
throw new InvalidDataException("Input image is too small to contain an ELF header.");
}
var magic = BinaryPrimitives.ReadUInt32BigEndian(imageData[..sizeof(uint)]);
if (magic is Ps4SelfMagic or Ps5SelfMagic)
{
var selfHeader = ReadUnmanaged<SelfHeader>(imageData, 0);
if (!selfHeader.HasKnownLayout || selfHeader.Unknown != 0x22)
{
throw new InvalidDataException("SELF header signature is not recognized.");
}
var segmentCount = selfHeader.SegmentCount;
var elfOffset = checked(SelfHeaderSize + (segmentCount * SelfSegmentSize));
EnsureRange(imageData.Length, (ulong)elfOffset, (ulong)Unsafe.SizeOf<ElfHeader>());
var segments = segmentCount == 0 ? Array.Empty<SelfSegment>() : GC.AllocateUninitializedArray<SelfSegment>(segmentCount);
for (var i = 0; i < segmentCount; i++)
{
var segmentOffset = checked(SelfHeaderSize + (i * SelfSegmentSize));
segments[i] = ReadUnmanaged<SelfSegment>(imageData, segmentOffset);
}
return new LoadContext(IsSelf: true, elfOffset, selfHeader.FileSize, segments);
}
if (magic != ElfMagic)
{
throw new InvalidDataException(
$"Unsupported executable signature 0x{magic:X8}");
}
return new LoadContext(IsSelf: false, ElfOffset: 0, SelfFileSize: 0, Array.Empty<SelfSegment>());
}
private static ProgramHeader[] ParseProgramHeaders(
ReadOnlySpan<byte> imageData,
LoadContext loadContext,
ElfHeader elfHeader)
{
if (elfHeader.ProgramHeaderCount == 0)
{
return Array.Empty<ProgramHeader>();
}
if (elfHeader.ProgramHeaderEntrySize < ProgramHeaderSize)
{
throw new InvalidDataException("Program header entry size is smaller than expected.");
}
var tableOffset = checked(loadContext.ElfOffset + (int)elfHeader.ProgramHeaderOffset);
EnsureRange(
imageData.Length,
(ulong)tableOffset,
(ulong)elfHeader.ProgramHeaderCount * elfHeader.ProgramHeaderEntrySize);
var headers = GC.AllocateUninitializedArray<ProgramHeader>(elfHeader.ProgramHeaderCount);
for (var i = 0; i < headers.Length; i++)
{
var entryOffset = checked(tableOffset + (i * elfHeader.ProgramHeaderEntrySize));
headers[i] = ReadUnmanaged<ProgramHeader>(imageData, entryOffset);
}
return headers;
}
private static void MapLoadSegments(
ReadOnlySpan<byte> imageData,
LoadContext loadContext,
IReadOnlyList<ProgramHeader> programHeaders,
IVirtualMemory virtualMemory,
ulong imageBase)
{
for (var index = 0; index < programHeaders.Count; index++)
{
var header = programHeaders[index];
if (header.HeaderType != ProgramHeaderType.Load || header.MemorySize == 0)
{
continue;
}
if (header.FileSize > header.MemorySize)
{
throw new InvalidDataException("ELF segment file size cannot exceed memory size.");
}
var sourceOffset = header.FileSize == 0
? 0UL
: ResolvePhysicalSegmentOffset(imageData.Length, loadContext, header, index);
var virtualAddress = header.VirtualAddress + imageBase;
Log.Debug($"Segment {index}: VAddr=0x{virtualAddress:X16}, FileSize=0x{header.FileSize:X}, MemSize=0x{header.MemorySize:X}, Align=0x{header.Alignment:X}");
if (header.Alignment > 1)
{
var vaddrMod = virtualAddress % header.Alignment;
var offsetMod = header.Offset % header.Alignment;
if (vaddrMod != offsetMod)
{
Log.Warning(
$"Segment {index} ELF alignment mismatch! " +
$"VAddr=0x{virtualAddress:X}, Offset=0x{header.Offset:X}, Align=0x{header.Alignment:X}, " +
$"VAddr%Align=0x{vaddrMod:X}, Offset%Align=0x{offsetMod:X}");
}
}
ReadOnlySpan<byte> fileData = ReadOnlySpan<byte>.Empty;
if (header.FileSize != 0)
{
if (header.FileSize > int.MaxValue)
{
throw new NotSupportedException("Segments larger than 2 GB are not currently supported.");
}
EnsureRange(imageData.Length, sourceOffset, header.FileSize);
fileData = imageData.Slice((int)sourceOffset, (int)header.FileSize);
}
virtualMemory.Map(
virtualAddress,
header.MemorySize,
sourceOffset,
fileData,
header.Flags);
}
}
private static ulong ResolveProcParamAddress(IReadOnlyList<ProgramHeader> programHeaders, ulong imageBase)
{
for (var index = 0; index < programHeaders.Count; index++)
{
var header = programHeaders[index];
if (header.HeaderType != ProgramHeaderType.SceProcParam)
{
continue;
}
return header.VirtualAddress + imageBase;
}
return 0;
}
private static IReadOnlyDictionary<ulong, string> ResolveAndPatchImportStubs(
ReadOnlySpan<byte> imageData,
LoadContext loadContext,
ElfHeader elfHeader,
IReadOnlyList<ProgramHeader> programHeaders,
IVirtualMemory virtualMemory,
ulong imageBase,
IModuleManager? moduleManager,
uint tlsModuleId,
out IReadOnlyList<ImportedSymbolRelocation> importedRelocations)
{
importedRelocations = Array.Empty<ImportedSymbolRelocation>();
if (!TryGetProgramHeader(programHeaders, ProgramHeaderType.Dynamic, out var dynamicHeader, out var dynamicHeaderIndex))
{
return EmptyImportStubs;
}
if (dynamicHeader.FileSize == 0)
{
return EmptyImportStubs;
}
if (dynamicHeader.FileSize > int.MaxValue)
{
throw new NotSupportedException("Dynamic metadata segments larger than 2 GB are not currently supported.");
}
if (!TryLoadDynamicTableBytes(
imageData,
loadContext,
virtualMemory,
imageBase,
dynamicHeader,
dynamicHeaderIndex,
out var dynamicTable))
{
return EmptyImportStubs;
}
var elfData = imageData;
var dynamicInfo = ParseDynamicInfo(dynamicTable);
Console.WriteLine($"[LOADER] Dynamic Info: StrTab=0x{dynamicInfo.StrTabOffset:X}, StrTabSize=0x{dynamicInfo.StrTabSize:X}");
Console.WriteLine($"[LOADER] Dynamic Info: SymTab=0x{dynamicInfo.SymTabOffset:X}, SymTabSize=0x{dynamicInfo.SymTabSize:X}");
Console.WriteLine($"[LOADER] Dynamic Info: Rela=0x{dynamicInfo.RelaOffset:X}, RelaSize=0x{dynamicInfo.RelaSize:X}");
Console.WriteLine($"[LOADER] Dynamic Info: JmpRel=0x{dynamicInfo.JmpRelOffset:X}, JmpRelSize=0x{dynamicInfo.JmpRelSize:X}");
Console.WriteLine($"[LOADER] Dynamic Info: PltGot=0x{dynamicInfo.PltGotOffset:X}");
Console.WriteLine($"[LOADER] TLS module id: {tlsModuleId}");
Console.WriteLine($"[LOADER] HasImportMetadata: {dynamicInfo.HasImportMetadata}");
var relocations = new List<ElfRelocation>(512);
if (dynamicInfo.RelaSize != 0 &&
TryLoadTableBytes(elfData, virtualMemory, imageBase, dynamicInfo.RelaOffset, dynamicInfo.RelaSize, out var relaBytes))
{
CollectRelocations(relaBytes, relocations);
}
if (dynamicInfo.JmpRelSize != 0 &&
TryLoadTableBytes(elfData, virtualMemory, imageBase, dynamicInfo.JmpRelOffset, dynamicInfo.JmpRelSize, out var jmpRelBytes))
{
CollectRelocations(jmpRelBytes, relocations);
}
if (!dynamicInfo.HasImportMetadata)
{
Console.WriteLine($"[LOADER] No import metadata found in ELF!");
}
if (relocations.Count != 0)
{
Console.WriteLine($"[LOADER] ImageBase runtime: 0x{imageBase:X16}");
Console.WriteLine($"[LOADER] Processing {relocations.Count} relocations...");
}
uint maxSymbolIndex = 0;
foreach (var relocation in relocations)
{
if (!IsSupportedRelocationType(relocation.Type))
{
continue;
}
if (relocation.Type is RelocationTypeRelative or RelocationTypeTlsModuleId)
{
continue;
}
if (relocation.SymbolIndex > maxSymbolIndex)
{
maxSymbolIndex = relocation.SymbolIndex;
}
}
byte[] stringTable = Array.Empty<byte>();
byte[] symbolTable = Array.Empty<byte>();
if (maxSymbolIndex != 0)
{
if (!TryLoadTableBytes(
elfData,
virtualMemory,
imageBase,
dynamicInfo.StrTabOffset,
dynamicInfo.StrTabSize,
out stringTable))
{
return EmptyImportStubs;
}
var symTableSize = dynamicInfo.SymTabSize != 0
? dynamicInfo.SymTabSize
: checked(((ulong)maxSymbolIndex + 1) * ElfSymbolSize);
if (!TryLoadTableBytes(
elfData,
virtualMemory,
imageBase,
dynamicInfo.SymTabOffset,
symTableSize,
out symbolTable))
{
return EmptyImportStubs;
}
}
var descriptors = new List<RelocationDescriptor>(256);
var orderedImportNids = new List<string>(128);
var seenImportNids = new HashSet<string>(StringComparer.Ordinal);
AppendRelocationDescriptors(
relocations,
symbolTable,
stringTable,
virtualMemory,
imageBase,
tlsModuleId,
descriptors,
orderedImportNids,
seenImportNids);
if (descriptors.Count == 0)
{
var sectionFallbackRelocCount = AppendSectionRelocationDescriptors(
imageData,
loadContext,
elfHeader,
virtualMemory,
imageBase,
tlsModuleId,
descriptors,
orderedImportNids,
seenImportNids);
if (sectionFallbackRelocCount != 0)
{
Console.WriteLine(
$"[LOADER] Section relocation fallback recovered {sectionFallbackRelocCount} relocation entries, {orderedImportNids.Count} unique NIDs, {descriptors.Count} descriptors");
}
}
Console.WriteLine($"[LOADER] Found {orderedImportNids.Count} unique NIDs, {descriptors.Count} descriptors");
if (descriptors.Count == 0)
{
Console.WriteLine($"[LOADER] No relocation descriptors!");
return EmptyImportStubs;
}
importedRelocations = BuildImportedRelocations(descriptors);
var stubsByAddress = CreateImportStubMapping(virtualMemory, orderedImportNids);
Console.WriteLine($"[LOADER] Created {stubsByAddress.Count} import stubs");
int printCount = Math.Min(10, orderedImportNids.Count);
for (int i = 0; i < printCount; i++)
{
var nid = orderedImportNids[i];
var addr = stubsByAddress.First(x => x.Value == nid).Key;
}
var nidNames = Aerolib.Instance.GetAllNidNames();
var nidCounts = new Dictionary<string, int>(StringComparer.Ordinal);
foreach (var descriptor in descriptors)
{
if (descriptor.ImportNid is not null)
{
nidCounts.TryGetValue(descriptor.ImportNid, out var count);
nidCounts[descriptor.ImportNid] = count + 1;
}
}
var addressesByNid = new Dictionary<string, ulong>(orderedImportNids.Count, StringComparer.Ordinal);
foreach (var entry in stubsByAddress)
{
addressesByNid[entry.Value] = entry.Key;
}
foreach (var descriptor in descriptors)
{
ulong targetValue;
if (descriptor.ImportNid is null)
{
targetValue = AddSigned(descriptor.SymbolValue, descriptor.Addend);
}
else
{
if (!addressesByNid.TryGetValue(descriptor.ImportNid, out var stubAddress))
{
throw new InvalidOperationException($"Import stub not found for NID '{descriptor.ImportNid}'.");
}
targetValue = AddSigned(stubAddress, descriptor.Addend);
}
if (targetValue < 0x1000)
{
if (descriptor.ValueKind == RelocationValueKind.TlsModuleId)
{
Log.Debug(
$"Patching DTPMOD64 at 0x{descriptor.TargetAddress:X} with module id 0x{targetValue:X}");
}
else
{
Log.Warning($"!!! CRITICAL !!! Patching address 0x{descriptor.TargetAddress:X} with INVALID value 0x{targetValue:X} for NID {descriptor.ImportNid ?? "(null)"}");
Log.Warning($" SymbolValue=0x{descriptor.SymbolValue:X}, Addend=0x{descriptor.Addend:X}, StubAddress=0x{(addressesByNid.TryGetValue(descriptor.ImportNid ?? "", out var sa) ? sa : 0):X}");
}
}
if (!TryWriteUInt64(virtualMemory, descriptor.TargetAddress, targetValue))
{
throw new InvalidDataException($"Failed to patch relocation at 0x{descriptor.TargetAddress:X16}.");
}
if (descriptor.TargetAddress >= 0x00000008030FC300UL &&
descriptor.TargetAddress <= 0x00000008030FC3F0UL)
{
Log.Debug(
$"[RELOC] target=0x{descriptor.TargetAddress:X16} value=0x{targetValue:X16} addend=0x{descriptor.Addend:X} nid={(descriptor.ImportNid ?? "<sym>")}");
}
}
return stubsByAddress;
}
private static int AppendSectionRelocationDescriptors(
ReadOnlySpan<byte> imageData,
LoadContext loadContext,
ElfHeader elfHeader,
IVirtualMemory virtualMemory,
ulong imageBase,
uint tlsModuleId,
ICollection<RelocationDescriptor> descriptors,
IList<string> orderedImportNids,
ISet<string> seenImportNids)
{
if (elfHeader.SectionHeaderOffset == 0 ||
elfHeader.SectionHeaderCount == 0 ||
elfHeader.SectionHeaderEntrySize < ElfSectionHeaderSize)
{
return 0;
}
var appendedRelocations = 0;
for (var sectionIndex = 0; sectionIndex < elfHeader.SectionHeaderCount; sectionIndex++)
{
if (!TryReadSectionHeader(imageData, loadContext, elfHeader, sectionIndex, out var relocationHeader) ||
relocationHeader.Type != SectionTypeRela ||
relocationHeader.Size == 0 ||
relocationHeader.EntrySize < ElfRelocationSize)
{
continue;
}
if (!TryReadElfRelativeSlice(imageData, loadContext, relocationHeader.Offset, relocationHeader.Size, out var relocationTable))
{
continue;
}
var relocations = new List<ElfRelocation>(checked((int)(relocationHeader.Size / relocationHeader.EntrySize)));
CollectRelocations(relocationTable, relocations);
if (relocations.Count == 0)
{
continue;
}
ReadOnlySpan<byte> symbolTable = ReadOnlySpan<byte>.Empty;
ReadOnlySpan<byte> stringTable = ReadOnlySpan<byte>.Empty;
if (relocationHeader.Link < elfHeader.SectionHeaderCount &&
TryReadSectionHeader(imageData, loadContext, elfHeader, (int)relocationHeader.Link, out var symbolHeader) &&
symbolHeader.Size != 0 &&
symbolHeader.EntrySize >= ElfSymbolSize &&
TryReadElfRelativeSlice(imageData, loadContext, symbolHeader.Offset, symbolHeader.Size, out symbolTable) &&
symbolHeader.Link < elfHeader.SectionHeaderCount &&
TryReadSectionHeader(imageData, loadContext, elfHeader, (int)symbolHeader.Link, out var stringHeader) &&
stringHeader.Size != 0 &&
TryReadElfRelativeSlice(imageData, loadContext, stringHeader.Offset, stringHeader.Size, out stringTable))
{
}
AppendRelocationDescriptors(
relocations,
symbolTable,
stringTable,
virtualMemory,
imageBase,
tlsModuleId,
descriptors,
orderedImportNids,
seenImportNids);
appendedRelocations += relocations.Count;
}
return appendedRelocations;
}
private static void AppendRelocationDescriptors(
IReadOnlyList<ElfRelocation> relocations,
ReadOnlySpan<byte> symbolTable,
ReadOnlySpan<byte> stringTable,
IVirtualMemory virtualMemory,
ulong imageBase,
uint tlsModuleId,
ICollection<RelocationDescriptor> descriptors,
IList<string> orderedImportNids,
ISet<string> seenImportNids)
{
foreach (var relocation in relocations)
{
if (IsFocusRelocationOffset(relocation.Offset, imageBase))
{
Log.Debug(
$"[FOCUS][SCAN] off=0x{relocation.Offset:X16} type={relocation.Type} sym={relocation.SymbolIndex} addend=0x{relocation.Addend:X}");
}
if (!IsSupportedRelocationType(relocation.Type))
{
if (IsFocusRelocationOffset(relocation.Offset, imageBase))
{
Log.Debug($"[FOCUS][SKIP] unsupported type={relocation.Type}");
}
continue;
}
if (!TryResolveMappedAddress(virtualMemory, relocation.Offset, imageBase, sizeof(ulong), out var targetAddress))
{
if (IsFocusRelocationOffset(relocation.Offset, imageBase))
{
Log.Debug("[FOCUS][SKIP] target address not mapped");
}
continue;
}
if (relocation.Type == RelocationTypeRelative)
{
descriptors.Add(new RelocationDescriptor(
targetAddress,
relocation.Addend,
null,
imageBase,
RelocationValueKind.Pointer,
IsDataImport: false));
continue;
}
if (relocation.Type == RelocationTypeTlsModuleId)
{
var dtpmodValue = tlsModuleId == 0 ? 1u : tlsModuleId;
descriptors.Add(new RelocationDescriptor(
targetAddress,
0,
null,
dtpmodValue,
RelocationValueKind.TlsModuleId,
IsDataImport: false));
continue;
}
var symbolIndex = relocation.SymbolIndex;
ElfSymbol symbol;
if (symbolIndex == 0)
{
symbol = default;
}
else if (!TryReadSymbol(symbolTable, symbolIndex, out symbol))
{
if (targetAddress >= FocusRelocGuestStart && targetAddress <= FocusRelocGuestEnd)
{
Log.Debug($"[FOCUS][SKIP] symbol read failed index={symbolIndex}");
}
continue;
}
var addend = relocation.Type is RelocationTypeGlobalData or RelocationTypeJumpSlot ? 0 : relocation.Addend;
var symbolBind = GetSymbolBind(symbol.Info);
if (symbolBind == SymbolBindLocal)
{
var symbolAddress = ResolveMappedAddressOrFallback(virtualMemory, symbol.Value, imageBase);
if (symbolAddress == 0)
{
Log.Warning(
$"Skipping local relocation with invalid symbol value 0x{symbol.Value:X} " +
$"at target 0x{targetAddress:X16}, type={relocation.Type}, sym={symbolIndex}");
}
descriptors.Add(new RelocationDescriptor(
targetAddress,
addend,
null,
symbolAddress,
RelocationValueKind.Pointer,
IsDataImport: false));
continue;
}
if (symbol.Value != 0)
{
var symbolAddress = ResolveMappedAddressOrFallback(virtualMemory, symbol.Value, imageBase);
if (symbolAddress == 0)
{
Log.Warning(
$"Skipping relocation with invalid symbol value 0x{symbol.Value:X} " +
$"at target 0x{targetAddress:X16}, type={relocation.Type}, sym={symbolIndex}");
}
descriptors.Add(new RelocationDescriptor(
targetAddress,
addend,
null,
symbolAddress,
RelocationValueKind.Pointer,
IsDataImport: false));
continue;
}
if (symbolBind is not (SymbolBindGlobal or SymbolBindWeak))
{
if (targetAddress >= FocusRelocGuestStart && targetAddress <= FocusRelocGuestEnd)
{
Log.Debug($"[FOCUS][SKIP] bind={symbolBind} not importable");
}
continue;
}
if (!TryReadNullTerminatedAscii(stringTable, symbol.NameOffset, out var symbolName))
{
if (targetAddress >= FocusRelocGuestStart && targetAddress <= FocusRelocGuestEnd)
{
Log.Debug($"[FOCUS][SKIP] symbol name read failed offset={symbol.NameOffset}");
}
continue;
}
var nid = ExtractNid(symbolName);
if (string.IsNullOrWhiteSpace(nid))
{
continue;
}
if (seenImportNids.Add(nid))
{
orderedImportNids.Add(nid);
}
descriptors.Add(new RelocationDescriptor(
targetAddress,
addend,
nid,
0,
RelocationValueKind.Pointer,
IsDataImport: GetSymbolType(symbol.Info) == SymbolTypeObject));
}
}
private static void RegisterRuntimeSymbolsAndHooks(
ReadOnlySpan<byte> imageData,
LoadContext loadContext,
IReadOnlyList<ProgramHeader> programHeaders,
ElfHeader elfHeader,
IVirtualMemory virtualMemory,
ulong imageBase,
IDictionary<ulong, string> importStubs,
IDictionary<string, ulong> runtimeSymbols)
{
var sectionSymbols = RegisterSectionRuntimeSymbols(imageData, loadContext, elfHeader, imageBase, importStubs, runtimeSymbols);
var dynamicSymbols = RegisterDynamicRuntimeSymbols(
imageData,
loadContext,
programHeaders,
virtualMemory,
imageBase,
importStubs,
runtimeSymbols);
if (sectionSymbols > 0 || dynamicSymbols > 0)
{
Log.Info(
$"Runtime symbol index populated: section={sectionSymbols}, dynamic={dynamicSymbols}, total={runtimeSymbols.Count}");
}
}
private static void CollectInitializerFunctions(
ReadOnlySpan<byte> imageData,
LoadContext loadContext,
IReadOnlyList<ProgramHeader> programHeaders,
IVirtualMemory virtualMemory,
ulong imageBase,
out ulong initFunctionEntryPoint,
out IReadOnlyList<ulong> preInitializerFunctions,
out IReadOnlyList<ulong> initializerFunctions)
{
initFunctionEntryPoint = 0;
preInitializerFunctions = EmptyInitializerFunctions;
initializerFunctions = EmptyInitializerFunctions;
if (!TryGetProgramHeader(programHeaders, ProgramHeaderType.Dynamic, out var dynamicHeader, out var dynamicHeaderIndex) ||
dynamicHeader.FileSize == 0)
{
return;
}
if (!TryLoadDynamicTableBytes(
imageData,
loadContext,
virtualMemory,
imageBase,
dynamicHeader,
dynamicHeaderIndex,
out var dynamicTable))
{
return;
}
var dynamicInfo = ParseDynamicInfo(dynamicTable);
var preInitializers = new List<ulong>(4);
var initializers = new List<ulong>(8);
initFunctionEntryPoint = ResolveMappedAddressOrFallback(virtualMemory, dynamicInfo.InitOffset, imageBase);
if (initFunctionEntryPoint < 0x10000)
{
initFunctionEntryPoint = 0;
}
AppendInitializerArrayEntries(
preInitializers,
imageData,
virtualMemory,
imageBase,
dynamicInfo.PreInitArrayOffset,
dynamicInfo.PreInitArraySize);
AppendResolvedInitializer(initializers, dynamicInfo.InitOffset, virtualMemory, imageBase);
AppendInitializerArrayEntries(
initializers,
imageData,
virtualMemory,
imageBase,
dynamicInfo.InitArrayOffset,
dynamicInfo.InitArraySize);
if (preInitializers.Count != 0)
{
preInitializerFunctions = preInitializers;
}
if (initializers.Count != 0)
{
initializerFunctions = initializers;
}
if (preInitializers.Count != 0 || initializers.Count != 0)
{
Log.Info(
$"Initializers discovered: preinit={preInitializers.Count}, init={initializers.Count}");
}
}
private static void AppendInitializerArrayEntries(
ICollection<ulong> destination,
ReadOnlySpan<byte> imageData,
IVirtualMemory virtualMemory,
ulong imageBase,
ulong arrayOffset,
ulong arraySize)
{
if (arrayOffset == 0 || arraySize < sizeof(ulong))
{
return;
}
if (!TryLoadTableBytes(imageData, virtualMemory, imageBase, arrayOffset, arraySize, out var arrayBytes))
{
return;
}
var entryCount = arrayBytes.Length / sizeof(ulong);
for (var i = 0; i < entryCount; i++)
{
var entryOffset = i * sizeof(ulong);
var entryAddress = BinaryPrimitives.ReadUInt64LittleEndian(arrayBytes.AsSpan(entryOffset, sizeof(ulong)));
AppendResolvedInitializer(destination, entryAddress, virtualMemory, imageBase);
}
}
private static void AppendResolvedInitializer(
ICollection<ulong> destination,
ulong functionAddress,
IVirtualMemory virtualMemory,
ulong imageBase)
{
var resolvedAddress = ResolveMappedAddressOrFallback(virtualMemory, functionAddress, imageBase);
if (resolvedAddress < 0x10000)
{
return;
}
foreach (var existing in destination)
{
if (existing == resolvedAddress)
{
return;
}
}
destination.Add(resolvedAddress);
}
private static IReadOnlyList<ImportedSymbolRelocation> BuildImportedRelocations(
IReadOnlyList<RelocationDescriptor> descriptors)
{
if (descriptors.Count == 0)
{
return Array.Empty<ImportedSymbolRelocation>();
}
var importedRelocations = new List<ImportedSymbolRelocation>(descriptors.Count);
foreach (var descriptor in descriptors)
{
if (descriptor.ImportNid is null || descriptor.ValueKind != RelocationValueKind.Pointer)
{
continue;
}
importedRelocations.Add(new ImportedSymbolRelocation(
descriptor.TargetAddress,
descriptor.Addend,
descriptor.ImportNid,
descriptor.IsDataImport));
}
return importedRelocations.Count == 0
? Array.Empty<ImportedSymbolRelocation>()
: importedRelocations;
}
private static int RegisterSectionRuntimeSymbols(
ReadOnlySpan<byte> imageData,
LoadContext loadContext,
ElfHeader elfHeader,
ulong imageBase,
IDictionary<ulong, string> importStubs,
IDictionary<string, ulong> runtimeSymbols)
{
if (elfHeader.SectionHeaderOffset == 0 ||
elfHeader.SectionHeaderCount == 0 ||
elfHeader.SectionHeaderEntrySize < ElfSectionHeaderSize)
{
return 0;
}
var added = 0;
for (var sectionIndex = 0; sectionIndex < elfHeader.SectionHeaderCount; sectionIndex++)
{
if (!TryReadSectionHeader(imageData, loadContext, elfHeader, sectionIndex, out var sectionHeader))
{
continue;
}
if (sectionHeader.Type != SectionTypeSymbolTable ||
sectionHeader.Size == 0 ||
sectionHeader.EntrySize < ElfSymbolSize ||
sectionHeader.Link >= elfHeader.SectionHeaderCount)
{
continue;
}
if (!TryReadSectionHeader(imageData, loadContext, elfHeader, (int)sectionHeader.Link, out var stringHeader))
{
continue;
}
if (!TryReadElfRelativeSlice(imageData, loadContext, stringHeader.Offset, stringHeader.Size, out var stringTable))
{
continue;
}
var symbolCount = sectionHeader.Size / sectionHeader.EntrySize;
for (ulong symbolIndex = 0; symbolIndex < symbolCount; symbolIndex++)
{
var symbolOffset = sectionHeader.Offset + (symbolIndex * sectionHeader.EntrySize);
if (!TryReadElfRelativeSlice(imageData, loadContext, symbolOffset, ElfSymbolSize, out var symbolBytes))
{
continue;
}
var symbol = ReadUnmanaged<ElfSymbol>(symbolBytes, 0);
if (symbol.Value == 0 || symbol.NameOffset == 0)
{
continue;
}
if (!TryReadNullTerminatedAscii(stringTable, symbol.NameOffset, out var symbolName))
{
continue;
}
var symbolAddress = symbol.Value >= imageBase
? symbol.Value
: unchecked(imageBase + symbol.Value);
if (symbolAddress < 0x10000)
{
continue;
}
if (RegisterRuntimeSymbol(runtimeSymbols, importStubs, symbolName, symbolAddress))
{
added++;
}
}
}
return added;
}
private static int RegisterDynamicRuntimeSymbols(
ReadOnlySpan<byte> imageData,
LoadContext loadContext,
IReadOnlyList<ProgramHeader> programHeaders,
IVirtualMemory virtualMemory,
ulong imageBase,
IDictionary<ulong, string> importStubs,
IDictionary<string, ulong> runtimeSymbols)
{
if (!TryGetProgramHeader(programHeaders, ProgramHeaderType.Dynamic, out var dynamicHeader, out var dynamicHeaderIndex))
{
return 0;
}
if (dynamicHeader.FileSize == 0 || dynamicHeader.FileSize > int.MaxValue)
{
return 0;
}
if (!TryLoadDynamicTableBytes(
imageData,
loadContext,
virtualMemory,
imageBase,
dynamicHeader,
dynamicHeaderIndex,
out var dynamicTable))
{
return 0;
}
var dynamicInfo = ParseDynamicInfo(dynamicTable);
if (dynamicInfo.SymTabOffset == 0 || dynamicInfo.StrTabOffset == 0)
{
return 0;
}
if (!TryLoadTableBytes(
imageData,
virtualMemory,
imageBase,
dynamicInfo.StrTabOffset,
dynamicInfo.StrTabSize,
out var stringTable) ||
stringTable.Length == 0)
{
return 0;
}
ulong symTableSize = dynamicInfo.SymTabSize;
if (symTableSize == 0 || symTableSize > int.MaxValue)
{
return 0;
}
if (!TryLoadTableBytes(
imageData,
virtualMemory,
imageBase,
dynamicInfo.SymTabOffset,
symTableSize,
out var symbolTable) ||
symbolTable.Length < ElfSymbolSize)
{
return 0;
}
var symbolCount = (uint)(symbolTable.Length / ElfSymbolSize);
var added = 0;
for (uint symbolIndex = 0; symbolIndex < symbolCount; symbolIndex++)
{
if (!TryReadSymbol(symbolTable, symbolIndex, out var symbol) ||
symbol.Value == 0 ||
symbol.NameOffset == 0 ||
!TryReadNullTerminatedAscii(stringTable, symbol.NameOffset, out var symbolName))
{
continue;
}
var symbolAddress = symbol.Value >= imageBase
? symbol.Value
: unchecked(imageBase + symbol.Value);
if (symbolAddress < 0x10000)
{
continue;
}
if (RegisterRuntimeSymbol(runtimeSymbols, importStubs, symbolName, symbolAddress))
{
added++;
}
}
return added;
}
private static bool RegisterRuntimeSymbol(
IDictionary<string, ulong> runtimeSymbols,
IDictionary<ulong, string> importStubs,
string symbolName,
ulong symbolAddress)
{
if (string.IsNullOrWhiteSpace(symbolName) || symbolAddress < 0x10000)
{
return false;
}
var addedAny = false;
if (!runtimeSymbols.ContainsKey(symbolName))
{
runtimeSymbols[symbolName] = symbolAddress;
addedAny = true;
}
var nid = ExtractNid(symbolName);
if (!string.IsNullOrWhiteSpace(nid) &&
!string.Equals(symbolName, nid, StringComparison.Ordinal) &&
!runtimeSymbols.ContainsKey(nid))
{
runtimeSymbols[nid] = symbolAddress;
addedAny = true;
}
if (symbolName.Length > 1 &&
symbolName[0] == '_' &&
!runtimeSymbols.ContainsKey(symbolName[1..]))
{
runtimeSymbols[symbolName[1..]] = symbolAddress;
addedAny = true;
}
if (string.Equals(symbolName, "kernel_dynlib_dlsym", StringComparison.Ordinal))
{
importStubs[symbolAddress] = RuntimeStubNids.KernelDynlibDlsym;
}
return addedAny;
}
private static bool TryReadSectionHeader(
ReadOnlySpan<byte> imageData,
LoadContext loadContext,
ElfHeader elfHeader,
int sectionIndex,
out ElfSectionHeader sectionHeader)
{
if ((uint)sectionIndex >= elfHeader.SectionHeaderCount)
{
sectionHeader = default;
return false;
}
var headerOffset = elfHeader.SectionHeaderOffset + ((ulong)sectionIndex * elfHeader.SectionHeaderEntrySize);
if (!TryReadElfRelativeSlice(imageData, loadContext, headerOffset, ElfSectionHeaderSize, out var sectionBytes))
{
sectionHeader = default;
return false;
}
sectionHeader = ReadUnmanaged<ElfSectionHeader>(sectionBytes, 0);
return true;
}
private static bool TryLoadDynamicTableBytes(
ReadOnlySpan<byte> imageData,
LoadContext loadContext,
IVirtualMemory virtualMemory,
ulong imageBase,
ProgramHeader dynamicHeader,
int dynamicHeaderIndex,
out ReadOnlySpan<byte> dynamicTable)
{
if (TryLoadTableBytes(
imageData,
virtualMemory,
imageBase,
dynamicHeader.VirtualAddress,
dynamicHeader.FileSize,
out var loadedDynamicTable))
{
dynamicTable = loadedDynamicTable;
return true;
}
var dynamicOffset = ResolvePhysicalSegmentOffset(imageData.Length, loadContext, dynamicHeader, dynamicHeaderIndex);
if (!TrySlice(imageData, dynamicOffset, dynamicHeader.FileSize, out dynamicTable))
{
dynamicTable = default;
return false;
}
return true;
}
private static bool TryReadElfRelativeSlice(
ReadOnlySpan<byte> imageData,
LoadContext loadContext,
ulong elfRelativeOffset,
ulong size,
out ReadOnlySpan<byte> slice)
{
try
{
var absoluteOffset = checked((ulong)loadContext.ElfOffset + elfRelativeOffset);
return TrySlice(imageData, absoluteOffset, size, out slice);
}
catch (OverflowException)
{
slice = default;
return false;
}
}
private static Dictionary<ulong, string> CreateImportStubMapping(IVirtualMemory virtualMemory, IReadOnlyList<string> orderedImportNids)
{
if (orderedImportNids.Count == 0)
{
return new Dictionary<ulong, string>();
}
var requiredBytes = checked((ulong)orderedImportNids.Count * ImportStubSlotSize);
var mapSize = AlignUp(Math.Max(requiredBytes, (ulong)PageSize), PageSize);
Log.Debug(
$"CreateImportStubMapping: nids={orderedImportNids.Count}, required=0x{requiredBytes:X}, map_size=0x{mapSize:X}");
if (mapSize > int.MaxValue)
{
throw new NotSupportedException("Import stub mapping exceeds 2 GB and is not supported.");
}
var stubData = new byte[(int)mapSize];
for (var i = 0; i < orderedImportNids.Count; i++)
{
var slotOffset = checked((int)((ulong)i * ImportStubSlotSize));
stubData[slotOffset] = StubTrapOpcode;
stubData[slotOffset + 1] = StubReturnOpcode;
var nid = orderedImportNids[i];
var nidHash = NidToUInt32(nid);
BinaryPrimitives.WriteUInt32LittleEndian(stubData.AsSpan(slotOffset + 8), nidHash);
}
var stubBaseAddress = TryMapImportStubRegion(virtualMemory, mapSize, stubData);
var byAddress = new Dictionary<ulong, string>(orderedImportNids.Count);
for (var i = 0; i < orderedImportNids.Count; i++)
{
var address = stubBaseAddress + ((ulong)i * ImportStubSlotSize);
byAddress[address] = orderedImportNids[i];
}
return byAddress;
}
private static ulong TryMapImportStubRegion(IVirtualMemory virtualMemory, ulong mapSize, ReadOnlySpan<byte> mapData)
{
for (var i = 0; i < 64; i++)
{
var candidateBase = ImportStubBaseAddress - ((ulong)i * ImportStubAddressStride);
if (IsAddressRangeMapped(virtualMemory, candidateBase, mapSize))
{
continue;
}
try
{
virtualMemory.Map(
candidateBase,
mapSize,
fileOffset: 0,
mapData,
ProgramHeaderFlags.Read | ProgramHeaderFlags.Execute);
return candidateBase;
}
catch (InvalidOperationException)
{
continue;
}
}
throw new InvalidOperationException("Unable to reserve an import stub region in virtual memory.");
}
private static bool IsAddressRangeMapped(IVirtualMemory virtualMemory, ulong start, ulong size)
{
var end = checked(start + size);
var regions = virtualMemory.SnapshotRegions();
for (var i = 0; i < regions.Count; i++)
{
var region = regions[i];
var regionStart = region.VirtualAddress;
var regionEnd = checked(regionStart + region.MemorySize);
if (start < regionEnd && end > regionStart)
{
return true;
}
}
return false;
}
private static void CollectRelocations(ReadOnlySpan<byte> relocationTable, ICollection<ElfRelocation> relocations)
{
for (var offset = 0; offset + ElfRelocationSize <= relocationTable.Length; offset += ElfRelocationSize)
{
relocations.Add(ReadRelocation(relocationTable, offset));
}
}
private static bool TryGetProgramHeader(
IReadOnlyList<ProgramHeader> programHeaders,
ProgramHeaderType targetType,
out ProgramHeader header,
out int headerIndex)
{
for (var i = 0; i < programHeaders.Count; i++)
{
if (programHeaders[i].HeaderType != targetType)
{
continue;
}
header = programHeaders[i];
headerIndex = i;
return true;
}
header = default;
headerIndex = -1;
return false;
}
private static DynamicInfo ParseDynamicInfo(ReadOnlySpan<byte> dynamicTable)
{
ulong strTabOffset = 0;
ulong strTabSize = 0;
ulong symTabOffset = 0;
ulong symTabSize = 0;
ulong initOffset = 0;
ulong relaOffset = 0;
ulong relaSize = 0;
ulong jmpRelOffset = 0;
ulong jmpRelSize = 0;
ulong pltGotOffset = 0;
ulong initArrayOffset = 0;
ulong initArraySize = 0;
ulong preInitArrayOffset = 0;
ulong preInitArraySize = 0;
for (var offset = 0; offset + DynamicEntrySize <= dynamicTable.Length; offset += DynamicEntrySize)
{
var tag = BinaryPrimitives.ReadInt64LittleEndian(dynamicTable.Slice(offset, sizeof(long)));
var value = BinaryPrimitives.ReadUInt64LittleEndian(dynamicTable.Slice(offset + sizeof(long), sizeof(ulong)));
if (tag == DtNull)
{
break;
}
switch (tag)
{
case DtStrTab:
if (strTabOffset == 0)
{
strTabOffset = value;
}
break;
case DtStrSize:
if (strTabSize == 0)
{
strTabSize = value;
}
break;
case DtSymTab:
if (symTabOffset == 0)
{
symTabOffset = value;
}
break;
case DtRela:
if (relaOffset == 0)
{
relaOffset = value;
}
break;
case DtRelaSize:
if (relaSize == 0)
{
relaSize = value;
}
break;
case DtInit:
if (initOffset == 0)
{
initOffset = value;
}
break;
case DtJmpRel:
if (jmpRelOffset == 0)
{
jmpRelOffset = value;
}
break;
case DtInitArray:
if (initArrayOffset == 0)
{
initArrayOffset = value;
}
break;
case DtInitArraySize:
if (initArraySize == 0)
{
initArraySize = value;
}
break;
case DtPreInitArray:
if (preInitArrayOffset == 0)
{
preInitArrayOffset = value;
}
break;
case DtPreInitArraySize:
if (preInitArraySize == 0)
{
preInitArraySize = value;
}
break;
case DtPltRelSize:
if (jmpRelSize == 0)
{
jmpRelSize = value;
}
break;
case DtPltGot:
if (pltGotOffset == 0)
{
pltGotOffset = value;
}
break;
case DtSceStrTab:
strTabOffset = value;
break;
case DtSceStrSize:
strTabSize = value;
break;
case DtSceSymTab:
symTabOffset = value;
break;
case DtSceSymTabSize:
symTabSize = value;
break;
case DtSceRela:
relaOffset = value;
break;
case DtSceRelaSize:
relaSize = value;
break;
case DtSceJmpRel:
jmpRelOffset = value;
break;
case DtScePltRelSize:
jmpRelSize = value;
break;
}
}
return new DynamicInfo(
strTabOffset,
strTabSize,
symTabOffset,
symTabSize,
initOffset,
relaOffset,
relaSize,
jmpRelOffset,
jmpRelSize,
pltGotOffset,
initArrayOffset,
initArraySize,
preInitArrayOffset,
preInitArraySize);
}
private static bool IsSupportedRelocationType(uint relocationType)
{
return relocationType is
RelocationTypeAbsolute64 or
RelocationTypeGlobalData or
RelocationTypeJumpSlot or
RelocationTypeRelative or
RelocationTypeTlsModuleId;
}
private static ulong DetermineRequestedImageBase(
IVirtualMemory virtualMemory,
ulong totalImageSize,
bool isNextGen,
bool clearVirtualMemory)
{
if (clearVirtualMemory)
{
return isNextGen ? Ps5MainImageBase : Ps4MainImageBase;
}
var (searchStart, searchEnd) = GetModuleSearchRange(isNextGen);
var alignedSize = AlignUp(Math.Max(totalImageSize, (ulong)PageSize), (ulong)PageSize);
var candidate = searchStart;
foreach (var region in virtualMemory.SnapshotRegions())
{
if (region.VirtualAddress >= searchEnd)
{
continue;
}
var regionEnd = region.VirtualAddress + region.MemorySize;
if (regionEnd <= searchStart)
{
continue;
}
var regionAlignedEnd = AlignUp(regionEnd + ModulePlacementStep, (ulong)PageSize);
if (regionAlignedEnd > candidate)
{
candidate = regionAlignedEnd;
}
}
if (candidate < searchStart)
{
candidate = searchStart;
}
if (candidate + alignedSize > searchEnd)
{
candidate = searchStart;
}
return AlignUp(candidate, (ulong)PageSize);
}
private static bool TryAllocateAdditionalImageAtExact(
PhysicalVirtualMemory physicalVm,
ulong preferredBase,
ulong totalImageSize,
bool isNextGen,
out ulong allocatedBase)
{
var (searchStart, searchEnd) = GetModuleSearchRange(isNextGen);
var alignedSize = AlignUp(Math.Max(totalImageSize, (ulong)PageSize), (ulong)PageSize);
if (preferredBase < searchStart || preferredBase + alignedSize > searchEnd)
{
preferredBase = searchStart;
}
for (var attempt = 0; attempt < 256; attempt++)
{
var candidate = AlignUp(preferredBase + ((ulong)attempt * ModulePlacementStep), (ulong)PageSize);
if (candidate + alignedSize > searchEnd)
{
break;
}
if (physicalVm.TryAllocateAtExact(candidate, alignedSize, executable: true, out allocatedBase))
{
return true;
}
}
allocatedBase = 0;
return false;
}
private static (ulong Start, ulong End) GetModuleSearchRange(bool isNextGen)
{
return isNextGen
? (Ps5ModuleSearchStart, Ps5ModuleSearchEnd)
: (Ps4ModuleSearchStart, Ps4ModuleSearchEnd);
}
private static ulong CalculateTotalImageSize(IReadOnlyList<ProgramHeader> programHeaders)
{
ulong minAddr = ulong.MaxValue;
ulong maxAddr = 0;
foreach (var header in programHeaders)
{
if (header.HeaderType == ProgramHeaderType.Load && header.MemorySize > 0)
{
if (header.VirtualAddress < minAddr)
minAddr = header.VirtualAddress;
var endAddr = header.VirtualAddress + header.MemorySize;
if (endAddr > maxAddr)
maxAddr = endAddr;
}
}
if (minAddr == ulong.MaxValue)
return 0;
return maxAddr - minAddr;
}
private static ulong ComputeImageBase(IReadOnlyList<ProgramHeader> programHeaders)
{
var hasBase = false;
ulong imageBase = 0;
for (var i = 0; i < programHeaders.Count; i++)
{
var header = programHeaders[i];
if (header.HeaderType != ProgramHeaderType.Load || header.MemorySize == 0)
{
continue;
}
if (!hasBase || header.VirtualAddress < imageBase)
{
imageBase = header.VirtualAddress;
hasBase = true;
}
}
return hasBase ? imageBase : 0;
}
private static bool TryLoadTableBytes(
ReadOnlySpan<byte> elfData,
IVirtualMemory virtualMemory,
ulong imageBase,
ulong location,
ulong size,
out byte[] tableBytes)
{
if (size == 0 || size > int.MaxValue)
{
Console.WriteLine($"[LOADER] TryLoadTableBytes: size=0 or too big (0x{size:X})");
tableBytes = Array.Empty<byte>();
return false;
}
Console.WriteLine($"[LOADER] TryLoadTableBytes: trying location=0x{location:X}, size=0x{size:X}, imageBase=0x{imageBase:X}");
tableBytes = GC.AllocateUninitializedArray<byte>((int)size);
var guestAddr = location + imageBase;
Log.Debug($"TryLoadTableBytes: trying guest address 0x{guestAddr:X}");
if (virtualMemory.TryRead(guestAddr, tableBytes))
{
Log.Debug($"TryLoadTableBytes: loaded from guest memory at 0x{guestAddr:X}");
return true;
}
if (virtualMemory.TryRead(location, tableBytes))
{
Log.Debug($"TryLoadTableBytes: loaded from absolute guest address 0x{location:X}");
return true;
}
if (!elfData.IsEmpty && location <= int.MaxValue && location + size <= (ulong)elfData.Length)
{
var slice = elfData.Slice((int)location, (int)size);
tableBytes = slice.ToArray();
Log.Debug($"TryLoadTableBytes: loaded from elfData as file offset at 0x{location:X}");
return true;
}
Log.Warning($"TryLoadTableBytes: FAILED for location 0x{location:X}");
tableBytes = Array.Empty<byte>();
return false;
}
private static bool TryReadSymbol(ReadOnlySpan<byte> symbolTable, uint symbolIndex, out ElfSymbol symbol)
{
symbol = default;
var offset64 = (long)symbolIndex * ElfSymbolSize;
if (offset64 < 0 || offset64 + ElfSymbolSize > symbolTable.Length)
{
return false;
}
var offset = (int)offset64;
var entry = symbolTable.Slice(offset, ElfSymbolSize);
symbol = new ElfSymbol(
BinaryPrimitives.ReadUInt32LittleEndian(entry.Slice(0, sizeof(uint))),
entry[4],
entry[5],
BinaryPrimitives.ReadUInt16LittleEndian(entry.Slice(6, sizeof(ushort))),
BinaryPrimitives.ReadUInt64LittleEndian(entry.Slice(8, sizeof(ulong))),
BinaryPrimitives.ReadUInt64LittleEndian(entry.Slice(16, sizeof(ulong))));
return true;
}
private static byte GetSymbolBind(byte info)
{
return (byte)(info >> 4);
}
private static byte GetSymbolType(byte info)
{
return (byte)(info & 0x0F);
}
private static bool IsFocusRelocationOffset(ulong relocationOffset, ulong imageBase)
{
if (relocationOffset >= FocusRelocGuestStart && relocationOffset <= FocusRelocGuestEnd)
{
return true;
}
if (relocationOffset <= ulong.MaxValue - imageBase)
{
var rebased = relocationOffset + imageBase;
if (rebased >= FocusRelocGuestStart && rebased <= FocusRelocGuestEnd)
{
return true;
}
}
return false;
}
private static ulong ResolveMappedAddressOrFallback(IVirtualMemory virtualMemory, ulong address, ulong imageBase)
{
Log.Debug($"ResolveMappedAddressOrFallback addr=0x{address:X} imageBase=0x{imageBase:X16}");
if (address == 0)
{
Log.Debug("-> return 0 (null)");
return 0;
}
if (TryResolveMappedAddress(virtualMemory, address, imageBase, 1, out var resolved))
{
Log.Debug($"-> resolved raw 0x{resolved:X16}");
return resolved;
}
if (address <= ulong.MaxValue - imageBase)
{
var rebased = address + imageBase;
if (TryResolveMappedAddress(virtualMemory, rebased, imageBase, 1, out var resolvedRebased))
{
Log.Debug($"-> resolved rebased 0x{resolvedRebased:X16}");
return resolvedRebased;
}
}
if (address < 0x10000)
{
Log.Debug($"-> reject small 0x{address:X}");
return 0;
}
Log.Debug($"-> fallback raw 0x{address:X}");
return address;
}
private static bool TryResolveMappedAddress(
IVirtualMemory virtualMemory,
ulong address,
ulong imageBase,
int requiredBytes,
out ulong resolvedAddress)
{
if (CanAccessAddress(virtualMemory, address, requiredBytes))
{
resolvedAddress = address;
return true;
}
if (address <= ulong.MaxValue - imageBase)
{
var rebased = address + imageBase;
if (CanAccessAddress(virtualMemory, rebased, requiredBytes))
{
resolvedAddress = rebased;
return true;
}
}
resolvedAddress = 0;
return false;
}
private static bool CanAccessAddress(IVirtualMemory virtualMemory, ulong address, int requiredBytes)
{
if (requiredBytes <= 0)
{
return true;
}
Span<byte> probe = stackalloc byte[sizeof(ulong)];
var length = Math.Min(requiredBytes, probe.Length);
return virtualMemory.TryRead(address, probe[..length]);
}
private static ElfRelocation ReadRelocation(ReadOnlySpan<byte> relocationTable, int offset)
{
var entry = relocationTable.Slice(offset, ElfRelocationSize);
return new ElfRelocation(
BinaryPrimitives.ReadUInt64LittleEndian(entry.Slice(0, sizeof(ulong))),
BinaryPrimitives.ReadUInt64LittleEndian(entry.Slice(8, sizeof(ulong))),
BinaryPrimitives.ReadInt64LittleEndian(entry.Slice(16, sizeof(long))));
}
private static bool TryReadNullTerminatedAscii(ReadOnlySpan<byte> source, uint offset, out string value)
{
if (offset >= (uint)source.Length)
{
value = string.Empty;
return false;
}
var slice = source[(int)offset..];
var terminatorIndex = slice.IndexOf((byte)0);
if (terminatorIndex < 0)
{
value = string.Empty;
return false;
}
value = Encoding.ASCII.GetString(slice[..terminatorIndex]);
return true;
}
private static string ExtractNid(string symbolName)
{
if (string.IsNullOrWhiteSpace(symbolName))
{
return string.Empty;
}
var separator = symbolName.IndexOf('#');
return separator <= 0 ? symbolName : symbolName[..separator];
}
private static bool TryWriteUInt64(IVirtualMemory virtualMemory, ulong address, ulong value)
{
Span<byte> buffer = stackalloc byte[sizeof(ulong)];
BinaryPrimitives.WriteUInt64LittleEndian(buffer, value);
return virtualMemory.TryWrite(address, buffer);
}
private static ulong AlignUp(ulong value, ulong alignment)
{
var mask = alignment - 1;
return (value + mask) & ~mask;
}
private static ulong AddSigned(ulong value, long addend)
{
if (addend >= 0)
{
return unchecked(value + (ulong)addend);
}
var magnitude = unchecked((ulong)(-(addend + 1))) + 1;
return unchecked(value - magnitude);
}
private static uint NidToUInt32(string nid)
{
if (string.IsNullOrEmpty(nid))
return 0;
uint hash = 0;
for (int i = 0; i < Math.Min(nid.Length, 8); i++)
{
hash = (hash << 4) | (byte)nid[i];
}
hash ^= (uint)nid.Length;
return hash;
}
private static bool TrySlice(ReadOnlySpan<byte> source, ulong offset, ulong size, out ReadOnlySpan<byte> slice)
{
if (size == 0)
{
slice = ReadOnlySpan<byte>.Empty;
return true;
}
if (size > int.MaxValue || offset > (ulong)source.Length)
{
slice = default;
return false;
}
var end = offset + size;
if (end < offset || end > (ulong)source.Length)
{
slice = default;
return false;
}
slice = source.Slice((int)offset, (int)size);
return true;
}
private static ulong ResolvePhysicalSegmentOffset(int imageLength, LoadContext loadContext, ProgramHeader header, int headerIndex)
{
if (!loadContext.IsSelf)
{
return checked((ulong)loadContext.ElfOffset + header.Offset);
}
if (!TryResolveSelfSegmentOffset(
imageLength,
loadContext.SelfSegments,
header,
headerIndex,
out var offset,
out var resolveStatus))
{
if (TryResolveSelfFallbackOffset(imageLength, loadContext, header, out var fallbackOffset))
{
return fallbackOffset;
}
if (resolveStatus is SelfSegmentResolveStatus.Encrypted or SelfSegmentResolveStatus.Compressed)
{
throw new NotSupportedException(
$"SELF segment for program header {headerIndex} is marked as {resolveStatus.ToString().ToLowerInvariant()} and no dumped payload could be resolved. " +
"Runtime decryption is not implemented yet. Use a decrypted ELF/FSELF image.");
}
throw new NotSupportedException($"SELF segment mapping for program header {headerIndex} could not be resolved.");
}
return offset;
}
private static bool TryResolveSelfFallbackOffset(
int imageLength,
LoadContext loadContext,
ProgramHeader header,
out ulong offset)
{
if (TryIsInRange(imageLength, header.Offset, header.FileSize))
{
offset = header.Offset;
return true;
}
var elfRelative = checked((ulong)loadContext.ElfOffset + header.Offset);
if (TryIsInRange(imageLength, elfRelative, header.FileSize))
{
offset = elfRelative;
return true;
}
if (loadContext.SelfFileSize <= (ulong)imageLength)
{
var tailSize = (ulong)imageLength - loadContext.SelfFileSize;
if (tailSize == header.FileSize && TryIsInRange(imageLength, loadContext.SelfFileSize, header.FileSize))
{
offset = loadContext.SelfFileSize;
return true;
}
}
offset = 0;
return false;
}
private static bool TryIsInRange(int imageLength, ulong offset, ulong size)
{
if (offset > (ulong)imageLength)
{
return false;
}
var end = offset + size;
return end >= offset && end <= (ulong)imageLength;
}
private static bool TryResolveSelfSegmentOffset(
int imageLength,
IReadOnlyList<SelfSegment> selfSegments,
ProgramHeader header,
int headerIndex,
out ulong offset,
out SelfSegmentResolveStatus status)
{
foreach (var segment in selfSegments)
{
if (!segment.IsBlocked)
{
continue;
}
var phdrId = segment.ProgramHeaderId;
if (phdrId != (ulong)headerIndex)
{
continue;
}
if (segment.IsEncrypted || segment.IsCompressed)
{
if (TryIsInRange(imageLength, segment.Offset, header.FileSize))
{
offset = segment.Offset;
status = SelfSegmentResolveStatus.ResolvedDumped;
return true;
}
offset = 0;
status = segment.IsEncrypted
? SelfSegmentResolveStatus.Encrypted
: SelfSegmentResolveStatus.Compressed;
return false;
}
if (!TryIsInRange(imageLength, segment.Offset, header.FileSize))
{
continue;
}
offset = segment.Offset;
status = SelfSegmentResolveStatus.Resolved;
return true;
}
offset = 0;
status = SelfSegmentResolveStatus.NotFound;
return false;
}
private static void ValidateElfHeader(ElfHeader header)
{
if (!header.HasElfMagic)
{
throw new InvalidDataException("Input does not contain a valid ELF header.");
}
if (!header.Is64Bit)
{
throw new InvalidDataException("Only ELF64 images are currently supported.");
}
if (!header.IsLittleEndian)
{
throw new InvalidDataException("Only little-endian ELF images are currently supported.");
}
if (header.ProgramHeaderEntrySize != ProgramHeaderSize)
{
throw new InvalidDataException($"Unsupported ELF program header entry size: {header.ProgramHeaderEntrySize}.");
}
}
private static unsafe T ReadUnmanaged<T>(ReadOnlySpan<byte> source, int offset)
where T : unmanaged
{
var size = Unsafe.SizeOf<T>();
EnsureRange(source.Length, checked((ulong)offset), (ulong)size);
fixed (byte* ptr = &MemoryMarshal.GetReference(source))
{
return Unsafe.ReadUnaligned<T>(ptr + offset);
}
}
private static void EnsureRange(int length, ulong offset, ulong size)
{
if (offset > (ulong)length)
{
throw new InvalidDataException("Segment offset exceeds image length.");
}
var end = offset + size;
if (end < offset || end > (ulong)length)
{
throw new InvalidDataException("Segment extends beyond image bounds.");
}
}
private readonly record struct LoadContext(bool IsSelf, int ElfOffset, ulong SelfFileSize, IReadOnlyList<SelfSegment> SelfSegments);
private readonly record struct DynamicInfo(
ulong StrTabOffset,
ulong StrTabSize,
ulong SymTabOffset,
ulong SymTabSize,
ulong InitOffset,
ulong RelaOffset,
ulong RelaSize,
ulong JmpRelOffset,
ulong JmpRelSize,
ulong PltGotOffset,
ulong InitArrayOffset,
ulong InitArraySize,
ulong PreInitArrayOffset,
ulong PreInitArraySize)
{
public bool HasImportMetadata =>
StrTabOffset != 0 &&
StrTabSize != 0 &&
SymTabOffset != 0 &&
(RelaSize != 0 || JmpRelSize != 0);
}
private readonly record struct ElfSectionHeader(
uint NameOffset,
uint Type,
ulong Flags,
ulong Address,
ulong Offset,
ulong Size,
uint Link,
uint Info,
ulong AddressAlign,
ulong EntrySize);
private readonly record struct ElfSymbol(
uint NameOffset,
byte Info,
byte Other,
ushort SectionIndex,
ulong Value,
ulong Size);
private readonly record struct ElfRelocation(ulong Offset, ulong Info, long Addend)
{
public uint SymbolIndex => (uint)(Info >> 32);
public uint Type => (uint)(Info & uint.MaxValue);
}
private enum RelocationValueKind : byte
{
Pointer = 0,
TlsModuleId = 1
}
private readonly record struct RelocationDescriptor(
ulong TargetAddress,
long Addend,
string? ImportNid,
ulong SymbolValue,
RelocationValueKind ValueKind,
bool IsDataImport);
private enum SelfSegmentResolveStatus
{
NotFound = 0,
Resolved = 1,
ResolvedDumped = 2,
Encrypted = 3,
Compressed = 4,
}
[StructLayout(LayoutKind.Sequential, Pack = 1)]
private readonly struct SelfHeader
{
private readonly byte _ident0;
private readonly byte _ident1;
private readonly byte _ident2;
private readonly byte _ident3;
private readonly byte _ident4;
private readonly byte _ident5;
private readonly byte _ident6;
private readonly byte _ident7;
private readonly byte _ident8;
private readonly byte _ident9;
private readonly byte _ident10;
private readonly byte _ident11;
private readonly ushort _size1;
private readonly ushort _size2;
private readonly ulong _fileSize;
private readonly ushort _segmentCount;
private readonly ushort _unknown;
private readonly uint _padding;
public ushort SegmentCount => _segmentCount;
public ushort Unknown => _unknown;
public ulong FileSize => _fileSize;
public bool HasKnownLayout =>
((_ident0 == 0x4F &&
_ident1 == 0x15 &&
_ident2 == 0x3D &&
_ident3 == 0x1D) ||
(_ident0 == 0x54 &&
_ident1 == 0x14 &&
_ident2 == 0xF5 &&
_ident3 == 0xEE)) &&
_ident4 == 0x00 &&
_ident5 == 0x01 &&
_ident6 == 0x01 &&
_ident7 == 0x12 &&
_ident8 == 0x01 &&
_ident9 == 0x01 &&
_ident10 == 0x00 &&
_ident11 == 0x00;
}
[StructLayout(LayoutKind.Sequential, Pack = 1)]
private readonly struct SelfSegment
{
private readonly ulong _type;
private readonly ulong _offset;
private readonly ulong _compressedSize;
private readonly ulong _decompressedSize;
public ulong Type => _type;
public bool IsBlocked => (Type & SelfSegmentFlag) != 0;
public ulong ProgramHeaderId => (Type >> 20) & 0xFFF;
public bool IsEncrypted => (Type & 0x2) != 0;
public bool IsCompressed => (Type & 0x8) != 0;
public ulong Offset => _offset;
public ulong CompressedSize => _compressedSize;
public ulong DecompressedSize => _decompressedSize;
}
}