add initial ported nso loader

This commit is contained in:
LukeFZ
2026-03-19 13:28:13 +01:00
parent df61d2d425
commit 67bba092e4
3 changed files with 597 additions and 0 deletions

View File

@@ -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<TReader>(ref Reader<TReader> 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<uint>();
Info = reader.ReadPrimitive<byte>();
Other = reader.ReadPrimitive<byte>();
SectionIndex = reader.ReadPrimitive<ushort>();
}
else
{
Info = reader.ReadPrimitive<byte>();
Other = reader.ReadPrimitive<byte>();
SectionIndex = reader.ReadPrimitive<ushort>();
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;
}

View File

@@ -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<byte> 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<T>(ulong address, in StructVersion version) where T : unmanaged, IReadable
{
Debug.Assert(Initialized);
return IsLittleEndian
? Reader.LittleEndian(Data, TranslateVaToRva(address), new ReaderConfig(Is32Bit))
.ReadVersionedObject<T>(in version)
: Reader.BigEndian(Data, TranslateVaToRva(address), new ReaderConfig(Is32Bit))
.ReadVersionedObject<T>(in version);
}
public ImmutableArray<T> ReadObjectArray<T>(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<T>(count, in version)
: Reader.BigEndian(Data, TranslateVaToRva(address), new ReaderConfig(Is32Bit))
.ReadVersionedObjectArray<T>(count, in version);
}
public T ReadPrimitive<T>(ulong address) where T : unmanaged
{
Debug.Assert(Initialized);
return IsLittleEndian
? Reader.LittleEndian(Data, TranslateVaToRva(address), new ReaderConfig(Is32Bit))
.ReadPrimitive<T>()
: Reader.BigEndian(Data, TranslateVaToRva(address), new ReaderConfig(Is32Bit))
.ReadPrimitive<T>();
}
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<byte> 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<byte> 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<NsoReader>
{
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<uint>();
if (magic != NsoHeader.ExpectedMagic)
return false;
LoadInternal();
Position = 0;
Write(_mappedExecutable.Buffer);
return true;
}
private readonly ByteArrayBackingBuffer _mappedExecutable = new();
private bool _is32Bit;
private FrozenDictionary<DynamicTag, ulong> _dynamicEntries = FrozenDictionary<DynamicTag, ulong>.Empty;
private ImmutableArray<(ulong Start, ulong End)> _relocationEntryRegions = [];
private void LoadInternal()
{
var reader = VersionedSerialization.Reader.LittleEndian(GetBuffer());
var header = reader.ReadVersionedObject<NsoHeader>();
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<ModInfoHeader>(header.TextSegment.MemoryOffset, default);
var modHeader = _mappedExecutable.ReadObject<ModHeader>(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<ulong>(modInfoHeader.ModOffset + modHeader.DynamicOffset);
var thirdEntry = _mappedExecutable.ReadPrimitive<ulong>(modInfoHeader.ModOffset + modHeader.DynamicOffset + (2 * 0x8));
_is32Bit = firstEntry > uint.MaxValue || thirdEntry > uint.MaxValue;
var currentDynamicOffset = modInfoHeader.ModOffset + modHeader.DynamicOffset;
var dynamicEntries = new Dictionary<DynamicTag, ulong>();
while (true)
{
var entry = _mappedExecutable.ReadObject<DynamicEntry>(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<T>(ref Reader<T> 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<byte>.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<byte>.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<uint, SymbolEntry>();
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<long>(offset)));
if (!symbolCache.TryGetValue(symbolIndex, out var symbolEntry))
{
var symtabEntryAddress = symtabAddress + symbolIndex * symtabEntrySize;
symbolEntry = _mappedExecutable.ReadObject<SymbolEntry>(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<RelEntry>(address, checked((int)entryCount), default)
.Select<RelEntry, (ulong, ulong, long?)>(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<RelaEntry>(address, checked((int)entryCount), default)
.Select<RelaEntry, (ulong, ulong, long?)>(x => (x.Offset, x.Info, x.Addend))
.ToList();
}
}
}

View File

@@ -35,6 +35,7 @@
<ItemGroup>
<PackageReference Include="dnlib" Version="4.4.0" />
<PackageReference Include="K4os.Compression.LZ4" Version="1.3.8" />
<PackageReference Include="McMaster.NETCore.Plugins" Version="2.0.0" />
<PackageReference Include="Spectre.Console" Version="0.50.0" />
</ItemGroup>