From 67bba092e459094db1e80f7659581e5e8220cfad Mon Sep 17 00:00:00 2001 From: LukeFZ <17146677+LukeFZ@users.noreply.github.com> Date: Thu, 19 Mar 2026 13:28:13 +0100 Subject: [PATCH] add initial ported nso loader --- .../FileFormatStreams/FormatLayouts/Nso.cs | 257 +++++++++++++ .../FileFormatStreams/NsoReader.cs | 339 ++++++++++++++++++ Il2CppInspector.Common/Il2CppInspector.csproj | 1 + 3 files changed, 597 insertions(+) create mode 100644 Il2CppInspector.Common/FileFormatStreams/FormatLayouts/Nso.cs create mode 100644 Il2CppInspector.Common/FileFormatStreams/NsoReader.cs diff --git a/Il2CppInspector.Common/FileFormatStreams/FormatLayouts/Nso.cs b/Il2CppInspector.Common/FileFormatStreams/FormatLayouts/Nso.cs new file mode 100644 index 0000000..ee84e40 --- /dev/null +++ b/Il2CppInspector.Common/FileFormatStreams/FormatLayouts/Nso.cs @@ -0,0 +1,257 @@ +using System.Runtime.CompilerServices; +using VersionedSerialization; +using VersionedSerialization.Attributes; + +namespace Il2CppInspector; + +[VersionedStruct] +public partial struct NsoHeader +{ + public const uint ExpectedMagic = 0x304F534E; // NSO0 (LE) + + public uint Magic; + public uint Version; + public uint Reserved; + public SegmentFlags Flags; + + public SegmentHeader TextSegment; + public uint ModuleNameOffset; + + public SegmentHeader RoSegment; + public uint ModuleNameSize; + + public SegmentHeader DataSegment; + public uint BssSize; + + public ModuleIdBuffer ModuleId; + + public uint TextFileSize; + public uint RoFileSize; + public uint DataFileSize; + + public ReservedBuffer Reserved2; + + public SegmentHeaderRelative ApiInfoSection; + public SegmentHeaderRelative DynStrSection; + public SegmentHeaderRelative DynSymInfo; + + public HashBuffer TextHash; + public HashBuffer RoHash; + public HashBuffer DataHash; + + [InlineArray(32)] + public struct ModuleIdBuffer + { + private byte _value0; + } + + [InlineArray(0x1c)] + public struct ReservedBuffer + { + private byte _value0; + } + + [InlineArray(32)] + public struct HashBuffer + { + private byte _value0; + } +} + +[VersionedStruct] +public partial struct SegmentHeaderRelative +{ + public uint Offset; + public uint Size; +} + +[VersionedStruct] +public partial struct SegmentHeader +{ + public uint FileOffset; + public uint MemoryOffset; + public uint Size; +} + +[Flags] +public enum SegmentFlags : uint +{ + TextCompress = 1 << 0, + RoCompress = 1 << 1, + DataCompress = 1 << 2, + TextHash = 1 << 3, + RoHash = 1 << 4, + DataHash = 1 << 5 +} + +[VersionedStruct] +public partial struct ModInfoHeader +{ + public uint Reserved; + public uint ModOffset; +} + +[VersionedStruct] +public partial struct ModHeader +{ + public const uint ExpectedMagic = 0x30444f4d; // MOD0 + + public readonly bool ValidMagic => Magic == ExpectedMagic; + + public uint Magic; + public uint DynamicOffset; + public uint BssStartOffset; + public uint BssEndOffset; + public uint EhFrameHdrStartOffset; + public uint EhFrameHdrEndOffset; + public uint ModuleOffset; +} + +// These are just regular ELF stucts, but as long as the ELF parser still +// uses the old format we'll duplicate them here + +[VersionedStruct] +public partial struct DynamicEntry +{ + [NativeInteger] + public DynamicTag Tag; + + [NativeInteger] + public ulong Value; +} + +public enum DynamicTag : long +{ + DT_NULL = 0, + DT_NEEDED = 1, + DT_PLTRELSZ = 2, + DT_PLTGOT = 0x3, + DT_HASH = 0x4, + DT_STRTAB = 0x5, + DT_SYMTAB = 0x6, + DT_RELA = 0x7, + DT_RELASZ = 0x8, + DT_RELAENT = 0x9, + DT_STRSZ = 0xa, + DT_SYMENT = 0xb, + DT_INIT = 0xC, + DT_FINI = 0xD, + DT_REL = 0x11, + DT_RELSZ = 0x12, + DT_RELENT = 0x13, + DT_PLTREL = 0x14, + DT_DEBUG = 0x15, + DT_TEXTREL = 0x16, + DT_JMPREL = 0x17, + DT_BIND_NOW = 0x18, + DT_INIT_ARRAY = 0x19, + DT_FINI_ARRAY = 0x1A, + DT_INIT_ARRAYSZ = 0x1B, + DT_FINI_ARRAYSZ = 0x1C, + DT_RUNPATH = 0x1D, + DT_FLAGS = 0x1E, + DT_PREINIT_ARRAY = 0x20, + DT_PREINIT_ARRAYSZ = 0x21, + DT_LOOS = 0x6000000D, + DT_ANDROID_REL = DT_LOOS + 2, + DT_ANDROID_RELSZ = DT_LOOS + 3, + DT_ANDROID_RELA = DT_LOOS + 4, + DT_ANDROID_RELASZ = DT_LOOS + 5 +} + +public struct SymbolEntry : IReadable +{ + public const int Size32Bit = sizeof(uint) + sizeof(uint) + sizeof(byte) + sizeof(byte) + sizeof(ushort); + public const int Size64Bit = sizeof(byte) + sizeof(byte) + sizeof(ushort) + sizeof(ulong) + sizeof(ulong); + + public uint Name; + public ulong Value; + public ulong Size; + public byte Info; + public byte Other; + public ushort SectionIndex; + + public readonly ElfSymbolBind Bind => (ElfSymbolBind)(Info >> 4); + public readonly ElfSymbolType Type => (ElfSymbolType)(Info & 0xf); + + public void Read(ref Reader reader, in StructVersion version = default) + where TReader : IReader, allows ref struct + { + reader.ReadPrimitive(ref Name); + + if (reader.Config.Is32Bit) + { + Value = reader.ReadNativeUInt(); + Size = reader.ReadPrimitive(); + Info = reader.ReadPrimitive(); + Other = reader.ReadPrimitive(); + SectionIndex = reader.ReadPrimitive(); + } + else + { + Info = reader.ReadPrimitive(); + Other = reader.ReadPrimitive(); + SectionIndex = reader.ReadPrimitive(); + Value = reader.ReadNativeUInt(); + Size = reader.ReadNativeUInt(); + } + } + + static int IReadable.Size(in StructVersion version, in ReaderConfig config) + => config.Is32Bit ? Size32Bit : Size64Bit; +} + +public enum ElfSymbolBind +{ + STB_LOCAL, + STB_GLOBAL, + STB_WEAK +} + +public enum ElfSymbolType +{ + STT_NOTYPE, + STT_OBJECT, + STT_FUNC, + STT_SECTION, + STT_FILE, + STT_COMMON, + STT_TLS +} + +public enum RelocationType : uint +{ + R_ARM_ABS32 = 2, + R_ARM_REL32 = 3, + R_ARM_PC13 = 4, + R_ARM_COPY = 20, + + R_AARCH64_ABS64 = 0x101, + R_AARCH64_PREL64 = 0x104, + R_AARCH64_GLOB_DAT = 0x401, + R_AARCH64_JUMP_SLOT = 0x402, + R_AARCH64_RELATIVE = 0x403, +} + +[VersionedStruct] +public partial struct RelEntry +{ + [NativeInteger] + public ulong Offset; + + [NativeInteger] + public ulong Info; +} + +[VersionedStruct] +public partial struct RelaEntry +{ + [NativeInteger] + public ulong Offset; + + [NativeInteger] + public ulong Info; + + [NativeInteger] + public long Addend; +} \ No newline at end of file diff --git a/Il2CppInspector.Common/FileFormatStreams/NsoReader.cs b/Il2CppInspector.Common/FileFormatStreams/NsoReader.cs new file mode 100644 index 0000000..d17aa5e --- /dev/null +++ b/Il2CppInspector.Common/FileFormatStreams/NsoReader.cs @@ -0,0 +1,339 @@ +#nullable enable + +using K4os.Compression.LZ4; +using System.Buffers; +using System.Buffers.Binary; +using System.Collections.Frozen; +using System.Collections.Immutable; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Text; +using VersionedSerialization; + +namespace Il2CppInspector; + +internal abstract class MemoryBackingBuffer : IDisposable +{ + public abstract bool Initialized { get; protected set; } + + public bool IsLittleEndian { get; protected set; } + public bool Is32Bit { get; protected set; } + public ulong BaseAddress { get; protected set; } + + protected abstract Span Data { get; } + + public virtual void Initialize(long capacity, bool littleEndian, bool is32Bit, ulong baseAddress) + { + IsLittleEndian = littleEndian; + Is32Bit = is32Bit; + BaseAddress = baseAddress; + } + + protected int TranslateVaToRva(ulong address) + { + Debug.Assert(address >= BaseAddress); + var relativeAddress = address - BaseAddress; + return checked((int)relativeAddress); + } + + public T ReadObject(ulong address, in StructVersion version) where T : unmanaged, IReadable + { + Debug.Assert(Initialized); + + return IsLittleEndian + ? Reader.LittleEndian(Data, TranslateVaToRva(address), new ReaderConfig(Is32Bit)) + .ReadVersionedObject(in version) + : Reader.BigEndian(Data, TranslateVaToRva(address), new ReaderConfig(Is32Bit)) + .ReadVersionedObject(in version); + } + + public ImmutableArray ReadObjectArray(ulong address, long count, in StructVersion version) where T : unmanaged, IReadable + { + if (count == 0) + return []; + + Debug.Assert(Initialized); + + return IsLittleEndian + ? Reader.LittleEndian(Data, TranslateVaToRva(address), new ReaderConfig(Is32Bit)) + .ReadVersionedObjectArray(count, in version) + : Reader.BigEndian(Data, TranslateVaToRva(address), new ReaderConfig(Is32Bit)) + .ReadVersionedObjectArray(count, in version); + } + + public T ReadPrimitive(ulong address) where T : unmanaged + { + Debug.Assert(Initialized); + + return IsLittleEndian + ? Reader.LittleEndian(Data, TranslateVaToRva(address), new ReaderConfig(Is32Bit)) + .ReadPrimitive() + : Reader.BigEndian(Data, TranslateVaToRva(address), new ReaderConfig(Is32Bit)) + .ReadPrimitive(); + } + + public void WriteNUInt(ulong address, ulong value) + { + var region = Data.Slice(TranslateVaToRva(address), Is32Bit ? sizeof(uint) : sizeof(ulong)); + if (Is32Bit) + { + if (IsLittleEndian) + BinaryPrimitives.WriteUInt32LittleEndian(region, (uint)value); + else + BinaryPrimitives.WriteUInt32BigEndian(region, (uint)value); + } + else + { + if (IsLittleEndian) + BinaryPrimitives.WriteUInt64LittleEndian(region, value); + else + BinaryPrimitives.WriteUInt64BigEndian(region, value); + } + } + + public void WriteBytes(ulong address, ReadOnlySpan bytes) + { + bytes.CopyTo(Data.Slice(TranslateVaToRva(address), bytes.Length)); + } + + public void Clear(ulong address, long size) + { + Data.Slice(TranslateVaToRva(address), checked((int)size)).Clear(); + } + + public void Dispose() + { + GC.SuppressFinalize(this); + } +} + +internal class ByteArrayBackingBuffer : MemoryBackingBuffer +{ + [MemberNotNullWhen(true, nameof(Buffer))] + public override bool Initialized { get; protected set; } + + public byte[]? Buffer { get; private set; } + + protected override Span Data => Buffer.AsSpan(); + + public override void Initialize(long capacity, bool littleEndian, bool is32Bit, ulong baseAddress) + { + base.Initialize(capacity, littleEndian, is32Bit, baseAddress); + Buffer = new byte[capacity]; + Initialized = true; + } +} + +public class NsoReader : FileFormatStream +{ + public override string DefaultFilename => "main"; + public override int Bits => _is32Bit ? 32 : 64; + public override string Arch => "ARM64"; + public override string Format => "NSO"; + + // NOTE: This does not work for some reason? + // public override ulong ImageBase => 0x7100000000; + + protected override void Dispose(bool disposing) + { + if (disposing) + { + _mappedExecutable.Dispose(); + } + + base.Dispose(disposing); + } + + protected override bool Init() + { + var reader = VersionedSerialization.Reader.LittleEndian(GetBuffer()); + var magic = reader.ReadPrimitive(); + if (magic != NsoHeader.ExpectedMagic) + return false; + + LoadInternal(); + + Position = 0; + Write(_mappedExecutable.Buffer); + + return true; + } + + private readonly ByteArrayBackingBuffer _mappedExecutable = new(); + private bool _is32Bit; + private FrozenDictionary _dynamicEntries = FrozenDictionary.Empty; + private ImmutableArray<(ulong Start, ulong End)> _relocationEntryRegions = []; + + private void LoadInternal() + { + var reader = VersionedSerialization.Reader.LittleEndian(GetBuffer()); + var header = reader.ReadVersionedObject(); + + var totalLength = header.TextSegment.Size + header.RoSegment.Size + header.DataSegment.Size + header.BssSize; + + _mappedExecutable.Initialize(totalLength, true, false, 0); + + LoadSegment(ref reader, in header.TextSegment, header.TextFileSize, header.Flags.HasFlag(SegmentFlags.TextCompress)); + LoadSegment(ref reader, in header.RoSegment, header.RoFileSize, header.Flags.HasFlag(SegmentFlags.RoCompress)); + LoadSegment(ref reader, in header.DataSegment, header.DataFileSize, header.Flags.HasFlag(SegmentFlags.DataCompress)); + + var modInfoHeader = _mappedExecutable.ReadObject(header.TextSegment.MemoryOffset, default); + var modHeader = _mappedExecutable.ReadObject(modInfoHeader.ModOffset, default); + + Debug.Assert(modHeader.ValidMagic); + + // check if we are loading a 32-bit binary + // by checking if we have a full .dynamic entry in the first (or third) dynamic slot + var firstEntry = _mappedExecutable.ReadPrimitive(modInfoHeader.ModOffset + modHeader.DynamicOffset); + var thirdEntry = _mappedExecutable.ReadPrimitive(modInfoHeader.ModOffset + modHeader.DynamicOffset + (2 * 0x8)); + _is32Bit = firstEntry > uint.MaxValue || thirdEntry > uint.MaxValue; + + var currentDynamicOffset = modInfoHeader.ModOffset + modHeader.DynamicOffset; + var dynamicEntries = new Dictionary(); + while (true) + { + var entry = _mappedExecutable.ReadObject(currentDynamicOffset, default); + if (entry.Tag == DynamicTag.DT_NULL) + break; + + dynamicEntries[entry.Tag] = entry.Value; + currentDynamicOffset += 2 * (Is32Bit ? 4u : 8u); + } + + _dynamicEntries = dynamicEntries.ToFrozenDictionary(); + + ApplyRelocations(); + } + + private void LoadSegment(ref Reader reader, ref readonly SegmentHeader header, uint fileSize, bool isCompressed) + where T : ISeekableReader, allows ref struct + { + reader.Offset = checked((int)header.FileOffset); + var data = reader.ReadBytes(checked((int)fileSize)); + + if (!isCompressed) + { + Debug.Assert(header.Size == fileSize); + _mappedExecutable.WriteBytes(header.MemoryOffset, data); + } + else + { + var rented = ArrayPool.Shared.Rent(checked((int)header.Size)); + var decompressed = rented.AsSpan(0, checked((int)header.Size)); + + var result = LZ4Codec.Decode(data, decompressed); + Debug.Assert(result == header.Size); + _ = result; + + _mappedExecutable.WriteBytes(header.MemoryOffset, decompressed); + + ArrayPool.Shared.Return(rented); + } + } + + // Copied and trimmed down from ElfBinary + private void ApplyRelocations() + { + if (_mappedExecutable == null) + throw new InvalidOperationException("Cannot apply relocations without executable being mapped"); + + if (!_dynamicEntries.TryGetValue(DynamicTag.DT_SYMTAB, out var symtabAddress) + || !_dynamicEntries.TryGetValue(DynamicTag.DT_SYMENT, out var symtabEntrySize)) + return; + + List<(ulong Start, ulong End)> relocationRegions = []; + + if (_dynamicEntries.TryGetValue(DynamicTag.DT_REL, out var relAddress)) + { + var relocationSize = _dynamicEntries[DynamicTag.DT_RELSZ]; + ApplyRelocationsImpl(ParseRelSection(relAddress, relocationSize)); + relocationRegions.Add((relAddress, relAddress + relocationSize)); + } + else if (_dynamicEntries.TryGetValue(DynamicTag.DT_RELA, out var relaAddress)) + { + var relocationSize = _dynamicEntries[DynamicTag.DT_RELASZ]; + ApplyRelocationsImpl(ParseRelaSection(relaAddress, relocationSize)); + relocationRegions.Add((relaAddress, relaAddress + relocationSize)); + } + + if (_dynamicEntries.TryGetValue(DynamicTag.DT_JMPREL, out var jmprelAddress)) + { + var size = _dynamicEntries[DynamicTag.DT_PLTRELSZ]; + var type = (DynamicTag)_dynamicEntries[DynamicTag.DT_PLTREL]; + ApplyRelocationsImpl(type == DynamicTag.DT_REL + ? ParseRelSection(jmprelAddress, size) + : ParseRelaSection(jmprelAddress, size)); + + relocationRegions.Add((jmprelAddress, jmprelAddress + size)); + } + + _relocationEntryRegions = [.. relocationRegions]; + + // Clear out relocation sections in memory so searching is faster + foreach (var (start, end) in _relocationEntryRegions) + { + _mappedExecutable.Clear(start, checked((long)(end - start))); + } + + return; + + void ApplyRelocationsImpl(ICollection<(ulong Offset, ulong Info, long? Addend)> relocations) + { + if (relocations.Count == 0) + return; + + var symbolCache = new Dictionary(); + + foreach (var relocation in relocations) + { + var offset = relocation.Offset; + var type = (RelocationType)(Is32Bit ? relocation.Info & byte.MaxValue : relocation.Info & uint.MaxValue); + var symbolIndex = (uint)(relocation.Info >> (Is32Bit ? 8 : 32)); + var addend = checked((ulong)(relocation.Addend ?? _mappedExecutable.ReadPrimitive(offset))); + + if (!symbolCache.TryGetValue(symbolIndex, out var symbolEntry)) + { + var symtabEntryAddress = symtabAddress + symbolIndex * symtabEntrySize; + symbolEntry = _mappedExecutable.ReadObject(symtabEntryAddress, default); + symbolCache[symbolIndex] = symbolEntry; + } + + var (value, handled) = type switch + { + RelocationType.R_ARM_ABS32 or RelocationType.R_AARCH64_ABS64 => (symbolEntry.Value + addend, true), + RelocationType.R_ARM_REL32 or RelocationType.R_AARCH64_PREL64 => (symbolEntry.Value + relocation.Offset - addend, true), + RelocationType.R_ARM_COPY => (symbolEntry.Value, true), + RelocationType.R_AARCH64_GLOB_DAT => (symbolEntry.Value + addend, true), + RelocationType.R_AARCH64_JUMP_SLOT => (symbolEntry.Value + addend, true), + RelocationType.R_AARCH64_RELATIVE => (symbolEntry.Value + addend, true), + _ => (0uL, false) + }; + + if (handled) + { + _mappedExecutable.WriteNUInt(offset, value); + } + } + } + + List<(ulong, ulong, long?)> ParseRelSection(ulong address, ulong size) + { + var entrySize = _dynamicEntries[DynamicTag.DT_RELENT]; + var entryCount = size / entrySize; + + return _mappedExecutable.ReadObjectArray(address, checked((int)entryCount), default) + .Select(x => (x.Offset, x.Info, null)) + .ToList(); + } + + List<(ulong, ulong, long?)> ParseRelaSection(ulong address, ulong size) + { + var entrySize = _dynamicEntries[DynamicTag.DT_RELAENT]; + var entryCount = size / entrySize; + + return _mappedExecutable.ReadObjectArray(address, checked((int)entryCount), default) + .Select(x => (x.Offset, x.Info, x.Addend)) + .ToList(); + } + } +} \ No newline at end of file diff --git a/Il2CppInspector.Common/Il2CppInspector.csproj b/Il2CppInspector.Common/Il2CppInspector.csproj index c2052eb..e742b74 100644 --- a/Il2CppInspector.Common/Il2CppInspector.csproj +++ b/Il2CppInspector.Common/Il2CppInspector.csproj @@ -35,6 +35,7 @@ +